Repository: ergoplatform/sigma-rust (language: rust)

Issue:
Better ErgoTree evaluation error messages
### Motivation
The error message only describes what IR node evaluation failed without any context information. 

The following error message communicates the context better:
```
...
val v5 = OUTPUTS(2)
                ^^^ - index out of bounds
...
```

For a deserialized ErgoTree the source code can be generated by pretty printer #371 .


### Solution
Ideally, we'd want a pretty printed tree with evaluation results(values) printed at ValDef's.

One of the options is to add a source span to every node and pretty print the tree on an error at the top-level evaluator (ErgoTree). 



Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 202b75360..4c49a8063 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,7 +17,7 @@ members = [
     "bindings/ergo-lib-wasm",
     "bindings/ergo-lib-c-core",
     "bindings/ergo-lib-c",
-    "bindings/ergo-lib-jni"
+    "bindings/ergo-lib-jni",
 ]
 
 [workspace.package]
@@ -37,7 +37,7 @@ ergo-merkle-tree = { version = "^0.11.1", path = "./ergo-merkle-tree" }
 ergo-rest = { version = "^0.9.1", path = "./ergo-rest" }
 ergo-lib = { version = "^0.24.1", path = "./ergo-lib"}
 k256 = { version = "0.13.1", features = ["arithmetic", "ecdsa"] }
-elliptic-curve = {version = "0.12", features = [ "ff"]}
+elliptic-curve = { version = "0.12", features = ["ff"] }
 thiserror = "1"
 bounded-vec = { version = "^0.7.0" }
 bitvec = { version = "1.0.1" }
@@ -52,9 +52,12 @@ lazy_static = "1.4"
 bs58 = "0.4.0"
 base16 = "0.2.1"
 base64 = "0.13.0"
-indexmap = {version ="1.3.2", features = ["serde"]}
+indexmap = { version = "1.3.2", features = ["serde"] }
 serde = { version = "1.0", features = ["derive"] }
-serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
+serde_json = { version = "1.0", features = [
+    "arbitrary_precision",
+    "preserve_order",
+] }
 serde_with = { version = "1.9.1", features = ["json"] }
 rand = "0.8.5"
 bytes = "1.1"
@@ -67,13 +70,14 @@ bounded-integer = { version = "^0.5", features = ["types"] }
 url = "2.2"
 getrandom = { version = "0.2.7" }
 itertools = "0.10.3"
+miette = { version = "5", features = ["fancy"] }
 
 # dev-dependencies
 proptest = { version = "=1.0", default-features = false, features = ["std"] }
 proptest-derive = "0.3"
 pretty_assertions = "1.3"
 wasm-bindgen-test = "0.3.37"
-expect-test = "1.0.1"
+expect-test = "1.4.1"
 
 [profile.release]
 # Tell `rustc` to optimize for small code size.
diff --git a/ergo-chain-types/src/ec_point.rs b/ergo-chain-types/src/ec_point.rs
index 97b89c45d..f71f4e4d6 100644
--- a/ergo-chain-types/src/ec_point.rs
+++ b/ergo-chain-types/src/ec_point.rs
@@ -28,6 +28,15 @@ impl std::fmt::Debug for EcPoint {
     }
 }
 
