Skip to content

Commit f7dbc80

Browse files
committed
Initial commit
0 parents  commit f7dbc80

File tree

18 files changed

+1692
-0
lines changed

18 files changed

+1692
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "miden-decompiler"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
clap = { version = "4.5", features = ["derive"] }
8+
miden-assembly-syntax = { version = "0.20", default-features = false, features = ["std", "arbitrary"] }

src/analysis/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! Analysis passes (expression propagation, DCE, etc.).
2+
//!
3+
//! This module currently only contains placeholders to keep the crate layout close to `rewasm`.
4+
5+
/// Placeholder result type for future analysis passes.
6+
#[derive(Debug, Default, Clone, Copy)]
7+
pub struct AnalysisState;

src/callgraph/mod.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::collections::HashMap;
2+
3+
use miden_assembly_syntax::ast::{path::PathBuf as MasmPathBuf, Invoke, InvokeKind, InvocationTarget};
4+
5+
use crate::frontend::{Program, Workspace};
6+
7+
#[derive(Debug, Clone, PartialEq, Eq)]
8+
pub enum CallTarget {
9+
Direct(String),
10+
Opaque,
11+
}
12+
13+
#[derive(Debug, Clone, PartialEq, Eq)]
14+
pub struct CallEdge {
15+
pub kind: InvokeKind,
16+
pub target: CallTarget,
17+
}
18+
19+
#[derive(Debug, Clone)]
20+
pub struct ProcNode {
21+
pub name: String,
22+
pub module_path: MasmPathBuf,
23+
pub edges: Vec<CallEdge>,
24+
}
25+
26+
#[derive(Debug, Default)]
27+
pub struct CallGraph {
28+
pub nodes: Vec<ProcNode>,
29+
pub name_to_id: HashMap<String, usize>,
30+
}
31+
32+
impl CallGraph {
33+
pub fn build(program: &Program) -> Self {
34+
let mut graph = CallGraph::default();
35+
for (idx, proc) in program.procedures().enumerate() {
36+
let module_path = program.module_path().clone();
37+
let module_path_str = <MasmPathBuf as AsRef<str>>::as_ref(&module_path);
38+
let name = format!("{}::{}", module_path_str, proc.name().as_str());
39+
let edges = proc
40+
.invoked()
41+
.map(|invoke| edge_from_invoke(invoke, program))
42+
.collect();
43+
graph.name_to_id.insert(name.clone(), idx);
44+
graph.nodes.push(ProcNode { name, module_path, edges });
45+
}
46+
graph
47+
}
48+
49+
pub fn build_for_workspace(ws: &Workspace) -> Self {
50+
let mut graph = CallGraph::default();
51+
for prog in ws.modules() {
52+
let module_path = prog.module_path().clone();
53+
let module_path_str = <MasmPathBuf as AsRef<str>>::as_ref(&module_path);
54+
for proc in prog.procedures() {
55+
let name = format!("{}::{}", module_path_str, proc.name().as_str());
56+
let idx = graph.nodes.len();
57+
graph.name_to_id.insert(name.clone(), idx);
58+
let edges = proc
59+
.invoked()
60+
.map(|invoke| edge_from_invoke(invoke, prog))
61+
.collect();
62+
graph.nodes.push(ProcNode {
63+
name,
64+
module_path: module_path.clone(),
65+
edges,
66+
});
67+
}
68+
}
69+
graph
70+
}
71+
}
72+
73+
fn edge_from_invoke(invoke: &Invoke, program: &Program) -> CallEdge {
74+
CallEdge {
75+
kind: invoke.kind,
76+
target: match &invoke.target {
77+
InvocationTarget::Symbol(name) => CallTarget::Direct(format!(
78+
"{}::{}",
79+
<MasmPathBuf as AsRef<str>>::as_ref(program.module_path()),
80+
name.as_str()
81+
)),
82+
InvocationTarget::Path(path) => CallTarget::Direct(path.to_string()),
83+
InvocationTarget::MastRoot(_) => CallTarget::Opaque,
84+
},
85+
}
86+
}
87+
88+
pub trait EdgeTargetString {
89+
fn target_string(&self) -> String;
90+
}
91+
92+
impl EdgeTargetString for CallEdge {
93+
fn target_string(&self) -> String {
94+
match &self.target {
95+
CallTarget::Direct(s) => s.clone(),
96+
CallTarget::Opaque => "__opaque__".to_string(),
97+
}
98+
}
99+
}

