Skip to content

feat(languages): add MQL4/MQL5 extractor + mql-lsp-server registration - #823

Merged
zzet merged 13 commits into
zzet:mainfrom
davalillo:pr/mql-language-support
Sep 28, 2026
Merged

zzet merged 13 commits into
zzet:mainfrom
davalillo:pr/mql-language-support

Conversation

@davalillo

@davalillo davalillo commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds MQL4/MQL5 (.mq4/.mq5/.mqh) as a first-class language: bespoke tree-sitter extraction (following the #805 shape: extractor + tests + registration + docs) plus the LSP registration that upgrades MQL edges and unlocks the on-demand action tools.

  • One grammar serves all three extensions: modern MQL4 (build 600+) shares MQL5 syntax; pre-600 legacy sources are a C subset. The grammar, davalillo/tree-sitter-mql5, is independently maintained since 2026 — it began as a fork of mskelton/tree-sitter-mql5 (tree-sitter-cpp + the MQL5 input storage class), but the upstream has been dormant since 2023, so that repo is now the canonical home: full git history preserved, per-contribution attribution in ACKNOWLEDGMENTS.md. It has moved well past the original fork — rebased onto tree-sitter-cpp v0.23.4 (tree-sitter CLI 0.25, ABI 15) with a Go binding and corpus-driven extensions: sinput, interface_specifier, MQL-native C'…'/D'…' literals, input group, MQL primitive types, declarations as un-braced if/else consequences, and empty member declarations after inline constructor bodies. The external scanner is the C scanner of the cpp base (renamed tree_sitter_mql5_*; raw strings are not MQL5), keeping the build free of a C++ toolchain.
  • Extraction reuses the shared machinery (combined query, deferred calls, funcRanges attribution) and adds the MQL surface: input/sinput/extern variables, Meta["methods"] interfaces, methods with EdgeMemberOf, #include imports, Meta["dialect"] on file nodes (content-sniffed for .mqh).
  • LSP: registers mql-lsp-server (my project, davalillo/mql-language-server: an OmniSharp-based stdio LSP) so definition/implementation enrichment produces lsp_resolved edges and get_diagnostics/get_code_actions/fix_all_in_file work on MQL. .mqh deliberately maps to a languageId the server does not short-circuit on, so its LanguageDetection routes headers by content sniffing — a header's dialect isn't knowable from its extension. Plain stdio, no args (the --stdio flag its docs show is accepted and ignored). Install: self-contained binary from GitHub Releases (no .NET runtime) or dotnet tool install -g mql-language-server (.NET 10).
  • Grammar module location (discussed with @zzetorg on Discord): it stays under my account, github.com/davalillo/tree-sitter-mql5 — I'm its maintainer and will keep evolving it alongside mql-language-server. Happy to transfer to gortexhq in the future if that ever becomes the better home.

Corpus audit

The grammar extensions are data-driven, not speculative. Parsing a 53-file MQL corpus with the stock grammar leaves 46 files clean with 37 ERROR nodes; after the extensions, 47 files clean and 10 ERROR nodes — every remaining one is a macro-expansion-only declaration (Controls event map), the same boundary mql-language-server enforces with its macro layer.

On a real 21.4k-line / 720 KB production EA, the stock grammar produced 40 contained ERROR nodes; the two rules added for it (declaration-as-statement, stray semicolon after inline constructor bodies) bring it to zero ERROR nodes. The v0.2.0 grammar also ships a 111-file real-corpus regression harness with CI (fails if the per-file error count rises). 100/100 tree-sitter tests pass, including the full upstream C++ corpus (no C++ regressions).

Extraction performance on that EA: ~145 ms per file (parse + graph), 400 nodes (198 functions, 175 variables, 18 types) and 4,668 edges (4,263 calls).

Changes

File Change
go.mod, go.sum pin github.com/davalillo/tree-sitter-mql5 v0.2.0
internal/parser/tsitter/mql/mql.go grammar shim (dart.go pattern)
internal/parser/languages/mql.go combined-query extractor
internal/parser/languages/mql_test.go happy path, empty input, dialect stamping
internal/parser/languages/register.go register before forest languages
docs/languages.md count 257, matrix row, MQL specifics + not-covered boundaries
internal/parser/languages/mql.go (+test) input variables now mint for array and multi-declarator forms (input double Rates[][6];, extern int A, B;) — previously silently dropped
internal/semantic/lsp/registry.go mql-lsp-server spec (priority 5)
docs/lsp.md server matrix row + install instructions, pinned to the v2.4.2 tag with SHA256 verification (CHECKSUMS.txt) and the full platform asset matrix
docs/languages.md grammar described as independently maintained (no longer 'a fork of mskelton', no 'scanner stub')

Testing

  • New tests added: go test -race ./internal/parser/languages/ -run TestMQL → 7/7
  • go test -race ./internal/semantic/lsp/ → 376/376
  • Meta-tests (extension collisions, coverage clusters) pass; gofmt clean; go build ./... passes
  • go test -race ./... in full — blocked on my machine by a pre-existing glibc heap corruption inside tree-sitter cgo (SIGABRT, moving crash site), reproduced identically at base 2b5480bf with no MQL code present. Happy to file a separate issue with the reproduction details.
    • Addendum (independent environment, Linux/glibc): the same corruption reproduces without -race on ./internal/parser/languages (corrupted size vs. prev_size, moving crash site mid-Go-extractor tests) and at the PR base with zero MQL changes (~5/6 runs), so it is not -race-specific. All MQL tests, the LSP suite (376/376) and meta-tests pass; only the flaky abort interrupts the full-package run.
    • Same environment shows a second, unrelated pre-existing breakage: internal/parser/forest does not compile (go-sitter-forest perl scanner.c vs system gcc/glibc) — no MQL code involved.

Checklist

  • Code follows existing patterns in the codebase (shim over binding, one combined query, shared helpers)
  • No unnecessary abstractions added
  • Language extractor includes Meta["methods"] for interfaces
  • Methods have EdgeMemberOf edges to their containing type

Publishes the grammar foundation for MQL4/MQL5 indexing:

- Pin github.com/davalillo/tree-sitter-mql5 v0.1.0: a maintained fork
  of mskelton/tree-sitter-mql5 (tree-sitter-cpp + the MQL5 'input'
  storage class) with a Go binding added and corpus-driven grammar
  extensions (sinput, interface_specifier, color/datetime literals).
  The fork's external scanner is a C-only stub: MQL5 has no raw string
  literals, so the upstream C++ scanner is dead code there.
- internal/parser/tsitter/mql: thin shim bridging the binding into
  tsitter.Language (dart.go pattern). One grammar serves .mq4/.mq5/
  .mqh — modern MQL4 shares MQL5 syntax since build 600.
- odd/tasks: ODD feature tracking with the corpus audit evidence
  (53-file LSP corpus: 47 clean; remaining errors are macro-expansion-
  only fixtures, left to ERROR tolerance by design).

Full CGO build passes; tsitter package tests pass.
Registers 'mql' (.mq4/.mq5/.mqh) as a bespoke tree-sitter language over
the davalillo/tree-sitter-mql5 fork pinned in the previous commit.

The combined query walks class/struct/enum/interface definitions,
in-body methods, free and template functions, #include directives,
#define macros, and plain/member/template calls. MQL-specific surface:

- input/sinput/extern top-level variables mint variable nodes with
  Meta["storage_class"] — an EA's user-facing configuration surface.
- interface declarations index as KindInterface with Meta["methods"]
  (Java convention) for implementation matching.
- The file node carries Meta["dialect"] (mql4/mql5; .mqh content-
  sniffed with the same MQL5 markers mql-language-server uses).
- Calls attribute to the enclosing function/method via the shared
  funcRanges mechanism; member calls target unresolved::*.<method>.

Known boundary (documented in docs/languages.md): macro-expansion-only
declarations (Controls event map) stay ERROR nodes and mint nothing;
out-of-line member definitions (Cls::method) emit as free functions.

Covered by mql_test.go: happy path (method+member_of, interface
methods, storage-class variables, include edge, call attribution),
empty input, and dialect stamping across all three extensions.
Full package suite aborts with a pre-existing environmental glibc
heap corruption inside tree-sitter cgo — reproduced identically at
the pre-MQL base commit (2b5480b) with a moving crash site; MQL
tests pass in isolation and all meta-tests (extension collisions,
coverage clusters) pass.
Language count 256 -> 257, bespoke tier ~31, core extraction matrix
row, and an MQL specifics section covering dialect stamping, storage-
class variables, interface method sets, includes, call attribution,
and the macro-expansion / out-of-line-member coverage boundaries.
Adds the davalillo/mql-language-server spec to the LSP registry so
enrichment upgrades MQL edges (definition/implementation -> lsp_resolved)
and the on-demand action tools (get_diagnostics, get_code_actions,
apply_code_action, fix_all_in_file) become available for .mq4/.mq5/.mqh.

Routing notes:
- Languages "mql" matches the extractor's language key.
- .mq4/.mq5 short-circuit via languageId; .mqh deliberately maps to a
  non-recognized languageId so the server's LanguageDetection routes
  headers by content sniffing — its documented fallback, since a
  header's dialect is not knowable from its extension.
- The server speaks plain LSP over stdio with no args (the --stdio
  flag its docs show is accepted and ignored).

Install: self-contained binary from GitHub Releases (no .NET needed)
or 'dotnet tool install -g mql-language-server' (.NET 10). Documented
in docs/lsp.md server matrix and install section.
Consumes the ABI 15 parser released as
github.com/davalillo/tree-sitter-mql5 v0.2.0 (tree-sitter-cpp v0.23.4
base, C-only scanner, zero real-corpus errors). Replaces the
development-time local replace directive used for verification.
Registers the grammar fork dependency in the generated third-party
list, following the same pattern as the other tree-sitter grammar
modules.
@davalillo

Copy link
Copy Markdown
Contributor Author

Context: why MQL belongs in a code-intelligence engine

MQL4/MQL5 is the proprietary language of MetaTrader 4/5, the retail trading platform family by MetaQuotes. Far from niche:

  • Per MetaQuotes, "hundreds of brokers and banks worldwide" run MetaTrader 5, serving millions of traders; Finance Magnates Intelligence measured that 83.8% of retail brokers offered MT4/MT5 (Q2 2022).
  • MQL5.com — the language's community hub — reports 7M+ monthly visitors, and its public CodeBase holds 12,000+ programs. On GitHub alone there are ~2,700 MQL5 repos and ~29,500 .mq5 files.
  • The code is financial logic in production: Expert Advisors trading real accounts, often as decade-old, multi-thousand-line files that urgently need code navigation and refactoring tooling. Gortex's graph (callers, usages, blast radius) is precisely the tool that codebase lacks.

The grammar and extractor in this PR were validated against a real 21.4k-line production EA from that ecosystem (zero parse errors after the corpus-driven grammar extensions).

@davalillo
davalillo marked this pull request as draft September 22, 2026 22:07
…arations

emitMqlInputs only read initialized declarators, so 'input double
Rates[][6];', 'extern int A, B;' and 'extern CArrayObj *Ptr;' minted no
variable nodes. Collect one entry per declarator instead, resolving
array/pointer declarators down to their base identifier. Gating on
FieldNameForChild('declarator') is not usable: with comma-separated
declarators only the first carries the field name in the
tree-sitter-c grammar.
…cksum verification

releases/latest silently tracks whatever shipped last. Install from the
v2.4.2 tag, verify against the release CHECKSUMS.txt (sha256sum -c
--ignore-missing), point at the other platform assets, and pin the
dotnet tool path to --version 2.4.2. Upgrades become a deliberate tag
bump instead of an untracked drift.
…d, scanner included

davalillo/tree-sitter-mql5 is no longer a plain fork of mskelton: it has
rebased onto tree-sitter-cpp v0.23.4 (ABI 15), grown corpus-driven
extensions and highlight catalogs, and vendors its C scanner (renamed
tree_sitter_mql5_*). Drop the 'fork' and 'scanner stub' wording.
The marker list is a shared contract between the extractor and
mql-language-server (Mql5Tokens is that server's single source of
truth). Anchor the copy in a comment — last verified identical against
v2.4.2 — so a server-side change cannot silently diverge the .mqh
dialect sniffing.
languages.md was bumped to ~31 when MQL4/MQL5 joined the bespoke tier;
features.md still said ~30 from before this PR (its 257 total was also
already ahead of languages.md's 256).
@davalillo
davalillo marked this pull request as ready for review September 24, 2026 00:04
@davalillo

Copy link
Copy Markdown
Contributor Author

Hi @zzet, the macOS test job failed on the latest run, but the failure looks pre-existing and unrelated to this diff. Could you re-run the failed jobs when you have a minute?

Details: test (macos-latest, 1.27) failed on TestCheckoutStaleDiscoveryProofNeverReturnsBaseData in internal/mcp/checkout_control_test.go, an area this PR does not touch (the diff is parser, LSP registry, docs, and go.mod/go.sum pins). The failure mode is a freshness fallback to the base view with fallback_reason: "view_building" while the checkout view was still building, which is timing-sensitive. Two data points that point at a flake rather than a regression:

  • The same checkout_control family already failed on main on Sept 23, on Windows (run 35849558723: TestObserveCheckoutPathRejectsInventoryWithReboundGitMarker, TestCheckoutMutationConcurrentAdmissionIsBoundedByCaller), with no MQL code involved.
  • The earlier CI run on this branch (38bc3ea) was fully green, including macOS.

If it reproduces after the re-run I'll dig deeper, but right now it looks like a timing flake in the checkout control tests rather than something introduced here.

@zzet zzet left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hey @davalillo,

I found one issue I'd like to highlight: Template member methods are silently omitted

walkMqlTypeBody handles direct function_definition nodes but does not handle template_declaration. At the same time, emitMqlTemplateFunction explicitly skips templates inside a type.

As a result, valid templated MQL methods produce no method or member_of node, and calls inside them cannot be attributed to their enclosing method.

Suggested fix:

  • Handle template_declaration in walkMqlTypeBody.
  • Extract its wrapped function_definition and pass it through the method-emission path.
  • Add a regression test asserting:
    • the method node exists;
    • the member_of edge exists;
    • calls inside the template method are attributed correctly.

Relevant code:

…r functions

Review finding (PR zzet#823, zzet): walkMqlTypeBody had no template_declaration
case while the query's template dispatch skips in-body templates, so a
templated member method was mis-emitted as a free function (plain declarator)
or dropped entirely together with its body's calls (pointer declarator).

- walkMqlTypeBody: unwrap a template_declaration's function_definition into
  the method path; the _method_L seen marker keeps the func.def dispatch from
  double-emitting it. A template wrapping a nested class keeps its class path.
- qMqlAll: copy the pointer / double-pointer / reference declarator patterns
  from the C++ query — free functions like `CObject *New()` (the idiomatic
  MQL5 factory shape) were silently dropped before.
- Regression tests: method node, member_of edge, call attribution, the
  pointer-return template form, and free pointer-return functions.
- docs/languages.md: templated member methods listed as covered.
@davalillo

Copy link
Copy Markdown
Contributor Author

Thanks @zzet, spot on. I verified the finding empirically before fixing, and it was actually two shapes: a plain-declarator template member leaked out as a free function, while a pointer-return one (T *Create(...)) was dropped entirely, calls and all.

Fixed in 93a256d:

  • walkMqlTypeBody now handles template_declaration, unwraps its function_definition, and passes it through addMqlMethodFromNode. The _method_L marker that path seeds keeps the func.def dispatch from double-emitting it, and a template wrapping a nested class keeps the class path.
  • The regression test asserts exactly what you listed (method node, member_of edge, call attribution) plus the pointer-return form.

Fixing it also surfaced a sibling gap worth flagging: qMqlAll only had the plain declarator pattern, so any free function with a pointer return (CObject *New(), the idiomatic MQL5 factory shape) was silently dropped. The C/C++ queries carry pointer/double-pointer/reference variants; I copied those three patterns over and added tests for those too.

TestMQL passes with -race, LSP suite still 376/376. The full-package glibc abort mentioned in the PR body still reproduces with TestMQL skipped, so it stays pre-existing.

@zzet
zzet merged commit bdbcdf6 into zzet:main Sep 28, 2026
10 checks passed
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.

2 participants