+#[allow(clippy::unwrap_used)]
+impl std::fmt::Display for EcPoint {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        f.write_str(&base16::encode_lower(
+            &self.scorex_serialize_bytes().unwrap(),
+        ))
+    }
+}
+
 impl EcPoint {
     /// Number of bytes to represent any group element as byte array
     pub const GROUP_SIZE: usize = 33;
diff --git a/ergo-lib/src/chain/transaction/reduced.rs b/ergo-lib/src/chain/transaction/reduced.rs
index 4e2c542b9..c3a19a98c 100644
--- a/ergo-lib/src/chain/transaction/reduced.rs
+++ b/ergo-lib/src/chain/transaction/reduced.rs
@@ -123,7 +123,11 @@ impl SigmaSerializable for ReducedTransaction {
                 let cost = r.get_u64()?;
                 let extension = input.spending_proof.extension;
                 let reduced_input = ReducedInput {
-                    reduction_result: ReductionResult { sigma_prop, cost },
+                    reduction_result: ReductionResult {
+                        sigma_prop,
+                        cost,
+                        env: Env::empty(),
+                    },
                     extension: extension.clone(),
                 };
                 let unsigned_input = UnsignedInput {
@@ -167,6 +171,7 @@ pub mod arbitrary {
                         reduction_result: ReductionResult {
                             sigma_prop: sb.clone(),
                             cost: 0,
+                            env: Env::empty(),
                         },
                         extension: unsigned_input.extension,
                     }),
diff --git a/ergoscript-compiler/src/mir/lower.rs b/ergoscript-compiler/src/mir/lower.rs
index c4540679d..5ddfc8bd4 100644
--- a/ergoscript-compiler/src/mir/lower.rs
+++ b/ergoscript-compiler/src/mir/lower.rs
@@ -110,19 +110,25 @@ mod tests {
         check(
             "HEIGHT + HEIGHT",
             expect![[r#"
-            BinOp(
-                BinOp {
-                    kind: Arith(
-                        Plus,
-                    ),
-                    left: GlobalVars(
-                        Height,
-                    ),
-                    right: GlobalVars(
-                        Height,
-                    ),
-                },
-            )"#]],
+                BinOp(
+                    Spanned {
+                        source_span: SourceSpan {
+                            offset: 0,
+                            length: 0,
+                        },
+                        expr: BinOp {
+                            kind: Arith(
+                                Plus,
+                            ),
+                            left: GlobalVars(
+                                Height,
+                            ),
+                            right: GlobalVars(
+                                Height,
+                            ),
+                        },
+                    },
+                )"#]],
         )
     }
 
@@ -154,16 +160,22 @@ mod tests {
             "4+2",
             expect![[r#"
                 BinOp(
-                    BinOp {
-                        kind: Arith(
-                            Plus,
-                        ),
-                        left: Const(
-                            "4: SInt",
-                        ),
-                        right: Const(
-                            "2: SInt",
-                        ),
+                    Spanned {
+                        source_span: SourceSpan {
+                            offset: 0,
+                            length: 0,
+                        },
+                        expr: BinOp {
+                            kind: Arith(
+                                Plus,
+                            ),
+                            left: Const(
+                                "4: SInt",
+                            ),
+                            right: Const(
+                                "2: SInt",
+                            ),
+                        },
                     },
                 )"#]],
         );
@@ -174,19 +186,25 @@ mod tests {
         check(
             "4L+2L",
             expect![[r#"
-            BinOp(
-                BinOp {
-                    kind: Arith(
-                        Plus,
-                    ),
-                    left: Const(
-                        "4: SLong",
-                    ),
-                    right: Const(
-                        "2: SLong",
-                    ),
-                },
-            )"#]],
+                BinOp(
+                    Spanned {
+                        source_span: SourceSpan {
+                            offset: 0,
+                            length: 0,
+                        },
+                        expr: BinOp {
+                            kind: Arith(
+                                Plus,
+                            ),
+                            left: Const(
+                                "4: SLong",
+                            ),
+                            right: Const(
+                                "2: SLong",
+                            ),
+                        },
+                    },
+                )"#]],
         );
     }
 }
diff --git a/ergotree-interpreter/Cargo.toml b/ergotree-interpreter/Cargo.toml
index ce6934ac2..8009b1ee3 100644
--- a/ergotree-interpreter/Cargo.toml
+++ b/ergotree-interpreter/Cargo.toml
@@ -6,9 +6,7 @@ authors = ["Denys Zadorozhnyi <denys@zadorozhnyi.com>"]
 repository.workspace = true
 edition.workspace = true
 description = "ErgoTree interpreter"
-exclude = [
-    "proptest-regressions/*"
-]
+exclude = ["proptest-regressions/*"]
 
 [lib]
 crate-type = ["cdylib", "rlib"]
@@ -31,22 +29,29 @@ base16 = { workspace = true }
 proptest-derive = { workspace = true, optional = true }
 bytes = { workspace = true }
 num-bigint = { workspace = true }
-bounded-vec = { workspace = true, features=["serde"] }
+bounded-vec = { workspace = true, features = ["serde"] }
 serde = { workspace = true, optional = true }
 serde_json = { workspace = true, optional = true }
 serde_with = { workspace = true, optional = true }
-proptest = { workspace = true , optional = true }
+proptest = { workspace = true, optional = true }
 scorex_crypto_avltree = "0.1.0"
 gf2_192 = { version = "^0.24.1", path = "../gf2_192" }
+miette = { workspace = true }
 
 [features]
 default = ["json"]
 json = ["serde", "serde_json", "serde_with", "bounded-vec/serde"]
-arbitrary = ["proptest", "proptest-derive", "ergotree-ir/arbitrary", "ergo-chain-types/arbitrary", "gf2_192/arbitrary"]
+arbitrary = [
+    "proptest",
+    "proptest-derive",
+    "ergotree-ir/arbitrary",
+    "ergo-chain-types/arbitrary",
+    "gf2_192/arbitrary",
+]
 
 [dev-dependencies]
 ergotree-ir = { workspace = true, features = ["arbitrary"] }
 ergoscript-compiler = { workspace = true }
 proptest = { workspace = true }
 sigma-test-util = { workspace = true }
-
+expect-test = { workspace = true }
diff --git a/ergotree-interpreter/src/contracts.rs b/ergotree-interpreter/src/contracts.rs
index a1c27154f..b3e47cc07 100644
--- a/ergotree-interpreter/src/contracts.rs
+++ b/ergotree-interpreter/src/contracts.rs
@@ -213,11 +213,9 @@ mod tests {
         let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
         let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
         assert!(addr.script().unwrap().proposition().is_ok());
-        let _script = addr.script().unwrap().proposition().unwrap();
+        // let script = addr.script().unwrap().proposition().unwrap();
         // dbg!(&script);
-        // let res: bool = eval_out_wo_ctx::<SigmaProp>(script.as_ref())
-        //     .try_into()
-        //     .unwrap();
+        // let res: bool = eval_out_wo_ctx::<SigmaProp>(&script).try_into().unwrap();
         // assert!(!res);
     }
 
@@ -227,12 +225,9 @@ mod tests {
         let p2s_addr_str = "cLPHJ3MHuKAHoCUwGhcEFw5sWJqvPwFyKxTRj1aUoMwgAz78Fg3zLXRhBup9Te1WLau1gZXNmXvUmeXGCd7QLeqB7ArrT3v5cg26piEtqymM6j2SkgYVCobgoAGKeTf6nMLxv1uVrLdjt1GnPxG1MuWj7Es7Dfumotbx9YEaxwqtTUC5SKsJc9LCpAmNWRAQbU6tVVEvmfwWivrGoZ3L5C4DMisxN3U";
         let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
         let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
-        let _script = addr.script().unwrap().proposition().unwrap();
-        // dbg!(&script);
-        // let res: bool = eval_out_wo_ctx::<SigmaProp>(script.as_ref())
-        //     .try_into()
-        //     .unwrap();
-        // assert!(!res);
+        let _ = addr.script().unwrap().proposition().unwrap();
+        // let ctx = Rc::new(force_any_val::<Context>());
+        // let _ = reduce_to_crypto(&script, &Env::empty(), ctx).unwrap();
     }
 
     #[test]
@@ -284,6 +279,15 @@ mod tests {
 
         let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
         let addr = encoder.parse_address_from_str(p2s_str).unwrap();
-        let _script = addr.script().unwrap().proposition().unwrap();
+        let _ = addr.script().unwrap().proposition().unwrap();
+        // let ctx = Rc::new(force_any_val::<Context>());
+        // let res = reduce_to_crypto(&script, &Env::empty(), ctx).unwrap();
+        // match res.sigma_prop {
+        //     SigmaBoolean::TrivialProp(b) => assert!(b),
+        //     SigmaBoolean::ProofOfKnowledge(_) => {
+        //         todo!()
+        //     }
+        //     SigmaBoolean::SigmaConjecture(_) => todo!(),
+        // }
     }
 }
diff --git a/ergotree-interpreter/src/eval.rs b/ergotree-interpreter/src/eval.rs
index aefeda105..84c82474f 100644
--- a/ergotree-interpreter/src/eval.rs
+++ b/ergotree-interpreter/src/eval.rs
@@ -1,25 +1,18 @@
 //! Interpreter
-use bounded_vec::BoundedVecOutOfBounds;
 use ergotree_ir::mir::constant::TryExtractInto;
+use ergotree_ir::pretty_printer::PosTrackingWriter;
+use ergotree_ir::pretty_printer::Print;
 use ergotree_ir::sigma_protocol::sigma_boolean::SigmaProp;
-use sigma_ser::ScorexParsingError;
-use sigma_ser::ScorexSerializationError;
 use std::rc::Rc;
 
-use ergotree_ir::ergo_tree::ErgoTreeError;
-use ergotree_ir::mir::constant::TryExtractFromError;
 use ergotree_ir::mir::expr::Expr;
 use ergotree_ir::mir::value::Value;
-use ergotree_ir::serialization::SigmaParsingError;
-use ergotree_ir::serialization::SigmaSerializationError;
 use ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean;
 
 use cost_accum::CostAccumulator;
 use ergotree_ir::types::smethod::SMethod;
-use thiserror::Error;
 
 use self::context::Context;
-use self::cost_accum::CostError;
 use self::env::Env;
 
 /// Context(blockchain) for the interpreter
@@ -57,6 +50,7 @@ pub(crate) mod decode_point;
 mod deserialize_context;
 mod deserialize_register;
 pub(crate) mod downcast;
+mod error;
 pub(crate) mod exponentiate;
 pub(crate) mod expr;
 pub(crate) mod extract_amount;
@@ -101,61 +95,7 @@ pub(crate) mod val_use;
 pub(crate) mod xor;
 pub(crate) mod xor_of;
 
-/// Interpreter errors
-#[derive(Error, PartialEq, Eq, Debug, Clone)]
-pub enum EvalError {
-    /// AVL tree errors
-    #[error("AvlTree: {0}")]
-    AvlTree(String),
-    /// Only boolean or SigmaBoolean is a valid result expr type
-    #[error("Only boolean or SigmaBoolean is a valid result expr type")]
-    InvalidResultType,
-    /// Unexpected Expr encountered during the evaluation
-    #[error("Unexpected Expr: {0}")]
-    UnexpectedExpr(String),
-    /// Error on cost calculation
-    #[error("Error on cost calculation: {0:?}")]
-    CostError(#[from] CostError),
-    /// Unexpected value type
-    #[error("Unexpected value type: {0:?}")]
-    TryExtractFrom(#[from] TryExtractFromError),
-    /// Not found (missing value, argument, etc.)
-    #[error("Not found: {0}")]
-    NotFound(String),
-    /// Register id out of bounds
-    #[error("{0}")]
-    RegisterIdOutOfBounds(String),
-    /// Unexpected value
-    #[error("Unexpected value: {0}")]
-    UnexpectedValue(String),
-    /// Arithmetic exception error
-    #[error("Arithmetic exception: {0}")]
-    ArithmeticException(String),
-    /// Misc error
-    #[error("error: {0}")]
-    Misc(String),
-    /// Sigma serialization error
-    #[error("Serialization error: {0}")]
-    SigmaSerializationError(#[from] SigmaSerializationError),
-    /// Sigma serialization parsing error
-    #[error("Serialization parsing error: {0}")]
-    SigmaParsingError(#[from] SigmaParsingError),
-    /// ErgoTree error
-    #[error("ErgoTree error: {0}")]
-    ErgoTreeError(#[from] ErgoTreeError),
-    /// Not yet implemented
-    #[error("evaluation is not yet implemented: {0}")]
-    NotImplementedYet(&'static str),
-    /// Invalid item quantity for BoundedVec
-    #[error("Invalid item quantity for BoundedVec: {0}")]
-    BoundedVecError(#[from] BoundedVecOutOfBounds),
-    /// Scorex serialization error
-    #[error("Serialization error: {0}")]
-    ScorexSerializationError(#[from] ScorexSerializationError),
-    /// Scorex serialization parsing error
-    #[error("Serialization parsing error: {0}")]
-    ScorexParsingError(#[from] ScorexParsingError),
-}
+pub use error::EvalError;
 
 /// Result of expression reduction procedure (see `reduce_to_crypto`).
 #[derive(PartialEq, Eq, Debug, Clone)]
@@ -164,6 +104,8 @@ pub struct ReductionResult {
     pub sigma_prop: SigmaBoolean,
     /// estimated cost of expression evaluation
     pub cost: u64,
+    /// environment after the evaluation
+    pub env: Env,
 }
 
 /// Evaluate the given expression by reducing it to SigmaBoolean value.
@@ -172,22 +114,40 @@ pub fn reduce_to_crypto(
     env: &Env,
     ctx: Rc<Context>,
 ) -> Result<ReductionResult, EvalError> {
-    let cost_accum = CostAccumulator::new(0, None);
-    let mut ectx = EvalContext::new(ctx, cost_accum);
-    expr.eval(env, &mut ectx)
-        .and_then(|v| -> Result<ReductionResult, EvalError> {
-            match v {
-                Value::Boolean(b) => Ok(ReductionResult {
-                    sigma_prop: SigmaBoolean::TrivialProp(b),
-                    cost: 0,
-                }),
-                Value::SigmaProp(sp) => Ok(ReductionResult {
-                    sigma_prop: sp.value().clone(),
-                    cost: 0,
-                }),
-                _ => Err(EvalError::InvalidResultType),
-            }
-        })
+    let ctx_clone = ctx.clone();
+    fn inner(expr: &Expr, env: &Env, ctx: Rc<Context>) -> Result<ReductionResult, EvalError> {
+        let cost_accum = CostAccumulator::new(0, None);
+        let mut ectx = EvalContext::new(ctx, cost_accum);
+        let mut env_mut = env.clone();
+        expr.eval(&mut env_mut, &mut ectx)
+            .and_then(|v| -> Result<ReductionResult, EvalError> {
+                match v {
+                    Value::Boolean(b) => Ok(ReductionResult {
+                        sigma_prop: SigmaBoolean::TrivialProp(b),
+                        cost: 0,
+                        env: env_mut.clone(),
+                    }),
+                    Value::SigmaProp(sp) => Ok(ReductionResult {
+                        sigma_prop: sp.value().clone(),
+                        cost: 0,
+                        env: env_mut.clone(),
+                    }),
+                    _ => Err(EvalError::InvalidResultType),
+                }
+            })
+    }
+
+    let res = inner(expr, env, ctx);
+    if res.is_ok() {
+        return res;
+    }
+    let mut printer = PosTrackingWriter::new();
+    let spanned_expr = expr
+        .print(&mut printer)
+        .map_err(|e| EvalError::Misc(format!("printer error: {}", e)))?;
+    let printed_expr_str = printer.get_buf();
+    inner(&spanned_expr, env, ctx_clone)
+        .map_err(|e| e.wrap_spanned_with_src(printed_expr_str.to_string()))
 }
 
 /// Expects SigmaProp constant value and returns it's value. Otherwise, returns an error.
@@ -214,10 +174,11 @@ impl EvalContext {
 /// Should be implemented by every node that can be evaluated.
 pub(crate) trait Evaluable {
     /// Evaluation routine to be implement by each node
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError>;
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError>;
 }
 
-type EvalFn = fn(env: &Env, ctx: &mut EvalContext, Value, Vec<Value>) -> Result<Value, EvalError>;
+type EvalFn =
+    fn(env: &mut Env, ctx: &mut EvalContext, Value, Vec<Value>) -> Result<Value, EvalError>;
 
 fn smethod_eval_fn(method: &SMethod) -> Result<EvalFn, EvalError> {
     use ergotree_ir::types::*;
@@ -387,7 +348,8 @@ pub(crate) mod tests {
     pub fn eval_out<T: TryExtractFrom<Value>>(expr: &Expr, ctx: Rc<Context>) -> T {
         let cost_accum = CostAccumulator::new(0, None);
         let mut ectx = EvalContext::new(ctx, cost_accum);
-        expr.eval(&Env::empty(), &mut ectx)
+        let mut env = Env::empty();
+        expr.eval(&mut env, &mut ectx)
             .unwrap()
             .try_extract_into::<T>()
             .unwrap()
@@ -399,7 +361,8 @@ pub(crate) mod tests {
     ) -> Result<T, EvalError> {
         let cost_accum = CostAccumulator::new(0, None);
         let mut ectx = EvalContext::new(ctx, cost_accum);
-        expr.eval(&Env::empty(), &mut ectx)
+        let mut env = Env::empty();
+        expr.eval(&mut env, &mut ectx)
             .and_then(|v| v.try_extract_into::<T>().map_err(EvalError::TryExtractFrom))
     }
 
diff --git a/ergotree-interpreter/src/eval/and.rs b/ergotree-interpreter/src/eval/and.rs
index 668520bb5..882227600 100644
--- a/ergotree-interpreter/src/eval/and.rs
+++ b/ergotree-interpreter/src/eval/and.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for And {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let input_v_bools = input_v.try_extract_into::<Vec<bool>>()?;
         Ok(input_v_bools.iter().all(|b| *b).into())
diff --git a/ergotree-interpreter/src/eval/apply.rs b/ergotree-interpreter/src/eval/apply.rs
index ceefbe8ea..9a3c93352 100644
--- a/ergotree-interpreter/src/eval/apply.rs
+++ b/ergotree-interpreter/src/eval/apply.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Apply {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let func_v = self.func.eval(env, ctx)?;
         let args_v_res: Result<Vec<Value>, EvalError> =
             self.args.iter().map(|arg| arg.eval(env, ctx)).collect();
@@ -16,11 +16,10 @@ impl Evaluable for Apply {
         match func_v {
             Value::Lambda(fv) => {
                 let arg_ids: Vec<ValId> = fv.args.iter().map(|a| a.idx).collect();
-                let mut cur_env = env.clone();
                 arg_ids.iter().zip(args_v).for_each(|(idx, arg_v)| {
-                    cur_env.insert(*idx, arg_v);
+                    env.insert(*idx, arg_v);
                 });
-                fv.body.eval(&cur_env, ctx)
+                fv.body.eval(env, ctx)
             }
             _ => Err(EvalError::UnexpectedValue(format!(
                 "expected func_v to be Value::FuncValue got: {0:?}",
@@ -54,31 +53,37 @@ mod tests {
     #[test]
     fn eval_user_defined_func_call() {
         let arg = Expr::Const(1i32.into());
-        let bin_op = Expr::BinOp(BinOp {
-            kind: RelationOp::Eq.into(),
-            left: Box::new(
-                ValUse {
-                    val_id: 1.into(),
-                    tpe: SType::SInt,
-                }
-                .into(),
-            ),
-            right: Box::new(
-                ValUse {
-                    val_id: 2.into(),
-                    tpe: SType::SInt,
+        let bin_op = Expr::BinOp(
+            BinOp {
+                kind: RelationOp::Eq.into(),
+                left: Box::new(
+                    ValUse {
+                        val_id: 1.into(),
+                        tpe: SType::SInt,
+                    }
+                    .into(),
+                ),
+                right: Box::new(
+                    ValUse {
+                        val_id: 2.into(),
+                        tpe: SType::SInt,
+                    }
+                    .into(),
+                ),
+            }
+            .into(),
+        );
+        let body = Expr::BlockValue(
+            BlockValue {
+                items: vec![ValDef {
+                    id: 2.into(),
+                    rhs: Box::new(Expr::Const(1i32.into())),
                 }
-                .into(),
-            ),
-        });
-        let body = Expr::BlockValue(BlockValue {
-            items: vec![ValDef {
-                id: 2.into(),
-                rhs: Box::new(Expr::Const(1i32.into())),
+                .into()],
+                result: Box::new(bin_op),
             }
-            .into()],
-            result: Box::new(bin_op),
-        });
+            .into(),
+        );
         let apply: Expr = Apply::new(
             FuncValue::new(
                 vec![FuncArg {
diff --git a/ergotree-interpreter/src/eval/atleast.rs b/ergotree-interpreter/src/eval/atleast.rs
index e15f31442..93b56ff36 100644
--- a/ergotree-interpreter/src/eval/atleast.rs
+++ b/ergotree-interpreter/src/eval/atleast.rs
@@ -14,7 +14,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Atleast {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let bound_v = self.bound.eval(env, ctx)?;
         let input_v = self.input.eval(env, ctx)?;
 
diff --git a/ergotree-interpreter/src/eval/bin_op.rs b/ergotree-interpreter/src/eval/bin_op.rs
index 146c0387d..2a47f9ceb 100644
--- a/ergotree-interpreter/src/eval/bin_op.rs
+++ b/ergotree-interpreter/src/eval/bin_op.rs
@@ -171,7 +171,7 @@ where
 }
 
 impl Evaluable for BinOp {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         ctx.cost_accum.add(Costs::DEFAULT.eq_const_size)?;
         let lv = self.left.eval(env, ctx)?;
         // using closure to keep right value from evaluation (for lazy AND, OR, XOR)
diff --git a/ergotree-interpreter/src/eval/bit_inversion.rs b/ergotree-interpreter/src/eval/bit_inversion.rs
index 61bd51653..18541f474 100644
--- a/ergotree-interpreter/src/eval/bit_inversion.rs
+++ b/ergotree-interpreter/src/eval/bit_inversion.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for BitInversion {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::Byte(v) => Ok(Value::Byte(!v)),
diff --git a/ergotree-interpreter/src/eval/block.rs b/ergotree-interpreter/src/eval/block.rs
index 2562d66d3..dfe5b3c21 100644
--- a/ergotree-interpreter/src/eval/block.rs
+++ b/ergotree-interpreter/src/eval/block.rs
@@ -2,6 +2,7 @@ use ergotree_ir::mir::block::BlockValue;
 use ergotree_ir::mir::constant::TryExtractInto;
 use ergotree_ir::mir::val_def::ValDef;
 use ergotree_ir::mir::value::Value;
+use ergotree_ir::source_span::Spanned;
 
 use crate::eval::env::Env;
 use crate::eval::EvalContext;
@@ -9,14 +10,15 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for BlockValue {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
-        let mut cur_env = env.clone();
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         for i in &self.items {
-            let val_def = i.clone().try_extract_into::<ValDef>()?;
-            let v: Value = val_def.rhs.eval(&cur_env, ctx)?;
-            cur_env.insert(val_def.id, v);
+            // TODO: new try_extract_spanned_into?
+            let spanned_val_def = &i.clone().try_extract_into::<Spanned<ValDef>>()?;
+            let val_def = spanned_val_def.expr();
+            let v: Value = val_def.rhs.eval(env, ctx)?;
+            env.insert(val_def.id, v);
         }
-        self.result.eval(&cur_env, ctx)
+        self.result.eval(env, ctx)
     }
 }
 
@@ -32,7 +34,7 @@ mod tests {
 
         #[test]
         fn ser_roundtrip(block in any::<BlockValue>()) {
-            let e = Expr::BlockValue(block);
+            let e = Expr::BlockValue(block.into());
             prop_assert_eq![sigma_serialize_roundtrip(&e), e];
         }
     }
diff --git a/ergotree-interpreter/src/eval/bool_to_sigma.rs b/ergotree-interpreter/src/eval/bool_to_sigma.rs
index 591ce4760..3f249c052 100644
--- a/ergotree-interpreter/src/eval/bool_to_sigma.rs
+++ b/ergotree-interpreter/src/eval/bool_to_sigma.rs
@@ -10,7 +10,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for BoolToSigmaProp {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let input_v_bool = input_v.try_extract_into::<bool>()?;
         Ok((SigmaProp::new(SigmaBoolean::TrivialProp(input_v_bool))).into())
diff --git a/ergotree-interpreter/src/eval/byte_array_to_bigint.rs b/ergotree-interpreter/src/eval/byte_array_to_bigint.rs
index a31d3a751..695607cc4 100644
--- a/ergotree-interpreter/src/eval/byte_array_to_bigint.rs
+++ b/ergotree-interpreter/src/eval/byte_array_to_bigint.rs
@@ -12,7 +12,7 @@ use crate::eval::EvalError::UnexpectedValue;
 use crate::eval::Evaluable;
 
 impl Evaluable for ByteArrayToBigInt {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input = self.input.eval(env, ctx)?.try_extract_into::<Vec<u8>>()?;
         if input.is_empty() {
             return Err(UnexpectedValue(
diff --git a/ergotree-interpreter/src/eval/byte_array_to_long.rs b/ergotree-interpreter/src/eval/byte_array_to_long.rs
index e97ca35ed..bd2c3b8c0 100644
--- a/ergotree-interpreter/src/eval/byte_array_to_long.rs
+++ b/ergotree-interpreter/src/eval/byte_array_to_long.rs
@@ -9,7 +9,7 @@ use crate::eval::Evaluable;
 use ergotree_ir::mir::constant::TryExtractInto;
 
 impl Evaluable for ByteArrayToLong {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input = self.input.eval(env, ctx)?.try_extract_into::<Vec<i8>>()?;
         if input.len() < 8 {
             return Err(UnexpectedValue(
diff --git a/ergotree-interpreter/src/eval/calc_blake2b256.rs b/ergotree-interpreter/src/eval/calc_blake2b256.rs
index 3d5485ace..4c97ef5c6 100644
--- a/ergotree-interpreter/src/eval/calc_blake2b256.rs
+++ b/ergotree-interpreter/src/eval/calc_blake2b256.rs
@@ -11,7 +11,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for CalcBlake2b256 {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v.clone() {
             Value::Coll(CollKind::NativeColl(NativeColl::CollByte(coll_byte))) => {
diff --git a/ergotree-interpreter/src/eval/calc_sha256.rs b/ergotree-interpreter/src/eval/calc_sha256.rs
index 3d01d786e..3cefa834c 100644
--- a/ergotree-interpreter/src/eval/calc_sha256.rs
+++ b/ergotree-interpreter/src/eval/calc_sha256.rs
@@ -11,7 +11,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for CalcSha256 {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v.clone() {
             Value::Coll(CollKind::NativeColl(NativeColl::CollByte(coll_byte))) => {
diff --git a/ergotree-interpreter/src/eval/coll_append.rs b/ergotree-interpreter/src/eval/coll_append.rs
index 7b2d58144..9e0f5a423 100644
--- a/ergotree-interpreter/src/eval/coll_append.rs
+++ b/ergotree-interpreter/src/eval/coll_append.rs
@@ -35,7 +35,7 @@ fn extract_elem_tpe(inp: &Value) -> Result<SType, EvalError> {
 }
 
 impl Evaluable for Append {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let col2_v = self.col_2.eval(env, ctx)?;
         let input_elem_tpe = extract_elem_tpe(&input_v)?;
@@ -111,7 +111,7 @@ mod tests {
     fn append_byte_array_and_byte() {
         let byte_coll: Constant = vec![1i8, 2i8].into();
         let byte: Expr = Expr::Collection(Collection::new(SType::SByte, vec![3i8.into()]).unwrap());
-        let expr: Expr = Expr::Append(Append::new(byte_coll.into(), byte).unwrap());
+        let expr: Expr = Expr::Append(Append::new(byte_coll.into(), byte).unwrap().into());
         assert_eq!(eval_out_wo_ctx::<Vec<i8>>(&expr), vec![1i8, 2, 3]);
     }
 }
diff --git a/ergotree-interpreter/src/eval/coll_by_index.rs b/ergotree-interpreter/src/eval/coll_by_index.rs
index a79683b59..e6f47ec53 100644
--- a/ergotree-interpreter/src/eval/coll_by_index.rs
+++ b/ergotree-interpreter/src/eval/coll_by_index.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ByIndex {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let index_v = self.index.eval(env, ctx)?;
         let normalized_input_vals: Vec<Value> = match input_v {
diff --git a/ergotree-interpreter/src/eval/coll_exists.rs b/ergotree-interpreter/src/eval/coll_exists.rs
index 4819d0b5a..2a97500c2 100644
--- a/ergotree-interpreter/src/eval/coll_exists.rs
+++ b/ergotree-interpreter/src/eval/coll_exists.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Exists {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let condition_v = self.condition.eval(env, ctx)?;
         let input_v_clone = input_v.clone();
@@ -19,8 +19,8 @@ impl Evaluable for Exists {
                         "Exists: evaluated condition has empty arguments list".to_string(),
                     )
                 })?;
-                let env1 = env.clone().extend(func_arg.idx, arg);
-                func_value.body.eval(&env1, ctx)
+                env.insert(func_arg.idx, arg);
+                func_value.body.eval(env, ctx)
             }
             _ => Err(EvalError::UnexpectedValue(format!(
                 "expected Exists::condition to be Value::FuncValue got: {0:?}",
diff --git a/ergotree-interpreter/src/eval/coll_filter.rs b/ergotree-interpreter/src/eval/coll_filter.rs
index 28fc94d82..951adaf63 100644
--- a/ergotree-interpreter/src/eval/coll_filter.rs
+++ b/ergotree-interpreter/src/eval/coll_filter.rs
@@ -9,7 +9,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Filter {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let condition_v = self.condition.eval(env, ctx)?;
         let input_v_clone = input_v.clone();
@@ -20,8 +20,8 @@ impl Evaluable for Filter {
                         "Filter: evaluated condition has empty arguments list".to_string(),
                     )
                 })?;
-                let env1 = env.clone().extend(func_arg.idx, arg);
-                func_value.body.eval(&env1, ctx)
+                env.insert(func_arg.idx, arg);
+                func_value.body.eval(env, ctx)
             }
             _ => Err(EvalError::UnexpectedValue(format!(
                 "expected Filter::condition to be Value::FuncValue got: {0:?}",
diff --git a/ergotree-interpreter/src/eval/coll_fold.rs b/ergotree-interpreter/src/eval/coll_fold.rs
index 6b3e10fc1..385870eaa 100644
--- a/ergotree-interpreter/src/eval/coll_fold.rs
+++ b/ergotree-interpreter/src/eval/coll_fold.rs
@@ -9,7 +9,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Fold {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let zero_v = self.zero.eval(env, ctx)?;
         let fold_op_v = self.fold_op.eval(env, ctx)?;
@@ -20,8 +20,8 @@ impl Evaluable for Fold {
                     .args
                     .first()
                     .ok_or_else(|| EvalError::NotFound("empty argument for fold op".to_string()))?;
-                let env1 = env.clone().extend(func_arg.idx, arg);
-                func_value.body.eval(&env1, ctx)
+                env.insert(func_arg.idx, arg);
+                func_value.body.eval(env, ctx)
             }
             _ => Err(EvalError::UnexpectedValue(format!(
                 "expected fold_op to be Value::FuncValue got: {0:?}",
@@ -97,11 +97,11 @@ mod tests {
             let fold_op_body: Expr = BinOp {
                 kind: ArithOp::Plus.into(),
                 left: Box::new(Expr::SelectField(
-                    SelectField::new(tuple.clone(), 1.try_into().unwrap()).unwrap(),
+                    SelectField::new(tuple.clone(), 1.try_into().unwrap()).unwrap().into(),
                 )),
                 right: Box::new(Expr::ExtractAmount(
                     ExtractAmount::try_build(Expr::SelectField(
-                        SelectField::new(tuple, 2.try_into().unwrap()).unwrap(),
+                        SelectField::new(tuple, 2.try_into().unwrap()).unwrap().into(),
                     ))
                     .unwrap(),
                 )),
diff --git a/ergotree-interpreter/src/eval/coll_forall.rs b/ergotree-interpreter/src/eval/coll_forall.rs
index 186ec5f45..0dec1520a 100644
--- a/ergotree-interpreter/src/eval/coll_forall.rs
+++ b/ergotree-interpreter/src/eval/coll_forall.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ForAll {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let condition_v = self.condition.eval(env, ctx)?;
         let input_v_clone = input_v.clone();
@@ -19,8 +19,8 @@ impl Evaluable for ForAll {
                         "ForAll: evaluated condition has empty arguments list".to_string(),
                     )
                 })?;
-                let env1 = env.clone().extend(func_arg.idx, arg);
-                func_value.body.eval(&env1, ctx)
+                env.insert(func_arg.idx, arg);
+                func_value.body.eval(env, ctx)
             }
             _ => Err(EvalError::UnexpectedValue(format!(
                 "expected ForAll::condition to be Value::FuncValue got: {0:?}",
diff --git a/ergotree-interpreter/src/eval/coll_map.rs b/ergotree-interpreter/src/eval/coll_map.rs
index 9b76ada8b..6745d08f3 100644
--- a/ergotree-interpreter/src/eval/coll_map.rs
+++ b/ergotree-interpreter/src/eval/coll_map.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Map {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let mapper_v = self.mapper.eval(env, ctx)?;
         let input_v_clone = input_v.clone();
@@ -19,8 +19,8 @@ impl Evaluable for Map {
                         "Map: evaluated mapper has empty arguments list".to_string(),
                     )
                 })?;
-                let env1 = env.clone().extend(func_arg.idx, arg);
-                func_value.body.eval(&env1, ctx)
+                env.insert(func_arg.idx, arg);
+                func_value.body.eval(env, ctx)
             }
             _ => Err(EvalError::UnexpectedValue(format!(
                 "expected mapper to be Value::FuncValue got: {0:?}",
diff --git a/ergotree-interpreter/src/eval/coll_size.rs b/ergotree-interpreter/src/eval/coll_size.rs
index 87db1e37c..2f8cb4614 100644
--- a/ergotree-interpreter/src/eval/coll_size.rs
+++ b/ergotree-interpreter/src/eval/coll_size.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for SizeOf {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let normalized_input_vals: Vec<Value> = match input_v {
             Value::Coll(coll) => Ok(coll.as_vec()),
diff --git a/ergotree-interpreter/src/eval/coll_slice.rs b/ergotree-interpreter/src/eval/coll_slice.rs
index feef572f0..c39de608a 100644
--- a/ergotree-interpreter/src/eval/coll_slice.rs
+++ b/ergotree-interpreter/src/eval/coll_slice.rs
@@ -9,7 +9,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Slice {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let from_v = self.from.eval(env, ctx)?;
         let until_v = self.until.eval(env, ctx)?;
diff --git a/ergotree-interpreter/src/eval/collection.rs b/ergotree-interpreter/src/eval/collection.rs
index 185acad0d..e497e22f6 100644
--- a/ergotree-interpreter/src/eval/collection.rs
+++ b/ergotree-interpreter/src/eval/collection.rs
@@ -12,7 +12,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Collection {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         Ok(match self {
             Collection::BoolConstants(bools) => bools.clone().into(),
             Collection::Exprs { elem_tpe, items } => {
diff --git a/ergotree-interpreter/src/eval/create_avl_tree.rs b/ergotree-interpreter/src/eval/create_avl_tree.rs
index e493d08a0..e9a3dd4c5 100644
--- a/ergotree-interpreter/src/eval/create_avl_tree.rs
+++ b/ergotree-interpreter/src/eval/create_avl_tree.rs
@@ -11,7 +11,7 @@ use sigma_util::AsVecU8;
 use std::convert::TryFrom;
 
 impl Evaluable for CreateAvlTree {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let flags_v = self.flags.eval(env, ctx)?.try_extract_into::<i8>()? as u8;
         let digest_v = self.digest.eval(env, ctx)?.try_extract_into::<Vec<i8>>()?;
         let key_length = self.key_length.eval(env, ctx)?.try_extract_into::<i32>()? as u32;
diff --git a/ergotree-interpreter/src/eval/create_prove_dh_tuple.rs b/ergotree-interpreter/src/eval/create_prove_dh_tuple.rs
index d06f9c604..e58311b16 100644
--- a/ergotree-interpreter/src/eval/create_prove_dh_tuple.rs
+++ b/ergotree-interpreter/src/eval/create_prove_dh_tuple.rs
@@ -10,7 +10,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for CreateProveDhTuple {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let g = self.g.eval(env, ctx)?.try_extract_into::<EcPoint>()?;
         let h = self.h.eval(env, ctx)?.try_extract_into::<EcPoint>()?;
         let u = self.u.eval(env, ctx)?.try_extract_into::<EcPoint>()?;
diff --git a/ergotree-interpreter/src/eval/create_provedlog.rs b/ergotree-interpreter/src/eval/create_provedlog.rs
index b672cc79e..dc0036a1b 100644
--- a/ergotree-interpreter/src/eval/create_provedlog.rs
+++ b/ergotree-interpreter/src/eval/create_provedlog.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for CreateProveDlog {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let value_v = self.input.eval(env, ctx)?;
         match value_v {
             Value::GroupElement(ecpoint) => {
diff --git a/ergotree-interpreter/src/eval/decode_point.rs b/ergotree-interpreter/src/eval/decode_point.rs
index 8ef37704b..f088e6078 100644
--- a/ergotree-interpreter/src/eval/decode_point.rs
+++ b/ergotree-interpreter/src/eval/decode_point.rs
@@ -11,11 +11,12 @@ use ergotree_ir::mir::constant::TryExtractInto;
 use ergotree_ir::serialization::SigmaSerializable;
 
 impl Evaluable for DecodePoint {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let point_bytes = self.input.eval(env, ctx)?.try_extract_into::<Vec<u8>>()?;
         let point: EcPoint = SigmaSerializable::sigma_parse_bytes(&point_bytes).map_err(|_| {
-            Misc(String::from(
-                "DecodePoint: Failed to parse EC point from bytes",
+            Misc(format!(
+                "DecodePoint: Failed to parse EC point from bytes {:?}",
+                point_bytes,
             ))
         })?;
         Ok(point.into())
diff --git a/ergotree-interpreter/src/eval/deserialize_context.rs b/ergotree-interpreter/src/eval/deserialize_context.rs
index 4fa22e2fe..6a4e48f6e 100644
--- a/ergotree-interpreter/src/eval/deserialize_context.rs
+++ b/ergotree-interpreter/src/eval/deserialize_context.rs
@@ -11,23 +11,27 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for DeserializeContext {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         match ctx.ctx.extension.values.get(&self.id) {
             Some(c) => {
-                if c.tpe != SType::SColl(SType::SByte.into()) {
-                    Err(EvalError::UnexpectedExpr(format!("DeserializeContext: expected extension value to have type SColl(SByte), got {:?}", c.tpe)))
+                let expected_tpe = SType::SColl(SType::SByte.into());
+                if c.tpe != expected_tpe {
+                    Err(EvalError::UnexpectedExpr(format!(
+                        "DeserializeContext: expected extension value {} with id {} to have type {:?} got {:?}",
+                        c, self.id, expected_tpe, c.tpe
+                    )))
                 } else {
                     let bytes = c.v.clone().try_extract_into::<Vec<u8>>()?;
                     let expr = Expr::sigma_parse_bytes(bytes.as_slice())?;
                     if expr.tpe() != self.tpe {
-                        return Err(EvalError::UnexpectedExpr(format!("DeserializeContext: expected deserialized expr to have type {:?}, got {:?}", self.tpe, expr.tpe())));
+                        return Err(EvalError::UnexpectedExpr(format!("DeserializeContext: expected deserialized expr from extension value {} with id {} to have type {:?}, got {:?}", c, self.id, self.tpe, expr.tpe())));
                     }
                     expr.eval(env, ctx)
                 }
             }
             None => Err(EvalError::NotFound(format!(
-                "DeserializeContext: no value with id {} in context extension",
-                self.id
+                "DeserializeContext: no value with id {} in context extension map {}",
+                self.id, ctx.ctx.extension
             ))),
         }
     }
diff --git a/ergotree-interpreter/src/eval/deserialize_register.rs b/ergotree-interpreter/src/eval/deserialize_register.rs
index 92d4dfad0..10ee394a9 100644
--- a/ergotree-interpreter/src/eval/deserialize_register.rs
+++ b/ergotree-interpreter/src/eval/deserialize_register.rs
@@ -1,5 +1,6 @@
 use std::convert::TryInto;
 
+use ergotree_ir::chain::ergo_box::RegisterId;
 use ergotree_ir::mir::constant::TryExtractInto;
 use ergotree_ir::mir::deserialize_register::DeserializeRegister;
 use ergotree_ir::mir::expr::Expr;
@@ -13,27 +14,23 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for DeserializeRegister {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
-        match ctx
-            .ctx
-            .self_box
-            .get_register(self.reg.try_into().map_err(|e| {
-                EvalError::RegisterIdOutOfBounds(format!(
-                    "register index is out of bounds: {:?} ",
-                    e
-                ))
-            })?) {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+        let reg_id: RegisterId = self.reg.try_into().map_err(|e| {
+            EvalError::RegisterIdOutOfBounds(format!("register index is out of bounds: {:?} ", e))
+        })?;
+        match ctx.ctx.self_box.get_register(reg_id) {
             Ok(Some(c)) => {
                 if c.tpe != SType::SColl(SType::SByte.into()) {
                     Err(EvalError::UnexpectedExpr(format!(
-                        "DeserializeRegister: expected value to have type SColl(SByte), got {:?}",
-                        c.tpe
+                        "DeserializeRegister: expected register {} value {} to have type SColl(SByte), got {:?}",
+                        reg_id, c, c.tpe
                     )))
                 } else {
                     let bytes = c.v.try_extract_into::<Vec<u8>>()?;
                     let expr = Expr::sigma_parse_bytes(bytes.as_slice())?;
                     if expr.tpe() != self.tpe {
-                        Err(EvalError::UnexpectedExpr(format!("DeserializeRegister: expected deserialized expr to have type {:?}, got {:?}", self.tpe, expr.tpe())))
+                        let pretty_expr = expr.to_string_pretty();
+                        Err(EvalError::UnexpectedExpr(format!("DeserializeRegister: expected register {reg_id} deserialized expr {pretty_expr} to have type {:?}, got {:?}", self.tpe, expr.tpe())))
                     } else {
                         expr.eval(env, ctx)
                     }
@@ -42,15 +39,13 @@ impl Evaluable for DeserializeRegister {
             Ok(None) => match &self.default {
                 Some(default_expr) => eval_default(&self.tpe, default_expr, env, ctx),
                 None => Err(EvalError::NotFound(format!(
-                    "DeserializeRegister: register {:?} is empty",
-                    self.reg
+                    "DeserializeRegister: register {reg_id} is empty"
                 ))),
             },
             Err(e) => match &self.default {
                 Some(default_expr) => eval_default(&self.tpe, default_expr, env, ctx),
                 None => Err(EvalError::NotFound(format!(
-                    "DeserializeRegister: failed to get the register id {} with error: {e:?}",
-                    self.reg
+                    "DeserializeRegister: failed to get the register id {reg_id} with error: {e:?}"
                 ))),
             },
         }
@@ -60,7 +55,7 @@ impl Evaluable for DeserializeRegister {
 fn eval_default(
     deserialize_reg_tpe: &SType,
     default_expr: &Expr,
-    env: &Env,
+    env: &mut Env,
     ctx: &mut EvalContext,
 ) -> Result<Value, EvalError> {
     if &default_expr.tpe() != deserialize_reg_tpe {
diff --git a/ergotree-interpreter/src/eval/downcast.rs b/ergotree-interpreter/src/eval/downcast.rs
index 639cbd0ab..003c5f1b4 100644
--- a/ergotree-interpreter/src/eval/downcast.rs
+++ b/ergotree-interpreter/src/eval/downcast.rs
@@ -104,7 +104,7 @@ fn downcast_to_byte(in_v: Value) -> Result<Value, EvalError> {
 }
 
 impl Evaluable for Downcast {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match self.tpe {
             SType::SBigInt => downcast_to_bigint(input_v),
diff --git a/ergotree-interpreter/src/eval/error.rs b/ergotree-interpreter/src/eval/error.rs
new file mode 100644
index 000000000..0f4b539ed
--- /dev/null
+++ b/ergotree-interpreter/src/eval/error.rs
@@ -0,0 +1,339 @@
+use miette::miette;
+use miette::LabeledSpan;
+use std::fmt::Display;
+
+use bounded_vec::BoundedVecOutOfBounds;
+use derive_more::TryInto;
+use ergotree_ir::ergo_tree::ErgoTreeError;
+use ergotree_ir::mir::constant::TryExtractFromError;
+use ergotree_ir::serialization::SigmaParsingError;
+use ergotree_ir::serialization::SigmaSerializationError;
+use ergotree_ir::source_span::SourceSpan;
+use sigma_ser::ScorexParsingError;
+use sigma_ser::ScorexSerializationError;
+use thiserror::Error;
+
+use super::cost_accum::CostError;
+use super::env::Env;
+
+/// Interpreter errors
+#[derive(Error, PartialEq, Eq, Debug, Clone, TryInto)]
+pub enum EvalError {
+    /// AVL tree errors
+    #[error("AvlTree: {0}")]
+    AvlTree(String),
+    /// Only boolean or SigmaBoolean is a valid result expr type
+    #[error("Only boolean or SigmaBoolean is a valid result expr type")]
+    InvalidResultType,
+    /// Unexpected Expr encountered during the evaluation
+    #[error("Unexpected Expr: {0}")]
+    UnexpectedExpr(String),
+    /// Error on cost calculation
+    #[error("Error on cost calculation: {0:?}")]
+    CostError(#[from] CostError),
+    /// Unexpected value type
+    #[error("Unexpected value type: {0:?}")]
+    TryExtractFrom(#[from] TryExtractFromError),
+    /// Not found (missing value, argument, etc.)
+    #[error("Not found: {0}")]
+    NotFound(String),
+    /// Register id out of bounds
+    #[error("{0}")]
+    RegisterIdOutOfBounds(String),
+    /// Unexpected value
+    #[error("Unexpected value: {0}")]
+    UnexpectedValue(String),
+    /// Arithmetic exception error
+    #[error("Arithmetic exception: {0}")]
+    ArithmeticException(String),
+    /// Misc error
+    #[error("error: {0}")]
+    Misc(String),
+    /// Sigma serialization error
+    #[error("Serialization error: {0}")]
+    SigmaSerializationError(#[from] SigmaSerializationError),
+    /// Sigma serialization parsing error
+    #[error("Serialization parsing error: {0}")]
+    SigmaParsingError(#[from] SigmaParsingError),
+    /// ErgoTree error
+    #[error("ErgoTree error: {0}")]
+    ErgoTreeError(#[from] ErgoTreeError),
+    /// Invalid item quantity for BoundedVec
+    #[error("Invalid item quantity for BoundedVec: {0}")]
+    BoundedVecError(#[from] BoundedVecOutOfBounds),
+    /// Scorex serialization error
+    #[error("Serialization error: {0}")]
+    ScorexSerializationError(#[from] ScorexSerializationError),
+    /// Scorex serialization parsing error
+    #[error("Serialization parsing error: {0}")]
+    ScorexParsingError(#[from] ScorexParsingError),
+    /// Wrapped error with source span and source code
+    #[error("eval error: {0}")]
+    SpannedWithSource(SpannedWithSourceEvalError),
+    /// Wrapped error with source span
+    #[error("eval error: {0:?}")]
+    Spanned(SpannedEvalError),
+}
+
+/// Wrapped error with source span
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct SpannedEvalError {
+    /// eval error
+    error: Box<EvalError>,
+    /// source span for the expression where error occurred
+    source_span: SourceSpan,
+    /// environment at the time when error occurred
+    env: Env,
+}
+
+/// Wrapped error with source span and source code
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct SpannedWithSourceEvalError {
+    /// eval error
+    error: Box<EvalError>,
+    /// source span for the expression where error occurred
+    source_span: SourceSpan,
+    /// environment at the time when error occurred
+    env: Env,
+    /// source code
+    source: String,
+}
+
+impl Display for SpannedWithSourceEvalError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        #[allow(clippy::unwrap_used)]
+        miette::set_hook(Box::new(|_| {
+            Box::new(
+                miette::MietteHandlerOpts::new()
+                    .terminal_links(false)
+                    .unicode(false)
+                    .color(false)
+                    .context_lines(5)
+                    .tab_width(2)
+                    .build(),
+            )
+        }))
+        .unwrap();
+        let err_msg = self.error.to_string();
+        let report = miette!(
+            labels = vec![LabeledSpan::at(self.source_span, err_msg,)],
+            // help = "Help msg",
+            "Evaluation error"
+        )
+        .with_source_code(self.source.clone());
+        write!(f, "{:?}", report)
+    }
+}
+
+impl EvalError {
+    /// Wrap eval error with source span
+    pub fn wrap(self, source_span: SourceSpan, env: Env) -> Self {
+        EvalError::Spanned(SpannedEvalError {
+            error: Box::new(self),
+            source_span,
+            env,
+        })
+    }
+
+    /// Wrap eval error with source code
+    pub fn wrap_spanned_with_src(self, source: String) -> Self {
+        #[allow(clippy::panic)]
+        match self {
+            EvalError::Spanned(e) => EvalError::SpannedWithSource(SpannedWithSourceEvalError {
+                error: e.error,
+                source_span: e.source_span,
+                env: e.env,
+                source,
+            }),
+            e => panic!("Expected Spanned, got {:?}", e),
+        }
+    }
+}
+
+pub trait ExtResultEvalError<T> {
+    fn enrich_err(self, span: SourceSpan, env: Env) -> Result<T, EvalError>;
+}
+
+impl<T> ExtResultEvalError<T> for Result<T, EvalError> {
+    fn enrich_err(self, span: SourceSpan, env: Env) -> Result<T, EvalError> {
+        self.map_err(|e| match e {
+            // skip already wrapped errors
+            w @ EvalError::Spanned { .. } => w,
+            e => e.wrap(span, env),
+        })
+    }
+}
+
+#[allow(clippy::unwrap_used, unused_imports, dead_code)]
+#[cfg(test)]
+mod tests {
+    use std::rc::Rc;
+
+    use ergotree_ir::mir::coll_by_index::ByIndex;
+    use ergotree_ir::mir::global_vars::GlobalVars;
+    use ergotree_ir::source_span::SourceSpan;
+    use expect_test::expect;
+
+    use ergotree_ir::mir::bin_op::ArithOp;
+    use ergotree_ir::mir::bin_op::BinOp;
+    use ergotree_ir::mir::block::BlockValue;
+    use ergotree_ir::mir::expr::Expr;
+    use ergotree_ir::mir::val_def::ValDef;
+    use ergotree_ir::mir::val_use::ValUse;
+    use ergotree_ir::pretty_printer::PosTrackingWriter;
+    use ergotree_ir::pretty_printer::Print;
+    use ergotree_ir::types::stype::SType;
+    use sigma_test_util::force_any_val;
+
+    use crate::eval::context::Context;
+    use crate::eval::error::SpannedEvalError;
+    use crate::eval::error::SpannedWithSourceEvalError;
+    use crate::eval::tests::try_eval_out;
+
+    fn check(expr: Expr, expected_tree: expect_test::Expect) {
+        let mut w = PosTrackingWriter::new();
+        let spanned_expr = expr.print(&mut w).unwrap();
+        dbg!(&spanned_expr);
+        let ctx = Rc::new(force_any_val::<Context>());
+        let err_raw: SpannedEvalError = try_eval_out::<i32>(&spanned_expr, ctx)
+            .err()
+            .unwrap()
+            .try_into()
+            .unwrap();
+        let err = SpannedWithSourceEvalError {
+            error: err_raw.error,
+            source_span: err_raw.source_span,
+            env: err_raw.env,
+            source: w.get_buf().to_string(),
+        };
+        expected_tree.assert_eq(&err.to_string());
+    }
+
+    fn check_error_span(expr: Expr, expected_span: SourceSpan) {
+        let mut w = PosTrackingWriter::new();
+        let spanned_expr = expr.print(&mut w).unwrap();
+        dbg!(&spanned_expr);
+        let ctx = Rc::new(force_any_val::<Context>());
+        let err_raw: SpannedEvalError = try_eval_out::<i32>(&spanned_expr, ctx)
+            .err()
+            .unwrap()
+            .try_into()
+            .unwrap();
+        assert_eq!(err_raw.source_span, expected_span);
+    }
+
+    #[test]
+    fn pretty_binop_div_zero() {
+        let lhs_val_id = 1.into();
+        let rhs_val_id = 2.into();
+        let res_val_id = 3.into();
+        let expr = Expr::BlockValue(
+            BlockValue {
+                items: vec![
+                    ValDef {
+                        id: lhs_val_id,
+                        rhs: Box::new(Expr::Const(42i32.into())),
+                    }
+                    .into(),
+                    ValDef {
+                        id: rhs_val_id,
+                        rhs: Box::new(Expr::Const(0i32.into())),
+                    }
+                    .into(),
+                    ValDef {
+                        id: res_val_id,
+                        rhs: Box::new(
+                            BinOp {
+                                kind: ArithOp::Divide.into(),
+                                left: Box::new(
+                                    ValUse {
+                                        val_id: lhs_val_id,
+                                        tpe: SType::SInt,
+                                    }
+                                    .into(),
+                                ),
+                                right: Box::new(
+                                    ValUse {
+                                        val_id: rhs_val_id,
+                                        tpe: SType::SInt,
+                                    }
+                                    .into(),
+                                ),
+                            }
+                            .into(),
+                        ),
+                    }
+                    .into(),
+                ],
+                result: Box::new(
+                    ValUse {
+                        val_id: res_val_id,
+                        tpe: SType::SInt,
+                    }
+                    .into(),
+                ),
+            }
+            .into(),
+        );
+        // check(
+        //     expr,
+        //     expect![[r#"
+        //           x Evaluation error
+        //            ,-[1:1]
+        //          1 | {
+        //          2 |   val v1 = 42
+        //          3 |   val v2 = 0
+        //          4 |   val v3 = v1 / v2
+        //            :            ^^^|^^^
+        //            :               `-- Arithmetic exception: (42) / (0) resulted in exception
+        //          5 |   v3
+        //          6 | }
+        //            `----
+        //     "#]],
+        // );
+        check_error_span(expr, (40, 7).into());
+    }
+
+    #[test]
+    fn pretty_out_of_bounds() {
+        let v1_id = 1.into();
+        let expr = Expr::BlockValue(
+            BlockValue {
+                items: vec![ValDef {
+                    id: v1_id,
+                    rhs: Box::new(Expr::Const(99999999i32.into())),
+                }
+                .into()],
+                result: Box::new(Expr::ByIndex(
+                    ByIndex::new(
+                        Expr::GlobalVars(GlobalVars::Outputs),
+                        ValUse {
+                            val_id: v1_id,
+                            tpe: SType::SInt,
+                        }
+                        .into(),
+                        None,
+                    )
+                    .unwrap()
+                    .into(),
+                )),
+            }
+            .into(),
+        );
+        // check(
+        //     expr,
+        //     expect![[r#"
+        //           x Evaluation error
+        //            ,-[1:1]
+        //          1 | {
+        //          2 |   val v1 = 99999999
+        //          3 |   OUTPUTS(v1)
+        //            :          ^^|^
+        //            :            `-- error: ByIndex: index Int(99999999) out of bounds for collection size 1
+        //          4 | }
+        //            `----
+        //     "#]],
+        // );
+        check_error_span(expr, (31, 4).into());
+    }
+}
diff --git a/ergotree-interpreter/src/eval/exponentiate.rs b/ergotree-interpreter/src/eval/exponentiate.rs
index 5326d8a9e..dc00bd0f7 100644
--- a/ergotree-interpreter/src/eval/exponentiate.rs
+++ b/ergotree-interpreter/src/eval/exponentiate.rs
@@ -9,7 +9,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Exponentiate {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let left_v = self.left.eval(env, ctx)?;
         let right_v = self.right.eval(env, ctx)?;
 
diff --git a/ergotree-interpreter/src/eval/expr.rs b/ergotree-interpreter/src/eval/expr.rs
index e36aff9a0..ebbe93f66 100644
--- a/ergotree-interpreter/src/eval/expr.rs
+++ b/ergotree-interpreter/src/eval/expr.rs
@@ -1,35 +1,39 @@
+//! Evaluation of ErgoTree expressions
+
 use ergotree_ir::mir::expr::Expr;
 use ergotree_ir::mir::value::Value;
+use ergotree_ir::source_span::Spanned;
 
+use super::error::ExtResultEvalError;
 use super::Env;
 use super::EvalContext;
 use super::EvalError;
 use super::Evaluable;
 
 impl Evaluable for Expr {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         ctx.cost_accum.add_cost_of(self)?;
-        match self {
+        let res = match self {
             Expr::Const(c) => Ok(Value::from(c.v.clone())),
-            Expr::SubstConstants(op) => op.eval(env, ctx),
-            Expr::ByteArrayToLong(op) => op.eval(env, ctx),
-            Expr::ByteArrayToBigInt(op) => op.eval(env, ctx),
+            Expr::SubstConstants(op) => op.expr().eval(env, ctx),
+            Expr::ByteArrayToLong(op) => op.expr().eval(env, ctx),
+            Expr::ByteArrayToBigInt(op) => op.expr().eval(env, ctx),
             Expr::LongToByteArray(op) => op.eval(env, ctx),
             Expr::CalcBlake2b256(op) => op.eval(env, ctx),
             Expr::CalcSha256(op) => op.eval(env, ctx),
             Expr::Fold(op) => op.eval(env, ctx),
-            Expr::ExtractRegisterAs(op) => op.eval(env, ctx),
+            Expr::ExtractRegisterAs(op) => op.expr().eval(env, ctx),
             Expr::GlobalVars(op) => op.eval(env, ctx),
-            Expr::MethodCall(op) => op.eval(env, ctx),
-            Expr::ProperyCall(op) => op.eval(env, ctx),
-            Expr::BinOp(op) => op.eval(env, ctx),
+            Expr::MethodCall(op) => op.expr().eval(env, ctx),
+            Expr::PropertyCall(op) => op.expr().eval(env, ctx),
+            Expr::BinOp(op) => op.expr().eval(env, ctx),
             Expr::Global => Ok(Value::Global),
             Expr::Context => Ok(Value::Context),
-            Expr::OptionGet(v) => v.eval(env, ctx),
+            Expr::OptionGet(v) => v.expr().eval(env, ctx),
             Expr::Apply(op) => op.eval(env, ctx),
             Expr::FuncValue(op) => op.eval(env, ctx),
             Expr::ValUse(op) => op.eval(env, ctx),
-            Expr::BlockValue(op) => op.eval(env, ctx),
+            Expr::BlockValue(op) => op.expr().eval(env, ctx),
             Expr::SelectField(op) => op.eval(env, ctx),
             Expr::ExtractAmount(op) => op.eval(env, ctx),
             Expr::ConstPlaceholder(_) => Err(EvalError::UnexpectedExpr(
@@ -37,7 +41,7 @@ impl Evaluable for Expr {
             )),
             Expr::Collection(op) => op.eval(env, ctx),
             Expr::ValDef(_) => Err(EvalError::UnexpectedExpr(
-                ("ValDef is evaluated in BlockValue").to_string(),
+                ("ValDef should be evaluated in BlockValue").to_string(),
             )),
             Expr::And(op) => op.eval(env, ctx),
             Expr::Or(op) => op.eval(env, ctx),
@@ -50,8 +54,8 @@ impl Evaluable for Expr {
             Expr::Upcast(op) => op.eval(env, ctx),
             Expr::Downcast(op) => op.eval(env, ctx),
             Expr::If(op) => op.eval(env, ctx),
-            Expr::Append(op) => op.eval(env, ctx),
-            Expr::ByIndex(op) => op.eval(env, ctx),
+            Expr::Append(op) => op.expr().eval(env, ctx),
+            Expr::ByIndex(op) => op.expr().eval(env, ctx),
             Expr::ExtractScriptBytes(op) => op.eval(env, ctx),
             Expr::SizeOf(op) => op.eval(env, ctx),
             Expr::Slice(op) => op.eval(env, ctx),
@@ -61,9 +65,9 @@ impl Evaluable for Expr {
             Expr::Exists(op) => op.eval(env, ctx),
             Expr::ExtractId(op) => op.eval(env, ctx),
             Expr::SigmaPropBytes(op) => op.eval(env, ctx),
-            Expr::OptionIsDefined(op) => op.eval(env, ctx),
-            Expr::OptionGetOrElse(op) => op.eval(env, ctx),
-            Expr::Negation(op) => op.eval(env, ctx),
+            Expr::OptionIsDefined(op) => op.expr().eval(env, ctx),
+            Expr::OptionGetOrElse(op) => op.expr().eval(env, ctx),
+            Expr::Negation(op) => op.expr().eval(env, ctx),
             Expr::BitInversion(op) => op.eval(env, ctx),
             Expr::ForAll(op) => op.eval(env, ctx),
             Expr::Tuple(op) => op.eval(env, ctx),
@@ -80,6 +84,13 @@ impl Evaluable for Expr {
             Expr::ExtractBytesWithNoRef(op) => op.eval(env, ctx),
             Expr::TreeLookup(op) => op.eval(env, ctx),
             Expr::CreateAvlTree(op) => op.eval(env, ctx),
-        }
+        };
+        res.enrich_err(self.span(), env.clone())
+    }
+}
+
+impl<T: Evaluable> Evaluable for Spanned<T> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+        self.expr.eval(env, ctx)
     }
 }
diff --git a/ergotree-interpreter/src/eval/extract_amount.rs b/ergotree-interpreter/src/eval/extract_amount.rs
index 196ad030e..25c4e3cce 100644
--- a/ergotree-interpreter/src/eval/extract_amount.rs
+++ b/ergotree-interpreter/src/eval/extract_amount.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ExtractAmount {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::CBox(b) => Ok(Value::Long(b.value.as_i64())),
diff --git a/ergotree-interpreter/src/eval/extract_bytes.rs b/ergotree-interpreter/src/eval/extract_bytes.rs
index 98af3d964..060fe203e 100644
--- a/ergotree-interpreter/src/eval/extract_bytes.rs
+++ b/ergotree-interpreter/src/eval/extract_bytes.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ExtractBytes {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::CBox(b) => Ok(b.sigma_serialize_bytes()?.into()),
diff --git a/ergotree-interpreter/src/eval/extract_bytes_with_no_ref.rs b/ergotree-interpreter/src/eval/extract_bytes_with_no_ref.rs
index bd30d2723..f97e8fd1e 100644
--- a/ergotree-interpreter/src/eval/extract_bytes_with_no_ref.rs
+++ b/ergotree-interpreter/src/eval/extract_bytes_with_no_ref.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ExtractBytesWithNoRef {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::CBox(b) => Ok(b.bytes_without_ref()?.into()),
diff --git a/ergotree-interpreter/src/eval/extract_creation_info.rs b/ergotree-interpreter/src/eval/extract_creation_info.rs
index bda52b36c..0cebb3376 100644
--- a/ergotree-interpreter/src/eval/extract_creation_info.rs
+++ b/ergotree-interpreter/src/eval/extract_creation_info.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ExtractCreationInfo {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::CBox(b) => Ok(b.creation_info().into()),
diff --git a/ergotree-interpreter/src/eval/extract_id.rs b/ergotree-interpreter/src/eval/extract_id.rs
index 83571158a..ce7d9c18d 100644
--- a/ergotree-interpreter/src/eval/extract_id.rs
+++ b/ergotree-interpreter/src/eval/extract_id.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ExtractId {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::CBox(b) => {
diff --git a/ergotree-interpreter/src/eval/extract_reg_as.rs b/ergotree-interpreter/src/eval/extract_reg_as.rs
index a9bd6efa7..656220c63 100644
--- a/ergotree-interpreter/src/eval/extract_reg_as.rs
+++ b/ergotree-interpreter/src/eval/extract_reg_as.rs
@@ -12,18 +12,20 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ExtractRegisterAs {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let ir_box = self
             .input
             .eval(env, ctx)?
             .try_extract_into::<Arc<ErgoBox>>()?;
         let id = self.register_id.try_into().map_err(|e| {
-            EvalError::RegisterIdOutOfBounds(format!("register index is out of bounds: {:?} ", e))
+            EvalError::RegisterIdOutOfBounds(format!(
+                "register index {} is out of bounds: {:?} ",
+                self.register_id, e
+            ))
         })?;
         let reg_val_opt = ir_box.get_register(id).map_err(|e| {
             EvalError::NotFound(format!(
-                "Error getting the register id {:?} with error {e:?}",
-                id
+                "Error getting the register id {id} with error {e:?}"
             ))
         })?;
         Ok(Value::Opt(Box::new(reg_val_opt.map(|c| Value::from(c.v)))))
diff --git a/ergotree-interpreter/src/eval/extract_script_bytes.rs b/ergotree-interpreter/src/eval/extract_script_bytes.rs
index 745936e4f..71075ced4 100644
--- a/ergotree-interpreter/src/eval/extract_script_bytes.rs
+++ b/ergotree-interpreter/src/eval/extract_script_bytes.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ExtractScriptBytes {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::CBox(b) => Ok(b.script_bytes()?.into()),
diff --git a/ergotree-interpreter/src/eval/func_value.rs b/ergotree-interpreter/src/eval/func_value.rs
index f342bb1e6..1cf0d944a 100644
--- a/ergotree-interpreter/src/eval/func_value.rs
+++ b/ergotree-interpreter/src/eval/func_value.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for FuncValue {
-    fn eval(&self, _env: &Env, _ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, _env: &mut Env, _ctx: &mut EvalContext) -> Result<Value, EvalError> {
         Ok(Value::Lambda(Lambda {
             args: self.args().to_vec(),
             body: self.body().clone().into(),
diff --git a/ergotree-interpreter/src/eval/get_var.rs b/ergotree-interpreter/src/eval/get_var.rs
index 276742a51..1ac5e34e6 100644
--- a/ergotree-interpreter/src/eval/get_var.rs
+++ b/ergotree-interpreter/src/eval/get_var.rs
@@ -1,4 +1,3 @@
-// use ergotree_ir::mir::constant::TryExtractInto;
 use ergotree_ir::mir::constant::TryExtractFromError;
 use ergotree_ir::mir::get_var::GetVar;
 use ergotree_ir::mir::value::Value;
@@ -9,13 +8,13 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for GetVar {
-    fn eval(&self, _env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, _env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         match ctx.ctx.extension.values.get(&self.var_id) {
             None => Ok(Value::Opt(None.into())),
             Some(v) if v.tpe == self.var_tpe => Ok((Some(v.v.clone())).into()),
             Some(v) => Err(TryExtractFromError(format!(
-                "GetVar: expected {:?}, found {:?}",
-                self.var_tpe, v.tpe
+                "GetVar: expected extension value id {} to have type {:?}, found {:?} in context extension map {}",
+                self.var_id, self.var_tpe, v, ctx.ctx.extension
             ))
             .into()),
         }
diff --git a/ergotree-interpreter/src/eval/global_vars.rs b/ergotree-interpreter/src/eval/global_vars.rs
index 9f1371ee6..148c6e7de 100644
--- a/ergotree-interpreter/src/eval/global_vars.rs
+++ b/ergotree-interpreter/src/eval/global_vars.rs
@@ -8,7 +8,7 @@ use super::EvalError;
 use super::Evaluable;
 
 impl Evaluable for GlobalVars {
-    fn eval(&self, _env: &Env, ectx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, _env: &mut Env, ectx: &mut EvalContext) -> Result<Value, EvalError> {
         match self {
             GlobalVars::Height => Ok((ectx.ctx.height as i32).into()),
             GlobalVars::SelfBox => Ok(ectx.ctx.self_box.clone().into()),
diff --git a/ergotree-interpreter/src/eval/if_op.rs b/ergotree-interpreter/src/eval/if_op.rs
index 0795c7674..fce7a6ede 100644
--- a/ergotree-interpreter/src/eval/if_op.rs
+++ b/ergotree-interpreter/src/eval/if_op.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for If {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let condition_v = self.condition.eval(env, ctx)?;
         if condition_v.try_extract_into::<bool>()? {
             self.true_branch.eval(env, ctx)
diff --git a/ergotree-interpreter/src/eval/logical_not.rs b/ergotree-interpreter/src/eval/logical_not.rs
index 9d9829b13..2e2c8ec62 100644
--- a/ergotree-interpreter/src/eval/logical_not.rs
+++ b/ergotree-interpreter/src/eval/logical_not.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for LogicalNot {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let input_v_bool = input_v.try_extract_into::<bool>()?;
         Ok((!input_v_bool).into())
diff --git a/ergotree-interpreter/src/eval/long_to_byte_array.rs b/ergotree-interpreter/src/eval/long_to_byte_array.rs
index cba7a4a98..745e0ff3e 100644
--- a/ergotree-interpreter/src/eval/long_to_byte_array.rs
+++ b/ergotree-interpreter/src/eval/long_to_byte_array.rs
@@ -8,7 +8,7 @@ use crate::eval::Evaluable;
 use ergotree_ir::mir::constant::TryExtractInto;
 
 impl Evaluable for LongToByteArray {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let mut val = self.input.eval(env, ctx)?.try_extract_into::<i64>()?;
         let mut buf = vec![42_i8; 8];
         for i in (0..8).rev() {
diff --git a/ergotree-interpreter/src/eval/method_call.rs b/ergotree-interpreter/src/eval/method_call.rs
index 5791db03f..fe6ac8174 100644
--- a/ergotree-interpreter/src/eval/method_call.rs
+++ b/ergotree-interpreter/src/eval/method_call.rs
@@ -8,7 +8,7 @@ use super::EvalError;
 use super::Evaluable;
 
 impl Evaluable for MethodCall {
-    fn eval(&self, env: &Env, ectx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ectx: &mut EvalContext) -> Result<Value, EvalError> {
         let ov = self.obj.eval(env, ectx)?;
         let argsv: Result<Vec<Value>, EvalError> =
             self.args.iter().map(|arg| arg.eval(env, ectx)).collect();
diff --git a/ergotree-interpreter/src/eval/multiply_group.rs b/ergotree-interpreter/src/eval/multiply_group.rs
index de6d8a732..aa1973f52 100644
--- a/ergotree-interpreter/src/eval/multiply_group.rs
+++ b/ergotree-interpreter/src/eval/multiply_group.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for MultiplyGroup {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let left_v = self.left.eval(env, ctx)?;
         let right_v = self.right.eval(env, ctx)?;
 
diff --git a/ergotree-interpreter/src/eval/negation.rs b/ergotree-interpreter/src/eval/negation.rs
index dc58a609b..5a6b15129 100644
--- a/ergotree-interpreter/src/eval/negation.rs
+++ b/ergotree-interpreter/src/eval/negation.rs
@@ -8,7 +8,7 @@ use crate::eval::Evaluable;
 use num_traits::CheckedNeg;
 
 impl Evaluable for Negation {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
 
         fn overflow_err<T: std::fmt::Display>(v: &T) -> EvalError {
diff --git a/ergotree-interpreter/src/eval/option_get.rs b/ergotree-interpreter/src/eval/option_get.rs
index d844887bf..bcbace334 100644
--- a/ergotree-interpreter/src/eval/option_get.rs
+++ b/ergotree-interpreter/src/eval/option_get.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for OptionGet {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let v = self.input.eval(env, ctx)?;
         match v {
             Value::Opt(opt_v) => {
diff --git a/ergotree-interpreter/src/eval/option_get_or_else.rs b/ergotree-interpreter/src/eval/option_get_or_else.rs
index c29844eff..5e307518a 100644
--- a/ergotree-interpreter/src/eval/option_get_or_else.rs
+++ b/ergotree-interpreter/src/eval/option_get_or_else.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for OptionGetOrElse {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let v = self.input.eval(env, ctx)?;
         let default_v = self.default.eval(env, ctx)?;
         match v {
diff --git a/ergotree-interpreter/src/eval/option_is_defined.rs b/ergotree-interpreter/src/eval/option_is_defined.rs
index a85bc6969..814667ab9 100644
--- a/ergotree-interpreter/src/eval/option_is_defined.rs
+++ b/ergotree-interpreter/src/eval/option_is_defined.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for OptionIsDefined {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let v = self.input.eval(env, ctx)?;
         match v {
             Value::Opt(opt_v) => Ok(opt_v.is_some().into()),
diff --git a/ergotree-interpreter/src/eval/or.rs b/ergotree-interpreter/src/eval/or.rs
index 8e212e06d..561fb4dc7 100644
--- a/ergotree-interpreter/src/eval/or.rs
+++ b/ergotree-interpreter/src/eval/or.rs
@@ -8,7 +8,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Or {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let input_v_bools = input_v.try_extract_into::<Vec<bool>>()?;
         Ok(input_v_bools.iter().any(|b| *b).into())
diff --git a/ergotree-interpreter/src/eval/property_call.rs b/ergotree-interpreter/src/eval/property_call.rs
index c36479b25..79996caa3 100644
--- a/ergotree-interpreter/src/eval/property_call.rs
+++ b/ergotree-interpreter/src/eval/property_call.rs
@@ -8,7 +8,7 @@ use super::EvalError;
 use super::Evaluable;
 
 impl Evaluable for PropertyCall {
-    fn eval(&self, env: &Env, ectx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ectx: &mut EvalContext) -> Result<Value, EvalError> {
         let ov = self.obj.eval(env, ectx)?;
         smethod_eval_fn(&self.method)?(env, ectx, ov, vec![])
     }
diff --git a/ergotree-interpreter/src/eval/sbox.rs b/ergotree-interpreter/src/eval/sbox.rs
index aa28cf416..26d130584 100644
--- a/ergotree-interpreter/src/eval/sbox.rs
+++ b/ergotree-interpreter/src/eval/sbox.rs
@@ -20,19 +20,20 @@ pub(crate) static GET_REG_EVAL_FN: EvalFn = |_env, _ctx, obj, args| {
         .get(0)
         .cloned()
         .ok_or_else(|| EvalError::NotFound("register index is missing".to_string()))?
-        .try_extract_into::<i8>()?
-        .try_into()
-        .map_err(|e| {
-            EvalError::RegisterIdOutOfBounds(format!("register index is out of bounds: {:?} ", e))
-        })?;
+        .try_extract_into::<i8>()?;
+    let reg_id = reg_id.try_into().map_err(|e| {
+        EvalError::RegisterIdOutOfBounds(format!(
+            "register index {reg_id} is out of bounds: {:?} ",
+            e
+        ))
+    })?;
 
     Ok(Value::Opt(Box::new(
         obj.try_extract_into::<Arc<ErgoBox>>()?
             .get_register(reg_id)
             .map_err(|e| {
                 EvalError::NotFound(format!(
-                    "Error getting the register id {:?} with error {e:?}",
-                    reg_id
+                    "Error getting the register id {reg_id} with error {e:?}"
                 ))
             })?
             .map(|c| Value::from(c.v)),
diff --git a/ergotree-interpreter/src/eval/scoll.rs b/ergotree-interpreter/src/eval/scoll.rs
index 8256e5657..4e1f0c625 100644
--- a/ergotree-interpreter/src/eval/scoll.rs
+++ b/ergotree-interpreter/src/eval/scoll.rs
@@ -60,7 +60,7 @@ pub(crate) static FLATMAP_EVAL_FN: EvalFn = |env, ctx, obj, args| {
         "unsupported lambda in flatMap: allowed usage `xs.flatMap(x => x.property)".to_string();
     match &*lambda.body {
         Expr::MethodCall(mc) => {
-            if !mc.args.is_empty() {
+            if !mc.expr().args.is_empty() {
                 return Err(EvalError::UnexpectedValue(unsupported_msg));
             }
         }
@@ -75,8 +75,8 @@ pub(crate) static FLATMAP_EVAL_FN: EvalFn = |env, ctx, obj, args| {
         let func_arg = lambda.args.first().ok_or_else(|| {
             EvalError::NotFound("flatmap: lambda has empty arguments list".to_string())
         })?;
-        let env1 = env.clone().extend(func_arg.idx, arg);
-        lambda.body.eval(&env1, ctx)
+        env.insert(func_arg.idx, arg);
+        lambda.body.eval(env, ctx)
     };
     let mapper_input_tpe = lambda
         .args
diff --git a/ergotree-interpreter/src/eval/select_field.rs b/ergotree-interpreter/src/eval/select_field.rs
index c5aea0a92..9bb4ebbf9 100644
--- a/ergotree-interpreter/src/eval/select_field.rs
+++ b/ergotree-interpreter/src/eval/select_field.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for SelectField {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::Tup(items) => items
diff --git a/ergotree-interpreter/src/eval/sigma_and.rs b/ergotree-interpreter/src/eval/sigma_and.rs
index 7912d6c7b..de9437520 100644
--- a/ergotree-interpreter/src/eval/sigma_and.rs
+++ b/ergotree-interpreter/src/eval/sigma_and.rs
@@ -10,7 +10,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for SigmaAnd {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let items_v_res = self.items.try_mapped_ref(|it| it.eval(env, ctx));
         let items_sigmabool = items_v_res?
             .try_mapped(|it| it.try_extract_into::<SigmaProp>())?
diff --git a/ergotree-interpreter/src/eval/sigma_or.rs b/ergotree-interpreter/src/eval/sigma_or.rs
index 1ef93782e..b2324e06a 100644
--- a/ergotree-interpreter/src/eval/sigma_or.rs
+++ b/ergotree-interpreter/src/eval/sigma_or.rs
@@ -10,7 +10,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for SigmaOr {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let items_v_res = self.items.try_mapped_ref(|it| it.eval(env, ctx));
         let items_sigmabool = items_v_res?
             .try_mapped(|it| it.try_extract_into::<SigmaProp>())?
diff --git a/ergotree-interpreter/src/eval/sigma_prop_bytes.rs b/ergotree-interpreter/src/eval/sigma_prop_bytes.rs
index 09a395b07..11cbde0ed 100644
--- a/ergotree-interpreter/src/eval/sigma_prop_bytes.rs
+++ b/ergotree-interpreter/src/eval/sigma_prop_bytes.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for SigmaPropBytes {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match input_v {
             Value::SigmaProp(sigma_prop) => Ok(sigma_prop.prop_bytes()?.into()),
diff --git a/ergotree-interpreter/src/eval/soption.rs b/ergotree-interpreter/src/eval/soption.rs
index e2c4932fb..f981f1444 100644
--- a/ergotree-interpreter/src/eval/soption.rs
+++ b/ergotree-interpreter/src/eval/soption.rs
@@ -23,8 +23,8 @@ pub(crate) static MAP_EVAL_FN: EvalFn = |env, ctx, obj, args| {
         let func_arg = lambda.args.first().ok_or_else(|| {
             EvalError::NotFound("map: lambda has empty arguments list".to_string())
         })?;
-        let env1 = env.clone().extend(func_arg.idx, arg);
-        lambda.body.eval(&env1, ctx)
+        env.insert(func_arg.idx, arg);
+        lambda.body.eval(env, ctx)
     };
     let normalized_input_val: Option<Value> = match input_v {
         Value::Opt(opt) => Ok(*opt),
@@ -58,8 +58,8 @@ pub(crate) static FILTER_EVAL_FN: EvalFn = |env, ctx, obj, args| {
         let func_arg = lambda.args.first().ok_or_else(|| {
             EvalError::NotFound("filter: lambda has empty arguments list".to_string())
         })?;
-        let env1 = env.clone().extend(func_arg.idx, arg);
-        lambda.body.eval(&env1, ctx)
+        env.insert(func_arg.idx, arg);
+        lambda.body.eval(env, ctx)
     };
     let normalized_input_val: Option<Value> = match input_v {
         Value::Opt(opt) => Ok(*opt),
diff --git a/ergotree-interpreter/src/eval/subst_const.rs b/ergotree-interpreter/src/eval/subst_const.rs
index f99e52481..c92943f1f 100644
--- a/ergotree-interpreter/src/eval/subst_const.rs
+++ b/ergotree-interpreter/src/eval/subst_const.rs
@@ -15,7 +15,7 @@ use sigma_util::AsVecU8;
 use std::convert::TryFrom;
 
 impl Evaluable for SubstConstants {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let script_bytes_v = self.script_bytes.eval(env, ctx)?;
         let positions_v = self.positions.eval(env, ctx)?;
         let new_values_v = self.new_values.eval(env, ctx)?;
@@ -131,11 +131,14 @@ mod tests {
         let positions = Expr::Const(Constant::from(vec![0])).into();
         let new_values = Expr::Const(Constant::from(vec![new.clone()])).into();
 
-        let subst_const = Expr::SubstConstants(SubstConstants {
-            script_bytes,
-            positions,
-            new_values,
-        });
+        let subst_const = Expr::SubstConstants(
+            SubstConstants {
+                script_bytes,
+                positions,
+                new_values,
+            }
+            .into(),
+        );
 
         let x: Value = try_eval_out_wo_ctx(&subst_const).unwrap();
         if let Value::Coll(CollKind::NativeColl(NativeColl::CollByte(b))) = x {
@@ -150,15 +153,21 @@ mod tests {
     fn test_3_substitutions(original: (i32, i32, i32), new: (i32, i32, i32)) {
         let (o0, o1, o2) = original;
         let (n0, n1, n2) = new;
-        let expr = Expr::BinOp(BinOp {
-            kind: BinOpKind::Arith(ArithOp::Plus),
-            left: Box::new(Expr::Const(o0.into())),
-            right: Box::new(Expr::BinOp(BinOp {
-                kind: BinOpKind::Arith(ArithOp::Multiply),
-                left: Box::new(Expr::Const(o1.into())),
-                right: Box::new(Expr::Const(o2.into())),
-            })),
-        });
+        let expr = Expr::BinOp(
+            BinOp {
+                kind: BinOpKind::Arith(ArithOp::Plus),
+                left: Box::new(Expr::Const(o0.into())),
+                right: Box::new(Expr::BinOp(
+                    BinOp {
+                        kind: BinOpKind::Arith(ArithOp::Multiply),
+                        left: Box::new(Expr::Const(o1.into())),
+                        right: Box::new(Expr::Const(o2.into())),
+                    }
+                    .into(),
+                )),
+            }
+            .into(),
+        );
         let ergo_tree = ErgoTree::new(ErgoTreeHeader::v0(true), &expr).unwrap();
         assert_eq!(ergo_tree.constants_len().unwrap(), 3);
         assert_eq!(ergo_tree.get_constant(0).unwrap().unwrap(), o0.into());
@@ -172,11 +181,14 @@ mod tests {
 
         let new_values = Expr::Const(Constant::from(vec![n0, n1, n2])).into();
 
-        let subst_const = Expr::SubstConstants(SubstConstants {
-            script_bytes,
-            positions,
-            new_values,
-        });
+        let subst_const = Expr::SubstConstants(
+            SubstConstants {
+                script_bytes,
+                positions,
+                new_values,
+            }
+            .into(),
+        );
 
         let x: Value = try_eval_out_wo_ctx(&subst_const).unwrap();
         if let Value::Coll(CollKind::NativeColl(NativeColl::CollByte(b))) = x {
diff --git a/ergotree-interpreter/src/eval/tree_lookup.rs b/ergotree-interpreter/src/eval/tree_lookup.rs
index 379dfefb9..b4a8e7ae3 100644
--- a/ergotree-interpreter/src/eval/tree_lookup.rs
+++ b/ergotree-interpreter/src/eval/tree_lookup.rs
@@ -14,7 +14,7 @@ use scorex_crypto_avltree::operation::Operation;
 use sigma_util::AsVecU8;
 
 impl Evaluable for TreeLookup {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let normalized_tree_val = self
             .tree
             .eval(env, ctx)?
diff --git a/ergotree-interpreter/src/eval/tuple.rs b/ergotree-interpreter/src/eval/tuple.rs
index 0a1ea5261..7ed505b22 100644
--- a/ergotree-interpreter/src/eval/tuple.rs
+++ b/ergotree-interpreter/src/eval/tuple.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for Tuple {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let items_v = self.items.try_mapped_ref(|i| i.eval(env, ctx));
         Ok(Value::Tup(items_v?))
     }
diff --git a/ergotree-interpreter/src/eval/upcast.rs b/ergotree-interpreter/src/eval/upcast.rs
index 878e0a66f..647cdf1f1 100644
--- a/ergotree-interpreter/src/eval/upcast.rs
+++ b/ergotree-interpreter/src/eval/upcast.rs
@@ -69,7 +69,7 @@ fn upcast_to_byte(in_v: Value) -> Result<Value, EvalError> {
 }
 
 impl Evaluable for Upcast {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         match self.tpe {
             SType::SBigInt => upcast_to_bigint(input_v),
diff --git a/ergotree-interpreter/src/eval/val_use.rs b/ergotree-interpreter/src/eval/val_use.rs
index fa647df5a..53ddfa9ae 100644
--- a/ergotree-interpreter/src/eval/val_use.rs
+++ b/ergotree-interpreter/src/eval/val_use.rs
@@ -7,7 +7,7 @@ use crate::eval::EvalError;
 use crate::eval::Evaluable;
 
 impl Evaluable for ValUse {
-    fn eval(&self, env: &Env, _ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, _ctx: &mut EvalContext) -> Result<Value, EvalError> {
         env.get(self.val_id).cloned().ok_or_else(|| {
             EvalError::NotFound(format!("no value in env for id: {0:?}", self.val_id))
         })
diff --git a/ergotree-interpreter/src/eval/xor.rs b/ergotree-interpreter/src/eval/xor.rs
index 88a51ef84..254c3e208 100644
--- a/ergotree-interpreter/src/eval/xor.rs
+++ b/ergotree-interpreter/src/eval/xor.rs
@@ -14,7 +14,7 @@ fn helper_xor(mut x: Vec<i8>, y: Vec<i8>) -> Vec<i8> {
 }
 
 impl Evaluable for Xor {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let left_v = self.left.eval(env, ctx)?;
         let right_v = self.right.eval(env, ctx)?;
 
diff --git a/ergotree-interpreter/src/eval/xor_of.rs b/ergotree-interpreter/src/eval/xor_of.rs
index 028122074..8e7ca385c 100644
--- a/ergotree-interpreter/src/eval/xor_of.rs
+++ b/ergotree-interpreter/src/eval/xor_of.rs
@@ -8,7 +8,7 @@ use crate::eval::Evaluable;
 use ergotree_ir::mir::xor_of::XorOf;
 
 impl Evaluable for XorOf {
-    fn eval(&self, env: &Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
+    fn eval(&self, env: &mut Env, ctx: &mut EvalContext) -> Result<Value, EvalError> {
         let input_v = self.input.eval(env, ctx)?;
         let input_v_bools = input_v.try_extract_into::<Vec<bool>>()?;
         Ok(input_v_bools.into_iter().fold(false, |a, b| a ^ b).into())
diff --git a/ergotree-interpreter/src/sigma_protocol/prover.rs b/ergotree-interpreter/src/sigma_protocol/prover.rs
index 61a9b5324..3f016b5e9 100644
--- a/ergotree-interpreter/src/sigma_protocol/prover.rs
+++ b/ergotree-interpreter/src/sigma_protocol/prover.rs
@@ -132,7 +132,7 @@ pub trait Prover {
     /// The comments in this section are taken from the algorithm for the
     /// Sigma-protocol prover as described in the ErgoScript white-paper
     /// <https://ergoplatform.org/docs/ErgoScript.pdf>, Appendix A
-    ///  
+    ///
     /// Generate proofs for the given message for ErgoTree reduced to Sigma boolean expression
     fn prove(
         &self,
@@ -145,7 +145,6 @@ pub trait Prover {
         let expr = tree.proposition()?;
         let ctx_ext = ctx.extension.clone();
         let reduction_result = reduce_to_crypto(&expr, env, ctx).map_err(ProverError::EvalError)?;
-
         self.generate_proof(reduction_result.sigma_prop, message, hints_bag)
             .map(|p| ProverResult {
                 proof: p,
diff --git a/ergotree-interpreter/src/sigma_protocol/prover/context_extension.rs b/ergotree-interpreter/src/sigma_protocol/prover/context_extension.rs
index 09aed3e41..ed245147f 100644
--- a/ergotree-interpreter/src/sigma_protocol/prover/context_extension.rs
+++ b/ergotree-interpreter/src/sigma_protocol/prover/context_extension.rs
@@ -7,6 +7,7 @@ use ergotree_ir::serialization::SigmaSerializable;
 use ergotree_ir::serialization::SigmaSerializeResult;
 use indexmap::IndexMap;
 use std::convert::TryFrom;
+use std::fmt;
 use thiserror::Error;
 
 /// User-defined variables to be put into context
@@ -25,6 +26,12 @@ impl ContextExtension {
     }
 }
 
+impl fmt::Display for ContextExtension {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_map().entries(self.values.iter()).finish()
+    }
+}
+
 impl SigmaSerializable for ContextExtension {
     fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
         w.put_u8(self.values.len() as u8)?;
diff --git a/ergotree-ir/Cargo.toml b/ergotree-ir/Cargo.toml
index 7711bdaf5..7ce35189d 100644
--- a/ergotree-ir/Cargo.toml
+++ b/ergotree-ir/Cargo.toml
@@ -6,9 +6,7 @@ authors = ["Denys Zadorozhnyi <denys@zadorozhnyi.com>"]
 repository.workspace = true
 edition.workspace = true
 description = "ErgoTree IR, serialization"
-exclude = [
-    "proptest-regressions/*"
-]
+exclude = ["proptest-regressions/*"]
 
 [lib]
 crate-type = ["cdylib", "rlib"]
@@ -22,7 +20,7 @@ elliptic-curve = { workspace = true }
 thiserror = { workspace = true }
 lazy_static = { workspace = true }
 derive_more = { workspace = true }
-proptest = { workspace = true , optional = true }
+proptest = { workspace = true, optional = true }
 proptest-derive = { workspace = true, optional = true }
 bs58 = { workspace = true }
 base16 = { workspace = true }
@@ -32,13 +30,14 @@ num-traits = { workspace = true }
 num-derive = { workspace = true }
 num-integer = { workspace = true }
 indexmap = { workspace = true }
-serde = { workspace = true , optional = true }
+serde = { workspace = true, optional = true }
 serde_json = { workspace = true, optional = true }
 serde_with = { workspace = true, optional = true }
 num256 = "0.3.1"
 impl-trait-for-tuples = "0.2.0"
 strum = "0.21"
 strum_macros = "0.21"
+miette = { workspace = true }
 
 [features]
 default = ["json"]
@@ -49,3 +48,4 @@ json = ["serde", "serde_json", "serde_with", "bounded-vec/serde"]
 sigma-test-util = { workspace = true }
 rand = { workspace = true }
 pretty_assertions = { workspace = true }
+expect-test = { workspace = true }
diff --git a/ergotree-ir/src/chain/address.rs b/ergotree-ir/src/chain/address.rs
index ebd2dcdb2..43e2b9b5f 100644
--- a/ergotree-ir/src/chain/address.rs
+++ b/ergotree-ir/src/chain/address.rs
@@ -22,6 +22,7 @@ use crate::sigma_protocol::sigma_boolean::ProveDlog;
 use crate::sigma_protocol::sigma_boolean::SigmaBoolean;
 use crate::sigma_protocol::sigma_boolean::SigmaProofOfKnowledgeTree;
 use crate::sigma_protocol::sigma_boolean::SigmaProp;
+use crate::source_span::Spanned;
 use crate::types::stype::SType;
 use ergo_chain_types::EcPoint;
 
@@ -110,12 +111,21 @@ impl Address {
                     if let [Expr::BoolToSigmaProp(BoolToSigmaProp { input }), Expr::DeserializeContext(DeserializeContext { tpe, id })] =
                         items.as_slice()
                     {
-                        if let (Expr::BinOp(BinOp { kind, left, right }), SType::SSigmaProp, 1) =
-                            (*input.clone(), tpe.clone(), id)
+                        if let (
+                            Expr::BinOp(Spanned {
+                                source_span: _,
+                                expr: BinOp { kind, left, right },
+                            }),
+                            SType::SSigmaProp,
+                            1,
+                        ) = (*input.clone(), tpe.clone(), id)
                         {
                             if let (
                                 Relation(RelationOp::Eq),
-                                Expr::Slice(Slice { input, from, until }),
+                                Expr::Slice(Spanned {
+                                    expr: Slice { input, from, until },
+                                    source_span: _,
+                                }),
                                 Expr::Const(Constant { v, .. }),
                             ) = (kind, *left, *right)
                             {
@@ -200,23 +210,32 @@ impl Address {
             }
             Address::P2S(bytes) => ErgoTree::sigma_parse_bytes(bytes),
             Address::P2SH(script_hash) => {
-                let get_var_expr = Expr::GetVar(GetVar {
-                    var_id: 1,
-                    var_tpe: SType::SColl(Box::new(SType::SByte)),
-                });
+                let get_var_expr = Expr::GetVar(
+                    GetVar {
+                        var_id: 1,
+                        var_tpe: SType::SColl(Box::new(SType::SByte)),
+                    }
+                    .into(),
+                );
                 let hash_expr = Expr::CalcBlake2b256(CalcBlake2b256 {
                     input: Box::new(get_var_expr),
                 });
-                let slice_expr = Expr::Slice(Slice {
-                    input: Box::new(hash_expr),
-                    from: Box::new(0i32.into()),
-                    until: Box::new(24i32.into()),
-                });
-                let hash_equals = Expr::BinOp(BinOp {
-                    kind: Relation(RelationOp::Eq),
-                    left: Box::new(slice_expr),
-                    right: Box::new(Expr::Const(Constant::from(script_hash.to_vec()))),
-                });
+                let slice_expr = Expr::Slice(
+                    Slice {
+                        input: Box::new(hash_expr),
+                        from: Box::new(0i32.into()),
+                        until: Box::new(24i32.into()),
+                    }
+                    .into(),
+                );
+                let hash_equals = Expr::BinOp(
+                    BinOp {
+                        kind: Relation(RelationOp::Eq),
+                        left: Box::new(slice_expr),
+                        right: Box::new(Expr::Const(Constant::from(script_hash.to_vec()))),
+                    }
+                    .into(),
+                );
                 let script_is_correct = Expr::DeserializeContext(DeserializeContext {
                     tpe: SType::SSigmaProp,
                     id: 1,
diff --git a/ergotree-ir/src/chain/ergo_box/register/id.rs b/ergotree-ir/src/chain/ergo_box/register/id.rs
index 6fefa624e..65355bdb4 100644
--- a/ergotree-ir/src/chain/ergo_box/register/id.rs
+++ b/ergotree-ir/src/chain/ergo_box/register/id.rs
@@ -1,3 +1,5 @@
+use std::fmt::Display;
+
 use derive_more::From;
 use thiserror::Error;
 
@@ -52,8 +54,17 @@ impl TryFrom<u8> for RegisterId {
     }
 }
 
+impl Display for RegisterId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            RegisterId::MandatoryRegisterId(id) => write!(f, "{}", id),
+            RegisterId::NonMandatoryRegisterId(id) => write!(f, "{}", id),
+        }
+    }
+}
+
 /// Register ids that every box have (box properties exposed as registers)
-#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+#[derive(PartialEq, Eq, Debug, Clone, Copy, derive_more::Display)]
 pub enum MandatoryRegisterId {
     /// Monetary value, in Ergo tokens
     R0 = 0,
@@ -80,7 +91,7 @@ impl TryFrom<i8> for MandatoryRegisterId {
 }
 
 /// newtype for additional registers R4 - R9
-#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
+#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, derive_more::Display)]
 #[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "json", serde(into = "String", try_from = "String"))]
 #[repr(u8)]
diff --git a/ergotree-ir/src/lib.rs b/ergotree-ir/src/lib.rs
index 9f81748ec..b30bff3ae 100644
--- a/ergotree-ir/src/lib.rs
+++ b/ergotree-ir/src/lib.rs
@@ -26,8 +26,10 @@ pub mod bigint256;
 pub mod chain;
 pub mod ergo_tree;
 pub mod mir;
+pub mod pretty_printer;
 pub mod serialization;
 pub mod sigma_protocol;
+pub mod source_span;
 pub mod type_check;
 pub mod types;
 pub mod util;
diff --git a/ergotree-ir/src/mir/bin_op.rs b/ergotree-ir/src/mir/bin_op.rs
index 102cf9b25..cc82e4d8c 100644
--- a/ergotree-ir/src/mir/bin_op.rs
+++ b/ergotree-ir/src/mir/bin_op.rs
@@ -1,5 +1,7 @@
 //! Operators in ErgoTree
 
+use std::fmt::Display;
+
 use super::expr::Expr;
 use crate::has_opcode::HasOpCode;
 use crate::serialization::op_code::OpCode;
@@ -45,6 +47,20 @@ impl From<ArithOp> for OpCode {
     }
 }
 
+impl Display for ArithOp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ArithOp::Plus => write!(f, "+"),
+            ArithOp::Minus => write!(f, "-"),
+            ArithOp::Multiply => write!(f, "*"),
+            ArithOp::Divide => write!(f, "/"),
+            ArithOp::Max => write!(f, "max"),
+            ArithOp::Min => write!(f, "min"),
+            ArithOp::Modulo => write!(f, "%"),
+        }
+    }
+}
+
 /// Relational operations
 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
@@ -76,6 +92,19 @@ impl From<RelationOp> for OpCode {
     }
 }
 
+impl Display for RelationOp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            RelationOp::Eq => write!(f, "=="),
+            RelationOp::NEq => write!(f, "!="),
+            RelationOp::Ge => write!(f, ">="),
+            RelationOp::Gt => write!(f, ">"),
+            RelationOp::Le => write!(f, "<="),
+            RelationOp::Lt => write!(f, "<"),
+        }
+    }
+}
+
 /// Logical operations
 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
@@ -98,6 +127,16 @@ impl From<LogicalOp> for OpCode {
     }
 }
 
+impl Display for LogicalOp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            LogicalOp::And => write!(f, "&&"),
+            LogicalOp::Or => write!(f, "||"),
+            LogicalOp::Xor => write!(f, "^"),
+        }
+    }
+}
+
 /// Bitwise operations
 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
@@ -120,6 +159,16 @@ impl From<BitOp> for OpCode {
     }
 }
 
+impl Display for BitOp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            BitOp::BitOr => write!(f, "|"),
+            BitOp::BitAnd => write!(f, "&"),
+            BitOp::BitXor => write!(f, "^"),
+        }
+    }
+}
+
 /// Binary operations
 #[derive(PartialEq, Eq, Debug, Clone, Copy, From)]
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
@@ -145,6 +194,17 @@ impl From<BinOpKind> for OpCode {
     }
 }
 
+impl Display for BinOpKind {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            BinOpKind::Arith(op) => write!(f, "{}", op),
+            BinOpKind::Relation(op) => write!(f, "{}", op),
+            BinOpKind::Logical(op) => write!(f, "{}", op),
+            BinOpKind::Bit(op) => write!(f, "{}", op),
+        }
+    }
+}
+
 /// Binary operation
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct BinOp {
@@ -292,19 +352,22 @@ mod tests {
         let e = Expr::sigma_parse_bytes(&[0xed, 0x85, 0x03]);
         assert_eq!(
             e,
-            Ok(Expr::BinOp(BinOp {
-                kind: BinOpKind::Logical(LogicalOp::And,),
-                left: Expr::Const(Constant {
-                    tpe: SType::SBoolean,
-                    v: Boolean(true),
-                })
-                .into(),
-                right: Expr::Const(Constant {
-                    tpe: SType::SBoolean,
-                    v: Boolean(true),
-                })
-                .into(),
-            }))
+            Ok(Expr::BinOp(
+                BinOp {
+                    kind: BinOpKind::Logical(LogicalOp::And,),
+                    left: Expr::Const(Constant {
+                        tpe: SType::SBoolean,
+                        v: Boolean(true),
+                    })
+                    .into(),
+                    right: Expr::Const(Constant {
+                        tpe: SType::SBoolean,
+                        v: Boolean(true),
+                    })
+                    .into(),
+                }
+                .into()
+            ))
         );
     }
 }
diff --git a/ergotree-ir/src/mir/block.rs b/ergotree-ir/src/mir/block.rs
index e7870f8a6..186771970 100644
--- a/ergotree-ir/src/mir/block.rs
+++ b/ergotree-ir/src/mir/block.rs
@@ -87,7 +87,7 @@ pub mod tests {
 
         #[test]
         fn ser_roundtrip(block in any::<BlockValue>()) {
-            let e = Expr::BlockValue(block);
+            let e = Expr::BlockValue(block.into());
             prop_assert_eq![sigma_serialize_roundtrip(&e), e];
         }
     }
diff --git a/ergotree-ir/src/mir/collection.rs b/ergotree-ir/src/mir/collection.rs
index 82f650e10..574a082bd 100644
--- a/ergotree-ir/src/mir/collection.rs
+++ b/ergotree-ir/src/mir/collection.rs
@@ -51,6 +51,11 @@ impl Collection {
         }
     }
 
+    /// Create a collection from a vector of booleans
+    pub fn from_bools(bools: Vec<bool>) -> Self {
+        Collection::BoolConstants(bools)
+    }
+
     /// Type
     pub fn tpe(&self) -> SType {
         SType::SColl(
diff --git a/ergotree-ir/src/mir/constant.rs b/ergotree-ir/src/mir/constant.rs
index e3ff14b09..78f51a549 100644
--- a/ergotree-ir/src/mir/constant.rs
+++ b/ergotree-ir/src/mir/constant.rs
@@ -88,6 +88,12 @@ impl std::fmt::Debug for Constant {
     }
 }
 
+impl std::fmt::Display for Constant {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        self.v.fmt(f)
+    }
+}
+
 impl std::fmt::Debug for Literal {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         match self {
@@ -112,6 +118,63 @@ impl std::fmt::Debug for Literal {
     }
 }
 
+impl std::fmt::Display for Literal {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Literal::Coll(CollKind::NativeColl(NativeColl::CollByte(i8_bytes))) => {
+                write!(f, "Coll[Byte](")?;
+                for (i, b) in i8_bytes.iter().enumerate() {
+                    if i > 0 {
+                        write!(f, ", ")?;
+                    }
+                    write!(f, "{}", b)?;
+                }
+                write!(f, ")")
+            }
+            Literal::Coll(CollKind::WrappedColl { elem_tpe, items }) => {
+                write!(f, "Coll[{}](", elem_tpe)?;
+                for (i, item) in items.iter().enumerate() {
+                    if i > 0 {
+                        write!(f, ", ")?;
+                    }
+                    item.fmt(f)?;
+                }
+                write!(f, ")")
+            }
+            Literal::Opt(boxed_opt) => {
+                if let Some(v) = &**boxed_opt {
+                    write!(f, "Some(")?;
+                    v.fmt(f)?;
+                    write!(f, ")")
+                } else {
+                    write!(f, "None")
+                }
+            }
+            Literal::Tup(items) => {
+                write!(f, "(")?;
+                for (i, item) in items.iter().enumerate() {
+                    if i > 0 {
+                        write!(f, ", ")?;
+                    }
+                    item.fmt(f)?;
+                }
+                write!(f, ")")
+            }
+            Literal::Unit => write!(f, "()"),
+            Literal::Boolean(v) => v.fmt(f),
+            Literal::Byte(v) => v.fmt(f),
+            Literal::Short(v) => v.fmt(f),
+            Literal::Int(v) => v.fmt(f),
+            Literal::Long(v) => write!(f, "{}L", v),
+            Literal::BigInt(v) => v.fmt(f),
+            Literal::SigmaProp(v) => v.fmt(f),
+            Literal::GroupElement(v) => v.fmt(f),
+            Literal::AvlTree(v) => write!(f, "AvlTree({:?})", v),
+            Literal::CBox(v) => write!(f, "ErgoBox({:?})", v),
+        }
+    }
+}
+
 impl From<()> for Literal {
     fn from(_: ()) -> Literal {
         Literal::Unit
diff --git a/ergotree-ir/src/mir/expr.rs b/ergotree-ir/src/mir/expr.rs
index d75cee57d..f508c5009 100644
--- a/ergotree-ir/src/mir/expr.rs
+++ b/ergotree-ir/src/mir/expr.rs
@@ -3,6 +3,9 @@
 use std::convert::TryFrom;
 use std::convert::TryInto;
 
+use crate::pretty_printer::PosTrackingWriter;
+use crate::pretty_printer::Print;
+use crate::source_span::Spanned;
 use crate::types::stype::LiftIntoSType;
 use crate::types::stype::SType;
 
@@ -84,17 +87,17 @@ use thiserror::Error;
 /// Expression in ErgoTree
 pub enum Expr {
     /// Append - Concatenation of two collections
-    Append(Append),
+    Append(Spanned<Append>),
     /// Constant value
     Const(Constant),
     /// Placeholder for a constant
     ConstPlaceholder(ConstantPlaceholder),
     /// Substitute constants in serialized ergo tree
-    SubstConstants(SubstConstants),
+    SubstConstants(Spanned<SubstConstants>),
     /// Convert byte array to SLong
-    ByteArrayToLong(ByteArrayToLong),
+    ByteArrayToLong(Spanned<ByteArrayToLong>),
     /// Convert byte array to SLong
-    ByteArrayToBigInt(ByteArrayToBigInt),
+    ByteArrayToBigInt(Spanned<ByteArrayToBigInt>),
     /// Convert SLong to a byte array
     LongToByteArray(LongToByteArray),
     /// Collection declaration (array of expressions of the same type)
@@ -117,43 +120,43 @@ pub enum Expr {
     /// Function application
     Apply(Apply),
     /// Method call
-    MethodCall(MethodCall),
+    MethodCall(Spanned<MethodCall>),
     /// Property call
-    ProperyCall(PropertyCall),
+    PropertyCall(Spanned<PropertyCall>),
     /// Block (statements, followed by an expression)
-    BlockValue(BlockValue),
+    BlockValue(Spanned<BlockValue>),
     /// let-bound expression
-    ValDef(ValDef),
+    ValDef(Spanned<ValDef>),
     /// Reference to ValDef
     ValUse(ValUse),
     /// If, non-lazy - evaluate both branches
     If(If),
     /// Binary operation
-    BinOp(BinOp),
+    BinOp(Spanned<BinOp>),
     /// Logical AND
-    And(And),
+    And(Spanned<And>),
     /// Logical OR
-    Or(Or),
+    Or(Spanned<Or>),
     /// Byte-wise XOR
     Xor(Xor),
     /// THRESHOLD composition for sigma expressions
     Atleast(Atleast),
     /// LogicalNot
-    LogicalNot(LogicalNot),
+    LogicalNot(Spanned<LogicalNot>),
     /// Negation on numeric type
-    Negation(Negation),
+    Negation(Spanned<Negation>),
     /// Bit inversion on numeric type
     BitInversion(BitInversion),
     /// Option.get method
-    OptionGet(OptionGet),
+    OptionGet(Spanned<OptionGet>),
     /// Option.isDefined method
-    OptionIsDefined(OptionIsDefined),
+    OptionIsDefined(Spanned<OptionIsDefined>),
     /// Returns the option's value if the option is nonempty, otherwise return the result of evaluating `default`.
-    OptionGetOrElse(OptionGetOrElse),
+    OptionGetOrElse(Spanned<OptionGetOrElse>),
     /// Box monetary value
     ExtractAmount(ExtractAmount),
     /// Extract register's value (box.RX properties)
-    ExtractRegisterAs(ExtractRegisterAs),
+    ExtractRegisterAs(Spanned<ExtractRegisterAs>),
     /// Extract serialized box bytes
     ExtractBytes(ExtractBytes),
     /// Extract serialized box bytes excluding transaction_id & index
@@ -166,23 +169,23 @@ pub enum Expr {
     /// Box id, Blake2b256 hash of this box's content, basically equals to `blake2b256(bytes)`
     ExtractId(ExtractId),
     /// Collection, get element by index
-    ByIndex(ByIndex),
+    ByIndex(Spanned<ByIndex>),
     /// Collection size
     SizeOf(SizeOf),
     /// Collection slice
-    Slice(Slice),
+    Slice(Spanned<Slice>),
     /// Collection fold op
-    Fold(Fold),
+    Fold(Spanned<Fold>),
     /// Collection map op
-    Map(Map),
+    Map(Spanned<Map>),
     /// Collection filter op
-    Filter(Filter),
+    Filter(Spanned<Filter>),
     /// Tests whether a predicate holds for at least one element of this collection
-    Exists(Exists),
+    Exists(Spanned<Exists>),
     /// Tests whether a predicate holds for all elements of this collection.
-    ForAll(ForAll),
+    ForAll(Spanned<ForAll>),
     /// Tuple field access
-    SelectField(SelectField),
+    SelectField(Spanned<SelectField>),
     /// Bool to SigmaProp
     BoolToSigmaProp(BoolToSigmaProp),
     /// Upcast numeric value
@@ -202,7 +205,7 @@ pub enum Expr {
     /// OR conjunction for sigma propositions
     SigmaOr(SigmaOr),
     /// Extracts Context variable by id and type
-    GetVar(GetVar),
+    GetVar(Spanned<GetVar>),
     /// Extract register of SELF box as `Coll[Byte]`, deserialize it into Value and inline into
     /// the executing script.
     DeserializeRegister(DeserializeRegister),
@@ -218,7 +221,7 @@ pub enum Expr {
     /// XOR for collection of booleans
     XorOf(XorOf),
     /// Perform a lookup by key in an AVL tree
-    TreeLookup(TreeLookup),
+    TreeLookup(Spanned<TreeLookup>),
     /// Create an AVL tree
     CreateAvlTree(CreateAvlTree),
 }
@@ -227,12 +230,12 @@ impl Expr {
     /// Type of the expression
     pub fn tpe(&self) -> SType {
         match self {
-            Expr::Append(ap) => ap.tpe(),
+            Expr::Append(ap) => ap.expr().tpe(),
             Expr::Const(v) => v.tpe.clone(),
             Expr::Collection(v) => v.tpe(),
-            Expr::SubstConstants(v) => v.tpe(),
-            Expr::ByteArrayToLong(v) => v.tpe(),
-            Expr::ByteArrayToBigInt(v) => v.tpe(),
+            Expr::SubstConstants(v) => v.expr().tpe(),
+            Expr::ByteArrayToLong(v) => v.expr().tpe(),
+            Expr::ByteArrayToBigInt(v) => v.expr().tpe(),
             Expr::LongToByteArray(v) => v.tpe(),
             Expr::ConstPlaceholder(v) => v.tpe.clone(),
             Expr::CalcBlake2b256(v) => v.tpe(),
@@ -242,56 +245,56 @@ impl Expr {
             Expr::GlobalVars(v) => v.tpe(),
             Expr::FuncValue(v) => v.tpe(),
             Expr::Apply(v) => v.tpe(),
-            Expr::MethodCall(v) => v.tpe(),
-            Expr::ProperyCall(v) => v.tpe(),
-            Expr::BlockValue(v) => v.tpe(),
-            Expr::ValDef(v) => v.tpe(),
+            Expr::MethodCall(v) => v.expr().tpe(),
+            Expr::PropertyCall(v) => v.expr().tpe(),
+            Expr::BlockValue(v) => v.expr().tpe(),
+            Expr::ValDef(v) => v.expr().tpe(),
             Expr::ValUse(v) => v.tpe.clone(),
-            Expr::BinOp(v) => v.tpe(),
-            Expr::OptionGet(v) => v.tpe(),
-            Expr::ExtractRegisterAs(v) => v.tpe(),
-            Expr::Fold(v) => v.tpe(),
-            Expr::SelectField(v) => v.tpe(),
+            Expr::BinOp(v) => v.expr().tpe(),
+            Expr::OptionGet(v) => v.expr().tpe(),
+            Expr::ExtractRegisterAs(v) => v.expr().tpe(),
+            Expr::Fold(v) => v.expr().tpe(),
+            Expr::SelectField(v) => v.expr().tpe(),
             Expr::ExtractAmount(v) => v.tpe(),
-            Expr::And(v) => v.tpe(),
-            Expr::Or(v) => v.tpe(),
+            Expr::And(v) => v.expr().tpe(),
+            Expr::Or(v) => v.expr().tpe(),
             Expr::Xor(v) => v.tpe(),
             Expr::Atleast(v) => v.tpe(),
-            Expr::LogicalNot(v) => v.tpe(),
-            Expr::Map(v) => v.tpe(),
-            Expr::Filter(v) => v.tpe(),
+            Expr::LogicalNot(v) => v.expr().tpe(),
+            Expr::Map(v) => v.expr().tpe(),
+            Expr::Filter(v) => v.expr().tpe(),
             Expr::BoolToSigmaProp(v) => v.tpe(),
             Expr::Upcast(v) => v.tpe(),
             Expr::Downcast(v) => v.tpe(),
             Expr::If(v) => v.tpe(),
-            Expr::ByIndex(v) => v.tpe(),
+            Expr::ByIndex(v) => v.expr().tpe(),
             Expr::ExtractScriptBytes(v) => v.tpe(),
             Expr::SizeOf(v) => v.tpe(),
-            Expr::Slice(v) => v.tpe(),
+            Expr::Slice(v) => v.expr().tpe(),
             Expr::CreateProveDlog(v) => v.tpe(),
             Expr::CreateProveDhTuple(v) => v.tpe(),
             Expr::ExtractCreationInfo(v) => v.tpe(),
-            Expr::Exists(v) => v.tpe(),
+            Expr::Exists(v) => v.expr().tpe(),
             Expr::ExtractId(v) => v.tpe(),
             Expr::SigmaPropBytes(v) => v.tpe(),
-            Expr::OptionIsDefined(v) => v.tpe(),
-            Expr::OptionGetOrElse(v) => v.tpe(),
-            Expr::Negation(v) => v.tpe(),
+            Expr::OptionIsDefined(v) => v.expr().tpe(),
+            Expr::OptionGetOrElse(v) => v.expr().tpe(),
+            Expr::Negation(v) => v.expr().tpe(),
             Expr::BitInversion(v) => v.tpe(),
-            Expr::ForAll(v) => v.tpe(),
+            Expr::ForAll(v) => v.expr().tpe(),
             Expr::Tuple(v) => v.tpe(),
             Expr::DecodePoint(v) => v.tpe(),
             Expr::SigmaAnd(v) => v.tpe(),
             Expr::SigmaOr(v) => v.tpe(),
             Expr::DeserializeRegister(v) => v.tpe(),
             Expr::DeserializeContext(v) => v.tpe(),
-            Expr::GetVar(v) => v.tpe(),
+            Expr::GetVar(v) => v.expr().tpe(),
             Expr::MultiplyGroup(v) => v.tpe(),
             Expr::Exponentiate(v) => v.tpe(),
             Expr::XorOf(v) => v.tpe(),
             Expr::ExtractBytes(v) => v.tpe(),
             Expr::ExtractBytesWithNoRef(v) => v.tpe(),
-            Expr::TreeLookup(v) => v.tpe(),
+            Expr::TreeLookup(v) => v.expr().tpe(),
             Expr::CreateAvlTree(v) => v.tpe(),
         }
     }
@@ -327,6 +330,14 @@ impl Expr {
         let tree = format!("{:#?}", self);
         tree
     }
+
+    /// Pretty prints the tree
+    pub fn to_string_pretty(&self) -> String {
+        let mut printer = PosTrackingWriter::new();
+        #[allow(clippy::unwrap_used)] // it only fail due to formatting errors
+        let _spanned_expr = self.print(&mut printer).unwrap();
+        printer.as_string()
+    }
 }
 
 impl<T: Into<Literal> + LiftIntoSType> From<T> for Expr {
diff --git a/ergotree-ir/src/mir/func_value.rs b/ergotree-ir/src/mir/func_value.rs
index e313a35e0..83ab57552 100644
--- a/ergotree-ir/src/mir/func_value.rs
+++ b/ergotree-ir/src/mir/func_value.rs
@@ -1,3 +1,5 @@
+use std::fmt;
+
 use crate::has_opcode::HasStaticOpCode;
 use crate::serialization::op_code::OpCode;
 use crate::serialization::sigma_byte_reader::SigmaByteRead;
@@ -37,6 +39,12 @@ impl SigmaSerializable for FuncArg {
     }
 }
 
+impl std::fmt::Display for FuncArg {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "v{}: {}", self.idx, self.tpe)
+    }
+}
+
 /// User-defined function
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct FuncValue {
diff --git a/ergotree-ir/src/mir/global_vars.rs b/ergotree-ir/src/mir/global_vars.rs
index b2b40c196..ce5a35708 100644
--- a/ergotree-ir/src/mir/global_vars.rs
+++ b/ergotree-ir/src/mir/global_vars.rs
@@ -1,5 +1,7 @@
 //! Global variables
 
+use std::fmt::Display;
+
 use crate::has_opcode::HasOpCode;
 use crate::serialization::op_code::OpCode;
 use crate::types::stype::SType;
@@ -49,6 +51,19 @@ impl HasOpCode for GlobalVars {
     }
 }
 
+impl Display for GlobalVars {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            GlobalVars::SelfBox => write!(f, "SELF"),
+            GlobalVars::Inputs => write!(f, "INPUTS"),
+            GlobalVars::Outputs => write!(f, "OUTPUTS"),
+            GlobalVars::Height => write!(f, "HEIGHT"),
+            GlobalVars::MinerPubKey => write!(f, "MINER_PUBKEY"),
+            GlobalVars::GroupGenerator => write!(f, "GROUP_GENERATOR"),
+        }
+    }
+}
+
 #[cfg(test)]
 #[cfg(feature = "arbitrary")]
 mod tests {
diff --git a/ergotree-ir/src/mir/select_field.rs b/ergotree-ir/src/mir/select_field.rs
index c84dc42b1..02f5bd306 100644
--- a/ergotree-ir/src/mir/select_field.rs
+++ b/ergotree-ir/src/mir/select_field.rs
@@ -14,7 +14,7 @@ use super::expr::InvalidArgumentError;
 use crate::has_opcode::HasStaticOpCode;
 
 /// Tuple field access index (1..=255)
-#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone, derive_more::Display)]
 pub struct TupleFieldIndex(u8);
 
 /// Error for tuple index being out of bounds (1..=255)
diff --git a/ergotree-ir/src/mir/val_def.rs b/ergotree-ir/src/mir/val_def.rs
index 4858c5ad0..b2f5c0580 100644
--- a/ergotree-ir/src/mir/val_def.rs
+++ b/ergotree-ir/src/mir/val_def.rs
@@ -9,6 +9,7 @@ use crate::types::stype::SType;
 use super::expr::Expr;
 
 extern crate derive_more;
+use derive_more::Display;
 use derive_more::From;
 
 use crate::has_opcode::HasStaticOpCode;
@@ -16,7 +17,7 @@ use crate::has_opcode::HasStaticOpCode;
 use proptest_derive::Arbitrary;
 
 /// Variable id
-#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, From)]
+#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, From, Display)]
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
 pub struct ValId(pub u32);
 
@@ -86,7 +87,7 @@ mod tests {
 
         #[test]
         fn ser_roundtrip(v in any::<ValDef>()) {
-            let e = Expr::ValDef(v);
+            let e = Expr::ValDef(v.into());
             prop_assert_eq![sigma_serialize_roundtrip(&e), e];
         }
     }
diff --git a/ergotree-ir/src/pretty_printer.rs b/ergotree-ir/src/pretty_printer.rs
new file mode 100644
index 000000000..234eb8358
--- /dev/null
+++ b/ergotree-ir/src/pretty_printer.rs
@@ -0,0 +1,733 @@
+//! Pretty printer for ErgoTree IR
+
+use std::fmt::Write;
+
+mod print;
+pub use print::Print;
+
+// TODO: extract to a separate module
+/// Printer trait with tracking of current position and indent
+pub trait Printer: Write {
+    /// Current position (last printed char)
+    fn current_pos(&self) -> usize;
+    /// Increase indent
+    fn inc_ident(&mut self);
+    /// Decrease indent
+    fn dec_ident(&mut self);
+    /// Get current indent
+    fn get_indent(&self) -> usize;
+    /// Print the current indent
+    fn print_indent(&mut self) -> std::fmt::Result {
+        write!(self, "{:indent$}", "", indent = self.get_indent())
+    }
+}
+
+/// Printer implementation with tracking of current position and indent
+pub struct PosTrackingWriter {
+    print_buf: String,
+    current_pos: usize,
+    current_indent: usize,
+}
+
+impl Write for PosTrackingWriter {
+    fn write_str(&mut self, s: &str) -> std::fmt::Result {
+        let len = s.len();
+        self.current_pos += len;
+        write!(self.print_buf, "{}", s)
+    }
+}
+
+impl Printer for PosTrackingWriter {
+    fn current_pos(&self) -> usize {
+        self.current_pos
+    }
+
+    fn inc_ident(&mut self) {
+        self.current_indent += Self::INDENT;
+    }
+
+    fn dec_ident(&mut self) {
+        self.current_indent -= Self::INDENT;
+    }
+
+    fn get_indent(&self) -> usize {
+        self.current_indent
+    }
+}
+
+impl PosTrackingWriter {
+    const INDENT: usize = 2;
+
+    /// Create new printer
+    pub fn new() -> Self {
+        Self {
+            print_buf: String::new(),
+            current_pos: 0,
+            current_indent: 0,
+        }
+    }
+
+    /// Get printed buffer
+    pub fn get_buf(&self) -> &str {
+        &self.print_buf
+    }
+
+    /// Get printed buffer as String
+    pub fn as_string(self) -> String {
+        self.print_buf
+    }
+}
+
+impl Default for PosTrackingWriter {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[allow(clippy::unwrap_used)]
+#[cfg(test)]
+mod tests {
+
+    use expect_test::expect;
+
+    use crate::chain::address::AddressEncoder;
+    use crate::chain::address::NetworkPrefix;
+    use crate::ergo_tree::ErgoTree;
+    use crate::mir::bin_op::ArithOp;
+    use crate::mir::bin_op::BinOp;
+    use crate::mir::block::BlockValue;
+    use crate::mir::expr::Expr;
+    use crate::mir::val_def::ValDef;
+    use crate::mir::val_use::ValUse;
+    use crate::serialization::SigmaSerializable;
+    use crate::types::stype::SType;
+
+    use super::*;
+
+    fn check_pretty(expr: Expr, expected_tree: expect_test::Expect) {
+        let print_buf = String::new();
+        let mut w = PosTrackingWriter {
+            print_buf,
+            current_pos: 0,
+            current_indent: 0,
+        };
+        let _ = expr.print(&mut w).unwrap();
+        expected_tree.assert_eq(w.get_buf());
+    }
+
+    fn check_spans(expr: Expr, expected_tree: expect_test::Expect) {
+        let print_buf = String::new();
+        let mut w = PosTrackingWriter {
+            print_buf,
+            current_pos: 0,
+            current_indent: 0,
+        };
+        let spanned_expr = expr.print(&mut w).unwrap();
+        expected_tree.assert_eq(format!("{:?}", spanned_expr).as_str());
+    }
+
+    #[test]
+    fn print_block() {
+        let val_id = 1.into();
+        let expr = Expr::BlockValue(
+            BlockValue {
+                items: vec![ValDef {
+                    id: val_id,
+                    rhs: Box::new(Expr::Const(1i32.into())),
+                }
+                .into()],
+                result: Box::new(
+                    ValUse {
+                        val_id,
+                        tpe: SType::SInt,
+                    }
+                    .into(),
+                ),
+            }
+            .into(),
+        );
+        check_pretty(
+            expr,
+            expect![[r#"
+            {
+              val v1 = 1
+              v1
+            }
+            "#]],
+        );
+    }
+
+    #[test]
+    fn print_binop() {
+        let val_id = 1.into();
+        let expr = Expr::BlockValue(
+            BlockValue {
+                items: vec![ValDef {
+                    id: val_id,
+                    rhs: Box::new(
+                        BinOp {
+                            kind: ArithOp::Divide.into(),
+                            left: Expr::Const(4i32.into()).into(),
+                            right: Expr::Const(2i32.into()).into(),
+                        }
+                        .into(),
+                    ),
+                }
+                .into()],
+                result: Box::new(
+                    ValUse {
+                        val_id,
+                        tpe: SType::SInt,
+                    }
+                    .into(),
+                ),
+            }
+            .into(),
+        );
+        check_pretty(
+            expr.clone(),
+            expect![[r#"
+            {
+              val v1 = 4 / 2
+              v1
+            }
+            "#]],
+        );
+
+        check_spans(
+            expr,
+            expect![[
+                r#"BlockValue(Spanned { source_span: SourceSpan { offset: 0, length: 26 }, expr: BlockValue { items: [ValDef(Spanned { source_span: SourceSpan { offset: 4, length: 14 }, expr: ValDef { id: ValId(1), rhs: BinOp(Spanned { source_span: SourceSpan { offset: 13, length: 5 }, expr: BinOp { kind: Arith(Divide), left: Const("4: SInt"), right: Const("2: SInt") } }) } })], result: ValUse(ValUse { val_id: ValId(1), tpe: SInt }) } })"#
+            ]],
+        );
+    }
+
+    #[test]
+    fn eip23_refresh_contract() {
+        let ergo_tree_bytes = base16::decode("1016043c040004000e202a472d4a614e645267556b58703273357638792f423f4528482b4d625065536801000502010105000400040004020402040204080400040a05c8010e20472b4b6250655368566d597133743677397a24432646294a404d635166546a570400040404020408d80ed60199a37300d602b2a4730100d603b5a4d901036395e6c672030605eded928cc77203017201938cb2db6308720373020001730393e4c672030504e4c6720205047304d604b17203d605b0720386027305860273067307d901053c413d0563d803d607e4c68c7205020605d6088c720501d6098c720802860272078602ed8c720901908c72080172079a8c7209027207d6068c720502d6078c720501d608db63087202d609b27208730800d60ab2a5730900d60bdb6308720ad60cb2720b730a00d60db27208730b00d60eb2a5730c00ea02ea02ea02ea02ea02ea02ea02ea02ea02ea02ea02ea02ea02ea02ea02ea02ea02cde4c6b27203e4e30004000407d18f8cc77202017201d1927204730dd18c720601d190997207e4c6b27203730e0006059d9c72077e730f057310d1938c7209017311d193b2720b7312007209d1938c720c018c720d01d1928c720c02998c720d027e9c7204731305d193b1720bb17208d193e4c6720a04059d8c7206027e720405d193e4c6720a05049ae4c6720205047314d193c2720ac27202d192c1720ac17202d1928cc7720a0199a37315d193db6308720edb6308a7d193c2720ec2a7d192c1720ec1a7").unwrap();
+        let ergo_tree = ErgoTree::sigma_parse_bytes(&ergo_tree_bytes).unwrap();
+        check_pretty(
+            ergo_tree.proposition().unwrap(),
+            expect![[r#"
+                {
+                  val v1 = HEIGHT - 30
+                  val v2 = INPUTS(0)
+                  val v3 = INPUTS.filter({
+                      (v3: Box) => 
+                        if (v3.getReg(6).isDefined()) v3.creationInfo._1 >= v1 && v3.tokens(0)._1 == "2a472d4a614e645267556b58703273357638792f423f4528482b4d6250655368" && v3.getReg(5).get == v2.getReg(5).get else false
+                      }
+                    )
+                  val v4 = v3.size
+                  val v5 = v3.fold((, 1, (, true, 0)))({
+                      (v5: ((Long, (Boolean, Long)), Box)) => 
+                        {
+                          val v7 = v5._2.getReg(6).get
+                          val v8 = v5._1
+                          val v9 = v8._2
+                          (, v7, (, v9._1 && v8._1 <= v7, v9._2 + v7))
+                        }
+
+                      }
+                    )
+                  val v6 = v5._2
+                  val v7 = v5._1
+                  val v8 = v2.tokens
+                  val v9 = v8(0)
+                  val v10 = OUTPUTS(0)
+                  val v11 = v10.tokens
+                  val v12 = v11(1)
+                  val v13 = v8(1)
+                  val v14 = OUTPUTS(1)
+                  allOf(
+                    allOf(
+                      allOf(
+                        allOf(
+                          allOf(
+                            allOf(
+                              allOf(
+                                allOf(
+                                  allOf(
+                                    allOf(
+                                      allOf(
+                                        allOf(
+                                          allOf(
+                                            allOf(
+                                              allOf(
+                                                allOf(
+                                                  allOf(
+                                                    proveDlog(v3(getVar(0).get).getReg(4).get), 
+                                                    sigmaProp(v2.creationInfo._1 < v1), 
+                                                  ), 
+                                                  sigmaProp(v4 >= 4), 
+                                                ), 
+                                                sigmaProp(v6._1), 
+                                              ), 
+                                              sigmaProp(v7 - v3(0).getReg(6).get <= v7 * upcast(5) / 100), 
+                                            ), 
+                                            sigmaProp(v9._1 == "472b4b6250655368566d597133743677397a24432646294a404d635166546a57"), 
+                                          ), 
+                                          sigmaProp(v11(0) == v9), 
+                                        ), 
+                                        sigmaProp(v12._1 == v13._1), 
+                                      ), 
+                                      sigmaProp(v12._2 >= v13._2 - upcast(v4 * 2)), 
+                                    ), 
+                                    sigmaProp(v11.size == v8.size), 
+                                  ), 
+                                  sigmaProp(v10.getReg(4).get == v6._2 / upcast(v4)), 
+                                ), 
+                                sigmaProp(v10.getReg(5).get == v2.getReg(5).get + 1), 
+                              ), 
+                              sigmaProp(v10.propBytes == v2.propBytes), 
+                            ), 
+                            sigmaProp(v10.value >= v2.value), 
+                          ), 
+                          sigmaProp(v10.creationInfo._1 >= HEIGHT - 4), 
+                        ), 
+                        sigmaProp(v14.tokens == SELF.tokens), 
+                      ), 
+                      sigmaProp(v14.propBytes == SELF.propBytes), 
+                    ), 
+                    sigmaProp(v14.value >= SELF.value), 
+                  )
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn eip23_update_contract() {
+        let ergo_tree_bytes = base16::decode("100f0400040004000402040204020e20472b4b6250655368566d597133743677397a24432646294a404d635166546a570400040004000e203f4428472d4b6150645367566b5970337336763979244226452948404d625165010005000400040cd80ad601b2a4730000d602db63087201d603b27202730100d604b2a5730200d605db63087204d606b2a5730300d607b27205730400d6088c720701d6098c720702d60ab27202730500d1ededed938c7203017306edededed937203b2720573070093c17201c1720493c672010405c67204040593c672010504c672040504efe6c672040661edededed93db63087206db6308a793c27206c2a792c17206c1a7918cc77206018cc7a701efe6c67206046192b0b5a4d9010b63d801d60ddb6308720b9591b1720d7308d801d60ec6720b070eededed938cb2720d73090001730a93e4c6720b05048cc7a70193e4c6720b060ecbc2720495ede6720ee6c6720b0805ed93e4720e720893e4c6720b08057209ed938c720a017208938c720a027209730b730cd9010b41639a8c720b018cb2db63088c720b02730d00027e730e05").unwrap();
+        let ergo_tree = ErgoTree::sigma_parse_bytes(&ergo_tree_bytes).unwrap();
+        check_pretty(
+            ergo_tree.proposition().unwrap(),
+            expect![[r#"
+                {
+                  val v1 = INPUTS(0)
+                  val v2 = v1.tokens
+                  val v3 = v2(0)
+                  val v4 = OUTPUTS(0)
+                  val v5 = v4.tokens
+                  val v6 = OUTPUTS(1)
+                  val v7 = v5(1)
+                  val v8 = v7._1
+                  val v9 = v7._2
+                  val v10 = v2(1)
+                  sigmaProp(v3._1 == "472b4b6250655368566d597133743677397a24432646294a404d635166546a57" && v3 == v5(0) && v1.value == v4.value && v1.getReg(4) == v4.getReg(4) && v1.getReg(5) == v4.getReg(5) && !v4.getReg(6).isDefined() && v6.tokens == SELF.tokens && v6.propBytes == SELF.propBytes && v6.value >= SELF.value && v6.creationInfo._1 > SELF.creationInfo._1 && !v6.getReg(4).isDefined() && INPUTS.filter({
+                      (v11: Box) => 
+                        {
+                          val v13 = v11.tokens
+                          if (v13.size > 0) {
+                            val v14 = v11.getReg(7)
+                            v13(0)._1 == "3f4428472d4b6150645367566b5970337336763979244226452948404d625165" && v11.getReg(5).get == SELF.creationInfo._1 && v11.getReg(6).get == blake2b256(v4.propBytes) && if (v14.isDefined() && v11.getReg(8).isDefined()) v14.get == v8 && v11.getReg(8).get == v9 else v10._1 == v8 && v10._2 == v9
+                          }
+                 else false
+                        }
+
+                      }
+                    ).fold(0)({
+                      (v11: (Long, Box)) => 
+                        v11._1 + v11._2.tokens(0)._2
+                      }
+                    ) >= upcast(6))
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn eip23_ballot_contract() {
+        let ergo_tree_bytes = base16::decode("10070580dac409040204020400040204000e206251655468576d5a7134743777217a25432a462d4a404e635266556a586e3272d803d601e4c6a70407d602b2a5e4e3000400d603c672020407eb02cd7201d1edededede6720393c27202c2a793db63087202db6308a792c172027300ededededed91b1a4730191b1db6308b2a47302007303938cb2db6308b2a473040073050001730693e47203720192c17202c1a7efe6c672020561").unwrap();
+        let ergo_tree = ErgoTree::sigma_parse_bytes(&ergo_tree_bytes).unwrap();
+        check_pretty(
+            ergo_tree.proposition().unwrap(),
+            expect![[r#"
+                {
+                  val v1 = SELF.getReg(4).get
+                  val v2 = OUTPUTS(getVar(0).get)
+                  val v3 = v2.getReg(4)
+                  anyOf(
+                    proveDlog(v1), 
+                    sigmaProp(v3.isDefined() && v2.propBytes == SELF.propBytes && v2.tokens == SELF.tokens && v2.value >= 10000000 && INPUTS.size > 1 && INPUTS(1).tokens.size > 0 && INPUTS(1).tokens(0)._1 == "6251655468576d5a7134743777217a25432a462d4a404e635266556a586e3272" && v3.get == v1 && v2.value >= SELF.value && !v2.getReg(5).isDefined()), 
+                  )
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn eip23_oracle_contract() {
+        let ergo_tree_bytes = base16::decode("100a040004000580dac409040004000e20472b4b6250655368566d597133743677397a24432646294a404d635166546a570402040204020402d804d601b2a5e4e3000400d602db63087201d603db6308a7d604e4c6a70407ea02d1ededed93b27202730000b2720373010093c27201c2a7e6c67201040792c172017302eb02cd7204d1ededededed938cb2db6308b2a4730300730400017305938cb27202730600018cb2720373070001918cb27202730800028cb272037309000293e4c672010407720492c17201c1a7efe6c672010561").unwrap();
+        let ergo_tree = ErgoTree::sigma_parse_bytes(&ergo_tree_bytes).unwrap();
+        check_pretty(
+            ergo_tree.proposition().unwrap(),
+            expect![[r#"
+                {
+                  val v1 = OUTPUTS(getVar(0).get)
+                  val v2 = v1.tokens
+                  val v3 = SELF.tokens
+                  val v4 = SELF.getReg(4).get
+                  allOf(
+                    sigmaProp(v2(0) == v3(0) && v1.propBytes == SELF.propBytes && v1.getReg(4).isDefined() && v1.value >= 10000000), 
+                    anyOf(
+                      proveDlog(v4), 
+                      sigmaProp(INPUTS(0).tokens(0)._1 == "472b4b6250655368566d597133743677397a24432646294a404d635166546a57" && v2(1)._1 == v3(1)._1 && v2(1)._2 > v3(1)._2 && v1.getReg(4).get == v4 && v1.value >= SELF.value && !v1.getReg(5).isDefined()), 
+                    ), 
+                  )
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn ageusd_bank_full() {
+        // from eip-15 https://github.com/ergoplatform/eips/pull/27/files
+        let p2s_addr_str = "MUbV38YgqHy7XbsoXWF5z7EZm524Ybdwe5p9WDrbhruZRtehkRPT92imXer2eTkjwPDfboa1pR3zb3deVKVq3H7Xt98qcTqLuSBSbHb7izzo5jphEpcnqyKJ2xhmpNPVvmtbdJNdvdopPrHHDBbAGGeW7XYTQwEeoRfosXzcDtiGgw97b2aqjTsNFmZk7khBEQywjYfmoDc9nUCJMZ3vbSspnYo3LarLe55mh2Np8MNJqUN9APA6XkhZCrTTDRZb1B4krgFY1sVMswg2ceqguZRvC9pqt3tUUxmSnB24N6dowfVJKhLXwHPbrkHViBv1AKAJTmEaQW2DN1fRmD9ypXxZk8GXmYtxTtrj3BiunQ4qzUCu1eGzxSREjpkFSi2ATLSSDqUwxtRz639sHM6Lav4axoJNPCHbY8pvuBKUxgnGRex8LEGM8DeEJwaJCaoy8dBw9Lz49nq5mSsXLeoC4xpTUmp47Bh7GAZtwkaNreCu74m9rcZ8Di4w1cmdsiK1NWuDh9pJ2Bv7u3EfcurHFVqCkT3P86JUbKnXeNxCypfrWsFuYNKYqmjsix82g9vWcGMmAcu5nagxD4iET86iE2tMMfZZ5vqZNvntQswJyQqv2Wc6MTh4jQx1q2qJZCQe4QdEK63meTGbZNNKMctHQbp3gRkZYNrBtxQyVtNLR8xEY8zGp85GeQKbb37vqLXxRpGiigAdMe3XZA4hhYPmAAU5hpSMYaRAjtvvMT3bNiHRACGrfjvSsEG9G2zY5in2YWz5X9zXQLGTYRsQ4uNFkYoQRCBdjNxGv6R58Xq74zCgt19TxYZ87gPWxkXpWwTaHogG1eps8WXt8QzwJ9rVx6Vu9a5GjtcGsQxHovWmYixgBU8X9fPNJ9UQhYyAWbjtRSuVBtDAmoV1gCBEPwnYVP5GCGhCocbwoYhZkZjFZy6ws4uxVLid3FxuvhWvQrVEDYp7WRvGXbNdCbcSXnbeTrPMey1WPaXX";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = CONTEXT.dataInputs
+                  sigmaProp(if (v1.size > 0) {
+                    val v2 = v1(0)
+                    val v3 = v2.tokens(0)._1 == "011d3364de07e5a26f0c4eef0852cddb387039a921b7154ef3cab22c6eda887f"
+                    val v4 = OUTPUTS(0)
+                    val v5 = v4.value
+                    val v6 = SELF.tokens
+                    val v7 = v6(1)
+                    val v8 = v7._2
+                    val v9 = v4.tokens
+                    val v10 = v9(1)
+                    val v11 = v10._2
+                    val v12 = v8 != v11
+                    val v13 = v6(0)
+                    val v14 = v13._2
+                    val v15 = v9(0)
+                    val v16 = v15._2
+                    val v17 = v14 != v16
+                    val v18 = SELF.getReg(5).get
+                    val v19 = v4.getReg(5).get
+                    val v20 = SELF.getReg(4).get
+                    val v21 = v4.getReg(4).get
+                    val v22 = OUTPUTS(1)
+                    val v23 = v22.getReg(4).get
+                    val v24 = if (v12) 0 else v23
+                    val v25 = if (v12) v23 else 0
+                    val v26 = SELF.value
+                    val v27 = v22.getReg(5).get
+                    val v28 = v2.getReg(4).get / 100
+                    val v29 = v26 min v20 * v28 max 0
+                    val v30 = if (v17) v28 min if (v20 == 0) 9223372036854775807 else v29 / v20 * v24 else {
+                      val v30 = v26 - v29
+                      if (v30 == 0) 1000000 else if (v18 == 0) 1000000 else v30 / v18 * v25
+                    }
+
+                    val v31 = v30 * upcast(2) / 100
+                    val v32 = v21 * v28
+                    val v33 = if (HEIGHT > 460000) 800 else 1000000000
+                    val v34 = if (v32 == 0) v33 else v5 * 100 / v32
+                    v3 && v5 >= 10000000 && v4.propBytes == SELF.propBytes && v12 || v17 && !v12 && v17 && v8 + v18 == v11 + v19 && v14 + v20 == v16 + v21 && v20 + v24 == v21 && v18 + v25 == v19 && v26 + v27 == v5 && v21 >= 0 && v19 >= 0 && v15._1 == v13._1 && v10._1 == v7._1 && v9(2)._1 == v6(2)._1 && v27 == v30 + if (v31 < 0) -v31 else v31 && if (v17) if (v24 > 0) v34 >= 400 else true else if (v25 > 0) v34 <= v33 else v34 >= 400 && v3
+                  }
+                 else false || INPUTS(0).tokens(0)._1 == "239c170b7e82f94e6b05416f14b8a2a57e0bfff0e3c93f4abbcd160b6a5b271a")
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn ageusd_update() {
+        // from eip-15 https://github.com/ergoplatform/eips/pull/27/files
+        let p2s_addr_str = "VLyjpv3dse3PbatT83GnDkBQasGqY52dAEdi9XpXhuSUn1FS1Tm7XxtAgmBiqY9pJXtEAsDKwX9ygSjrFu7vnUQZudhC2sSmxhxqgD3ZxJ2VsGwmPG77F6EiEZhcq71oqEq31y9XvCCXL5nqqszdENPAVhu7xT296qZ7w1x6hmwdh9ZE89bjfgbhfNYopoqsCaNLWYHJ12TDSY93kaGqCVKSu6gEF1gLpXBfRCnAPPxYswJPmK8oWDn8PKrUGs3MjVsj6bGXiW3VTGP4VsNH8YSSkjyj1FZ9azLsyfnNJ3zah2zUHdCCqY6PjH9JfHf9joCPf6TusvXgr71XWvh5e2HPEPQr4eJMD4S96cGTiSs3J5XcRd1tCDYoiis8nxv99zFFhHgpqXHgeqjhJ5sPot9eRYTsmm4cRTVLXYAiuKPS2qW5";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = INPUTS(1)
+                  val v2 = v1.tokens
+                  val v3 = OUTPUTS(1)
+                  val v4 = SELF.id
+                  val v5 = OUTPUTS(0)
+                  sigmaProp(v2.size == 3 && v2(2)._1 == "7d672d1def471720ca5782fd6473e47e796d9ac0c138d9911346f118b2f6d9d9" && v2 == v3.tokens && v1.value == v3.value && v1.getReg(4).get == v3.getReg(4).get && v1.getReg(5).get == v3.getReg(5).get && v4 == INPUTS(0).id && SELF.tokens == v5.tokens && SELF.propBytes == v5.propBytes && v5.value >= SELF.value && INPUTS.filter({
+                      (v6: Box) => 
+                        {
+                          val v8 = v6.tokens
+                          v8.size > 0 && v8(0)._1 == "f7995f212216fcf21854f56df7a9a0a9fc9b7ae4c0f1cc40f5b406371286a5e0" && v6.getReg(6).get == v4 && v6.getReg(7).get == blake2b256(v3.propBytes)
+                        }
+
+                      }
+                    ).fold(0)({
+                      (v6: (Long, Box)) => 
+                        v6._1 + v6._2.tokens(0)._2
+                      }
+                    ) >= upcast(3))
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn ageusd_ballot() {
+        // from eip-15 https://github.com/ergoplatform/eips/pull/27/files
+        let p2s_addr_str = "22ELWBHzyWGjPRE48ZJDfFmD24myYdG3vHz8CipSS7rgE65ABmEj9QJiy3rG2PTJeCaZw9VX56GY6uoA3hQch7i5BfFU3AprUWTABi4X1VWtRdK9yrYJkmN6fq8hGfvmWTrsyh4fXZoGETpLuXQViYo194ajej2h7dr3oqNATdMskSXzxJi83bFdAvQ";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = OUTPUTS(INPUTS.indexOf(SELF0))
+                  val v2 = SELF.getReg(4).get
+                  allOf(
+                    sigmaProp(v1.getReg(4).get == v2 && v1.propBytes == SELF.propBytes && v1.tokens == SELF.tokens && v1.value >= SELF.value), 
+                    anyOf(
+                      proveDlog(v2), 
+                      sigmaProp(INPUTS(0).tokens(0)._1 == "239c170b7e82f94e6b05416f14b8a2a57e0bfff0e3c93f4abbcd160b6a5b271a" && !v1.getReg(7).isDefined()), 
+                    ), 
+                  )
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn amm_simple_pool() {
+        // from eip-14 https://github.com/ergoplatform/eips/pull/27/files
+        let p2s_addr_str = "k6fD5ht5e1itDejPFV2VzAoHv478KQCbDnLAL6XUVeEu8KDaboCVZAoFz2AtMoLqM3CgQfr2TZhpwz7K96AgwTXDvBVeTchJ31jjD46Di1W67H8wwFcivnY62UB6L7HWzCkbYuiZaAq2qSJta5Twt4A2Aaoy7xViWcyLUVNAyQYDJXKhVBAGwp76i2too5yWUmEU4zt9XnjJAUt1FFfurNtTNHNPDbqmTRE4crz347q6rfbvkMmg9Jtk9rSiPCQpKjdbZVzUnP4CUw6AvQH6rZXxgNMktAtjQdHhCnrCmf78FwCKqYS54asKd1MFgYNT4NzPwmdZF6JtQt1vvkjZXqpGkjy33xxDNYy8JZS8eeqVgZErPeJ1aj4aaK8gvmApUgGStMDFeFYjuQqZiZxEAHNdAXDg7hyGnmfzA6Hj9zcB7p9nKCDNhEQEMPL1kMG5aXvt2HUPXqiCkLrv596DaGmRMN3gMJaj1T1AfMYNwZozcJ9uUSK4i6Xham28HWAekTtDPhobnmjvkubwLVTtvUumWHtDWFxYSJPF7vqzgZqg6Y5unMF";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = OUTPUTS(0)
+                  val v2 = v1.tokens
+                  val v3 = SELF.tokens
+                  val v4 = v2(0)
+                  val v5 = v3(0)
+                  val v6 = v2(2)
+                  val v7 = v3(2)
+                  val v8 = v2(3)
+                  val v9 = v3(3)
+                  val v10 = 1000000000000000000 - v5._2
+                  val v11 = 1000000000000000000 - v4._2 - v10
+                  val v12 = v7._2
+                  val v13 = v6._2 - v12
+                  val v14 = v13 > 0
+                  val v15 = v9._2
+                  val v16 = upcast(v15)
+                  val v17 = upcast(v13)
+                  val v18 = v8._2 - v15
+                  val v19 = upcast(v12)
+                  val v20 = upcast(v18)
+                  val v21 = upcast(v10)
+                  val v22 = upcast(v11) / v21
+                  sigmaProp(v1.propBytes == SELF.propBytes && v1.value >= SELF.value && v2(1) == v3(1) && v4._1 == v5._1 && v6._1 == v7._1 && v8._1 == v9._1 && if (v11 == 0) if (v14) v16 * v17 * BigInt256(Int256(997)) >= upcast(-v18) * v19 * BigInt256(Int256(1000)) + upcast(v13 * 997) else v19 * v20 * BigInt256(Int256(997)) >= upcast(-v13) * v16 * BigInt256(Int256(1000)) + upcast(v18 * 997) else if (v14 && v18 > 0) upcast(-v11) <= v17 * v21 / v19 min v20 * v21 / v16 else v17 >= v22 * v19 && v20 >= v22 * v16)
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn amm_simple_swap() {
+        // from eip-14 https://github.com/ergoplatform/eips/pull/27/files
+        let p2s_addr_str = "cLPHJ3MHuKAHoCUwGhcEFw5sWJqvPwFyKxTRj1aUoMwgAz78Fg3zLXRhBup9Te1WLau1gZXNmXvUmeXGCd7QLeqB7ArrT3v5cg26piEtqymM6j2SkgYVCobgoAGKeTf6nMLxv1uVrLdjt1GnPxG1MuWj7Es7Dfumotbx9YEaxwqtTUC5SKsJc9LCpAmNWRAQbU6tVVEvmfwWivrGoZ3L5C4DMisxN3U";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = INPUTS(0).tokens
+                  val v2 = v1(2)
+                  val v3 = SELF.tokens(0)
+                  val v4 = v3._1
+                  val v5 = v1(3)
+                  val v6 = v3._2
+                  sigmaProp(v2._1 == v4 || v5._1 == v4 && OUTPUTS.exists({
+                      (v7: Box) => 
+                        {
+                          val v9 = v7.tokens(0)._2
+                          v9 >= upcast(1000) && upcast(v5._2) * upcast(v6) * BigInt256(Int256(997)) <= upcast(v9) * upcast(v2._2) * BigInt256(Int256(1000)) + upcast(v6 * 997)
+                        }
+
+                      }
+                ))
+                  }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn amm_conc_pool_root() {
+        // from eip-14 https://github.com/ergoplatform/eips/pull/27/files
+        let p2s_addr_str = "3STRfQWC9Xb5wAxBiEQ74uTFSemk1oHn43mwj9tMCeu2a3A4kie1bY2qsCdRaEmdQoq3B4tXQuzq9nm84A8PmBgCzgGDEZf2pgYoAUc6krZxUY3rvKWW44ZpzN3u5bFRpKDo6rxKtxX2tw99xmfyfaVBejgDaTfsib2PSVsu9hrLQ3SouECWHQMjDA3Pi8ZuCvQeW8GDkZfHPr3SgwaxY1jpY2njsmf3JBASMoVZ6Mfpg63Q6mBno7mKUSCE7vNHHUZe2V7JEikwjPkaxSWxnwy3J17faGtiEHZLKiNQ9WNtsJLbdVp56dQGfC2zaiXjhx1XJK6m4Nh2M8yEvSuBzanRBAJqrNseGS97tk2iLqqfHrqqmmDsHY3mujCURky4SLr7YLk4B";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = OUTPUTS(0)
+                  val v2 = SELF.tokens(0)
+                  val v3 = v2._1
+                  val v4 = SELF.getReg(4).get
+                  val v5 = SELF.getReg(5).get
+                  val v6 = SELF.getReg(6).get
+                  val v7 = OUTPUTS(1)
+                  val v8 = v7.tokens
+                  val v9 = v7.getReg(6).get
+                  val v10 = v5._2
+                  val v11 = v5._1
+                  val v12 = v7.getReg(5).get
+                  val v13 = v7.getReg(7).get
+                  sigmaProp(v1.propBytes == SELF.propBytes && v1.value >= SELF.value && v1.tokens(0) == (, v3, v2._2 - 1) && v1.getReg(4).get == v4 && v1.getReg(5).get == v5 && v1.getReg(6).get == v6 && v7.getReg(4).get == v4 && v7.getReg(8).get == v6 && v8(1)._1 == v3 && v8(0) == (, SELF.id, 1000000000000000000) && v9._1 * v10 == v9._2 * v11 * v12 && v13._1 * v10 == v13._2 * v11 * v12 + 1)
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn amm_conc_pool_boot() {
+        // from eip-14 https://github.com/ergoplatform/eips/pull/27/files
+        let p2s_addr_str = "6Mv73vd1MnJp6AQg5vHGP9nujFc3Y1PL5gzeUt9PzCaUiQug7ueQGU1bDkmFkCspq4LU8j3T8yY6UyJQKSfah5qEDzjx8QCJF47NBG5jxgPxmBHkM6cUgnYa5ngzn9jrpAn379UC7o5nugTg3HYWZGk3APMcRftkrC3EgroiVMEmSkDcDwaebkNWKfKe3JXgewoTrgZ2YLMafr3JfX47C1zddoWDhS8TWryQYEprkP334eisuh1Fr2iNTW9ruV6m38cRkfRfzSBHYq45mvNLH7JQo6uQZ4NFPx4t27Q5A3mSqCpk7ATThFcQmc2w3Pp2F6xL87c94gxk83G8UEqkAhmaNfoj19zji9rxqRzq9gJeTLBraHR2DchKtahH8HhFPg5DZ4SjwJ4MHqTDF";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = OUTPUTS(0)
+                  val v2 = v1.tokens
+                  val v3 = SELF.tokens
+                  val v4 = v1.getReg(5).get
+                  val v5 = v1.getReg(6).get
+                  val v6 = v2(3)
+                  val v7 = v6._2
+                  val v8 = upcast(v7)
+                  val v9 = v1.getReg(7).get
+                  val v10 = upcast(v9)
+                  val v11 = v2(2)
+                  val v12 = v3(0)
+                  sigmaProp(true && v1.value >= SELF.value && v2(0) == (, SELF.id, 1) && v2(1) == (, v3(1)._1, 1) && v1.getReg(4).get == SELF.getReg(4).get && v4 == SELF.getReg(6).get && v5 == SELF.getReg(7).get && (, v6._1, v2(4)._1) == SELF.getReg(8).get && v8 * v8 == v10 * v10 && if (v11._1 == v12._1) v11._2 else 0 >= v12._2 - v9 && v7 * upcast(v4._2) >= v7 * upcast(v4._1) && v7 * upcast(v5._2) < v7 * upcast(v5._1))
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn amm_conc_pool() {
+        // from eip-14 https://github.com/ergoplatform/eips/pull/27/files
+        let p2s_addr_str = "AhCu1UkNT4c9q3B2Lb7gNgvZWCdXL8iYgmNxTYiy4S3wgKWFFW6kz9v7pvY8NqC7g4wgXXwzJY1fQVn2xrLkiyiQWsorq5dR7d5KnDAY43H4GvSVjaDciadXCSHCb8jgk8mFSQCwoZHweLmMJ25312wT85AySJgYUuzdUxMz4EnQpiwZR2XVZq3M81gycuqP9gUryryjN4J1cAF3yL3kZR3rREubBvJ2CY5hF74Xaj2jwajivkESkqq22ieWWG2sK7dk1A7KHr1MmiXGcUBAMMGPAu3mVCeFW9SongxP9hodnJThLknjWRBBBC6wq5jNkSdHrMbdaQM3XesXqGTk9KwWpnSL92E96muU2k8FQbo5isps1r5ciYVrFptfEAC3tWbwcVmRKtrgxtCex6bP5aBZYjaH6L9QQbkYriDAcQ1iZcpf3hHCqURjRXL7i72C3aGBwzzspQvhLof6x4f4gPxTCtF1bNUxddUL6DJ1PbQWzVH8taivjhHohis6sRn3Akvv4xaZRJdKZ8rDuiounRKNXi8VoNgVEZbSFYtfweRSdsiXJCkhtehLWdtFTk1eg7djASdBGKaguvtEBcGaAALVDUoH479VskPUQ6hrfS7KcWrATBdb8sf4W5MFpx7UNitzq2fzSKC96mQRUzy5uELe7Y7vexm5ArNEyr6ARkypZypSzJ2CEifjVxxRBEWVtbdqHrwP4gWv6cMdbqFWwuXAw2BZQnWpZFtKAGQ9m";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = OUTPUTS(0)
+                  val v2 = v1.tokens
+                  val v3 = SELF.tokens
+                  val v4 = v2(2)
+                  val v5 = v3(2)
+                  val v6 = v2(3)
+                  val v7 = v3(3)
+                  val v8 = v2(4)
+                  val v9 = v3(4)
+                  val v10 = SELF.getReg(4).get
+                  val v11 = SELF.getReg(5).get
+                  val v12 = SELF.getReg(6).get
+                  val v13 = 1000000000000000000 - v5._2
+                  val v14 = 1000000000000000000 - v4._2 - v13
+                  val v15 = v6._2
+                  val v16 = upcast(v15)
+                  val v17 = v8._2
+                  val v18 = upcast(v17)
+                  val v19 = v16 * upcast(v11._2) >= v18 * upcast(v11._1) && v16 * upcast(v12._2) < v18 * upcast(v12._1)
+                  val v20 = v7._2
+                  val v21 = v15 - v20
+                  val v22 = v21 > 0
+                  val v23 = v9._2
+                  val v24 = upcast(v23)
+                  val v25 = upcast(v21)
+                  val v26 = v17 - v23
+                  val v27 = upcast(v20)
+                  val v28 = 1000
+                  val v29 = upcast(v26)
+                  val v30 = upcast(v13)
+                  val v31 = upcast(v14) / v30
+                  sigmaProp(v1.propBytes == SELF.propBytes && v1.value >= SELF.value && v2(0) == v3(0) && v2(1) == v3(1) && v4._1 == v5._1 && v6._1 == v7._1 && v8._1 == v9._1 && v1.getReg(4).get == v10 && v1.getReg(5).get == v11 && v1.getReg(6).get == v12 && if (v14 == 0) if (v22) v24 * v25 * upcast(v10) >= upcast(-v26) * v27 * upcast(v28) + upcast(v21 * upcast(v10)) else v27 * v29 * upcast(v10) >= upcast(-v21) * v24 * upcast(v28) + upcast(v26 * upcast(v10)) && v19 else if (v22 && v26 > 0) upcast(-v14) <= v25 * v30 / v27 min v29 * v30 / v24 && v19 else v25 >= v31 * v27 && v29 >= v31 * v24)
+                }
+            "#]],
+        )
+    }
+
+    #[test]
+    fn eip_22_auction() {
+        // from https://github.com/ergoplatform/eips/blob/adbe21512cadf51a2d9af8406cfd418f95335899/eip-0022.md
+        let p2s_addr_str = "GE68RH3VEgW6b4kN3GhYigrLxoXr9jMgMpmm3KnXJaYq1PzHieYhz7Uka86idxvBWLRLmpiA3HrxHPsX1jzQZEv5yaRDueiJqks1keM7iB1eYWMEVRUUq1MLFzdA1FHQwCfSakM3Uc8uBPqk2icxhoXvw1CVbUVtFCzcPrZzf8Jjf8gS5bCFpWQscHo14HTsdBxyV3dwL6wKu8LP8FuWJE7qCEgX9ToEiztH4ZLmFwBejnUFrCQqjLVLWpdgdnAXVyewiX9DxXKJKL4wNqhPUrYjmHEVvpZAezXjzfVMr7gKupTqAgx2AJYGh4winEDeYq9MVshX8xjJweGhbAm2RXN1euQpoepFaKqfrT2mQBTmr6edbbzYg6VJ7DoSCDzmcUupFAmZMjMiaUbgtyz2VEbPEKsmAFrZ6zdB5EUxhiYZMd6KdstsJwZCgKJSSCShTgpfqNLCdpR9JbZFQpA1uhUkuLMPvGi74V5EwijTEEtjmTVcWcVhJKv4GDr1Lqe2bMPq4jfEfqvemaY8FcrCsCSi2LZoQUeJ9VrBeotGTKccq8JhwnvNGhLUUrrm32v3bhU82jbtVBVFRD3FSv5hhS6pKHtTevjwuG7JWoR3LN7279A7zQGJWmkSWDoEhHjgxseqZ2G5bLB7ZVEzKM261QhwMwmXA1eWgq8zdBH1u9kFC9bMQ812q2DPZTuhzpBWJh74UGwaEgZLhnUrDKT58cEa4R3kfWyGCMoNw78q1E3a2eKDz8Va5wnixzT2SZFHU8DfHjPSz5rm8Mr3YxgRC6GzaasPDxTrZjuMJHU2exhqsoFvur7Q";
+        let encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
+        let addr = encoder.parse_address_from_str(p2s_addr_str).unwrap();
+        let expr = addr.script().unwrap().proposition().unwrap();
+        check_pretty(
+            expr,
+            expect![[r#"
+                {
+                  val v1 = OUTPUTS(0)
+                  val v2 = CONTEXT.preHeader.timestamp
+                  val v3 = SELF.getReg(7).get
+                  val v4 = SELF.tokens
+                  val v5 = v4.size
+                  val v6 = v5 == 1
+                  val v7 = {
+                      (v7: Box) => 
+                        if (v6) v7.value else v7.tokens(1)._2
+                      }
+
+                    val v8 = v7(SELF)
+                    val v9 = SELF.getReg(6).get
+                    val v10 = SELF.getReg(8).get
+                    val v11 = Coll[Coll[Byte]]()
+                    val v12 = OUTPUTS(1)
+                    val v13 = SELF.getReg(5).get
+                    val v14 = SELF.getReg(4).get
+                    val v15 = CONTEXT.dataInputs(0)
+                    val v16 = v15.getReg(8).get
+                    sigmaProp(v1.value >= SELF.value && v2 < v3 && v1.tokens(0) == v4(0) && {
+                      val v17 = v7(v1)
+                      v17 >= v8 + v9 || v10 != -1 && v17 >= v10
+                    }
+                 && v1.propBytes == SELF.propBytes && v5 == v1.tokens.size && {
+                        (v17: Box) => 
+                          if (v6) v11 else v17.tokens(1)._1
+                        }
+                (SELF) == {
+                          (v17: Box) => 
+                            if (v6) v11 else v17.tokens(1)._1
+                          }
+                (v1) && v12.propBytes == v13 && v7(v12) >= v8 && v1.getReg(4).get == v14 && v1.getReg(5).get.size > 0 && v1.getReg(6).get == v9 && v1.getReg(7).get == if (v3 - v2 <= v16(0)) v3 + v16(1) else v3 && v1.getReg(8).get == v10 && v1.getReg(9) == SELF.getReg(9) || if (OUTPUTS.size == 5) {
+                          val v17 = OUTPUTS(2)
+                          val v18 = v8 / upcast(v15.getReg(4).get)
+                          val v19 = v4(0)
+                          val v20 = v8 / upcast(v15.getReg(6).get)
+                          val v21 = OUTPUTS(3)
+                          val v22 = v21.getReg(4).get
+                          v2 >= v3 || v8 >= v10 && v10 != -1 && v7(v17) >= v18 && v17.propBytes == v15.getReg(5).get && v1.tokens(0) == v19 && v1.propBytes == v13 && v7(v12) >= v8 - v18 - v20 - if (v6) v15.getReg(7).get * 2 else 0 && v12.propBytes == v14 && blake2b256(v22.bytes) == v19._1 && v7(v21) >= v20 && v21.propBytes == v22.propBytes
+                        }
+                 else false)
+                      }
+            "#]],
+        )
+    }
+}
diff --git a/ergotree-ir/src/pretty_printer/print.rs b/ergotree-ir/src/pretty_printer/print.rs
new file mode 100644
index 000000000..870ff6c13
--- /dev/null
+++ b/ergotree-ir/src/pretty_printer/print.rs
@@ -0,0 +1,1112 @@
+use thiserror::Error;
+
+use crate::mir::and::And;
+use crate::mir::apply::Apply;
+use crate::mir::atleast::Atleast;
+use crate::mir::bin_op::BinOp;
+use crate::mir::bit_inversion::BitInversion;
+use crate::mir::block::BlockValue;
+use crate::mir::bool_to_sigma::BoolToSigmaProp;
+use crate::mir::byte_array_to_bigint::ByteArrayToBigInt;
+use crate::mir::byte_array_to_long::ByteArrayToLong;
+use crate::mir::calc_blake2b256::CalcBlake2b256;
+use crate::mir::calc_sha256::CalcSha256;
+use crate::mir::coll_append::Append;
+use crate::mir::coll_by_index::ByIndex;
+use crate::mir::coll_exists::Exists;
+use crate::mir::coll_filter::Filter;
+use crate::mir::coll_fold::Fold;
+use crate::mir::coll_forall::ForAll;
+use crate::mir::coll_map::Map;
+use crate::mir::coll_size::SizeOf;
+use crate::mir::coll_slice::Slice;
+use crate::mir::collection::Collection;
+use crate::mir::constant::Constant;
+use crate::mir::create_avl_tree::CreateAvlTree;
+use crate::mir::create_prove_dh_tuple::CreateProveDhTuple;
+use crate::mir::create_provedlog::CreateProveDlog;
+use crate::mir::decode_point::DecodePoint;
+use crate::mir::deserialize_context::DeserializeContext;
+use crate::mir::deserialize_register::DeserializeRegister;
+use crate::mir::downcast::Downcast;
+use crate::mir::exponentiate::Exponentiate;
+use crate::mir::expr::Expr;
+use crate::mir::extract_amount::ExtractAmount;
+use crate::mir::extract_bytes::ExtractBytes;
+use crate::mir::extract_bytes_with_no_ref::ExtractBytesWithNoRef;
+use crate::mir::extract_creation_info::ExtractCreationInfo;
+use crate::mir::extract_id::ExtractId;
+use crate::mir::extract_reg_as::ExtractRegisterAs;
+use crate::mir::extract_script_bytes::ExtractScriptBytes;
+use crate::mir::func_value::FuncValue;
+use crate::mir::get_var::GetVar;
+use crate::mir::global_vars::GlobalVars;
+use crate::mir::if_op::If;
+use crate::mir::logical_not::LogicalNot;
+use crate::mir::long_to_byte_array::LongToByteArray;
+use crate::mir::method_call::MethodCall;
+use crate::mir::multiply_group::MultiplyGroup;
+use crate::mir::negation::Negation;
+use crate::mir::option_get::OptionGet;
+use crate::mir::option_get_or_else::OptionGetOrElse;
+use crate::mir::option_is_defined::OptionIsDefined;
+use crate::mir::or::Or;
+use crate::mir::property_call::PropertyCall;
+use crate::mir::select_field::SelectField;
+use crate::mir::sigma_and::SigmaAnd;
+use crate::mir::sigma_or::SigmaOr;
+use crate::mir::sigma_prop_bytes::SigmaPropBytes;
+use crate::mir::subst_const::SubstConstants;
+use crate::mir::tree_lookup::TreeLookup;
+use crate::mir::tuple::Tuple;
+use crate::mir::unary_op::OneArgOpTryBuild;
+use crate::mir::upcast::Upcast;
+use crate::mir::val_def::ValDef;
+use crate::mir::val_use::ValUse;
+use crate::mir::xor::Xor;
+use crate::mir::xor_of::XorOf;
+use crate::source_span::SourceSpan;
+use crate::source_span::Spanned;
+use crate::types::stype::SType;
+
+use super::Printer;
+
+/// Print error
+#[allow(missing_docs)]
+#[derive(PartialEq, Eq, Debug, Clone, Error)]
+pub enum PrintError {
+    #[error("fmt error: {0:?}")]
+    FmtError(#[from] std::fmt::Error),
+}
+
+/// Print trait for Expr that sets the source span for the resulting Expr
+pub trait Print {
+    /// Print the expression and return the resulting expression with source span
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError>;
+}
+
+impl Print for Expr {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        match self {
+            Expr::Append(v) => v.expr().print(w),
+            Expr::BlockValue(v) => v.expr().print(w),
+            Expr::ValDef(v) => v.expr().print(w),
+            Expr::ValUse(v) => v.print(w),
+            Expr::Const(v) => v.print(w),
+            Expr::BinOp(v) => v.expr().print(w),
+            Expr::GlobalVars(v) => v.print(w),
+            Expr::ByIndex(v) => v.expr().print(w),
+            Expr::ConstPlaceholder(_) => Ok(self.clone()),
+            Expr::SubstConstants(v) => v.expr().print(w),
+            Expr::ByteArrayToLong(v) => v.expr().print(w),
+            Expr::ByteArrayToBigInt(v) => v.expr().print(w),
+            Expr::LongToByteArray(v) => v.print(w),
+            Expr::Collection(v) => v.print(w),
+            Expr::Tuple(v) => v.print(w),
+            Expr::CalcBlake2b256(v) => v.print(w),
+            Expr::CalcSha256(v) => v.print(w),
+            Expr::Context => {
+                write!(w, "CONTEXT")?;
+                Ok(self.clone())
+            }
+            Expr::Global => {
+                write!(w, "GLOBAL")?;
+                Ok(self.clone())
+            }
+            Expr::FuncValue(v) => v.print(w),
+            Expr::Apply(v) => v.print(w),
+            Expr::MethodCall(v) => v.expr().print(w),
+            Expr::PropertyCall(v) => v.expr().print(w),
+            Expr::If(v) => v.print(w),
+            Expr::And(v) => v.expr().print(w),
+            Expr::Or(v) => v.expr().print(w),
+            Expr::Xor(v) => v.print(w),
+            Expr::Atleast(v) => v.print(w),
+            Expr::LogicalNot(v) => v.expr().print(w),
+            Expr::Negation(v) => v.expr().print(w),
+            Expr::BitInversion(v) => v.print(w),
+            Expr::OptionGet(v) => v.expr().print(w),
+            Expr::OptionIsDefined(v) => v.expr().print(w),
+            Expr::OptionGetOrElse(v) => v.expr().print(w),
+            Expr::ExtractAmount(v) => v.print(w),
+            Expr::ExtractRegisterAs(v) => v.expr().print(w),
+            Expr::ExtractBytes(v) => v.print(w),
+            Expr::ExtractBytesWithNoRef(v) => v.print(w),
+            Expr::ExtractScriptBytes(v) => v.print(w),
+            Expr::ExtractCreationInfo(v) => v.print(w),
+            Expr::ExtractId(v) => v.print(w),
+            Expr::SizeOf(v) => v.print(w),
+            Expr::Slice(v) => v.expr().print(w),
+            Expr::Fold(v) => v.expr().print(w),
+            Expr::Map(v) => v.expr().print(w),
+            Expr::Filter(v) => v.expr().print(w),
+            Expr::Exists(v) => v.expr().print(w),
+            Expr::ForAll(v) => v.expr().print(w),
+            Expr::SelectField(v) => v.expr().print(w),
+            Expr::BoolToSigmaProp(v) => v.print(w),
+            Expr::Upcast(v) => v.print(w),
+            Expr::Downcast(v) => v.print(w),
+            Expr::CreateProveDlog(v) => v.print(w),
+            Expr::CreateProveDhTuple(v) => v.print(w),
+            Expr::SigmaPropBytes(v) => v.print(w),
+            Expr::DecodePoint(v) => v.print(w),
+            Expr::SigmaAnd(v) => v.print(w),
+            Expr::SigmaOr(v) => v.print(w),
+            Expr::GetVar(v) => v.expr().print(w),
+            Expr::DeserializeRegister(v) => v.print(w),
+            Expr::DeserializeContext(v) => v.print(w),
+            Expr::MultiplyGroup(v) => v.print(w),
+            Expr::Exponentiate(v) => v.print(w),
+            Expr::XorOf(v) => v.print(w),
+            Expr::TreeLookup(v) => v.expr().print(w),
+            Expr::CreateAvlTree(v) => v.print(w),
+        }
+    }
+}
+
+impl Print for BlockValue {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        writeln!(w, "{{")?;
+        w.inc_ident();
+        let mut items = Vec::new();
+        for item in &self.items {
+            w.print_indent()?;
+            items.push(item.print(w)?);
+            writeln!(w)?;
+        }
+        // indent for result
+        w.print_indent()?;
+        let res = self.result.print(w)?;
+        writeln!(w)?;
+        w.dec_ident();
+        w.print_indent()?;
+        writeln!(w, "}}")?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: BlockValue {
+                items,
+                result: Box::new(res),
+            },
+        }
+        .into())
+    }
+}
+
+impl Print for ValDef {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        write!(w, "val v{} = ", self.id)?;
+        let rhs = self.rhs.print(w)?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: ValDef {
+                id: self.id,
+                rhs: Box::new(rhs),
+            },
+        }
+        .into())
+    }
+}
+
+impl Print for Constant {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "{:?}", self.v)?;
+        Ok(self.clone().into())
+    }
+}
+
+impl Print for ValUse {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "v{}", self.val_id)?;
+        Ok(self.clone().into())
+    }
+}
+
+impl Print for Append {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let input = self.input.print(w)?;
+        write!(w, ".append(")?;
+        let col_2 = self.col_2.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: Append {
+                input: Box::new(input),
+                col_2: Box::new(col_2),
+            },
+        }
+        .into())
+    }
+}
+
+impl Print for BinOp {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let left = self.left.print(w)?;
+        write!(w, " {} ", self.kind)?;
+        let right = self.right.print(w)?;
+        let length = w.current_pos() - offset;
+        // dbg!(offset, length);
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: BinOp {
+                kind: self.kind,
+                left: Box::new(left),
+                right: Box::new(right),
+            },
+        }
+        .into())
+    }
+}
+
+impl Print for GlobalVars {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "{}", self)?;
+        Ok(self.clone().into())
+    }
+}
+
+impl Print for ByIndex {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        let offset = w.current_pos();
+        write!(w, "(")?;
+        let index = self.index.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: ByIndex::new(input, index, self.default.clone()).unwrap(),
+        }
+        .into())
+    }
+}
+
+impl Print for Map {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        let offset = w.current_pos();
+        write!(w, ".map(")?;
+        let mapper = self.mapper.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: Map::new(input, mapper).unwrap(),
+        }
+        .into())
+    }
+}
+
+impl Print for Exists {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        let offset = w.current_pos();
+        write!(w, ".exists(")?;
+        let condition = self.condition.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: Exists::new(input, condition).unwrap(),
+        }
+        .into())
+    }
+}
+
+impl Print for Fold {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        let offset = w.current_pos();
+        write!(w, ".fold(")?;
+        let zero = self.zero.print(w)?;
+        write!(w, ")(")?;
+        let fold_op = self.fold_op.print(w)?;
+        w.print_indent()?;
+        write!(w, ")")?;
+        w.dec_ident();
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: Fold::new(input, zero, fold_op).unwrap(),
+        }
+        .into())
+    }
+}
+
+impl Print for FuncValue {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        w.inc_ident();
+        writeln!(w, "{{")?;
+        w.inc_ident();
+        w.print_indent()?;
+        writeln!(
+            w,
+            "({}) => ",
+            self.args()
+                .iter()
+                .map(|a| format!("{}", a))
+                .collect::<Vec<_>>()
+                .join(", ")
+        )?;
+        w.inc_ident();
+        w.print_indent()?;
+        let body = self.body().print(w)?;
+        w.dec_ident();
+        writeln!(w)?;
+        w.print_indent()?;
+        writeln!(w, "}}")?;
+        w.dec_ident();
+        Ok(FuncValue::new(self.args().to_vec(), body).into())
+    }
+}
+
+impl Print for Filter {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        let offset = w.current_pos();
+        write!(w, ".filter(")?;
+        let condition = self.condition.print(w)?;
+        w.print_indent()?;
+        write!(w, ")")?;
+        w.dec_ident();
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: Filter::new(input, condition).unwrap(),
+        }
+        .into())
+    }
+}
+
+impl Print for If {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "if (")?;
+        let condition = self.condition.print(w)?;
+        write!(w, ") ")?;
+        let true_branch = self.true_branch.print(w)?;
+        write!(w, " else ")?;
+        let false_branch = self.false_branch.print(w)?;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(If {
+            condition: condition.into(),
+            true_branch: true_branch.into(),
+            false_branch: false_branch.into(),
+        }
+        .into())
+    }
+}
+
+impl Print for OptionIsDefined {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let input = self.input.print(w)?;
+        write!(w, ".isDefined()")?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: OptionIsDefined {
+                input: Box::new(input),
+            },
+        }
+        .into())
+    }
+}
+
+impl Print for ExtractRegisterAs {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let input = self.input.print(w)?;
+        write!(w, ".getReg")?;
+        write!(w, "({})", self.register_id)?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: ExtractRegisterAs::new(
+                input,
+                self.register_id,
+                SType::SOption(self.elem_tpe.clone().into()),
+            )
+            .unwrap(),
+        }
+        .into())
+    }
+}
+
+impl Print for SelectField {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let input = self.input.print(w)?;
+        write!(w, "._{}", self.field_index)?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: SelectField::new(input, self.field_index).unwrap(),
+        }
+        .into())
+    }
+}
+
+impl Print for ExtractCreationInfo {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        write!(w, ".creationInfo")?;
+        Ok(ExtractCreationInfo {
+            input: input.into(),
+        }
+        .into())
+    }
+}
+
+impl Print for PropertyCall {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let obj = self.obj.print(w)?;
+        write!(w, ".{}", self.method.name())?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: PropertyCall {
+                obj: Box::new(obj),
+                method: self.method.clone(),
+            },
+        }
+        .into())
+    }
+}
+
+impl Print for MethodCall {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let obj = self.obj.print(w)?;
+        write!(w, ".{}", self.method.name())?;
+        write!(w, "(")?;
+        let args = self
+            .args
+            .iter()
+            .map(|a| -> Result<Expr, PrintError> { a.print(w) })
+            .collect::<Result<Vec<_>, _>>()?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: MethodCall {
+                obj: Box::new(obj),
+                method: self.method.clone(),
+                args,
+            },
+        }
+        .into())
+    }
+}
+
+impl Print for OptionGet {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let input = self.input.print(w)?;
+        write!(w, ".get")?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            source_span: SourceSpan { offset, length },
+            expr: OptionGet::try_build(input).unwrap(),
+        }
+        .into())
+    }
+}
+
+impl Print for SizeOf {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        write!(w, ".size")?;
+        Ok(SizeOf {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for Tuple {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "(")?;
+        let items = self.items.try_mapped_ref(|i| {
+            write!(w, ", ")?;
+            i.print(w)
+        })?;
+        write!(w, ")")?;
+        Ok(Tuple { items }.into())
+    }
+}
+
+impl Print for SigmaAnd {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        writeln!(w, "allOf(")?;
+        let items = self.items.try_mapped_ref(|i| -> Result<Expr, PrintError> {
+            w.inc_ident();
+            w.print_indent()?;
+            let item = i.print(w)?;
+            write!(w, ", ")?;
+            writeln!(w)?;
+            w.dec_ident();
+            Ok(item)
+        })?;
+        w.print_indent()?;
+        write!(w, ")")?;
+        Ok(SigmaAnd { items }.into())
+    }
+}
+
+impl Print for SigmaOr {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        writeln!(w, "anyOf(")?;
+        let items = self.items.try_mapped_ref(|i| -> Result<Expr, PrintError> {
+            w.inc_ident();
+            w.print_indent()?;
+            let item = i.print(w)?;
+            write!(w, ", ")?;
+            writeln!(w)?;
+            w.dec_ident();
+            Ok(item)
+        })?;
+        w.print_indent()?;
+        write!(w, ")")?;
+        Ok(SigmaOr { items }.into())
+    }
+}
+
+impl Print for And {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        write!(w, "&&")?;
+        let input = self.input.print(w)?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            expr: And {
+                input: Box::new(input),
+            },
+            source_span: SourceSpan { offset, length },
+        }
+        .into())
+    }
+}
+
+impl Print for Or {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        write!(w, "||")?;
+        let input = self.input.print(w)?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            expr: Or {
+                input: Box::new(input),
+            },
+            source_span: SourceSpan { offset, length },
+        }
+        .into())
+    }
+}
+
+impl Print for CreateProveDlog {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "proveDlog(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(CreateProveDlog {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for GetVar {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "getVar({})", self.var_id)?;
+        Ok(self.clone().into())
+    }
+}
+
+impl Print for BoolToSigmaProp {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "sigmaProp(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(BoolToSigmaProp {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for Upcast {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "upcast(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Upcast::new(input, self.tpe.clone()).unwrap().into())
+    }
+}
+
+impl Print for ExtractScriptBytes {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        write!(w, ".propBytes")?;
+        Ok(ExtractScriptBytes {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for ExtractAmount {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        write!(w, ".value")?;
+        Ok(ExtractAmount {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for LogicalNot {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "!")?;
+        let input = self.input.print(w)?;
+        Ok(LogicalNot {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for CalcBlake2b256 {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "blake2b256(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(CalcBlake2b256 {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for Negation {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "-")?;
+        let input = self.input.print(w)?;
+        Ok(Negation {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for ExtractId {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        write!(w, ".id")?;
+        Ok(ExtractId {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for Apply {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let func = self.func.print(w)?;
+        write!(w, "(")?;
+        let args = self
+            .args
+            .iter()
+            .map(|a| -> Result<Expr, PrintError> { a.print(w) })
+            .collect::<Result<Vec<_>, _>>()?;
+        write!(w, ")")?;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Apply::new(func, args).unwrap().into())
+    }
+}
+
+impl Print for Collection {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "Coll[{}](", self.tpe())?;
+        match self {
+            Collection::BoolConstants(bools) => {
+                for b in bools {
+                    write!(w, "{}, ", b)?;
+                }
+                write!(w, ")")?;
+                Ok(Collection::from_bools(bools.clone()).into())
+            }
+            Collection::Exprs { elem_tpe, items } => {
+                let items = items
+                    .iter()
+                    .map(|i| {
+                        write!(w, ", ")?;
+                        i.print(w)
+                    })
+                    .collect::<Result<Vec<_>, _>>()?;
+                write!(w, ")")?;
+                #[allow(clippy::unwrap_used)] // we only added spans
+                Ok(Collection::new(elem_tpe.clone(), items).unwrap().into())
+            }
+        }
+    }
+}
+
+impl Print for ExtractBytes {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        write!(w, ".bytes")?;
+        Ok(ExtractBytes {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for ByteArrayToLong {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "byteArrayToLong(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(ByteArrayToLong {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for ByteArrayToBigInt {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "byteArrayToBigInt(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(ByteArrayToBigInt {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for LongToByteArray {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "longToByteArray(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(LongToByteArray {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for CalcSha256 {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "sha256(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(CalcSha256 {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for Xor {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let left = self.left.print(w)?;
+        write!(w, "^")?;
+        let right = self.right.print(w)?;
+        Ok(Xor {
+            left: left.into(),
+            right: right.into(),
+        }
+        .into())
+    }
+}
+
+impl Print for Atleast {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, ".atLeast(")?;
+        let bound = self.bound.print(w)?;
+        write!(w, ", ")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(Atleast {
+            input: Box::new(input),
+            bound: Box::new(bound),
+        }
+        .into())
+    }
+}
+
+impl Print for SubstConstants {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        write!(w, ".substConstants(")?;
+        let script_bytes = self.script_bytes.print(w)?;
+        write!(w, ", ")?;
+        let positions = self.positions.print(w)?;
+        write!(w, ", ")?;
+        let new_values = self.new_values.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            expr: SubstConstants {
+                script_bytes: script_bytes.into(),
+                positions: positions.into(),
+                new_values: new_values.into(),
+            },
+            source_span: SourceSpan { offset, length },
+        }
+        .into())
+    }
+}
+
+impl Print for BitInversion {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "~")?;
+        let input = self.input.print(w)?;
+        Ok(BitInversion {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for OptionGetOrElse {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let input = self.input.print(w)?;
+        write!(w, ".getOrElse(")?;
+        let default = self.default.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            expr: OptionGetOrElse::new(input, default).unwrap(),
+            source_span: SourceSpan { offset, length },
+        }
+        .into())
+    }
+}
+
+impl Print for ExtractBytesWithNoRef {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let input = self.input.print(w)?;
+        write!(w, ".bytesWithNoRef")?;
+        Ok(ExtractBytesWithNoRef {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for Slice {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let input = self.input.print(w)?;
+        write!(w, ".slice(")?;
+        let from = self.from.print(w)?;
+        write!(w, ", ")?;
+        let until = self.until.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            expr: Slice {
+                input: Box::new(input),
+                from: Box::new(from),
+                until: Box::new(until),
+            },
+            source_span: SourceSpan { offset, length },
+        }
+        .into())
+    }
+}
+
+impl Print for ForAll {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        let input = self.input.print(w)?;
+        write!(w, ".forall(")?;
+        let condition = self.condition.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        #[allow(clippy::unwrap_used)] // we only added spans
+        Ok(Spanned {
+            expr: ForAll::new(input, condition).unwrap(),
+            source_span: SourceSpan { offset, length },
+        }
+        .into())
+    }
+}
+
+impl Print for Downcast {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "downcast(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(Downcast {
+            input: Box::new(input),
+            tpe: self.tpe.clone(),
+        }
+        .into())
+    }
+}
+
+impl Print for CreateProveDhTuple {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "proveDHTuple(")?;
+        let g = self.g.print(w)?;
+        write!(w, ", ")?;
+        let h = self.h.print(w)?;
+        write!(w, ", ")?;
+        let u = self.u.print(w)?;
+        write!(w, ", ")?;
+        let v = self.v.print(w)?;
+        write!(w, ")")?;
+        Ok(CreateProveDhTuple {
+            g: Box::new(g),
+            h: Box::new(h),
+            u: Box::new(u),
+            v: Box::new(v),
+        }
+        .into())
+    }
+}
+
+impl Print for SigmaPropBytes {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "sigmaPropBytes(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(SigmaPropBytes {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for DecodePoint {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "decodePoint(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(DecodePoint {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for DeserializeRegister {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "deserializeRegister({})", self.reg)?;
+        Ok(self.clone().into())
+    }
+}
+
+impl Print for DeserializeContext {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "deserializeContext({})", self.id)?;
+        Ok(self.clone().into())
+    }
+}
+
+impl Print for MultiplyGroup {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "multiplyGroup(")?;
+        let left = self.left.print(w)?;
+        write!(w, ", ")?;
+        let right = self.right.print(w)?;
+        write!(w, ")")?;
+        Ok(MultiplyGroup {
+            left: left.into(),
+            right: right.into(),
+        }
+        .into())
+    }
+}
+
+impl Print for Exponentiate {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "exponentiate(")?;
+        let left = self.left.print(w)?;
+        write!(w, ", ")?;
+        let right = self.right.print(w)?;
+        write!(w, ")")?;
+        Ok(Exponentiate {
+            left: left.into(),
+            right: right.into(),
+        }
+        .into())
+    }
+}
+
+impl Print for XorOf {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "xorOf(")?;
+        let input = self.input.print(w)?;
+        write!(w, ")")?;
+        Ok(XorOf {
+            input: Box::new(input),
+        }
+        .into())
+    }
+}
+
+impl Print for TreeLookup {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        let offset = w.current_pos();
+        write!(w, "treeLookup(")?;
+        let tree = self.tree.print(w)?;
+        write!(w, ", ")?;
+        let key = self.key.print(w)?;
+        write!(w, ", ")?;
+        let proof = self.proof.print(w)?;
+        write!(w, ")")?;
+        let length = w.current_pos() - offset;
+        Ok(Spanned {
+            expr: TreeLookup {
+                tree: Box::new(tree),
+                key: Box::new(key),
+                proof: Box::new(proof),
+            },
+            source_span: SourceSpan { offset, length },
+        }
+        .into())
+    }
+}
+
+impl Print for CreateAvlTree {
+    fn print(&self, w: &mut dyn Printer) -> Result<Expr, PrintError> {
+        write!(w, "avlTree(")?;
+        let digest = self.digest.print(w)?;
+        write!(w, ", ")?;
+        let key_length = self.key_length.print(w)?;
+        write!(w, ")")?;
+        Ok(CreateAvlTree {
+            digest: Box::new(digest),
+            key_length: Box::new(key_length),
+            flags: self.flags.clone(),
+            value_length: self.value_length.clone(),
+        }
+        .into())
+    }
+}
diff --git a/ergotree-ir/src/serialization/expr.rs b/ergotree-ir/src/serialization/expr.rs
index f4b97ba50..4abb5eef8 100644
--- a/ergotree-ir/src/serialization/expr.rs
+++ b/ergotree-ir/src/serialization/expr.rs
@@ -79,6 +79,7 @@ use crate::serialization::{
 
 use crate::mir::xor_of::XorOf;
 use crate::serialization::types::TypeCode;
+use crate::source_span::Spanned;
 
 impl Expr {
     /// Parse expression from byte stream. This function should be used instead of
@@ -112,8 +113,10 @@ impl Expr {
                 OpCode::MINER_PUBKEY => Ok(Expr::GlobalVars(GlobalVars::MinerPubKey)),
                 OpCode::GROUP_GENERATOR => Ok(Expr::GlobalVars(GlobalVars::GroupGenerator)),
                 OpCode::GLOBAL => Ok(Expr::Global),
-                OpCode::PROPERTY_CALL => Ok(Expr::ProperyCall(PropertyCall::sigma_parse(r)?)),
-                OpCode::METHOD_CALL => Ok(Expr::MethodCall(MethodCall::sigma_parse(r)?)),
+                OpCode::PROPERTY_CALL => {
+                    Ok(Expr::PropertyCall(PropertyCall::sigma_parse(r)?.into()))
+                }
+                OpCode::METHOD_CALL => Ok(Expr::MethodCall(MethodCall::sigma_parse(r)?.into())),
                 OpCode::CONTEXT => Ok(Expr::Context),
                 OptionGet::OP_CODE => Ok(OptionGet::sigma_parse(r)?.into()),
                 OptionIsDefined::OP_CODE => Ok(OptionIsDefined::sigma_parse(r)?.into()),
@@ -146,13 +149,13 @@ impl Expr {
                 OpCode::BIT_OR => Ok(bin_op_sigma_parse(BitOp::BitOr.into(), r)?),
                 OpCode::BIT_AND => Ok(bin_op_sigma_parse(BitOp::BitAnd.into(), r)?),
                 OpCode::BIT_XOR => Ok(bin_op_sigma_parse(BitOp::BitXor.into(), r)?),
-                OpCode::BLOCK_VALUE => Ok(Expr::BlockValue(BlockValue::sigma_parse(r)?)),
+                OpCode::BLOCK_VALUE => Ok(Expr::BlockValue(BlockValue::sigma_parse(r)?.into())),
                 OpCode::FUNC_VALUE => Ok(Expr::FuncValue(FuncValue::sigma_parse(r)?)),
                 OpCode::APPLY => Ok(Expr::Apply(Apply::sigma_parse(r)?)),
-                OpCode::VAL_DEF => Ok(Expr::ValDef(ValDef::sigma_parse(r)?)),
+                OpCode::VAL_DEF => Ok(Expr::ValDef(ValDef::sigma_parse(r)?.into())),
                 OpCode::VAL_USE => Ok(Expr::ValUse(ValUse::sigma_parse(r)?)),
                 ExtractAmount::OP_CODE => Ok(Expr::ExtractAmount(ExtractAmount::sigma_parse(r)?)),
-                OpCode::SELECT_FIELD => Ok(Expr::SelectField(SelectField::sigma_parse(r)?)),
+                OpCode::SELECT_FIELD => Ok(Expr::SelectField(SelectField::sigma_parse(r)?.into())),
                 OpCode::CALC_BLAKE2B256 => Ok(CalcBlake2b256::sigma_parse(r)?.into()),
                 CalcSha256::OP_CODE => Ok(CalcSha256::sigma_parse(r)?.into()),
                 And::OP_CODE => Ok(And::sigma_parse(r)?.into()),
@@ -222,27 +225,27 @@ impl SigmaSerializable for Expr {
                 }
                 None => c.sigma_serialize(w),
             },
-            Expr::Append(ap) => ap.sigma_serialize_w_opcode(w),
-            Expr::Fold(op) => op.sigma_serialize_w_opcode(w),
+            Expr::Append(ap) => ap.expr().sigma_serialize_w_opcode(w),
+            Expr::Fold(op) => op.expr().sigma_serialize_w_opcode(w),
             Expr::ConstPlaceholder(cp) => cp.sigma_serialize_w_opcode(w),
-            Expr::SubstConstants(s) => s.sigma_serialize_w_opcode(w),
-            Expr::ByteArrayToLong(s) => s.sigma_serialize_w_opcode(w),
-            Expr::ByteArrayToBigInt(s) => s.sigma_serialize_w_opcode(w),
+            Expr::SubstConstants(s) => s.expr().sigma_serialize_w_opcode(w),
+            Expr::ByteArrayToLong(s) => s.expr().sigma_serialize_w_opcode(w),
+            Expr::ByteArrayToBigInt(s) => s.expr().sigma_serialize_w_opcode(w),
             Expr::LongToByteArray(s) => s.sigma_serialize_w_opcode(w),
             Expr::GlobalVars(op) => op.op_code().sigma_serialize(w),
-            Expr::MethodCall(mc) => mc.sigma_serialize_w_opcode(w),
-            Expr::ProperyCall(pc) => pc.sigma_serialize_w_opcode(w),
+            Expr::MethodCall(mc) => mc.expr().sigma_serialize_w_opcode(w),
+            Expr::PropertyCall(pc) => pc.expr().sigma_serialize_w_opcode(w),
             Expr::Global => OpCode::GLOBAL.sigma_serialize(w),
             Expr::Context => OpCode::CONTEXT.sigma_serialize(w),
-            Expr::OptionGet(v) => v.sigma_serialize_w_opcode(w),
-            Expr::ExtractRegisterAs(v) => v.sigma_serialize_w_opcode(w),
+            Expr::OptionGet(v) => v.expr().sigma_serialize_w_opcode(w),
+            Expr::ExtractRegisterAs(v) => v.expr().sigma_serialize_w_opcode(w),
             Expr::BinOp(op) => {
-                op.op_code().sigma_serialize(w)?;
-                bin_op_sigma_serialize(op, w)
+                op.expr().op_code().sigma_serialize(w)?;
+                bin_op_sigma_serialize(op.expr(), w)
             }
-            Expr::BlockValue(op) => op.sigma_serialize_w_opcode(w),
+            Expr::BlockValue(op) => op.expr().sigma_serialize_w_opcode(w),
             Expr::ValUse(op) => op.sigma_serialize_w_opcode(w),
-            Expr::ValDef(op) => op.sigma_serialize_w_opcode(w),
+            Expr::ValDef(op) => op.expr().sigma_serialize_w_opcode(w),
             Expr::FuncValue(op) => op.sigma_serialize_w_opcode(w),
             Expr::Apply(op) => op.sigma_serialize_w_opcode(w),
             Expr::ExtractAmount(op) => op.sigma_serialize_w_opcode(w),
@@ -264,19 +267,19 @@ impl SigmaSerializable for Expr {
             Expr::Upcast(op) => op.sigma_serialize_w_opcode(w),
             Expr::Downcast(op) => op.sigma_serialize_w_opcode(w),
             Expr::If(op) => op.sigma_serialize_w_opcode(w),
-            Expr::ByIndex(op) => op.sigma_serialize_w_opcode(w),
+            Expr::ByIndex(op) => op.expr().sigma_serialize_w_opcode(w),
             Expr::ExtractScriptBytes(op) => op.sigma_serialize_w_opcode(w),
             Expr::SizeOf(op) => op.sigma_serialize_w_opcode(w),
-            Expr::Slice(op) => op.sigma_serialize_w_opcode(w),
+            Expr::Slice(op) => op.expr().sigma_serialize_w_opcode(w),
             Expr::CreateProveDlog(op) => op.sigma_serialize_w_opcode(w),
             Expr::CreateProveDhTuple(op) => op.sigma_serialize_w_opcode(w),
             Expr::ExtractCreationInfo(op) => op.sigma_serialize_w_opcode(w),
             Expr::Exists(op) => op.sigma_serialize_w_opcode(w),
             Expr::ExtractId(op) => op.sigma_serialize_w_opcode(w),
             Expr::SigmaPropBytes(op) => op.sigma_serialize_w_opcode(w),
-            Expr::OptionIsDefined(op) => op.sigma_serialize_w_opcode(w),
-            Expr::OptionGetOrElse(op) => op.sigma_serialize_w_opcode(w),
-            Expr::Negation(op) => op.sigma_serialize_w_opcode(w),
+            Expr::OptionIsDefined(op) => op.expr().sigma_serialize_w_opcode(w),
+            Expr::OptionGetOrElse(op) => op.expr().sigma_serialize_w_opcode(w),
+            Expr::Negation(op) => op.expr().sigma_serialize_w_opcode(w),
             Expr::BitInversion(op) => op.sigma_serialize_w_opcode(w),
             Expr::ForAll(op) => op.sigma_serialize_w_opcode(w),
             Expr::Tuple(op) => op.sigma_serialize_w_opcode(w),
@@ -302,6 +305,22 @@ impl SigmaSerializable for Expr {
     }
 }
 
+impl<T: SigmaSerializable> SigmaSerializable for Spanned<T> {
+    fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
+        self.expr.sigma_serialize(w)
+    }
+
+    fn sigma_parse<R: SigmaByteRead>(r: &mut R) -> Result<Self, SigmaParsingError> {
+        T::sigma_parse(r).map(Into::into)
+    }
+}
+
+impl<T: HasOpCode> HasOpCode for Spanned<T> {
+    fn op_code(&self) -> OpCode {
+        self.expr.op_code()
+    }
+}
+
 #[cfg(test)]
 #[cfg(feature = "arbitrary")]
 #[allow(clippy::unwrap_used)]
diff --git a/ergotree-ir/src/serialization/property_call.rs b/ergotree-ir/src/serialization/property_call.rs
index 9551de5ac..1a33d86c3 100644
--- a/ergotree-ir/src/serialization/property_call.rs
+++ b/ergotree-ir/src/serialization/property_call.rs
@@ -40,7 +40,7 @@ mod tests {
     #[test]
     fn ser_roundtrip_property() {
         let mc = PropertyCall::new(Expr::Context, scontext::DATA_INPUTS_PROPERTY.clone()).unwrap();
-        let expr = Expr::ProperyCall(mc);
+        let expr = Expr::PropertyCall(mc.into());
         assert_eq![sigma_serialize_roundtrip(&expr), expr];
     }
 }
diff --git a/ergotree-ir/src/sigma_protocol/sigma_boolean.rs b/ergotree-ir/src/sigma_protocol/sigma_boolean.rs
index fe0f71866..6ed07ae5d 100644
--- a/ergotree-ir/src/sigma_protocol/sigma_boolean.rs
+++ b/ergotree-ir/src/sigma_protocol/sigma_boolean.rs
@@ -13,6 +13,7 @@ use crate::serialization::SigmaSerializable;
 use ergo_chain_types::EcPoint;
 use std::convert::TryFrom;
 use std::convert::TryInto;
+use std::fmt::Formatter;
 
 extern crate derive_more;
 use bounded_vec::BoundedVec;
@@ -53,6 +54,12 @@ impl From<EcPoint> for ProveDlog {
     }
 }
 
+impl std::fmt::Display for ProveDlog {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "proveDlog({})", self.h)
+    }
+}
+
 /// Construct a new SigmaProp value representing public key of Diffie Hellman signature protocol.
 /// Used in a proof that of equality of discrete logarithms (i.e., a proof of a Diffie-Hellman tuple):
 /// given group elements g, h, u, v, the proof convinces a verifier that the prover knows `w` such
@@ -86,8 +93,18 @@ impl ProveDhTuple {
     }
 }
 
+impl std::fmt::Display for ProveDhTuple {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "ProveDhTuple(g: {}, h: {}, u: {}, v: {})",
+            self.g, self.h, self.u, self.v
+        )
+    }
+}
+
 /// Sigma proposition
-#[derive(PartialEq, Eq, Debug, Clone, From)]
+#[derive(PartialEq, Eq, Debug, Clone, From, derive_more::Display)]
 pub enum SigmaProofOfKnowledgeTree {
     /// public key of Diffie Hellman signature protocol
     ProveDhTuple(ProveDhTuple),
@@ -125,6 +142,16 @@ impl HasOpCode for SigmaConjecture {
     }
 }
 
+impl std::fmt::Display for SigmaConjecture {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            SigmaConjecture::Cand(c) => write!(f, "{}", c),
+            SigmaConjecture::Cor(c) => write!(f, "{}", c),
+            SigmaConjecture::Cthreshold(c) => write!(f, "{}", c),
+        }
+    }
+}
+
 /// Algebraic data type of sigma proposition expressions
 /// Values of this type are used as values of SigmaProp type
 #[derive(PartialEq, Eq, Debug, Clone, From, TryInto)]
@@ -246,8 +273,18 @@ impl From<Cthreshold> for SigmaBoolean {
     }
 }
 
+impl std::fmt::Display for SigmaBoolean {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            SigmaBoolean::TrivialProp(b) => write!(f, "sigmaProp({})", b),
+            SigmaBoolean::ProofOfKnowledge(kt) => write!(f, "{}", kt),
+            SigmaBoolean::SigmaConjecture(sc) => write!(f, "{}", sc),
+        }
+    }
+}
+
 /// Proposition which can be proven and verified by sigma protocol.
-#[derive(PartialEq, Eq, Debug, Clone, From, Into)]
+#[derive(PartialEq, Eq, Debug, Clone, From, Into, derive_more::Display)]
 pub struct SigmaProp(SigmaBoolean);
 
 impl SigmaProp {
@@ -295,6 +332,7 @@ impl From<ProveDhTuple> for SigmaProp {
         ))
     }
 }
+
 /// Arbitrary impl for ProveDlog
 #[cfg(feature = "arbitrary")]
 #[allow(clippy::unwrap_used)]
diff --git a/ergotree-ir/src/sigma_protocol/sigma_boolean/cand.rs b/ergotree-ir/src/sigma_protocol/sigma_boolean/cand.rs
index 3b6c31afa..957f46354 100644
--- a/ergotree-ir/src/sigma_protocol/sigma_boolean/cand.rs
+++ b/ergotree-ir/src/sigma_protocol/sigma_boolean/cand.rs
@@ -48,6 +48,19 @@ impl Cand {
     }
 }
 
+impl std::fmt::Display for Cand {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("(")?;
+        for (i, item) in self.items.iter().enumerate() {
+            if i > 0 {
+                f.write_str(" && ")?;
+            }
+            item.fmt(f)?;
+        }
+        f.write_str(")")
+    }
+}
+
 impl SigmaSerializable for Cand {
     fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
         w.put_u16(self.items.len() as u16)?;
diff --git a/ergotree-ir/src/sigma_protocol/sigma_boolean/cor.rs b/ergotree-ir/src/sigma_protocol/sigma_boolean/cor.rs
index 04f589586..acf0c3f9e 100644
--- a/ergotree-ir/src/sigma_protocol/sigma_boolean/cor.rs
+++ b/ergotree-ir/src/sigma_protocol/sigma_boolean/cor.rs
@@ -49,6 +49,19 @@ impl Cor {
     }
 }
 
+impl std::fmt::Display for Cor {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("(")?;
+        for (i, item) in self.items.iter().enumerate() {
+            if i > 0 {
+                f.write_str(" || ")?;
+            }
+            item.fmt(f)?;
+        }
+        f.write_str(")")
+    }
+}
+
 impl SigmaSerializable for Cor {
     fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
         w.put_u16(self.items.len() as u16)?;
diff --git a/ergotree-ir/src/sigma_protocol/sigma_boolean/cthreshold.rs b/ergotree-ir/src/sigma_protocol/sigma_boolean/cthreshold.rs
index 8db1243d6..125661d68 100644
--- a/ergotree-ir/src/sigma_protocol/sigma_boolean/cthreshold.rs
+++ b/ergotree-ir/src/sigma_protocol/sigma_boolean/cthreshold.rs
@@ -81,6 +81,21 @@ impl Cthreshold {
     }
 }
 
+impl std::fmt::Display for Cthreshold {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("atLeast(")?;
+        f.write_str(self.k.to_string().as_str())?;
+        f.write_str(", (")?;
+        for (i, item) in self.children.iter().enumerate() {
+            if i > 0 {
+                f.write_str(", ")?;
+            }
+            item.fmt(f)?;
+        }
+        f.write_str(")")
+    }
+}
+
 impl HasStaticOpCode for Cthreshold {
     const OP_CODE: OpCode = OpCode::ATLEAST;
 }
diff --git a/ergotree-ir/src/source_span.rs b/ergotree-ir/src/source_span.rs
new file mode 100644
index 000000000..a48ae4a54
--- /dev/null
+++ b/ergotree-ir/src/source_span.rs
@@ -0,0 +1,204 @@
+//! Source position for an IR node in the source code
+
+use crate::mir::and::And;
+use crate::mir::bin_op::BinOp;
+use crate::mir::block::BlockValue;
+use crate::mir::byte_array_to_bigint::ByteArrayToBigInt;
+use crate::mir::byte_array_to_long::ByteArrayToLong;
+use crate::mir::coll_append::Append;
+use crate::mir::coll_by_index::ByIndex;
+use crate::mir::coll_exists::Exists;
+use crate::mir::coll_filter::Filter;
+use crate::mir::coll_fold::Fold;
+use crate::mir::coll_forall::ForAll;
+use crate::mir::coll_map::Map;
+use crate::mir::coll_slice::Slice;
+use crate::mir::expr::Expr;
+use crate::mir::extract_reg_as::ExtractRegisterAs;
+use crate::mir::get_var::GetVar;
+use crate::mir::logical_not::LogicalNot;
+use crate::mir::method_call::MethodCall;
+use crate::mir::negation::Negation;
+use crate::mir::option_get::OptionGet;
+use crate::mir::option_get_or_else::OptionGetOrElse;
+use crate::mir::option_is_defined::OptionIsDefined;
+use crate::mir::or::Or;
+use crate::mir::property_call::PropertyCall;
+use crate::mir::select_field::SelectField;
+use crate::mir::subst_const::SubstConstants;
+use crate::mir::tree_lookup::TreeLookup;
+use crate::mir::val_def::ValDef;
+
+/// Source position for the Expr
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+pub struct SourceSpan {
+    /// Start position in the span
+    pub offset: usize,
+    /// The length of the span
+    pub length: usize,
+}
+
+impl SourceSpan {
+    /// Empty span
+    pub fn empty() -> Self {
+        SourceSpan {
+            offset: 0,
+            length: 0,
+        }
+    }
+}
+
+impl From<(usize, usize)> for SourceSpan {
+    fn from(value: (usize, usize)) -> Self {
+        SourceSpan {
+            offset: value.0,
+            length: value.1,
+        }
+    }
+}
+
+impl From<SourceSpan> for miette::SourceSpan {
+    fn from(value: SourceSpan) -> Self {
+        miette::SourceSpan::new(value.offset.into(), value.length.into())
+    }
+}
+
+/// Wrapper for Expr with source position
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct Spanned<T> {
+    /// Source position
+    pub source_span: SourceSpan,
+    /// Wrapped value
+    pub expr: T,
+}
+
+impl<T> Spanned<T> {
+    /// Expression
+    pub fn expr(&self) -> &T {
+        &self.expr
+    }
+}
+
+macro_rules! into_expr {
+    ($variant: ident) => {
+        impl From<$variant> for Expr {
+            fn from(v: $variant) -> Self {
+                Expr::$variant(Spanned {
+                    source_span: SourceSpan::empty(),
+                    expr: v,
+                })
+            }
+        }
+    };
+}
+
+into_expr!(Append);
+into_expr!(BlockValue);
+into_expr!(ValDef);
+into_expr!(BinOp);
+into_expr!(ByIndex);
+into_expr!(SubstConstants);
+into_expr!(ByteArrayToLong);
+into_expr!(ByteArrayToBigInt);
+into_expr!(MethodCall);
+into_expr!(PropertyCall);
+into_expr!(Negation);
+into_expr!(OptionGet);
+into_expr!(OptionIsDefined);
+into_expr!(OptionGetOrElse);
+into_expr!(ExtractRegisterAs);
+into_expr!(Slice);
+into_expr!(Fold);
+into_expr!(Map);
+into_expr!(Filter);
+into_expr!(Exists);
+into_expr!(ForAll);
+into_expr!(SelectField);
+into_expr!(GetVar);
+into_expr!(TreeLookup);
+into_expr!(And);
+into_expr!(Or);
+into_expr!(LogicalNot);
+
+impl<T> From<T> for Spanned<T> {
+    fn from(v: T) -> Self {
+        Spanned {
+            source_span: SourceSpan::empty(),
+            expr: v,
+        }
+    }
+}
+
+impl Expr {
+    /// Source span for the Expr
+    pub fn span(&self) -> SourceSpan {
+        match self {
+            Expr::Append(op) => op.source_span,
+            Expr::Const(_) => SourceSpan::empty(),
+            Expr::ConstPlaceholder(_) => SourceSpan::empty(),
+            Expr::SubstConstants(op) => op.source_span,
+            Expr::ByteArrayToLong(op) => op.source_span,
+            Expr::ByteArrayToBigInt(op) => op.source_span,
+            Expr::LongToByteArray(_) => SourceSpan::empty(),
+            Expr::Collection(_) => SourceSpan::empty(),
+            Expr::Tuple(_) => SourceSpan::empty(),
+            Expr::CalcBlake2b256(_) => SourceSpan::empty(),
+            Expr::CalcSha256(_) => SourceSpan::empty(),
+            Expr::Context => SourceSpan::empty(),
+            Expr::Global => SourceSpan::empty(),
+            Expr::GlobalVars(_) => SourceSpan::empty(),
+            Expr::FuncValue(_) => SourceSpan::empty(),
+            Expr::Apply(_) => SourceSpan::empty(),
+            Expr::MethodCall(op) => op.source_span,
+            Expr::PropertyCall(op) => op.source_span,
+            Expr::BlockValue(op) => op.source_span,
+            Expr::ValDef(op) => op.source_span,
+            Expr::ValUse(_) => SourceSpan::empty(),
+            Expr::If(_) => SourceSpan::empty(),
+            Expr::BinOp(op) => op.source_span,
+            Expr::And(_) => SourceSpan::empty(),
+            Expr::Or(_) => SourceSpan::empty(),
+            Expr::Xor(_) => SourceSpan::empty(),
+            Expr::Atleast(_) => SourceSpan::empty(),
+            Expr::LogicalNot(_) => SourceSpan::empty(),
+            Expr::Negation(op) => op.source_span,
+            Expr::BitInversion(_) => SourceSpan::empty(),
+            Expr::OptionGet(op) => op.source_span,
+            Expr::OptionIsDefined(op) => op.source_span,
+            Expr::OptionGetOrElse(op) => op.source_span,
+            Expr::ExtractAmount(_) => SourceSpan::empty(),
+            Expr::ExtractRegisterAs(op) => op.source_span,
+            Expr::ExtractBytes(_) => SourceSpan::empty(),
+            Expr::ExtractBytesWithNoRef(_) => SourceSpan::empty(),
+            Expr::ExtractScriptBytes(_) => SourceSpan::empty(),
+            Expr::ExtractCreationInfo(_) => SourceSpan::empty(),
+            Expr::ExtractId(_) => SourceSpan::empty(),
+            Expr::ByIndex(op) => op.source_span,
+            Expr::SizeOf(_) => SourceSpan::empty(),
+            Expr::Slice(op) => op.source_span,
+            Expr::Fold(op) => op.source_span,
+            Expr::Map(op) => op.source_span,
+            Expr::Filter(op) => op.source_span,
+            Expr::Exists(op) => op.source_span,
+            Expr::ForAll(op) => op.source_span,
+            Expr::SelectField(op) => op.source_span,
+            Expr::BoolToSigmaProp(_) => SourceSpan::empty(),
+            Expr::Upcast(_) => SourceSpan::empty(),
+            Expr::Downcast(_) => SourceSpan::empty(),
+            Expr::CreateProveDlog(_) => SourceSpan::empty(),
+            Expr::CreateProveDhTuple(_) => SourceSpan::empty(),
+            Expr::SigmaPropBytes(_) => SourceSpan::empty(),
+            Expr::DecodePoint(_) => SourceSpan::empty(),
+            Expr::SigmaAnd(_) => SourceSpan::empty(),
+            Expr::SigmaOr(_) => SourceSpan::empty(),
+            Expr::GetVar(op) => op.source_span,
+            Expr::DeserializeRegister(_) => SourceSpan::empty(),
+            Expr::DeserializeContext(_) => SourceSpan::empty(),
+            Expr::MultiplyGroup(_) => SourceSpan::empty(),
+            Expr::Exponentiate(_) => SourceSpan::empty(),
+            Expr::XorOf(_) => SourceSpan::empty(),
+            Expr::TreeLookup(op) => op.source_span,
+            Expr::CreateAvlTree(_) => SourceSpan::empty(),
+        }
+    }
+}
diff --git a/ergotree-ir/src/type_check.rs b/ergotree-ir/src/type_check.rs
index 53b8bf457..447db8593 100644
--- a/ergotree-ir/src/type_check.rs
+++ b/ergotree-ir/src/type_check.rs
@@ -1,6 +1,7 @@
 //! Type checking
 
 use crate::mir::expr::Expr;
+use crate::source_span::Spanned;
 
 /// Typecheck error
 #[derive(Debug, PartialEq, Eq)]
@@ -24,7 +25,10 @@ impl TypeCheckError {
 pub fn type_check(e: Expr) -> Result<Expr, TypeCheckError> {
     // not really a relevant check, since such kind of check should be in BinOp::new()
     match &e {
-        Expr::BinOp(bin) => {
+        Expr::BinOp(Spanned {
+            source_span: _,
+            expr: bin,
+        }) => {
             if bin.left.tpe() == bin.right.tpe() {
                 Ok(e)
             } else {
diff --git a/ergotree-ir/src/types/sfunc.rs b/ergotree-ir/src/types/sfunc.rs
index f838a3304..1add72303 100644
--- a/ergotree-ir/src/types/sfunc.rs
+++ b/ergotree-ir/src/types/sfunc.rs
@@ -15,6 +15,20 @@ pub struct SFunc {
     pub tpe_params: Vec<STypeParam>,
 }
 
+impl std::fmt::Display for SFunc {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "(")?;
+        for (i, item) in self.t_dom.iter().enumerate() {
+            if i > 0 {
+                write!(f, ", ")?;
+            }
+            item.fmt(f)?;
+        }
+        write!(f, ") => ")?;
+        self.t_range.fmt(f)
+    }
+}
+
 impl SFunc {
     /// Create new SFunc
     pub fn new(t_dom: Vec<SType>, t_range: SType) -> Self {
diff --git a/ergotree-ir/src/types/stuple.rs b/ergotree-ir/src/types/stuple.rs
index 95afe5947..51f992d14 100644
--- a/ergotree-ir/src/types/stuple.rs
+++ b/ergotree-ir/src/types/stuple.rs
@@ -34,6 +34,19 @@ impl std::fmt::Debug for STuple {
     }
 }
 
+impl std::fmt::Display for STuple {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "(")?;
+        for (i, item) in self.items.iter().enumerate() {
+            if i > 0 {
+                write!(f, ", ")?;
+            }
+            item.fmt(f)?;
+        }
+        write!(f, ")")
+    }
+}
+
 impl STuple {
     /// Create a tuple type for a given type pair
     pub fn pair(t1: SType, t2: SType) -> Self {
diff --git a/ergotree-ir/src/types/stype.rs b/ergotree-ir/src/types/stype.rs
index eba90fea1..bc926bdfa 100644
--- a/ergotree-ir/src/types/stype.rs
+++ b/ergotree-ir/src/types/stype.rs
@@ -2,6 +2,7 @@
 
 use std::collections::HashMap;
 use std::convert::TryInto;
+use std::fmt::Debug;
 
 use impl_trait_for_tuples::impl_for_tuples;
 
@@ -126,6 +127,34 @@ impl From<SFunc> for SType {
     }
 }
 
+impl std::fmt::Display for SType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            SType::STypeVar(t) => write!(f, "{}", t.as_string()),
+            SType::SAny => write!(f, "Any"),
+            SType::SUnit => write!(f, "Unit"),
+            SType::SBoolean => write!(f, "Boolean"),
+            SType::SByte => write!(f, "Byte"),
+            SType::SShort => write!(f, "Short"),
+            SType::SInt => write!(f, "Int"),
+            SType::SLong => write!(f, "Long"),
+            SType::SBigInt => write!(f, "BigInt"),
+            SType::SGroupElement => write!(f, "GroupElement"),
+            SType::SSigmaProp => write!(f, "SigmaProp"),
+            SType::SBox => write!(f, "Box"),
+            SType::SAvlTree => write!(f, "AvlTree"),
+            SType::SOption(t) => write!(f, "Option[{}]", t),
+            SType::SColl(t) => write!(f, "Coll[{}]", t),
+            SType::STuple(t) => write!(f, "{}", t),
+            SType::SFunc(t) => write!(f, "{}", t),
+            SType::SContext => write!(f, "Context"),
+            SType::SHeader => write!(f, "Header"),
+            SType::SPreHeader => write!(f, "PreHeader"),
+            SType::SGlobal => write!(f, "Global"),
+        }
+    }
+}
+
 /// Conversion to SType
 pub trait LiftIntoSType {
     /// get SType


Test patch: