| """N64LLMDecompile - HuggingFace ZeroGPU Space |
| |
| Model: fine-tuned LLM4Decompile-v2 9B (ghidra-refine, merged). Input is Ghidra |
| pseudo-C; output is bare C. Completion-style prompt. |
| |
| Flow: |
| Paste Ghidra pseudo-C -> LLM -> C (+ optional match rating if you also upload |
| the target object to score against) |
| |
| The ONLY thing you must edit is MODEL_ID below (your HF username). Everything else |
| runs as-is; scoring degrades gracefully if its pieces aren't present yet. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import re |
| import shlex |
| import subprocess |
| import tempfile |
| from pathlib import Path |
| from typing import Any |
|
|
| import gradio as gr |
|
|
| try: |
| import diff as asm_differ |
| ASM_DIFFER_OK = True |
| except Exception as exc: |
| asm_differ = None |
| ASM_DIFFER_OK = False |
| ASM_DIFFER_IMPORT_ERROR = str(exc) |
|
|
| try: |
| import spaces |
| GPU = spaces.GPU |
| except Exception: |
| def GPU(fn=None, **_kwargs): |
| if fn is None: |
| return lambda f: f |
| return fn |
|
|
|
|
| |
| |
| |
|
|
| MODEL_ID = "MatthewReingold/llm4decompile-9b-v2-n64-finetune" |
|
|
| MAX_NEW_TOKENS = 2048 |
|
|
| COMPILERS_DIR = (Path(__file__).parent / "compilers").resolve() |
| COMPILER_PATHS = { |
| "ido5.3": str(COMPILERS_DIR / "ido5.3" / "cc"), |
| "ido7.1": str(COMPILERS_DIR / "ido7.1" / "cc"), |
| "gcc2.7.2kmc": str(COMPILERS_DIR / "gcc2.7.2kmc" / "gcc"), |
| "gcc2.8.1pm": str(COMPILERS_DIR / "gcc2.8.1pm" / "gcc"), |
| } |
| COMPILERS = list(COMPILER_PATHS) |
|
|
| FLAG_HINTS = { |
| "ido5.3": "-O2 -mips2 -Xfullwarn -signed -32 ...", |
| "ido7.1": "-O2 -mips2 -Xfullwarn -signed -32 ...", |
| "gcc2.7.2kmc": "-O2 -mips3 -G0 ...", |
| "gcc2.8.1pm": "-O2 -mips3 -G0 ...", |
| } |
|
|
| OBJDUMP = "mips-linux-gnu-objdump" |
| COMPILE_TIMEOUT_SECONDS = 30 |
| OBJDUMP_TIMEOUT_SECONDS = 30 |
| MAX_ASM_LINES = 10000 |
| LANGUAGE_ID = "MIPS:BE:32:default" |
|
|
|
|
| def score_label(score: int) -> str: |
| if score == 0: |
| return "Perfect match" |
| if score <= 10: |
| return "Near-perfect" |
| if score <= 50: |
| return "Close" |
| if score <= 100: |
| return "Partial" |
| if score <= 500: |
| return "Rough" |
| return "Far off" |
|
|
|
|
| |
| |
| |
|
|
| def strip_line_comments(source_code: str) -> str: |
| return re.sub(r"//[^\n]*", "", source_code) |
|
|
|
|
| def write_translation_unit(path: Path, context: str, source_code: str) -> None: |
| pieces = [] |
| if context.strip(): |
| pieces.append(strip_line_comments(context.rstrip())) |
| pieces.append(source_code.rstrip()) |
| path.write_text("\n\n".join(pieces) + "\n", encoding="utf-8") |
|
|
|
|
| def make_empty_stub_source(name: str) -> str: |
| if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name or ""): |
| name = "stub" |
| return f"void {name}(void) {{\n}}\n" |
|
|
|
|
| def run_command(command: list[str], timeout_seconds: int, |
| env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]: |
| return subprocess.run( |
| command, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| text=True, timeout=timeout_seconds, env=env, |
| ) |
|
|
|
|
| def compiler_command(compiler_name: str, flags: str, |
| source_path: Path, output_path: Path) -> list[str] | None: |
| compiler_path = COMPILER_PATHS.get(compiler_name) |
| if compiler_path is None: |
| return None |
| compiler = os.path.expanduser(compiler_path) |
| return [compiler, *shlex.split(flags or ""), "-c", str(source_path), "-o", str(output_path)] |
|
|
|
|
| def compile_source(compiler_name: str, flags: str, context: str, work_dir: Path, |
| source_code: str, output_name: str) -> tuple[Path | None, dict | None]: |
| source_path = work_dir / f"{output_name}.c" |
| output_path = work_dir / f"{output_name}.o" |
| write_translation_unit(source_path, context, source_code) |
|
|
| command = compiler_command(compiler_name, flags, source_path, output_path) |
| if command is None: |
| return None, {"type": "unsupported_compiler", "compiler": compiler_name} |
|
|
| env = os.environ.copy() |
| compiler_dir = os.path.dirname(os.path.expanduser(command[0])) |
| env["PATH"] = compiler_dir + ":" + env.get("PATH", "") |
|
|
| try: |
| completed = run_command(command, COMPILE_TIMEOUT_SECONDS, env=env) |
| except FileNotFoundError as exc: |
| return None, {"type": "compiler_not_found", "command": command, "error": str(exc)} |
| except subprocess.TimeoutExpired: |
| return None, {"type": "compile_timeout", "timeout_seconds": COMPILE_TIMEOUT_SECONDS} |
|
|
| if completed.returncode != 0 or not output_path.exists(): |
| return None, { |
| "type": "compile_failed", |
| "returncode": completed.returncode, |
| "stdout": completed.stdout, |
| "stderr": completed.stderr, |
| } |
| return output_path, None |
|
|
|
|
| def make_diff_config() -> Any: |
| return asm_differ.Config( |
| arch=asm_differ.get_arch("mips"), |
| diff_obj=True, file=None, make=False, source_old_binutils=False, |
| diff_section=".text", inlines=False, |
| max_function_size_lines=MAX_ASM_LINES, |
| max_function_size_bytes=MAX_ASM_LINES * 4, |
| formatter=asm_differ.PythonFormatter(arch_str="mips"), |
| diff_mode=asm_differ.DiffMode.NORMAL, |
| base_shift=0, skip_lines=0, compress=None, |
| show_rodata_refs=True, show_branches=True, show_line_numbers=False, |
| show_source=False, stop_at_ret=False, ignore_large_imms=False, |
| ignore_addr_diffs=False, algorithm="levenshtein", |
| reg_categories={}, diff_function_symbols=False, |
| ) |
|
|
|
|
| def strip_objdump_headers(objdump_output: str) -> str: |
| lines, in_disassembly = [], False |
| for line in objdump_output.splitlines(): |
| if line.startswith("Disassembly of section "): |
| in_disassembly = True |
| continue |
| if in_disassembly: |
| lines.append(line) |
| return "\n".join(lines) |
|
|
|
|
| def disassemble_elf(path: Path, config: Any) -> str: |
| command = [OBJDUMP, *config.arch.arch_flags, "-dr", str(path)] |
| completed = run_command(command, OBJDUMP_TIMEOUT_SECONDS) |
| if completed.returncode != 0: |
| raise RuntimeError(completed.stderr or completed.stdout) |
| if hasattr(asm_differ, "preprocess_objdump_out"): |
| return asm_differ.preprocess_objdump_out(None, path.read_bytes(), completed.stdout, config) |
| return strip_objdump_headers(completed.stdout) |
|
|
|
|
| def score_disassembly(expected_disasm: str, actual_disasm: str, config: Any) -> int: |
| expected_lines = asm_differ.process(expected_disasm, config) |
| actual_lines = asm_differ.process(actual_disasm, config) |
| return int(asm_differ.do_diff(expected_lines, actual_lines, config).score) |
|
|
|
|
| def compile_and_score(generated_c: str, object_bytes: bytes, compiler: str, |
| flags: str, context: str, function_name: str) -> dict: |
| if not ASM_DIFFER_OK: |
| return {"status": "scoring_unavailable"} |
| if compiler not in COMPILER_PATHS: |
| return {"status": "unsupported_compiler", "compiler": compiler} |
|
|
| config = make_diff_config() |
| predicted = strip_line_comments(generated_c) |
|
|
| with tempfile.TemporaryDirectory(prefix="n64_space_") as tmp_name: |
| tmp = Path(tmp_name) |
| expected_path = tmp / "expected.o" |
| expected_path.write_bytes(object_bytes) |
|
|
| actual_path, error = compile_source(compiler, flags, context, tmp, predicted, "actual") |
| if error is not None: |
| return {"status": error["type"], "error": error, "compiler": compiler} |
|
|
| baseline_path, baseline_error = compile_source( |
| compiler, flags, context, tmp, make_empty_stub_source(function_name), "baseline" |
| ) |
|
|
| try: |
| expected_disasm = disassemble_elf(expected_path, config) |
| actual_disasm = disassemble_elf(actual_path, config) |
| score = score_disassembly(expected_disasm, actual_disasm, config) |
| except Exception as exc: |
| return {"status": "diff_failed", "error": str(exc), "compiler": compiler} |
|
|
| result = {"status": "evaluated", "score": score, "compiler": compiler} |
| if baseline_error is None: |
| try: |
| baseline_disasm = disassemble_elf(baseline_path, config) |
| result["baseline_score"] = score_disassembly(expected_disasm, baseline_disasm, config) |
| except Exception: |
| pass |
| return result |
|
|
|
|
| |
| |
| |
| |
|
|
| _MODEL = None |
| _TOKENIZER = None |
| _MODEL_LOAD_ERROR = None |
| try: |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
| _TOKENIZER = AutoTokenizer.from_pretrained(MODEL_ID) |
| _MODEL = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype="auto") |
| except Exception as exc: |
| _MODEL_LOAD_ERROR = str(exc) |
|
|
|
|
| def l4d_prompt(pseudo_c: str) -> str: |
| return f"# This is the assembly code:\n{pseudo_c}\n# What is the source code?\n" |
|
|
|
|
| @GPU |
| def generate_c(pseudo_c: str) -> str: |
| prompt = l4d_prompt(pseudo_c) |
| if _MODEL is None or _TOKENIZER is None: |
| msg = _MODEL_LOAD_ERROR or "model not loaded — check MODEL_ID" |
| return f"/* model unavailable: {msg} */\n/* prompt that would be sent:\n\n{prompt}\n*/\n" |
|
|
| _MODEL.to("cuda") |
| inputs = _TOKENIZER(prompt, return_tensors="pt").to("cuda") |
| out = _MODEL.generate( |
| **inputs, max_new_tokens=MAX_NEW_TOKENS, do_sample=False, |
| repetition_penalty=1.1, pad_token_id=_TOKENIZER.eos_token_id, |
| ) |
| completion = out[0][inputs["input_ids"].shape[1]:] |
| return _TOKENIZER.decode(completion, skip_special_tokens=True) |
|
|
|
|
| |
| |
| |
|
|
| def render_rating(result: dict | None, scored: bool) -> str: |
| if not scored: |
| return ("**No match rating.** Upload the target object alongside your paste " |
| "(or use object mode) to score the compiled output against it.") |
| if result is None: |
| return "" |
| status = result.get("status") |
| if status == "scoring_unavailable": |
| return ("**Scoring unavailable** — `diff.py` (asm-differ) isn't in the repo yet. " |
| "The C is below; add diff.py to enable match ratings.") |
| if status == "unsupported_compiler": |
| return f"**Unsupported compiler** `{result.get('compiler')}`. The C is below." |
| if status == "compiler_not_found": |
| return ("**Compiler binary not found** — check that `compilers/` is in the Space " |
| "and executable. The C is below.") |
| if status == "compile_timeout": |
| return "**Compile timed out.** The C is below — you may be able to simplify it." |
| if status == "compile_failed": |
| err = (result.get("error") or {}).get("stderr") or "compile error" |
| return (f"**Did not compile** with `{result.get('compiler')}`. " |
| f"The C is below anyway — you may be able to fix it.\n\n```\n{err[:600]}\n```") |
| if status == "diff_failed": |
| return f"**Diff failed:** {str(result.get('error'))[:300]}. The C is below." |
|
|
| score = result["score"] |
| line = f"**{score_label(score)}** — asm-differ score `{score}` (0 = byte-perfect, lower is better)." |
| base = result.get("baseline_score") |
| if isinstance(base, int): |
| verb = "beats" if score < base else "does not beat" |
| line += f"\n\nThis {verb} the empty-stub baseline (`{base}`)." |
| return line |
|
|
|
|
| |
| |
| |
|
|
| def _read_bytes(file_path) -> bytes | None: |
| if file_path is None: |
| return None |
| with open(file_path, "rb") as handle: |
| return handle.read() |
|
|
|
|
| def run(pasted_pseudo_c, paste_object_file, compiler, flags, context): |
| context = context or "" |
|
|
| pseudo_c = (pasted_pseudo_c or "").strip() |
| if not pseudo_c: |
| return "", "**Paste some Ghidra pseudo-C to start.**" |
| generated_c = generate_c(pseudo_c) |
|
|
| object_bytes = _read_bytes(paste_object_file) |
| if object_bytes is not None: |
| result = compile_and_score(generated_c, object_bytes, compiler, flags, context, "") |
| return generated_c, render_rating(result, scored=True) |
| return generated_c, render_rating(None, scored=False) |
|
|
|
|
| |
| |
| |
|
|
| CSS = """ |
| .gradio-container { max-width: 1100px !important; } |
| #out-c textarea, #pseudo textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } |
| """ |
|
|
| with gr.Blocks(title="N64LLMDecompile", theme=gr.themes.Base(), css=CSS) as demo: |
| gr.Markdown( |
| "# N64LLMDecompile\n" |
| "Turn N64 MIPS into C with LLM4Decompile-v2 fine-tuned on decomp.me. " |
| "Paste Ghidra pseudo-C to get C back — optionally upload the target object " |
| "to score the result with a match rating." |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| pasted_pseudo_c = gr.Textbox( |
| label="Ghidra pseudo-C", placeholder="Paste the decompiler output here…", |
| lines=12, elem_id="pseudo", |
| ) |
| paste_object_file = gr.File( |
| label="Target object for scoring (optional) — upload to rate the output", |
| file_types=[".o", ".elf", ".bin"], type="filepath", |
| ) |
|
|
| compiler = gr.Dropdown(choices=COMPILERS, value="ido7.1", |
| label="Compiler (for scoring)") |
| flags = gr.Textbox(label="Compiler flags (for scoring)", |
| placeholder=FLAG_HINTS["ido7.1"]) |
| context = gr.Textbox( |
| label="Context / headers (optional, for scoring)", |
| placeholder="typedefs, structs, externs the function needs to compile", lines=4, |
| ) |
| run_btn = gr.Button("Decompile to C", variant="primary") |
|
|
| with gr.Column(scale=1): |
| out_c = gr.Code(label="Generated C", language="c", elem_id="out-c") |
| rating = gr.Markdown() |
|
|
| compiler.change(lambda c: gr.update(placeholder=FLAG_HINTS.get(c, "")), |
| inputs=compiler, outputs=flags) |
| run_btn.click( |
| run, |
| inputs=[pasted_pseudo_c, paste_object_file, compiler, flags, context], |
| outputs=[out_c, rating], |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch() |