src/cfg/mod.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//! Control-flow graph structures.
2+
//!
3+
//! These mirror the `rewasm` layout but are trimmed down until stack/SSA plumbing lands.
4+
5+
use crate::ssa::Stmt;
6+
7+
pub type NodeId = usize;
8+
9+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10+
pub struct InstrPos {
11+
pub node: NodeId,
12+
pub instr: usize,
13+
}
14+
15+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16+
pub enum EdgeType {
17+
Unconditional,
18+
Conditional(bool),
19+
}
20+
21+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22+
pub struct EdgeCond {
23+
pub expr_index: u32,
24+
pub edge_type: EdgeType,
25+
}
26+
27+
impl EdgeCond {
28+
pub const fn unconditional() -> Self {
29+
Self {
30+
expr_index: 0,
31+
edge_type: EdgeType::Unconditional,
32+
}
33+
}
34+
}
35+
36+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37+
pub struct Edge {
38+
pub cond: EdgeCond,
39+
pub node: NodeId,
40+
pub back_edge: bool,
41+
}
42+
43+
#[derive(Debug, Default)]
44+
pub struct BasicBlock {
45+
pub code: Vec<Stmt>,
46+
pub next: Vec<Edge>,
47+
pub prev: Vec<Edge>,
48+
}
49+
50+
/// Skeleton CFG used until the builder/structuring passes are in place.
51+
#[derive(Debug, Default)]
52+
pub struct Cfg {
53+
pub nodes: Vec<BasicBlock>,
54+
}

src/fmt/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//! Pretty-printing helpers.
2+
//!
3+
//! This will grow to match the `rewasm` `fmt` module; for now it just exposes a stub writer.
4+
5+
use crate::ssa::Stmt;
6+
7+
#[derive(Default)]
8+
pub struct CodeWriter {
9+
output: String,
10+
indent: usize,
11+
}
12+
13+
impl CodeWriter {
14+
pub fn new() -> Self {
15+
Self::default()
16+
}
17+
18+
pub fn write_stmt(&mut self, stmt: &Stmt) {
19+
self.output.push_str(&format!("{:?}", stmt));
20+
self.output.push('\n');
21+
}
22+
23+
pub fn indent(&mut self) {
24+
self.indent += 1;
25+
}
26+
27+
pub fn dedent(&mut self) {
28+
self.indent = self.indent.saturating_sub(1);
29+
}
30+
31+
pub fn finish(self) -> String {
32+
self.output
33+
}
34+
}

