Contributing to crashlink
First off, thank you for considering contributing to crashlink! We welcome any contribution, from fixing a typo in the documentation to implementing a new decompiler optimization pass. Every little bit helps.
This guide will help you get started. Please don't hesitate to ask for help if you get stuck!
Getting Started
Before you begin, make sure you have the following prerequisites installed:
- Python 3.10+ (3.13+ is preferred)
- just (recommended, but not required)
- uv (HIGHLY recommended for package management, but also not required)
- Graphviz (for generating CFG diagrams)
You can set up your development environment by following the instructions in the Development section of the README. TL;DR:
git clone https://github.com/N3rdL0rd/crashlink
cd crashlink
uv sync --extra dev
just test # or pytest
just has a handful of recipes to cover most of what you'll need day to day:
just dev: Full development workflow, runs format, check, test, and docs in sequence. Run this before committing.just install: Install dev dependencies and the package in editable mode.just build: Build the package.just build-tests: Compile the Haxe test samples intests/haxe/to.hl.just test: Run the test suite.just format: Format the codebase withruff.just check: Run static type checking (ty).just docs: Regenerate the API reference and build the Docusaurus site.just open-docs: Serve the docs site locally with live reload (needs the API reference to already exist, so runjust docsat least once first).just serve-docs: Serve the already-built docs site locally, without live reload.just clean: Remove build artifacts.
For the decompiler regression suite (crashtest), see its dedicated page.
How to Contribute
The general workflow for contributing is:
- Find an issue or feature to work on. You can check the open issues or the Roadmap in the README. If you have a new idea, please open an issue first to discuss it.
- Fork the repository to your own GitHub account.
- Create a new branch for your changes (e.g.,
git checkout -b feature/new-optimizer). - Make your changes. See the "Areas for Contribution" section below for guidance on specific parts of the codebase.
- Write tests for your changes. We take testing semi-seriously, but sometimes testing decompiler stuff is hard and we get it.
- Format your code and ensure all checks pass by running
just dev. This will run formatting, tests, and build the documentation. - Commit your changes with a clear and descriptive message.
- Push your branch to your fork and open a Pull Request against the main repository.
Areas for Contribution
crashlink is divided into several modules. Here's each part and what it does.
1. The Command-Line Interface (__main__.py)
Adding a new command is straightforward.
How to add a command:
- Open
crashlink/__main__.py. - In the
Commandsclass, add a new method for your command. - (Optional) Use the
@alias(...)decorator to add shortcuts. - Write a clear docstring for the command. The first line is the description, and a
...block specifies the usage string.
Example: Adding a stats command
@alias("st")
def stats(self, args: List[str]) -> None:
"""Prints some basic statistics about the bytecode. `stats`"""
print(f"Total Functions: {len(self.code.functions)}")
print(f"Total Strings: {len(self.code.strings.value)}")
print(f"Total Types: {len(self.code.types)}")
2. The Core Parser (core.py)
This is the heart of crashlink. Contributions here are for supporting new (or old) HashLink bytecode versions or fixing fundamental parsing errors.
- When to modify: When a new version of HashLink adds a new field to a structure (like
FunctionorObj), or changes how something is serialized, or when a core datatype is missing a good utility method that makes other code less verbose or difficult to work with. - What to do:
- Modify the
deserialiseandserialisemethods of the relevant class incore.py. - Add a new Haxe source file to
tests/haxe/that uses the new feature. - Run
just build-tests. This will compile your Haxe file to a.hlfile that will be used in the automated tests. - The existing test suite will automatically pick up the new
.hlfile and run a "round-trip" test (deserialize -> serialize -> compare).
- Modify the
3. The Disassembler (disasm.py)
This component is all about making the low-level bytecode human-readable. Contributions here often involve improving the text output.
- How to contribute:
- Improve Pseudocode: Modify the
pseudo_from_opfunction to provide a better one-line summary for an opcode. - Improve Formatting: Change the
fmt_oporfunc_headerfunctions to make the output clearer or more informative. - Add New Helpers: You could add new functions like
is_privateorget_class_for_methodif you can find a reliable heuristic.
- Improve Pseudocode: Modify the
4. The Decompiler (decomp.py)
This is the most complex and exciting area to contribute to. The goal is to transform low-level opcodes into a high-level, structured Intermediate Representation (IR).
The pipeline is: CFG -> IR Lifter -> IR Optimizers -> Final IR
Lifting a New Opcode
When an opcode isn't yet understood by the decompiler, it's represented as an IRUntranslatedOpcode. The goal is to replace this with a more meaningful IR node.
Example: Lifting the Neg opcode
- Find the lifting logic: Open
decomp.pyand go to the_lift_blockmethod in theIRFunctionclass. - Find the
elseblock: At the end of thefor op in enumerate(node.ops):loop, find the finalelse:that handles untranslated opcodes. - Add your logic: Add an
elif op.op == "Neg":block before the finalelse. - Create the IR: The
Negopcode is likedst = -src. This can be represented asdst = 0 - srcusing existing IR nodes.
# In IRFunction._lift_block
# ... inside the loop ...
elif op.op == "Mov":
# ...
# Add your new block here
elif op.op == "Neg":
dst_local = self.locals[op.df["dst"].value]
src_local = self.locals[op.df["src"].value]
zero_const = IRConst(self.code, IRConst.ConstType.INT, value=0)
# Create '0 - src' expression
arith_expr = IRArithmetic(self.code, zero_const, src_local, IRArithmetic.ArithmeticType.SUB)
# Create 'dst = (0 - src)' assignment
assign_stmt = IRAssign(self.code, dst_local, arith_expr)
block.statements.append(assign_stmt)
elif op.op in ["NullCheck", #...
# ...
- Add a test: Create a Python test in the
tests/directory that decompiles a function using theNegopcode and asserts that the resulting IR is correct.
Writing an IR Optimizer
Optimizers transform the IR to make it simpler and more readable. They are classes that inherit from TraversingIROptimizer.
Example: A simple if (true) optimizer
- Create a new class: In
decomp.py, create a new optimizer class.
class IRIfTrueOptimizer(TraversingIROptimizer):
"""
Simplifies `if (true) { ... } else { ... }` to just the true-block.
"""
def visit_conditional(self, conditional: IRConditional) -> None:
# Check if the condition is an IRConst boolean with value True
if (
isinstance(conditional.condition, IRConst) and
conditional.condition.const_type == IRConst.ConstType.BOOL and
conditional.condition.value is True
):
# This is where you would implement the logic to replace
# the IRConditional node with the statements from its true_block.
# This is a complex operation that requires modifying the parent block.
# For a real implementation, you would need a more robust way
# to replace a node.
dbg_print(f"IfTrueOptimizer: Found a foldable if-true at {conditional}")
- Add it to the pipeline: In
IRFunction.__init__, add your new optimizer to theself.optimizerslist in the desired order.
5. The Pseudocode Generator (pseudo.py)
This is the final step, turning the optimized IR into Haxe code. Contributions here involve changing how an IR node is "pretty-printed".
- How to contribute:
- To change how an Expression (like
IRArithmetic, a subclass ofIRArithmetic) is printed, modify_expression_to_haxe. - To change how a Statement (like
IRConditional, a subclass ofIRStatement) is printed, modify_generate_statements.
- To change how an Expression (like
For example, to change if (cond) to if cond (without parentheses), you would find this line in _generate_statements:
output_lines.append(f"{indent}if ({cond_str}) {")
and change it to:
output_lines.append(f"{indent}if {cond_str} {")
Thank you again for your interest in making crashlink better! This project desperately needs new contributors, so please, pretty please, consider contributing.
💖 N3rdL0rd