use std::collections::HashMap;
use stellar_xdr::curr::{ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef};
use crate::ir::{Expr, FunctionIR, Statement};
use crate::wasm_analysis::{AnalyzedBlock, AnalyzedModule, StackValue, TrackedHostCall};
pub mod host_calls;
pub mod val_decoding;
pub mod optimization;
mod guard_analysis;
mod error_branches;
mod loop_patterns;
mod token_fix;
mod param_subst;
mod event_recognition;
mod storage_keys;
pub use val_decoding::{
strip_val_boilerplate, extract_u32_val, decode_keys_from_linear_memory,
};
pub struct RecognitionContext<'a> {
pub analyzed: &'a AnalyzedModule,
pub all_entries: &'a [ScSpecEntry],
pub param_names: Vec<String>,
pub call_result_names: HashMap<usize, String>,
pub memory_strings: HashMap<usize, String>,
pub vec_contents: &'a HashMap<usize, Vec<StackValue>>,
pub map_contents: &'a HashMap<usize, (Vec<String>, Vec<StackValue>)>,
#[allow(dead_code)]
pub unpack_field_ids: &'a HashMap<usize, Vec<usize>>,
}
pub fn recognize(
analyzed: &AnalyzedModule,
spec: &ScSpecFunctionV0,
all_entries: &[ScSpecEntry],
) -> Option<FunctionIR> {
let export_name = spec.name.to_utf8_string_lossy();
let analysis = analyzed.analyze_export(&export_name).ok()?;
let impl_func_id = analysis.impl_func_id;
let stack_analysis = analyzed.analyze_function_stack(impl_func_id);
let wasm_params = analyzed.wasm_param_count(impl_func_id);
let spec_inputs = spec.inputs.len() as usize;
let has_implicit_env = wasm_params > spec_inputs;
let param_names = param_names_from_spec(spec, has_implicit_env);
let memory_strings = build_memory_strings(&stack_analysis.host_calls, analyzed);
val_decoding::set_memory_strings(&memory_strings);
let map_contents = &stack_analysis.map_contents;
let unpack_field_ids = &stack_analysis.unpack_field_ids;
let preliminary_ctx = RecognitionContext {
analyzed,
all_entries,
param_names: param_names.clone(),
call_result_names: HashMap::new(),
memory_strings: memory_strings.clone(),
vec_contents: &stack_analysis.vec_contents,
map_contents,
unpack_field_ids,
};
let call_result_names = build_call_result_names(
&stack_analysis.host_calls,
&preliminary_ctx,
);
let ctx = RecognitionContext {
call_result_names: call_result_names.clone(),
..preliminary_ctx
};
let mut statements = build_statements_from_blocks(
&stack_analysis.blocks, &ctx,
);
let effective_return = stack_analysis.return_expr.as_ref().and_then(|ret| {
val_decoding::unwrap_val_encoding(ret, &stack_analysis.host_calls)
.or(Some(ret.clone()))
});
if let Some(ret_val) = &effective_return {
let stripped = strip_val_boilerplate(ret_val);
let ret_expr = val_decoding::resolve_arg(&stripped, ¶m_names, &call_result_names);
if guard_analysis::should_emit_return_expr(&ret_expr) {
statements.push(Statement::Return(Some(ret_expr)));
}
}
if std::env::var("DECOMPILER_DEBUG_IR").is_ok() {
eprintln!("[IR] {} statements before optimization:", statements.len());
for (i, s) in statements.iter().enumerate() {
eprintln!(" [{}] {:?}", i, s);
}
}
statements = optimization::eliminate_common_subexprs(statements);
statements = optimization::eliminate_dead_vars(statements);
statements = optimization::collapse_i128_patterns(statements);
statements = optimization::fold_constant_guards(statements);
statements = optimization::hoist_scoped_bindings(statements);
statements = storage_keys::synthesize_tuple_keys(statements, all_entries);
statements = optimization::eliminate_common_subexprs(statements);
statements = optimization::eliminate_identity_bindings(statements);
statements = optimization::reconstruct_increment_pattern(statements);
statements = optimization::reconstruct_struct_mutation(statements);
statements = optimization::split_client_calls(statements);
statements = optimization::inline_single_use_bindings(statements);
statements = optimization::normalize_comparisons(statements);
statements = event_recognition::recognize_event_structs(statements, all_entries);
statements = optimization::eliminate_dead_vars(statements);
statements = param_subst::substitute_param_pass_through(statements, spec, all_entries);
statements = storage_keys::resolve_storage_keys(statements, all_entries);
statements = token_fix::fix_void_token_addresses(statements);
statements = optimization::eliminate_dead_vars(statements);
let is_result = spec.outputs.to_option()
.map_or(false, |t| matches!(t, ScSpecTypeDef::Result(_)));
if is_result {
statements = error_branches::reconstruct_error_branches(statements, all_entries);
}
if statements.is_empty() {
return None;
}
Some(FunctionIR {
name: export_name,
body: statements,
})
}
fn build_memory_strings(
calls: &[TrackedHostCall],
analyzed: &AnalyzedModule,
) -> HashMap<usize, String> {
let mut strings = HashMap::new();
for call in calls {
let name = call.host_func.name;
if name == "symbol_new_from_linear_memory" || name == "string_new_from_linear_memory" {
let ptr = call.args.get(0).and_then(extract_u32_val);
let len = call.args.get(1).and_then(extract_u32_val);
if let (Some(p), Some(l)) = (ptr, len) {
if let Some(bytes) = analyzed.read_linear_memory(p, l) {
if let Ok(s) = String::from_utf8(bytes) {
strings.insert(call.call_site_id, s);
}
}
}
}
}
strings
}
fn build_call_result_names(
calls: &[TrackedHostCall],
ctx: &RecognitionContext,
) -> HashMap<usize, String> {
let mut crn: HashMap<usize, String> = HashMap::new();
let mut name_counts: HashMap<String, usize> = HashMap::new();
let debug = std::env::var("DECOMPILER_DEBUG").is_ok();
for call in calls {
if debug && (call.host_func.name.contains("put_contract_data")
|| call.host_func.name.contains("symbol_new")
|| call.host_func.name.contains("vec_new_from_linear")
|| call.host_func.name.contains("map_new_from_linear"))
{
eprintln!("[CRN] call_site_id={} func={} args={:?}",
call.call_site_id, call.host_func.name,
call.args.iter().take(3).collect::<Vec<_>>());
}
if let Some(mut stmt) = host_calls::recognize_call(call, ctx, &crn) {
if let Statement::Let { name, .. } = &mut stmt {
let base = name.clone();
let count = name_counts
.entry(base.clone())
.or_insert(0);
if *count > 0 {
*name = format!("{base}_{count}");
}
*count += 1;
crn.insert(call.call_site_id, name.clone());
if debug && (call.host_func.name.contains("symbol_new")
|| call.host_func.name.contains("vec_new_from_linear")
|| call.host_func.name.contains("map_new_from_linear")
|| call.host_func.name.contains("put_contract_data"))
{
eprintln!("[CRN] -> Let name='{}' for call_site_id={}", name, call.call_site_id);
}
}
}
if !crn.contains_key(&call.call_site_id) {
let hname = call.host_func.name;
let base: String = match hname {
"vec_new_from_linear_memory" | "vec_new" => "args".into(),
"map_new_from_linear_memory" | "map_new" => "map_val".into(),
"bytes_new_from_linear_memory" | "bytes_new" => "bytes_val".into(),
"obj_from_u128_pieces" => "u128_val".into(),
"obj_from_i128_pieces" => "i128_val".into(),
"obj_from_u64" | "obj_from_i64" | "obj_from_u256_pieces"
| "obj_from_i256_pieces" => "val".into(),
"vec_len" | "map_len" | "bytes_len" => "len".into(),
"vec_get" | "map_get" => "item".into(),
_ => hname
.strip_prefix("bls12_381_")
.or_else(|| hname.strip_prefix("bn254_"))
.unwrap_or(hname)
.replace("_to_", "_")
.into(),
};
let count = name_counts.entry(base.clone()).or_insert(0);
let name = if *count > 0 {
format!("{base}_{count}")
} else {
base
};
*count += 1;
crn.insert(call.call_site_id, name.clone());
if debug && (hname.contains("symbol_new")
|| hname.contains("vec_new_from_linear")
|| hname.contains("map_new_from_linear")
|| hname.contains("put_contract_data"))
{
eprintln!("[CRN] -> heuristic name='{}' for call_site_id={} (func={})", name, call.call_site_id, hname);
}
}
name_unpack_fields(
call,
"map_unpack_to_linear_memory",
ctx,
&mut crn,
);
name_vec_unpack_fields(call, ctx, &mut crn);
}
crn
}
fn name_unpack_fields(
call: &TrackedHostCall,
expected_name: &str,
ctx: &RecognitionContext,
crn: &mut HashMap<usize, String>,
) {
if call.host_func.name != expected_name {
return;
}
let Some(field_ids) =
ctx.unpack_field_ids.get(&call.call_site_id)
else {
return;
};
let keys_ptr = extract_u32_val(
call.args.get(1).unwrap_or(&StackValue::Unknown),
);
let len = extract_u32_val(
call.args.get(3).unwrap_or(&StackValue::Unknown),
);
let (Some(kp), Some(l)) = (keys_ptr, len) else {
return;
};
let Some(keys) = decode_keys_from_linear_memory(
kp, l, ctx.analyzed,
) else {
return;
};
let source = crn
.get(&call.call_site_id)
.cloned()
.unwrap_or_else(|| "unpacked".into());
for (i, fid) in field_ids.iter().enumerate() {
if let Some(key) = keys.get(i) {
crn.insert(*fid, format!("{source}.{key}"));
}
}
}
fn name_vec_unpack_fields(
call: &TrackedHostCall,
ctx: &RecognitionContext,
crn: &mut HashMap<usize, String>,
) {
if call.host_func.name != "vec_unpack_to_linear_memory" {
return;
}
let Some(field_ids) =
ctx.unpack_field_ids.get(&call.call_site_id)
else {
return;
};
let source = crn
.get(&call.call_site_id)
.cloned()
.unwrap_or_else(|| "unpacked".into());
for (i, fid) in field_ids.iter().enumerate() {
crn.insert(*fid, format!("{source}[{i}]"));
}
}
fn build_statements_from_blocks(
blocks: &[AnalyzedBlock],
ctx: &RecognitionContext,
) -> Vec<Statement> {
let mut stmts = Vec::new();
for block in blocks {
match block {
AnalyzedBlock::HostCall(call) => {
if let Some(mut stmt) =
host_calls::recognize_call(call, ctx, &ctx.call_result_names)
{
if let Statement::Let { name, .. } = &mut stmt {
if let Some(assigned) =
ctx.call_result_names
.get(&call.call_site_id)
{
*name = assigned.clone();
}
}
stmts.push(stmt);
}
}
AnalyzedBlock::If {
condition,
then_block,
else_block,
guard_trap,
..
} => {
if *guard_trap {
let cond_expr = match condition {
Some(cond_val) => {
let stripped = strip_val_boilerplate(cond_val);
val_decoding::resolve_arg(
&stripped,
&ctx.param_names,
&ctx.call_result_names,
)
}
None => Expr::Raw("/* condition */".into()),
};
let cond_expr = guard_analysis::decode_val_comparison_constants(cond_expr);
let is_user_precondition = guard_analysis::is_user_comparison(&cond_expr);
if is_user_precondition {
stmts.push(Statement::If {
condition: cond_expr,
then_body: vec![Statement::Expr(
Expr::MacroCall {
name: "panic".into(),
args: vec![Expr::Literal(crate::ir::Literal::Str(
"precondition failed".into(),
))],
},
)],
else_body: vec![],
});
}
let nested = build_statements_from_blocks(else_block, ctx);
stmts.extend(nested);
continue;
}
let then_stmts =
build_statements_from_blocks(then_block, ctx);
let else_stmts =
build_statements_from_blocks(else_block, ctx);
if then_stmts.is_empty() && else_stmts.is_empty() {
continue;
}
let cond_expr = match condition {
Some(cond_val) => {
let stripped = strip_val_boilerplate(cond_val);
val_decoding::resolve_arg(
&stripped,
&ctx.param_names,
&ctx.call_result_names,
)
}
None => Expr::Raw("/* condition */".into()),
};
if then_stmts.is_empty() && !else_stmts.is_empty() {
let negated = Expr::UnOp {
op: crate::ir::UnOp::Not,
operand: Box::new(cond_expr),
};
stmts.push(Statement::If {
condition: negated,
then_body: else_stmts,
else_body: vec![],
});
} else {
stmts.push(Statement::If {
condition: cond_expr,
then_body: then_stmts,
else_body: else_stmts,
});
}
}
AnalyzedBlock::Loop { body, has_back_edge } => {
let body_stmts =
build_statements_from_blocks(body, ctx);
if *has_back_edge {
if let Some(loop_stmt) = loop_patterns::try_recognize_loop_pattern(body, &body_stmts, ctx) {
stmts.push(loop_stmt);
continue;
}
}
stmts.extend(body_stmts);
}
}
}
stmts
}
fn param_names_from_spec(spec: &ScSpecFunctionV0, has_implicit_env: bool) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
if has_implicit_env {
names.push("env".into()); }
for input in spec.inputs.iter() {
names.push(input.name.to_utf8_string_lossy());
}
names
}