Skip to content

feat(extract): MQL5 (.mq5/.mqh) extractor - #2481

Open
Dyrokk wants to merge 1 commit into
Graphify-Labs:v8from
Dyrokk:feat/mql5-extractor
Open

feat(extract): MQL5 (.mq5/.mqh) extractor#2481
Dyrokk wants to merge 1 commit into
Graphify-Labs:v8from
Dyrokk:feat/mql5-extractor

Conversation

@Dyrokk

@Dyrokk Dyrokk commented Aug 5, 2026

Copy link
Copy Markdown

What

Adds MQL5 (.mq5/.mqh) extraction — MetaTrader 5 expert advisors, indicators and include headers.

The interesting part is what this doesn't do: MQL5 is C++ plus a handful of trading-specific constructs, so rather than a bespoke extractor it masks the four MQL5-only forms tree-sitter-cpp cannot parse and runs the existing _CPP_CONFIG pipeline over the result, using _extract_generic's source_override. That gets real classes, structs, functions, calls and includes for ~200 lines of new code instead of a second grammar.

The masked constructs:

Construct Example
input / sinput / extern storage classes input double Lots = 0.1;
input group "..." inspector heading input group "=== GENERAL ==="
color literals C'255,128,0'
datetime literals D'2024.01.31 22:00'

Masking blanks bytes in place rather than deleting them, so every byte offset — and therefore every source_location — still matches the original file. Shifting them would silently point every node at the wrong line, which is the kind of bug that is invisible until someone clicks a link.

Inputs

Masking deliberately reduces input double Lots = 0.1; to an ordinary global, so input/extern declarations are recovered separately from the original source and re-attached as nodes, with references edges from the functions that read them.

This is the same call made for Pine in #2480, for the same reason: in an expert advisor the inputs are the strategy's tunable surface, and "which function reads magic_number" is the question people actually ask of one. The declared type is kept as mql5_input_type.

Attribution walks brace depth rather than parsing a second tree — exact here because MQL5 functions are file scope and never nested. String bodies are blanked before counting so a brace inside a literal cannot desynchronise it, and MetaEditor's house style (opening brace on the line after the signature) is handled explicitly, as is a single-line body.

Also: #property version/copyright/link land as metadata on the file node, and .mq5/.mqh join the C++ member-call resolver so CTrade-style object calls resolve.

Validation

Measured on a private corpus of live expert advisors, ~9,000 lines across 6 .mq5 files and 3 .mqh headers:

Parse errors: 196 → 0. Per file, before → after masking: 74→0, 61→0, 53→0, 4→0, 4→0, 0→0. The .mqh headers already parsed clean.

Extraction on the largest EA (3,051 lines): 133 nodes, 267 edges, 64 inputs recovered. Input reads were verified by hand against the source — every references edge for magic_number lands on a function that genuinely reads it (lines 261, 346, 621, 736, 783, 822, 869, 889, 1649), and no intra-file edge dangles.

Tests

tests/fixtures/sample.mq5 + tests/test_mql5.py, 13 cases covering masking (byte-offset preservation, each masked construct), extraction (functions, structs, inputs with their declared types, includes), attribution (enclosing function, single-line body, string-brace desync), the #property header, and a no-dangling-edges guard. One test asserts the fixture parses with zero tree-sitter errors after masking, so a future grammar bump that breaks this shows up immediately.

Full suite on this branch: 3904 passed. The 56 failures on my machine (test_skillgen, test_watch, test_uninstall_scope) are pre-existing on Windows — suite run with -p no:randomly before and after, failure set byte-identical.

Notes

MQL5 is C++ plus a few trading-specific constructs, so this does not add a
bespoke extractor. It masks the four MQL5-only forms tree-sitter-cpp cannot
parse and runs the existing _CPP_CONFIG pipeline over the result via
_extract_generic's source_override:

- input / sinput / extern storage classes
- input group "..." inspector headings (no type, no terminator)
- C'255,128,0' color literals
- D'2024.01.31 22:00' datetime literals

Masking blanks bytes in place rather than deleting them, so every byte offset
-- and therefore every source_location -- still matches the original file. On a
9,000-line corpus of live expert advisors this takes tree-sitter-cpp from 196
parse errors to 0.

Masking deliberately reduces `input double Lots = 0.1;` to an ordinary global,
so input/extern declarations are recovered separately and re-attached as nodes,
with references edges from the functions that read them: in an expert advisor
the inputs are the strategy's tunable surface, and "which function reads
magic_number" is the question people actually ask of one. Attribution walks
brace depth rather than parsing a second tree, which is exact here because MQL5
functions are file scope and never nested; string bodies are blanked first so a
brace inside a literal cannot desynchronise the counter.

#property version/copyright/link land as metadata on the file node, and
.mq5/.mqh join the C++ member-call resolver so CTrade-style object calls
resolve.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR adds MQL5 (.mq5/.mqh) file support to the extraction pipeline. It introduces a new graphify/extractors/mql5.py module and an extract_mql5 function that masks MQL5-only syntax the C++ grammar can't parse, then runs the existing C++ extraction pipeline over the result and separately recovers input/extern declarations plus #property metadata. Supporting changes register the new extensions across detection, analysis, build edge-family maps, the C++ member-call resolver, the dispatch table, README, and CHANGELOG, and a new test file (tests_test_mql5*) is included. The bulk of the remaining changed symbols appear to be renumbered rationale/internal helper identifiers, so the reviewer may want to confirm whether those are substantive changes or incidental renumbering.

Worth a look

  • decode uses errors='replace' for input fact scanning while masking uses raw bytes, causing offset driftgraphify/extract.py:1719 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2462 functions depend on the 624 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract_mql5() — 4 callers, 3 callees

Verification — 2462 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2204 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below.

Comment thread graphify/extract.py
return _extract_generic(path, _CPP_CONFIG)


def extract_mql5(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_mql5()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant