/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use super::{Op, SExprCode};
/// Renders an expr op tree back into a human-readable string. Used by the
/// netdebug backend so debug clients can see what an expr does. The output
/// is informational: it recompiles to an equivalent expr when restricted to
/// the compiler-expressible subset, but ops without source syntax render in
/// prefix form and u32 constants are tagged to keep their u32-ness visible.
pub fn decompile(code: &SExprCode) -> String {
let stmts: Vec = code.iter().map(render_op).collect();
stmts.join("; ")
}
fn render_op(op: &Op) -> String {
match op {
Op::Null => "null".to_string(),
Op::Add((lhs, rhs)) => format!("({} + {})", render_op(lhs), render_op(rhs)),
Op::Sub((lhs, rhs)) => format!("({} - {})", render_op(lhs), render_op(rhs)),
Op::Mul((lhs, rhs)) => format!("({} * {})", render_op(lhs), render_op(rhs)),
Op::Div((lhs, rhs)) => format!("({} / {})", render_op(lhs), render_op(rhs)),
Op::ConstBool(val) => format!("{val}"),
Op::ConstUint32(val) => format!("u32({val})"),
Op::ConstFloat32(val) => format!("{val}"),
Op::ConstStr(val) => format!("{val:?}"),
Op::LoadVar(var) => var.clone(),
Op::StoreVar((var, val)) => format!("{var} = {}", render_op(val)),
Op::Min((lhs, rhs)) => format!("min({}, {})", render_op(lhs), render_op(rhs)),
Op::Max((lhs, rhs)) => format!("max({}, {})", render_op(lhs), render_op(rhs)),
Op::IsEqual((lhs, rhs)) => format!("({} == {})", render_op(lhs), render_op(rhs)),
Op::LessThan((lhs, rhs)) => format!("({} < {})", render_op(lhs), render_op(rhs)),
Op::Float32ToUint32(val) => format!("as_u32({})", render_op(val)),
Op::IfElse((cond, if_val, else_val)) => {
format!(
"if {} {{ {} }} else {{ {} }}",
render_op(cond),
decompile(if_val),
decompile(else_val)
)
}
Op::NativeFn(_) => "".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::{
super::{Compiler, NativeFnCallback, SExprVal},
*,
};
#[test]
fn render_null() {
assert_eq!(decompile(&vec![Op::Null]), "null");
}
#[test]
fn render_arith() {
let code = vec![Op::Add((
Box::new(Op::ConstUint32(5)),
Box::new(Op::Div((
Box::new(Op::LoadVar("sw".to_string())),
Box::new(Op::ConstUint32(2)),
))),
))];
assert_eq!(decompile(&code), "(u32(5) + (sw / u32(2)))");
}
#[test]
fn render_consts() {
assert_eq!(decompile(&vec![Op::ConstBool(true)]), "true");
assert_eq!(decompile(&vec![Op::ConstBool(false)]), "false");
assert_eq!(decompile(&vec![Op::ConstUint32(110)]), "u32(110)");
assert_eq!(decompile(&vec![Op::ConstFloat32(2.5)]), "2.5");
assert_eq!(decompile(&vec![Op::ConstFloat32(200.)]), "200");
assert_eq!(decompile(&vec![Op::ConstStr("hello".to_string())]), "\"hello\"");
}
#[test]
fn render_vars() {
assert_eq!(decompile(&vec![Op::LoadVar("w".to_string())]), "w");
let code = vec![Op::StoreVar((
"r".to_string(),
Box::new(Op::Div((Box::new(Op::ConstFloat32(10.)), Box::new(Op::ConstFloat32(4.))))),
))];
assert_eq!(decompile(&code), "r = (10 / 4)");
}
#[test]
fn render_ops_without_source_syntax() {
let lhs = Box::new(Op::LoadVar("a".to_string()));
let rhs = Box::new(Op::LoadVar("b".to_string()));
assert_eq!(decompile(&vec![Op::Min((lhs.clone(), rhs.clone()))]), "min(a, b)");
assert_eq!(decompile(&vec![Op::Max((lhs.clone(), rhs.clone()))]), "max(a, b)");
assert_eq!(decompile(&vec![Op::IsEqual((lhs.clone(), rhs.clone()))]), "(a == b)");
assert_eq!(decompile(&vec![Op::LessThan((lhs, rhs))]), "(a < b)");
assert_eq!(
decompile(&vec![Op::Float32ToUint32(Box::new(Op::ConstFloat32(1.6)))]),
"as_u32(1.6)"
);
}
#[test]
fn render_if_else() {
let code = vec![Op::IfElse((
Box::new(Op::LessThan((
Box::new(Op::LoadVar("h".to_string())),
Box::new(Op::ConstFloat32(4.)),
))),
vec![Op::Sub((Box::new(Op::LoadVar("h".to_string())), Box::new(Op::ConstFloat32(1.))))],
vec![Op::Add((
Box::new(Op::Mul((
Box::new(Op::ConstFloat32(2.)),
Box::new(Op::LoadVar("h".to_string())),
))),
Box::new(Op::ConstFloat32(5.)),
))],
))];
assert_eq!(decompile(&code), "if (h < 4) { (h - 1) } else { ((2 * h) + 5) }");
}
#[test]
fn render_native_fn() {
let code = vec![Op::NativeFn(NativeFnCallback(|_| Ok(SExprVal::Null)))];
assert_eq!(decompile(&code), "");
}
#[test]
fn render_stmts() {
let code = vec![
Op::StoreVar(("r".to_string(), Box::new(Op::ConstFloat32(1.)))),
Op::Add((Box::new(Op::LoadVar("r".to_string())), Box::new(Op::ConstFloat32(1.)))),
];
assert_eq!(decompile(&code), "r = 1; (r + 1)");
}
#[test]
fn decompile_compiled() {
let cc = Compiler::new();
let code = cc.compile("h/2 - 200").unwrap();
assert_eq!(decompile(&code), "((h / 2) - 200)");
}
#[test]
fn roundtrip() {
let cc = Compiler::new();
let fixtures = [
"h/2 - 200",
"(x + h/2 + (y + 7)/5) - 200",
"h - 1",
"r = 10 / 4",
"r = 10 / 4;\n\ns = if h < 4 {\n h - 1\n} else {\n 2 * h + 5\n};\n\nr + 1",
];
for src in fixtures {
let once = cc.compile(src).unwrap();
let src2 = decompile(&once);
let twice = cc.compile(&src2).unwrap();
assert_eq!(once, twice, "fixture: {src} rendered as: {src2}");
}
}
}