src/frontend/mod.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//! Frontend: parse MASM source into an AST module plus lightweight metadata.
2+
3+
use std::path::{Path as FsPath, PathBuf as FsPathBuf};
4+
5+
use miden_assembly_syntax::{
6+
ast::{path::PathBuf as MasmPathBuf, Module, ModuleKind, Procedure},
7+
debuginfo::DefaultSourceManager,
8+
ModuleParser, Report,
9+
};
10+
use std::sync::Arc;
11+
12+
/// A library root maps a namespace (e.g. "std") to a filesystem directory.
13+
#[derive(Clone, Debug, PartialEq, Eq)]
14+
pub struct LibraryRoot {
15+
pub namespace: String,
16+
pub path: FsPathBuf,
17+
}
18+
19+
impl LibraryRoot {
20+
pub fn new(namespace: impl Into<String>, path: FsPathBuf) -> Self {
21+
Self {
22+
namespace: namespace.into(),
23+
path,
24+
}
25+
}
26+
}
27+
28+
/// Parsed MASM module plus its filesystem origin.
29+
#[derive(Debug)]
30+
pub struct Program {
31+
module: Box<Module>,
32+
source_path: FsPathBuf,
33+
module_path: MasmPathBuf,
34+
}
35+
36+
impl Program {
37+
pub fn from_path(path: impl AsRef<FsPath>, roots: &[LibraryRoot]) -> Result<Self, Report> {
38+
let path = path.as_ref();
39+
let mut parser = ModuleParser::new(ModuleKind::Executable);
40+
41+
let module_name = derive_module_path(path, roots)
42+
.unwrap_or_else(|_| MasmPathBuf::absolute(Module::ROOT));
43+
44+
let source_manager: Arc<dyn miden_assembly_syntax::debuginfo::SourceManager> =
45+
Arc::new(DefaultSourceManager::default());
46+
let module = parser.parse_file(&module_name, path, source_manager)?;
47+
48+
Ok(Self {
49+
module,
50+
source_path: path.to_path_buf(),
51+
module_path: module_name,
52+
})
53+
}
54+
55+
/// Construct a program from an already-parsed module and explicit metadata.
56+
pub fn from_parts(module: Box<Module>, source_path: FsPathBuf, module_path: MasmPathBuf) -> Self {
57+
Self {
58+
module,
59+
source_path,
60+
module_path,
61+
}
62+
}
63+
64+
pub fn module(&self) -> &Module {
65+
&self.module
66+
}
67+
68+
pub fn source_path(&self) -> &FsPathBuf {
69+
&self.source_path
70+
}
71+
72+
pub fn module_path(&self) -> &MasmPathBuf {
73+
&self.module_path
74+
}
75+
76+
pub fn procedures(&self) -> impl Iterator<Item = &Procedure> {
77+
self.module.procedures()
78+
}
79+
}
80+
81+
mod workspace;
82+
pub use workspace::Workspace;
83+
pub mod testing;
84+
85+
/// Derive a MASM module path (e.g. `std::math::u64`) from a filesystem path and library roots.
86+
///
87+
/// Roots are searched in order; the first that contains `file_path` is used. If no root matches,
88+
/// returns an error.
89+
pub fn derive_module_path(
90+
file_path: &FsPath,
91+
roots: &[LibraryRoot],
92+
) -> Result<MasmPathBuf, String> {
93+
let file_name = file_path
94+
.file_name()
95+
.and_then(|f| f.to_str())
96+
.ok_or_else(|| "module path derivation failed: missing file name".to_string())?;
97+
let is_mod = file_name == "mod.masm";
98+
99+
for root in roots {
100+
if let Ok(rel) = file_path.strip_prefix(&root.path) {
101+
let mut comps: Vec<String> = Vec::new();
102+
if !root.namespace.is_empty() {
103+
comps.push(root.namespace.clone());
104+
}
105+
106+
let mut parts: Vec<String> = rel
107+
.components()
108+
.map(|c| c.as_os_str().to_string_lossy().into_owned())
109+
.collect();
110+
if parts.is_empty() {
111+
continue;
112+
}
113+
114+
let file_part = parts.pop().unwrap();
115+
let stem = if is_mod {
116+
parts.pop().unwrap_or_else(|| "mod".to_string())
117+
} else {
118+
FsPath::new(&file_part)
119+
.file_stem()
120+
.and_then(|s| s.to_str())
121+
.map(|s| s.to_string())
122+
.ok_or_else(|| "invalid file stem".to_string())?
123+
};
124+
125+
comps.extend(parts);
126+
comps.push(stem);
127+
128+
let path_str = comps.join("::");
129+
return MasmPathBuf::new(&path_str)
130+
.map_err(|e| format!("invalid module path {path_str}: {e}"));
131+
}
132+
}
133+
134+
Err("module path derivation failed: file not under any library root".to_string())
135+
}

src/frontend/testing.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use std::path::PathBuf;
2+
3+
use miden_assembly_syntax::{
4+
ast::{path::PathBuf as MasmPathBuf, Module, ModuleKind},
5+
debuginfo::{DefaultSourceManager, SourceManager},
6+
ModuleParser,
7+
};
8+
use std::sync::Arc;
9+
10+
use super::{LibraryRoot, Program, Workspace};
11+
12+
/// Build a workspace from in-memory modules specified as (module_path, source).
13+
pub fn workspace_from_modules(mods: &[(&str, &str)]) -> Workspace {
14+
let mut ws = Workspace::new(vec![LibraryRoot::new("", PathBuf::from("."))]);
15+
for (path, source) in mods {
16+
let module_path =
17+
MasmPathBuf::new(path).unwrap_or_else(|_| MasmPathBuf::absolute(Module::ROOT));
18+
let mut parser = ModuleParser::new(ModuleKind::Library);
19+
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
20+
let module =
21+
parser.parse_str(module_path.clone(), *source, source_manager).expect("parse");
22+
let program =
23+
Program::from_parts(module, PathBuf::from(format!("{path}.masm")), module_path);
24+
ws.add_program(program);
25+
}
26+
ws
27+
}

0 commit comments

Comments
 (0)