mhlbc is a general-purpose Haxe/HashLink bytecode toolkit. It can parse, inspect, disassemble, analyze, and decompile HashLink bytecode files such as .hl fixtures and game hlboot.dat files into readable Haxe-like pseudocode.
The project is currently focused on Tier 1: Core Decompiler. The real-world benchmark is Farever, a Haxe/Heaps game with unavailable source code. Farever is used as a stress test and prioritization target, but mhlbc remains a general HashLink/Haxe tool. Core behavior must be backed by standard bytecode evidence, fixtures, tests, or diagnostics, not by Farever-only assumptions.
mhlbc does not promise recompilable Haxe source today. Its current output target is readable, evidence-backed Haxe-like pseudocode for inspection, preservation, and reverse engineering.
This README reflects the accepted state after Session 93 (next-frontier readability blocker selection). All measured scopes remain at 0 top-level gotos and 0 errors.
| Area | Accepted state |
|---|---|
| Branch | main |
| Active tier | Tier 1: Core Decompiler |
| Later tiers | Frozen unless explicitly unlocked |
| Full pytest baseline | 997 passed, 5 skipped |
| Guardrails | 226 (225 passed, 1 skipped) |
| Track A | 9/9 fixtures, 3014 functions, 0 errors |
| Track B sample=200 | 200 functions decompiled, 0 errors |
| Track B sample=500 | 500 functions decompiled, 0 errors |
| Structured switches (Track A) | 2 (testSwitch, Enums.hl main) |
| OSwitch remaining (Track A) | 36 (9 nested_oswitch, 27 shared_merge per Session 71) |
| Field-name fallbacks | Track A: 2084, TB200: 58, TB500: 356 |
| ControlStructurer top-level gotos | Track A: 0, TB200: 0, TB500: 0 |
| Current recommendation | Session 93 (diagnostic-only): Next-frontier readability blocker selection finds 2,859,450 source-visible raw register-name occurrences (vN=2,208,016, tN=585,687, uN=65,747) as the #1 measured readability frontier. These are regex hits in generated source, not unique variables or proven naming bugs. vN/tN may include expected compiler temporaries. uN ("used/unknown-style") is more suspicious and is the highest-priority subset for the next diagnostic. Recommended next step: diagnostic-only root-cause classification of raw register-name occurrences, prioritizing uN first, then classifying vN/tN expected temporaries vs actionable fallback naming gaps. No renaming behavior until evidence proves a safe rule. |
mhlbc currently supports:
- HashLink bytecode header parsing.
- Constant pool parsing for ints, floats, strings, and versioned bytes.
- Type, global, native, function, constant, and debug-data parsing.
- Bytecode versions v3, v4, and v5 where version-specific branches are implemented.
- Opcode decoding and instruction listings.
- CFG construction, jump-target recovery, basic-block analysis, and diagnostics.
- IR construction, register naming, expression reconstruction, function signatures, class grouping, enum grouping, and Haxe-like output.
- Headless CLI usage.
- PyQt6 GUI inspection.
- Track A fixture validation.
- Track B Farever sampled validation.
- Quality reports and diagnostic artifacts.
mhlbc intentionally avoids:
- Guessing names, types, fields, ownership, call targets, or control flow.
- Hiding malformed input silently.
- Specializing core behavior for Farever.
- Treating
MEMORY.mdas bytecode documentation. - Starting Tier 2-5 work without an explicit unlock.
mhlbc uses two validation tracks so that real-world progress does not corrupt general bytecode correctness.
Track A is the correctness baseline. It uses compiled Haxe/HL fixtures and protects the project from becoming benchmark-specific.
Current fixture set:
hello.hltypes.hlclasses.hlMain.hlShapes.hlEnums.hlNatives.hlSwitch.hlControlFlow.hl
Accepted status:
| Metric | Value |
|---|---|
| Fixtures | 9/9 |
| Functions | 3014 |
| Errors | 0 |
| Unknown opcodes | 0 |
| Field-name fallbacks | 2084 |
| ControlStructurer top-level gotos | 0 |
Track B is the large real-world benchmark. It measures behavior on Farever without allowing Farever to redefine standard HashLink semantics.
Accepted Farever bytecode identity:
| Property | Value |
|---|---|
| File | workspace/Farever/hlboot.dat |
| MD5 | b85480ed23f04f2efc408e4ebdd208a0 |
| Size | 13,358,488 bytes |
| Bytecode version | v4 |
| Functions | 45,463 |
| Types | 43,906 |
| Globals | 28,492 |
| Natives | 723 |
| Strings | 65,775 |
| Constants | 22,211 |
| Debug files | 2,051 |
| Entrypoint | 46,044 (init) |
Accepted sampled status:
| Sample | Seed | Decompiled | Errors | Field-name fallbacks | ControlStructurer top-level gotos |
|---|---|---|---|---|---|
| 200 | 42 | 200 | 0 | 58 | 22 |
| 500 | 42 | 500 | 0 | 356 | 91 |
Do not reopen these without new evidence.
| Frontier | Status | Accepted conclusion |
|---|---|---|
| Register source/destination semantics | Closed | Opcode register roles were audited; OEnumField operands were resolved as constants where appropriate. |
| Goto and switch diagnostic frontier | Closed | Conditional-jump gotos eliminated by B63+B65. OJAlways switch-case-break gotos eliminated by Sessions 67+68. All measured scopes at 0 top-level gotos. |
| Field-name / TypeResolver diagnostic | Exhausted | Zero recoverable field-name fallbacks were found. Remaining fN names are structural or expected. |
| ControlStructurer feasibility map | Complete | Session 60 feasibility map documented pre-B63 frontier. B63 suppressed header conditional-jump gotos. B65 suppressed no-merge conditional-jump gotos. Sessions 67+68 closed the OJAlways switch-case-break frontier (0 gotos across all scopes). |
| Conditional-jump header goto suppression (B63) | Complete | 7-line fix; 62-75% reduction. |
| Conditional-jump no-merge goto suppression (B65) | Complete | 6-line fix; 100% conditional-jump elimination. Track A: 553 -> 3 (-99.5%). |
| Reproducibility audit | Complete | Session 61 commands reproduced the accepted baseline exactly. |
The following require explicit project-owner approval before implementation:
- Broad ControlStructurer behavior changes.
- TypeResolver or type-system invention.
- Virtual structural typedef invention.
- Goto/label cleanup not backed by a narrow proven CFG class.
- Tier 2, Tier 3, Tier 4, or Tier 5 work.
Design-only planning for ControlStructurer or TypeResolver can proceed without unlocking implementation, but behavior changes need explicit approval.
Parser, CLI, scripts, and tests use Python. Install the repository dependencies first:
python -m pip install -r requirements.txtFor GUI usage, install PyQt6 separately:
python -m pip install PyQt6Haxe is only needed when regenerating compiled .hl fixtures from tests/fixtures/src/*.hx.
The CLI is headless and does not require PyQt6.
python cli.py --version
python cli.py header path/to/file.hl
python cli.py pools path/to/file.hl --preview
python cli.py types path/to/file.hl
python cli.py globals path/to/file.hl
python cli.py natives path/to/file.hl
python cli.py functions path/to/file.hl --limit 50
python cli.py disasm path/to/file.hl --function 0 --cfg
python cli.py decompile path/to/file.hl --function 0
python cli.py decompile path/to/file.hl --output-dir out_haxeCommon flags:
--json
--csv
--warnings-as-errors
-v
-vv
--quiet
--log-level {error,warn,info,debug,trace}
--verbose-stdout
--log-path logs_customCLI exit codes:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Parse error |
| 2 | Input or argument error |
| 3 | Internal tool error |
The GUI is optional.
python app.pyCurrent GUI capabilities:
- Overview tab.
- Strings, Types, Globals, Natives, and Functions browsers.
- CFG view.
- Decompilation view.
- Dark UI.
- Background parsing and decompilation through worker threads.
- Virtualized list views for large files.
Architecture rule: the parser and CLI must remain headless. GUI code must not become a dependency of parser or CLI behavior.
Use these commands for the current accepted baseline (post-Session 93).
# Full pytest baseline
cd ~/mhlbc && ~/.local/bin/uv run pytest --tb=no -qExpected accepted result:
997 passed, 5 skipped
# Guardrails (226 tests: B38-B55 + B63 + Sessions 67-91)
cd ~/mhlbc && ~/.local/bin/uv run pytest --tb=no -q -k "B38 or B39 or B40 or B41 or B42 or B43 or B44 or B45 or B46 or B47 or B48 or B49 or B50 or B51 or B52 or B53 or B54 or B55 or B63 or Session67 or Session68 or Session69 or Session70 or Session71 or Session72 or Session73 or Session74 or Session75 or Session76 or Session78 or Session79 or Session80 or Session81 or Session82 or Session83 or Session85 or Session86 or Session88 or Session89 or Session91"Expected accepted result:
225 passed, 1 skipped
# Track A quality report
cd ~/mhlbc && ~/.local/bin/uv run python3 scripts/decompiler_quality_report.py --track A
# Track B quality report (sample=200, seed=42)
cd ~/mhlbc && ~/.local/bin/uv run python3 scripts/decompiler_quality_report.py --track B --farever workspace/Farever/hlboot.dat --sample 200
# Track B quality report (sample=500, seed=42)
cd ~/mhlbc && ~/.local/bin/uv run python3 scripts/decompiler_quality_report.py --track B --farever workspace/Farever/hlboot.dat --sample 500
# Field-name / TypeResolver fallback diagnostic
cd ~/mhlbc && ~/.local/bin/uv run python3 scripts/analyze_field_name_fallbacks.py
# ControlStructurer feasibility diagnostic
cd ~/mhlbc && ~/.local/bin/uv run python3 scripts/analyze_controlstructurer_feasibility.py
# Session 85 full Farever readability census (bounded pass, 5000 functions)
cd ~/mhlbc && ~/.local/bin/uv run python3 scripts/session85_full_farever_census.py --farever workspace/Farever/hlboot.dat --max-functions 5000Accepted report results:
| Command | Accepted result |
|---|---|
| Track A quality report | 9 fixtures, 3014 functions, 0 errors |
| Track B sample=200 | 200 decompiled, 0 errors |
| Track B sample=500 | 500 decompiled, 0 errors |
| Field-name diagnostic | Track A: 2084, TB200: 58, TB500: 356 |
| ControlStructurer feasibility | Track A: 0, TB200: 0, TB500: 0 (post-Session 68) |
Reports and handoff artifacts should remain ASCII-safe.
Example ASCII check:
cd ~/mhlbc && ~/.local/bin/uv run python3 scripts/check_ascii_safety.py README.md MEMORY.md CONTRIBUTING.md AGENTS.mdFor a default check of process artifacts (README.md, MEMORY.md, TODO.md, CONTRIBUTING.md, AGENTS.md):
cd ~/mhlbc && ~/.local/bin/uv run python3 scripts/check_ascii_safety.pyNote: The default scope excludes technical docs (docs/) and report archives (reports/, decompiler_quality_report/) because they may contain intentional non-ASCII diagram characters. Use explicit path arguments to check those files.
mhlbc/
|-- app.py
|-- cli.py
|-- hl_decompile.py
|-- hl_disasm.py
|-- hl_logger.py
|-- hl_worker.py
|-- logalyzer.py
|-- AGENTS.md
|-- CONTRIBUTING.md
|-- MEMORY.md
|-- README.md
|-- pytest.ini
|-- requirements.txt
|-- docs/
| |-- decompilation_patterns.md
| |-- farever_ghidra_hl_code_read.md
| |-- function_format.md
| |-- getting_started.md
| |-- haxe_compilers.md
| |-- header_format.md
| |-- opcodes.md
| |-- performance_and_scalability.md
| |-- type_system.md
| |-- validation_matrix.md
| |-- varint_encoding.md
| `-- version_deltas.md
|-- hl_parser/
| |-- __init__.py
| |-- _consts.py
| |-- _diagnostics.py
| |-- _exceptions.py
| |-- _parser.py
| |-- _types.py
| |-- _validator.py
| |-- _varint.py
| `-- _version.py
|-- scripts/
| |-- legacy/
| |-- b36_analyze_field_names.py
| |-- b48_analyze_top_level_gotos.py
| |-- b50_analyze_backward_jumps.py
| |-- b51_analyze_forward_to_common_merge.py
| |-- b52_cross_tab.py
| |-- b53_frontier_rebaseline.py
| |-- analyze_field_name_fallbacks.py
| |-- analyze_controlstructurer_feasibility.py
| |-- decompiler_quality_report.py
| |-- extract_b23_null_detail.py
| `-- extract_b31_virtual_detail.py
`-- tests/
|-- __init__.py
|-- hl_helper.py
|-- test_cli.py
|-- test_decompile.py
|-- test_disasm.py
|-- test_field_diag_b6.py
|-- test_fixtures.py
|-- test_logger.py
|-- test_parser.py
|-- test_varint.py
`-- fixtures/src/
|-- Classes.hx
|-- ControlFlow.hx
|-- Enums.hx
|-- Hello.hx
|-- Main.hx
|-- Natives.hx
|-- Shapes.hx
|-- Switch.hx
`-- Types.hx
Layering rules:
hl_parser/is headless and must not import PyQt.cli.pymust remain headless and scriptable.app.pyhandles UI rendering and interaction, not heavy bytecode analysis.- Long-running GUI parsing and decompilation must run through worker threads.
- Backend comes first, CLI second, GUI third.
- Bytecode truth belongs in code, tests,
docs/, and diagnostic evidence.
The full technical specifications live in docs/. This section is only a quick reference.
HashLink bytecode headers are versioned. Version-conditional fields must never be read unconditionally.
Header fields are read in this order:
magicversionflagsnintsnfloatsnstringsnbytesfor version >= 5ntypesnglobalsnnativesnfunctionsnconstantsfor version >= 4entrypoint
Pools are read in this order:
- Int pool.
- Float pool.
- String pool.
- Bytes pool for version >= 5.
- Debug files when valid debug data exists.
- Types.
- Globals.
- Natives.
- Functions.
- Constants for version >= 4.
The string pool includes trailing UINDEX length markers after the raw string payload. Skipping them desynchronizes the stream.
HashLink uses variable-length integer encodings with 1-byte, 2-byte, and 4-byte forms. Multi-byte signed INDEX values use bit 5 (0x20) as the sign bit.
UINDEX uses the same byte layout but rejects negative decoded values. Counts, function indices, register counts, opcode counts, debug-file counts, and OSwitch counts/offsets use unsigned semantics.
- Opcode ID is one raw byte, not a VarInt.
- The opcode table has 103 slots, IDs 0 through 102.
- Fixed-argument opcodes use the synchronized opcode argument table.
- OCallN, OCallMethod, OCallThis, OCallClosure, and OMakeEnum are vararg opcodes.
- OSwitch has its own layout and must not be decoded like OCall-style varargs.
- Function debug info is RLE-encoded per opcode.
mhlbc uses local source-of-truth constants from hl_decompile.py and hl_parser/_consts.py. Do not assume external HashLink reference numbering matches local constants.
Important field-resolution constants:
| Symbol | Value | Meaning |
|---|---|---|
K_FUN |
10 | Function type |
K_OBJ |
11 | Object/class-like field-bearing type |
K_VIRTUAL |
15 | Virtual/anonymous structural type |
K_METHOD |
20 | Method function type, not object field metadata |
K_STRUCT |
21 | Struct field-bearing type |
K_OBJ=11 is already accepted by field resolution. Do not reopen field-kind acceptance without new evidence.
Use evidence-first work.
Before behavior-changing work:
- Classify the task.
- Read the relevant docs for the affected subsystem.
- Inspect current code and tests.
- Inspect fixtures and reports when relevant.
- Collect direct evidence.
- Add or update focused tests or diagnostics.
- Make the smallest safe change.
- Run targeted validation.
- Run broader validation when scope requires it.
- Update docs if proven truth changed.
- Update
MEMORY.mdonly with compact accepted state or handoff. - Report exact scope, files, commands, results, and skipped validation.
Do not:
- Guess bytecode semantics.
- Guess names, types, fields, ownership, call targets, or control flow.
- Reopen solved frontiers without new evidence.
- Mix unrelated cleanup into behavior work.
- Compare metrics across changed classifier definitions without saying so.
- Claim source-visible behavior changed when only IR counters were measured.
- Put volatile project state in
AGENTS.md. - Put long bytecode specifications in
MEMORY.md. - Expand into frozen tiers without explicit approval.
For the full contributor workflow, see CONTRIBUTING.md.
For standing agent behavior, see AGENTS.md.
Use the project documents as the source of truth for the subsystem you are touching.
| Task area | Read first |
|---|---|
| Parser header, pools, versions | docs/header_format.md, docs/varint_encoding.md, docs/version_deltas.md |
| Opcode decoding and function bodies | docs/opcodes.md, docs/function_format.md |
| Types, fields, methods, classes, enums | docs/type_system.md |
| Decompiler, IR, CFG, writer | docs/decompilation_patterns.md, docs/opcodes.md |
| Validation and reports | docs/validation_matrix.md, MEMORY.md |
| Performance and scalability | docs/performance_and_scalability.md |
| Contribution workflow | AGENTS.md, CONTRIBUTING.md |
MEMORY.md records current accepted state and handoff. It is not proof by itself and is not a technical specification.
The project uses named frontier milestones (Session N). The original B-series gates (B38-B60) established the core decompiler foundation. Recent milestones:
- B38--B60: Core infrastructure -- switch structuring, fixture coverage, goto/label diagnostics, field/TypeResolver audits, ControlStructurer feasibility map.
- Sessions 61--65: Conditional-jump goto suppression (B63/B65) -- 100% conditional-jump elimination, 0 top-level gotos across all scopes.
- Sessions 66--71: OJAlways switch-case-break absorption (Sessions 67+68) and OSwitch diagnostic (Session 71) -- 36 remaining Track A OSwitch characterized (9 nested, 27 shared_merge).
- Sessions 72--76: TODO verification, post-switch merge preservation, test tightening, switch case labels, output filename hardening.
- Sessions 80--84: Haxe identifier sanitization, string-literal escaping, GUI cancellation granularity, release-hardening checkpoint.
- Sessions 85--91: Full Farever readability census (Session 85), nested OSwitch structuring (Sessions 86, 88, 89), nested_internal_if_else diagnostic (Sessions 87, 90), shared_merge diagnostic (Session 91) -- OSwitch frontier fully characterized, no safe behavior change exists for remaining patterns.
- Session 92: Documentation/consistency cleanup.
- Session 93: Next-frontier readability blocker selection -- identified 2.86M raw register-name occurrences (vN/tN/uN) as #1 source-visible frontier.
See MEMORY.md for detailed session handoffs and reports/ for canonical milestone reports.
Implemented and under refinement:
- Header and pool parsing.
- Type, global, native, function, and constant parsing.
- Opcode decoding and disassembly.
- CFG construction.
- IR and Haxe-like output.
- CLI and GUI support.
- Track A and Track B validation.
- Diagnostic reporting and frontier classification.
Current Tier 1 state:
- Parser navigation is stable for current fixtures and the accepted Farever benchmark.
- Track A is locked at 9/9 fixtures with 0 errors.
- Track B samples 200 and 500 decompile with 0 errors.
- OSwitch frontier fully characterized (Session 91) -- no safe behavior change exists for remaining patterns.
- Session 93 identified raw register-name occurrences (vN/tN/uN) as the #1 source-visible readability frontier (2.86M occurrences). Next step: diagnostic-only root-cause classification.
- All TODO items resolved or blocked (Session 84 checkpoint).
Not started unless explicitly unlocked.
Possible future scope:
- In-place opcode and constant patching.
- Function injection.
- String replacement.
- Binary fixups for modified bytecode.
Not started unless explicitly unlocked.
Possible future scope:
- Heaps PAK parsing.
- Texture, model, audio, and level extraction.
- Asset replacement workflows.
Not started unless explicitly unlocked.
Possible future scope:
.hdllnative library analysis.- Native binding mapping.
- Heaps and shiroTools runtime interface documentation.
Not started.
Possible future scope:
- Integrated game workspace.
- Bytecode and asset editing.
- Mod packaging.
- Regression testing for modified games.
- Output is Haxe-like pseudocode, not guaranteed recompilable Haxe.
- Some control flow is intentionally emitted as
goto/labelcomments until a safe structuring proof exists. - Field names may fall back to
fNwhen bytecode metadata does not provide a recoverable name. - K_VIRTUAL anonymous structs are represented conservatively.
- TypeResolver invention is paused.
- Broad field recovery is paused.
- Broad ControlStructurer behavior work is paused.
- Try/catch and advanced irreducible control-flow structuring are not broadly solved.
- LLM-based naming, annotation, or semantic invention is outside the deterministic decompiler path.
Farever is the lighthouse, not the map.
When Farever exposes a failure or odd pattern, classify it before changing code:
- General HashLink format bug.
- Standard Haxe compiler pattern not yet handled.
- Robustness or recovery issue.
- Farever/shiroTools-specific quirk.
- Future patching or modding concern outside Tier 1.
Only categories 1 through 3 may change the core parser, disassembler, decompiler, or writer by default. Category 4 must stay isolated behind explicit compatibility logic, diagnostics, or documentation. Category 5 remains frozen.
Known Farever facts:
- Farever uses a Shiro Games
shiroToolsHashLink fork. - The bytecode reader is in
Farever.exe, not inlibhl.dll. - Prior Ghidra work found
hl_read_typematched open-source HashLink behavior. - No extra type kinds are currently known.
- Current parser navigation is resolved for the accepted Farever
hlboot.dat.
See CONTRIBUTING.md for contributor workflow, testing rules, validation/reporting requirements, and release discipline.
Minimum expectations:
- Keep milestones narrow.
- Prove before changing behavior.
- Add or preserve tests.
- Keep reports scoped and reproducible.
- Label Track A and Track B metrics separately.
- Keep generated reports and handoff artifacts ASCII-safe.
- Preserve existing reports and legacy metrics for continuity.
- Avoid Tier 2-5 work unless explicitly unlocked.
MIT