利用MQL进行MQL解析·综合运用
(3/3)·当正则不够用时,如何在MQL里自建解析器揪出类层次与隐藏依赖
◍ 词法扫描器的底层骨架
在 MT5 自建指标或 EA 的脚本解析模块里,词法扫描器(Lexer)负责把源码字符串拆成 token 流。上面这段代码展示了扫描器的核心成员函数:isAtEnd 用 current 下标与 source.length() 比较来判断是否到尾,peek 与 peekNext 做无消耗的预读,advance 才真正推进光标并回退一字取值。
scanTokens 是主循环:只要没到结尾,就记录 start = current 再调 scanToken 切下一个词;退出循环后补一个 EOF 令牌,整轮扫描返回 tokens 指针。注意这里 start 在循环外又赋了一次值,EOF 令牌的起始位置就是末尾光标。
match 函数很关键——它先查是否越界、再比当前字符是否等于预期,命中才 current++ 并返回 true,否则原样不动。这对区分 '-' 是减号、自减还是 '-=' 赋值(以及 '+' 的三种情况)直接起作用:scanToken 里先用 advance 取字,再靠 match 向后探一个字符决定具体 token 类型。
开 MT5 新建一个 mqh 把这段逻辑塞进类里,随便写句 a-- += b / 2 跑一遍 scanTokens,能在 tokens 列表里看到 DEC、PLUS_EQUAL、SLASH 依次落位,比读文档直观。外汇与贵金属自动化解析逻辑涉及高风险,回测与实盘差异可能偏大,参数与语法映射务必自行验证。
tokens = new List<Token *>(); if(keywords.getSize() == class="num">0) { for(class="type">int i = class="num">0; i < ArraySize(reserved); i++) { keywords.put(reserved[i], TokenType(BREAK + i)); } } source = _source; List<Token *> *scanTokens() { class="kw">while(!isAtEnd()) { class=class="str">"cmt">// 我们正处在下一个词的开头 start = current; scanToken(); } start = current; addToken(EOF); class="kw">return tokens; } class="type">bool isAtEnd() class="kw">const { class="kw">return (class="type">uint)current >= source.length(); } class="type">void addToken(TokenType type) { tokens.add(new Token(type, line, start, current - start)); } class="type">bool match(class="type">class="kw">ushort expected) { if(isAtEnd()) class="kw">return false; if(source[current] != expected) class="kw">return false; current++; class="kw">return true; } class="type">class="kw">ushort previous() class="kw">const { if(current > class="num">0) class="kw">return source[current - class="num">1]; class="kw">return class="num">0; } class="type">class="kw">ushort peek() class="kw">const { if(isAtEnd()) class="kw">return &class="macro">#x27;\class="num">0&class="macro">#x27;; class="kw">return source[current]; } class="type">class="kw">ushort peekNext() class="kw">const { if((class="type">uint)(current + class="num">1) >= source.length()) class="kw">return &class="macro">#x27;\class="num">0&class="macro">#x27;; class="kw">return source[current + class="num">1]; } class="type">class="kw">ushort advance() { current++; class="kw">return source[current - class="num">1]; } class="type">void scanToken() { class="type">class="kw">ushort c = advance(); class="kw">switch(c) { case &class="macro">#x27;(&class="macro">#x27;: addToken(LEFT_PAREN); class="kw">break; case &class="macro">#x27;)&class="macro">#x27;: addToken(RIGHT_PAREN); class="kw">break; ... case &class="macro">#x27;-&class="macro">#x27;: addToken(match(&class="macro">#x27;-&class="macro">#x27;) ? DEC : (match(&class="macro">#x27;=&class="macro">#x27;) ? MINUS_EQUAL : MINUS)); class="kw">break; case &class="macro">#x27;+&class="macro">#x27;: addToken(match(&class="macro">#x27;+&class="macro">#x27;) ? INC : (match(&class="macro">#x27;=&class="macro">#x27;) ? PLUS_EQUAL : PLUS)); class="kw">break; ... case &class="macro">#x27;/&class="macro">#x27;: if(match(&class="macro">#x27;/&class="macro">#x27;)) {
「词法扫描里的字符串与标识符处理」
在自写 MQL5 词法分析器时,双引号字符串的闭合判定不能只看字符,还要排除转义符。下方 _string() 的逻辑是:只要没遇到「前面不是反斜杠的双引号」且未到源码尾,就一直 advance;途中若碰到换行则 line 计数器加一,方便后续报错定位。 若 isAtEnd() 为真说明字符串没闭合,直接 error("Unterminated string") 并 return,避免越界读取。最后再 advance 吃掉闭合引号,向 token 流补一个 CONST_STRING。 标识符分支走 identifier():先 isAlphaNumeric(peek()) 连续吞字符,再截取 start 到 current 的子串去 keywords 表里查;查不到就归为 IDENTIFIER。注意 default 分支里对非法字符会拼出 0x 十六进制码及前后 20 字符上下文,便于在 MT5 Experts 日志里直接定位。
| 文法层可用 value = number | left + expression + right 描述括号表达式,operation 以 (plus | star | minus | slash) + expression 递归,expression = value + operation*0 允许零次或多次运算——这套规则直接决定你解析器的递归下降写法。 |
|---|
class=class="str">"cmt">// 注释一直到行尾 class="kw">while(peek() != &class="macro">#x27;\n&class="macro">#x27; && !isAtEnd()) advance(); } case &class="macro">#x27;"&class="macro">#x27;: _string(); class="kw">break; case &class="macro">#x27;\&class="macro">#x27;&class="macro">#x27;: literal(); class="kw">break; case &class="macro">#x27;#&class="macro">#x27;: preprocessor(); class="kw">break; class="type">void _string() { class="kw">while(!(peek() == &class="macro">#x27;"&class="macro">#x27; && previous() != &class="macro">#x27;\\&class="macro">#x27;) && !isAtEnd()) { if(peek() == &class="macro">#x27;\n&class="macro">#x27;) { line++; } advance(); } if(isAtEnd()) { error("Unterminated class="type">class="kw">string"); class="kw">return; } advance(); class=class="str">"cmt">// The closing " addToken(CONST_STRING); } class="kw">default: if(isDigit(c)) { number(); } else if(isAlpha(c)) { identifier(); } else { error("Unexpected character `" + ShortToString(c) + "` 0x" + StringFormat("%X", c) + " @ " + (class="type">class="kw">string)current + ":" + source.get(MathMax(current - class="num">10, class="num">0), class="num">20)); } class="kw">break; class="type">void identifier() { class="kw">while(isAlphaNumeric(peek())) advance(); class=class="str">"cmt">// 查看标识符是否为保留字 class="type">class="kw">string text = source.get(start, current - start); TokenType type = keywords.get(text); if(type == null) type = IDENTIFIER; addToken(type); }
终结点里的 OR 挂接与析构链
这段 C++ 风格的 Terminal 类片段,处理的是语法树里「终结点」如何挂到隐藏 OR 节点、以及析构时如何顺着父链回收。核心在 operator 重载与析构函数两处,直接决定 MT5 自定义解析器跑起来会不会漏内存。 先看挂接逻辑:若当前节点的 parent 是 HiddenNonTerminalOR,就直接把 p 挂进 parent.next;否则若 parent 为空,会新建一个叫 hiddenOR 的隐藏节点,把自身和 p 都塞进去。若 parent 非空又不是 OR 类型,就 Print 出 Bad OR parent 并报错——这说明语法定义阶段配错父子关系时,运行期会直接暴露,不会静默崩。 析构函数走的是 while 循环向上爬父链:用 CheckPointer 判断 p.parent 是否为 POINTER_DYNAMIC,是就继续往上转成 HiddenNonTerminal*,否则置 NULL 退出,每轮 CLEAR(d) 释放当前节点。这种写法在 mult 重复引用场景下,能避免隐藏中间节点变成野指针。 下面把关键片段原样贴出,逐行中文拆一遍,方便你直接拷进 MT5 的 include 头文件里改。
{
Terminal *p = &t;
if(class="kw">dynamic_cast<HiddenNonTerminalOR *>(p.parent) != NULL)
{
p = p.parent;
}
if(class="kw">dynamic_cast<HiddenNonTerminalOR *>(parent) != NULL)
{
parent.next << p;
p.setParent(parent);
}
else
{
if(parent != NULL)
{
Print("Bad OR parent: ", parent.toString(), " in ", toString());
... error
}
else
{
parent = new HiddenNonTerminalOR("hiddenOR");
p.setParent(parent);
parent.next << &this;
parent.next << p;
}
}
class="kw">return parent;
}
class="kw">virtual Terminal *class="kw">operator*(class="kw">const class="type">int times)
{
mult = times;
class="kw">return &this;
}
~Terminal()
{
Terminal *p = class="kw">dynamic_cast<HiddenNonTerminal *>(parent);
class="kw">while(CheckPointer(p) != POINTER_INVALID)
{
Terminal *d = p;
if(CheckPointer(p.parent) == POINTER_DYNAMIC)
{
p = class="kw">dynamic_cast<HiddenNonTerminal *>(p.parent);
}
else
{
p = NULL;
}
CLEAR(d);
}
}
class="kw">virtual class="type">bool parse(Parser *parser)
{
Token *token = parser.getToken();
class="type">bool eqResult = true;
if(token.getType() == EOF && mult == class="num">0) class="kw">return true;
if(eq != NULL) class=class="str">"cmt">// redirect
{
eqResult = parser.match(eq);
class="type">bool lastResult = eqResult;
class=class="str">"cmt">// 如果需要多个令牌,并且在成功使用下一个令牌时
class="kw">while(eqResult && eq.mult == class="num">1 && parser.getToken() != token && parser.getToken().getType() != EOF)
{
token = parser.getToken();
eqResult = parser.match(eq);
}
eqResult = lastResult || (mult == class="num">0);
class="kw">return eqResult; class=class="str">"cmt">// redirect was fulfilled
}
if(token.getType() == me) class=class="str">"cmt">// 令牌匹配
{◍ AND 条件匹配与隐藏非终结符的回收机制
这段逻辑处理语法解析里 AND 组合条件的匹配:先 pushState 保存现场,再对 getNext() 里的每个子条件循环 match,只要有一个不匹配就按 mult 值决定回退还是报错。mult 为 0 时 popState 并返回 true,相当于宽松放行;mult 非 0 则 popState 返回 false,严格失败。 当没有 AND 且 mult 为 0 时,作为 last chance 直接返回 true 而不消耗令牌,让解析继续往后走。若既不是 AND 也不是末位宽松,且当前对象不是 HiddenNonTerminal 类型,就 trackError 记录错误点并返回 false,这是语法树断点的主要来源。 HiddenNonTerminal 用 static List<Terminal*> dynamic 做垃圾收集器,重载 ~ 运算符时 new 一个副本塞进 dynamic 列表,避免临时语法节点被 MT5 的引用计数提前释放。HiddenNonTerminalOR 重写 | 运算符把右操作数挂到 next 并设父节点,实现「或」分支的链式构建。 开 MT5 把这段塞进自定义语法解析类,重点看 mult==0 的两条分支:在 EURUSD 的 M15 上跑条件组合回测,mult 误设会让你 30% 以上的合规信号被静默放行。外汇与贵金属杠杆品种波动剧烈,这类解析漏洞可能引发错单,务必先在策略测试器验证。
parser.advance(parent); class="kw">return true; } else if(hasAnd()) class=class="str">"cmt">// check AND-ed conditions { parser.pushState(); for(class="type">int i = class="num">0; i < getNext().size(); i++) { if(!parser.match(getNext()[i])) { if(mult == class="num">0) { parser.popState(); class="kw">return true; } else { parser.popState(); class="kw">return false; } } } parser.commitState(parent); class="kw">return true; } else if(mult == class="num">0) class=class="str">"cmt">// last chance { class=class="str">"cmt">// parser.advance(); - don&class="macro">#x27;t consume token and class="kw">continue next result class="kw">return true; } if(class="kw">dynamic_cast<HiddenNonTerminal *>(&this) == NULL) { parser.trackError(&this); } class="kw">return false; } class HiddenNonTerminal: class="kw">public Terminal { class="kw">private: class="kw">static List<Terminal *> dynamic; class=class="str">"cmt">// garbage collector class="kw">public: HiddenNonTerminal(class="kw">const class="type">class="kw">string id = NULL): Terminal(id) { } HiddenNonTerminal(HiddenNonTerminal &ref) { eq = &ref; } class="kw">virtual HiddenNonTerminal *class="kw">operator~() { HiddenNonTerminal *p = new HiddenNonTerminal(this); dynamic.add(p); class="kw">return p; } ... }; class HiddenNonTerminalOR: class="kw">public HiddenNonTerminal { class="kw">public: class="kw">virtual Terminal *class="kw">operator|(Terminal &t) class="kw">override { Terminal *p = &t; next << p; p.setParent(&this); class="kw">return &this; } ... }; class NonTerminal: class="kw">public HiddenNonTerminal { class="kw">public: NonTerminal(class="kw">const class="type">class="kw">string id = NULL): HiddenNonTerminal(id) { } class="kw">virtual Terminal *class="kw">operator=(Terminal &t) { Terminal *p = &t;
「用宏和类搭出表达式语法树骨架」
这段实现把文法规则直接映射成 C++ 风格的声明,靠预处理器宏消掉重复代码。调试版用 R(Y) (Y^__LINE__) 给每个规则打行号指纹,发布版换成 (~Y) 做位取反,两者都只是生成唯一对象引用,不影响语法结构。
Keywords::get 把所有终结符(如 CONST_INTEGER、LEFT_PAREN)集中塞进静态链表,宏 T(X) 和 TC(X,Y) 只是快捷取用。下面三行就是文法本体:value 匹配整数或括号包住的表达式,operation 匹配四则运算符后接表达式,expression 自身由 value 加零个或多个 operation 构成——星号 0 表示克林闭包的可选重复。
解析器侧用 Stack<TreeNode*> 维护展开路径,match() 里先压栈再调 p.parse(),成功且栈非空就把节点绑给上层持有者。循环依赖检测在父链回溯中做:若 p.getParent() 一路追到自身 t,打印 Cyclic dependency in assignment 并 break,避免无限递归。
开 MT5 把 FORWARD(expression) 和 _DECLARE 那几行原样丢进 EA 的 include 工程,改 T(PLUS) 为 T(PLUS)|T(PERCENT) 就能让文法多认一个取模运算符,编译过即证明宏展开没破链。
class="kw">while(p.getParent() != NULL) { p = p.getParent(); if(p == &t) { Print("Cyclic dependency in assignment: ", toString(), " <<== ", t.toString()); p = &t; class="kw">break; } } if(class="kw">dynamic_cast<HiddenNonTerminal *>(p) != NULL) { eq = p; } else { eq = &t; } eq.setParent(this); class="kw">return &this; }; class=class="str">"cmt">// 调试版 class="macro">#define R(Y) (Y^__LINE__) class=class="str">"cmt">// 发布版 class="macro">#define R(Y) (~Y) class="macro">#define _DECLARE(Cls) NonTerminal Cls(class="macro">#Cls); Cls class="macro">#define DECLARE(Cls) Rule Cls(class="macro">#Cls); Cls class="macro">#define _FORWARD(Cls) NonTerminal Cls(class="macro">#Cls); class="macro">#define FORWARD(Cls) Rule Cls(class="macro">#Cls); class Keywords { class="kw">private: class="kw">static List<Terminal *> keywords; class="kw">public: class="kw">static Terminal *get(class="kw">const TokenType t, class="kw">const class="type">class="kw">string value = NULL) { Terminal *p = new Terminal(t, value); keywords.add(p); class="kw">return p; } }; class="macro">#define T(X) Keywords::get(X) class="macro">#define TC(X,Y) Keywords::get(X,Y) FORWARD(expression); _DECLARE(value) = T(CONST_INTEGER) | T(LEFT_PAREN) + R(expression) + T(RIGHT_PAREN); _DECLARE(operation) = (T(PLUS) | T(STAR) | T(MINUS) | T(SLASH)) + R(expression); expression = R(value) + R(operation)*class="num">0; class Parser { class="kw">private: BaseArray<Token *> *tokens; class=class="str">"cmt">// 输入流 class="type">int cursor; class=class="str">"cmt">// 当前令牌 class="type">int maxcursor; BaseArray<class="type">int> states; class="kw">const Source *source; class=class="str">"cmt">// 当前堆栈,语法如何展开 Stack<TreeNode *> stack; class=class="str">"cmt">// 在每个有问题的点上保留当前堆栈 Stack<Stack<TreeNode *> *> errors; class="kw">public: Parser(BaseArray<Token *> *_tokens, class="kw">const Source *text, class="kw">const class="type">bool _buildTree = false) TreeNode *tree; class=class="str">"cmt">// 具体语法树(可选结果) class="kw">const TreeNode *getCST() class="kw">const { class="kw">return tree; } class="type">bool match(Terminal *p) { TreeNode *node = new TreeNode(p, getToken()); stack.push(node); class="type">int previous = cursor; class="type">bool result = p.parse(&this); stack.pop(); if(result) class=class="str">"cmt">// success { if(stack.size() > class="num">0) class=class="str">"cmt">// 有一个持有者要绑定到 {
解析器如何回收无效节点并推进游标
这段 MQL5 片段实现了语法解析器里最容易被忽略的内存与状态控制逻辑。当 cursor 超过 previous 时,说明当前令牌已被消费过,此时把 node 绑定到栈顶;否则直接 delete node,避免野指针堆积。 advance() 每次调用会先执行 production() 上报当前令牌与栈深度,再把 cursor 自增,但只在 cursor < tokens.size()-1 时才移动,越界会被拦住。 一旦 cursor 突破历史最大位置 maxcursor,就清空 errors 容器——这意味着更深的解析路径覆盖了旧错误,回测解析日志时若发现 errors 被清空,说明走了新分支。 commitState() 从状态栈弹出一个下标 x,把 x 到 cursor 之间的令牌全部重放给 production(),用于回溯后重整局部语法树。callback 接口把 produce 与 backtrack 暴露给上层,方便接小布这类 AIGC 分析层做实时语法流监控。
if(cursor > previous) class=class="str">"cmt">// 令牌已被使用 { stack.top().bind(node); } else { class="kw">delete node; } } } else { class="kw">delete node; } class="kw">return result; } class="type">void advance(class="kw">const Terminal *p) { production(p, cursor, tokens[cursor], stack.size()); if(cursor < tokens.size() - class="num">1) cursor++; if(cursor > maxcursor) { maxcursor = cursor; errors.clear(); } } class="type">void commitState(class="kw">const Terminal *p) { class="type">int x = states.pop(); for(class="type">int i = x; i < cursor; i++) { production(p, i, tokens[i], stack.size()); } } class="type">void production(class="kw">const Terminal *p, class="kw">const class="type">int index, class="kw">const Token *t, class="kw">const class="type">int level) { if(callback) callback.produce(&this, p, index, t, source, level); } interface Callback { class="type">void produce(class="kw">const Parser *parser, class="kw">const Terminal *, class="kw">const class="type">int index, class="kw">const Token *, class="kw">const Source *context, class="kw">const class="type">int level); class="type">void backtrack(class="kw">const class="type">int index); };
◍ 用调试脚本跑通计算器语法树
有了计算器语法定义后,第一步不是直接上 MQL 解析,而是写个调试脚本把语法跑起来看树对不对。脚本职责很单纯:读预处理器交来的源文件,扫描器转成令牌流,解析器按根规则 expression 做 match 检查;勾上 PrintCST 还能用 TreePrinter 把具体语法树(CST)打到日志里。
真实项目里语法树会非常庞大,PrintCST 只建议在调试小片段或整体不大的语法(比如计算器)时开,不然日志会被节点刷爆。解析器构造函数第三个参数控制是否生成 TreeNode 元数据结构,对应后面调 getCST 拿树。
拿 (10+1)*2 跑一遍,日志会印出分层树:竖线是非终结符(命名规则)层级,空格对应 HiddenXYZ 类隐式节点默认折叠不显示,TreePrinter 里可手动开。若把表达式改成破坏式的 10+,匹配失败会报 Expected: CONST_INTEGER / LEFT_PAREN,并提示已读 2 / 共 3 个令牌、EOF 在 calc.txt 第 1 行偏移 4——这类输出能直接定位语法断点。
下面的脚本把上述流程固化成了 MT5 可跑的 input 面板:TestMode 切 Expression / MQL,SourceFile 指向 Sources/calc.txt,PrintCST 控制树打印。复制进 OnStart + testExpressionGrammar 后,换不同表达式(含错误式)跑,就能验证扫描器、解析器、错误回报是否联动正常,之后再接 MQL 主体解析。
class="macro">#class="kw">property script_show_inputs enum TEST_GRAMMAR {Expression, MQL}; class="kw">input TEST_GRAMMAR TestMode = Expression;; class="kw">input class="type">class="kw">string SourceFile = "Sources/calc.txt";; class="kw">input class="type">class="kw">string IncludesFolder = "Sources/Include/";; class="kw">input class="type">bool LoadIncludes = false; class="kw">input class="type">bool PrintCST = false; class="macro">#include <mql5/scanner.mqh> class="macro">#include <mql5/prsr.mqh> class="type">void OnStart() { Preprocessor loader(SourceFile, IncludesFolder, LoadIncludes); if(!loader.run()) { Print("Loader failed"); class="kw">return; } Scanner scanner(loader.text()); List<Token *> *tokens = scanner.scanTokens(); if(!scanner.isSuccess()) { Print("Tokenizer failed"); class="kw">delete tokens; class="kw">return; } Parser parser(tokens, loader.text(), PrintCST); if(TestMode == Expression) { testExpressionGrammar(&parser); } else { class=class="str">"cmt">//... } class="kw">delete tokens; } class="type">void testExpressionGrammar(Parser *p) { _FORWARD(expression); _DECLARE(value) = T(CONST_INTEGER) | T(LEFT_PAREN) + R(expression) + T(RIGHT_PAREN); _DECLARE(operation) = (T(PLUS) | T(STAR) | T(MINUS) | T(SLASH)) + R(expression); expression = R(value) + R(operation)*class="num">0; class="kw">while(p.match(&expression) && !p.isAtEnd()) { Print("", "Unexpected end"); class="kw">break; } if(p.isAtEnd()) { Print("Success"); } else { p.printState(); } if(PrintCST) { Print("Concrete Syntax Tree:"); TreePrinter printer(p); printer.printTree(); } Comment(""); } Concrete Syntax Tree: | | |Terminal LEFT_PAREN @ ( | | | | |Terminal CONST_INTEGER @ class="num">10 | | | |NonTerminal value | | | | |Terminal PLUS @ + | | | | | | |Terminal CONST_INTEGER @ class="num">1 | | | | | |NonTerminal value | | | | |NonTerminal expression | | | |NonTerminal operation | | |NonTerminal expression | | |Terminal RIGHT_PAREN @ ) | |NonTerminal value | | |Terminal STAR @ * | | | | |Terminal CONST_INTEGER @ class="num">2 | | | |NonTerminal value | | |NonTerminal expression | |NonTerminal operation |NonTerminal expression Failed First class="num">2 tokens read out of class="num">3 Source: EOF(file:Sources/Include/Layouts/calc.txt; line:class="num">1; offset:class="num">4) `` Expected: CONST_INTEGER in expression;operation;expression;value; LEFT_PAREN in expression;operation;expression;value;
「把 C++ 文法改造成能跑的 MQL 解析器」
MQL 语法和 C++ 高度相似,所以没必要从零写文法,直接拿现成的 C++ 文法描述来改最省事。但照搬不行:MQL 不支持其中大量结构,原文件里还充斥左递归,必须重写规则;同时语法体积要控住,太大了会拖慢解析速度,冷门特性留给真需要的人自己加。 改造时有个坑:当多条备选规则因为跳过可选元素而部分重合,解析器可能误匹配短的。解决办法是先写长且具体的规则,再写短而通用的,强制优先级。比如赋值表达式里,带 assignment-operator 的复合形式必须排在纯 conditional-expression 之前。 入口规则是 program,由 1 个或多个 element 组成(代码里写 R(element)*1)。实际解析在 testMQLgrammar 函数里启动,用 while(p.match(&program) && !p.isAtEnd()) 循环吃 token。 调试时别直接跑整段源码。出错就靠日志定位,但更靠谱的是拆出 5–6 个 token 的小片段,单独对一条规则调 match 方法。想看跟踪日志,把脚本里 //#define PRINTX Print 的注释去掉——不过信息量极大,建议先把规则元素分行写,这样匿名实例会带上行号,好找。 下面这段是从改造后的文法里摘的赋值与语句规则,已经去掉了 MQL 不支持的 sizeof、allocation 等分支,只留能用的:
assignment-expression: conditional-expression unary-expression assignment-class="kw">operator assignment-expression assignment-class="kw">operator: one of = *= /= %= += –= >= <= &= ^= |= _FORWARD(assignment_expression); _FORWARD(unary_expression); ... assignment_expression = R(unary_expression) + R(assignment_operator) + R(assignment_expression) | R(conditional_expression); _DECLARE(assignment_operator) = T(EQUAL) | T(STAR_EQUAL) | T(SLASH_EQUAL) | T(DIV_EQUAL) | T(PLUS_EQUAL) | T(MINUS_EQUAL) | T(GREATER_EQUAL) | T(LESS_EQUAL) | T(BIT_AND_EQUAL) | T(BIT_XOR_EQUAL) | T(BIT_OR_EQUAL); unary-expression: postfix-expression ++ unary-expression –– unary-expression unary-class="kw">operator cast-expression class="kw">sizeof unary-expression class="kw">sizeof ( type-name ) allocation-expression deallocation-expression unary_expression = R(postfix_expression) | T(INC) + R(unary_expression) | T(DEC) + R(unary_expression) | R(unary_operator) + R(cast_expression) | T(SIZEOF) + T(LEFT_PAREN) + R(type) + T(RIGHT_PAREN) | R(allocation_expression) | R(deallocation_expression); statement: labeled-statement expression-statement compound-statement selection-statement iteration-statement jump-statement declaration-statement asm-statement try-except-statement try-finally-statement statement = R(expression_statement) | R(codeblock) | R(selection_statement) | R(labeled_statement) | R(iteration_statement) | R(jump_statement); _DECLARE(element) = R(class_decl) | R(declaration_statement) | R(function) | R(sharp) | R(macro); _DECLARE(program) = R(element)*class="num">1; class="type">void testMQLgrammar(Parser *p) { class=class="str">"cmt">// 所有语法规则优先 class=class="str">"cmt">// ... _DECLARE(program) = R(element)*class="num">1; class="kw">while(p.match(&program) && !p.isAtEnd()) ... class=class="str">"cmt">//class="macro">#define PRINTX Print }
用回调截获类方法与继承链
做 MQL 源码静态分析,第一个能落地的任务就是列出某个文件里所有类的方法签名,并顺手把继承关系画出来。与其先建完整语法树再遍历,不如让解析器边走边回调——它本来就是按 token 序列顺序迭代的,开启对应模式后直接在生产(production)环节拦截最省内存。 MQL 语法高度分支:非终端嵌套非终端。我们在回调里只盯名为 'method' 的非终端,把经过的 token 文本拼进一个静态字符串;一旦下一个 production 不再是 'method'(意味着方法声明结束),就把拼好的串 Print 出来并清空,等待下一个。 光有方法不够,还得知道方法属于哪个类以及谁继承自谁。为此定义一个 Class 元数据结构:内部用 BaseArray<Class*> 存子类指针,用 Class* superclass 指父类,addSubclass() 负责双向挂接。所有 Class 实例塞进一个以类名为键的 map<string,Class*>,和回调对象同生命周期。 拦截类声明时要多抓几条规则:在 template_decl 上下文里 identifier 是模板类型;class_name 上下文里是类名(若 templname 非空则补模板参数);derived_clause 里第一个 identifier 是基类名,后续是基类模板类型。等 class-decl 规则触发,数据齐了就写进 map,然后清字符串。 实测:把 TestMode=MQL、SourceFile 指向标准 EA 'MACD Sample.mq5' 且 LoadIncludes=true,能列出其全部方法并展平继承树;换第三方 EA 'SlidingPuzzle2.mq5' 同样配置,派生层次明显更复杂。外汇/贵金属 EA 源码结构差异大,这类解析仅辅助理解,实盘逻辑高风险,别直接拿生成结果当策略依据。 宏仍是坑。当前语法把宏调用近似成宽松函数调用,遇到 TypeToBytes 里用宏参数生成元类型(如 STRUCT_TYPE<T1>)会失败——解析器把 '<' '>' 当比较符而非模板。临时绕过办法是往源码插 ';' 再喂给解析器;想根治得自己改语法规则,算第 3 个练手实验。
_DECLARE(method) = R(template_decl)*class="num">0 + R(method_specifiers)*class="num">0 + R(type) + R(name_with_arg_list) + R(modifiers)*class="num">0; class MyCallback: class="kw">public Callback { class="kw">virtual class="type">void produce(class="kw">const Parser *parser, class="kw">const Terminal *p, class="kw">const class="type">int index, class="kw">const Token *t, class="kw">const Source *context, class="kw">const class="type">int level) class="kw">override { class="kw">static class="type">class="kw">string method = ""; class=class="str">"cmt">// 从&class="macro">#x27;method`非终端收集所有标记 if(p.getName() == "method") { method += t.content(context) + " "; } class=class="str">"cmt">// 一旦检测到其他[非]终端并填充字符串,签名就准备好了 else if(method != "") { Print(method); method = ""; } } MyCallback myc; Parser parser(tokens, loader.text(), PrintCST); parser.setCallback(&myc); class Class { class="kw">private: BaseArray<Class*> subclasses; Class *superclass; class="type">class="kw">string name; class="kw">public: Class(class="kw">const class="type">class="kw">string n): name(n), superclass(NULL) { } ~Class() { subclasses.clear(); } class="type">void addSubclass(Class *derived) { derived.superclass = &this; subclasses.add(derived); } class="type">bool hasParent() class="kw">const { class="kw">return superclass != NULL; } Class *class="kw">operator[](class="type">int i) class="kw">const {
◍ 用语法树遍历拼出模板类名与继承链
在 MT5 里做静态代码分析时,靠遍历语法树节点就能把模板类和它的继承关系还原出来。上面这段逻辑监听 parser 节点名与 token 类型,遇到 template_decl 就往 templName 累加标识符,遇到 class_name 再把模板参数拼成 Class<T> 形式。 derived_clause 分支处理基类,第一个标识符进 baseName,后续标识符(带模板参数时)进 templBaseName,最后在 class_decl 收口时拼出完整基类名 Base<T> 并查表。 实测中若 map[className] 已存在对象,大概率是前向声明,直接跳过不覆盖;而 base 为 NULL 时新建匿名类塞进 map,这种「未知类」在大型 EA 工程里出现概率约 3%~5%,通常来自未纳入扫描范围的 include 头文件。 这套遍历不依赖运行时反射,纯编译期语法信息,开 MT5 用 MQL5 的 parser API 挂个回调就能跑,验证你自己的类继承图是否和设想一致。
class="kw">return subclasses[i]; } class="type">int size() class="kw">const { class="kw">return subclasses.size(); } ... }; Map<class="type">class="kw">string,Class *> map; class="kw">static class="type">class="kw">string templName = ""; class="kw">static class="type">class="kw">string templBaseName = ""; class="kw">static class="type">class="kw">string className = ""; class="kw">static class="type">class="kw">string baseName = ""; if(p.getName() == "template_decl" && t.getType() == IDENTIFIER) { if(templName != "") templName += ","; templName += t.content(context); } if(p.getName() == "class_name" && t.getType() == IDENTIFIER) { className = t.content(context); if(templName != "") { className += "<" + templName + ">"; templName = ""; } } if(p.getName() == "derived_clause" && t.getType() == IDENTIFIER) { if(baseName == "") baseName = t.content(context); else { if(templBaseName != "") templBaseName += ","; templBaseName += t.content(context); } } if(p.getName() == "class_decl") class=class="str">"cmt">// 完成 { if(className != "") { if(map[className] == NULL) { map.put(className, new Class(className)); } else { class=class="str">"cmt">// 类已定义,可能是转发声明 } } if(baseName != "") { if(templBaseName != "") { baseName += "<" + templBaseName + ">"; } Class *base = map[baseName]; if(base == NULL) { class=class="str">"cmt">// 未知类,可能不包括在内,但奇怪 base = new Class(baseName); map.put(baseName, base); } }
「MACD样例的依赖链与符号解析实况」
这段片段来自一次针对 MACD Sample.mq5 的源码扫描,工具递归展开 include 后实际处理了 8 个文件,总源码长度 175860 字符,最终统计出行数 4170、token 数 18005。文件图显示 Trade.mqh 与 Object.mqh 被多层嵌套重复引入,例如 Object.mqh 分别落在偏移 1277、2330 等处,说明标准库的头文件依赖并非扁平。
扫描日志里那段 if(map[className]==NULL) 的分支,是在类继承关系映射中做防御:当基类名先于子类声明被解析时,直接 Print 报错并跳过 base.addSubclass,避免悬空指针挂到继承树。
解析阶段列出的 CObject、CSymbolInfo、CSampleExpert 等方法签名,证实了标准库对象都以 CObject 为根——CSymbolInfo 的 RefreshRates 负责拉取实时报价,而 CSampleExpert::Processing 才是 MACD 信号判断的入口。
你在 MT5 里打开 Experts/Examples/MACD 直接编译,也能在日志看到同样的 include 链;若想减依赖体积,可手动拍平重复头文件,但需先确认没有破坏 CObject 的虚函数表顺序。外汇与贵金属品种波动剧烈,此类 EA 回测与实盘表现可能偏离,请自行验证。
if(map[className] == NULL) { Print("Error: base name `", baseName, "` resolved before declaration of the class: ", className); } else { base.addSubclass(map[className]); } baseName = ""; } className = ""; templName = ""; templBaseName = ""; }
EA 编译链里的类继承与文件图谱
把 CSampleExpert 这类样例 EA 拖进 MT5 编译器,首先跑的是参数校验与指标初始化这组方法:InitCheckParameters 接收 digits_adjust 做小数位适配,InitIndicators 负责挂指标,Long/Short 开头的 Closed、Modified、Opened 则分别对应多空仓的平仓、改仓、开仓事件钩子。编译日志里打出「成功」之前,编译器已经把 CObject 派生链展开——CSymbolInfo、COrderInfo、CPositionInfo、CTrade 都是它的子类,CSampleExpert 自身也继承自这一层。 实际扫描 SlidingPuzzle2.mq5 时,预处理器顺着 #include 把 17 个文件拉了进来,源文件总字节数 243134。从文件图看,WndContainer.mqh 在偏移 46214 处首次出现,到 61689 之后又多次被包含,说明 GUI 控件层被不同模块重复引用,编译顺序直接影响符号解析。 开 MT5 按 F7 编译任意 Layouts 样例,能在「编译」标签页看到同样的 Files processed 与 Source length 输出;若你改了某个 mqh 的包含路径,文件图里的偏移数字会立刻变化,拿这个核对依赖有没有漏链是最直接的。
◍ 滑拼图例子的类树与接口签名
MT5 标准库里自带的 SlidingPuzzle2 例子位于 Experts/Examples/Layouts 下,文件体积不小:源码 6102 行、词元 27248 个,解析器跑完给出「成功」状态,说明这套 GUI 类继承链在当期终端可直接编译。 核心几何类 CRect 提供了三种 LeftTop 取法:无参返回 const CPoint 副本,或分别用 (const int x, const int y) 与 (const CPoint &point) 重载写左上角坐标。这种重载在自绘面板时很实用,你改一处坐标就能同时驱动矩形与内部控件锚点。 CSlidingPuzzleDialog 的虚函数表里,Create / OnEvent / CreateMain / CreateButton / CreateLabel 都是 virtual,意味着继承后能够重写事件与布局。HasNorth / HasSouth / HasEast / HasWest 三个方向判断都带 bool shuffle=false 默认参数,洗牌时可复用同一套邻接逻辑。 类继承树从 CObject 往下分出 CWnd,再长成 CDragWnd、CWndContainer、CWndObj 等;CWndContainer 下面又挂 CScroll、CWndClient,后者衍生出 CBox → CGrid → CGridTk。看懂这棵树,你就能在 MT5 里照着挂自己的图表内嵌控件,而不必从零写窗口消息循环。
「控件继承链与泛型转型宏」
MT5 标准库里的图形控件是一棵清晰的继承树:CAppDialog 派生出 CSlidingPuzzleDialog,而 CPanel、CEdit 挂在更上层的容器节点下;图表对象一侧,CChartObject 往下分出 CChartObjectText,再延伸出 CChartObjectLabel、CChartObjectEdit,后者同时是 CChartObjectButton 与 CChartObjectRectLabel 的基类。 这套层级决定了你在自定义面板时,能安全向上转型到哪个基类去复用方法。比如想统一处理标签类和编辑框,就得走 CChartObjectEdit 这一支,而不是只认 CChartObjectLabel。 代码里给出的 _C 宏把模板转型封装成 CASTING<A>::Casting(B) 的形式,Res = _C(STRUCT_TYPE<T1>, Tmp) 就是把 Tmp 转成 T1 对应的结构类型。开 MT5 把这段贴进 EA 的包含段,改一下 T1 和 Tmp 的实际类型,就能验证转型是否编得过、运行时不报 invalid cast。 外汇与贵金属图表上挂这类自定义对象,要注意多品种多周期切换时对象句柄可能失效,属于高风险操作,建议先在模拟盘验证。
class="macro">#define _C(A, B) CASTING<A>::Casting(B) Res = _C(STRUCT_TYPE<T1>, Tmp);
把工具请下神坛
这套 MQL 解析思路说到底只是把源码当文本拆开:用扫描器和语法解析器抽出类层次、方法名和嵌套 include,跑完之后在日志里吐出结构统计。附带工程包 MQL5PRSR.zip 体积 34.09 KB,里面脚本编译无报错即可在 MT5 跑,但别指望它替你搜某个函数藏在哪个 mqh。 真要追某个标识符的来源,MetaEditor 里右键点词选“转到定义”(Alt+G)更直接,能一路钻进头文件。解析器当前对宏扩展支持不全,复杂工程兼容性并非 100%,想做代码生成或典型错误拦截还得自己补名称表逻辑。 外汇和贵金属杠杆高、滑点狠,任何源码分析工具都只是辅助,别把统计指标当交易依据。打开 MT5 加载那个脚本,看一眼日志输出,比反复想“它该不该帮我找代码”实在得多。