利用MQL进行MQL解析·综合运用
🧩

利用MQL进行MQL解析·综合运用

(3/3)·当正则不够用时,如何在MQL里自建解析器揪出类层次与隐藏依赖

偏理论 第 3/3 篇
靠字符串搜索定位变量,常把方法名和注释里的同名词一起捞出来,语义全丢。用正则处理 MQL 结构,遇到嵌套类和继承就破功。本篇在 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 依次落位,比读文档直观。外汇与贵金属自动化解析逻辑涉及高风险,回测与实盘差异可能偏大,参数与语法映射务必自行验证。

MQL5 / C++
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 = numberleft + expression + right 描述括号表达式,operation 以 (plusstarminusslash) + expression 递归,expression = value + operation*0 允许零次或多次运算——这套规则直接决定你解析器的递归下降写法。
MQL5 / C++
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 头文件里改。

MQL5 / C++
{
   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% 以上的合规信号被静默放行。外汇与贵金属杠杆品种波动剧烈,这类解析漏洞可能引发错单,务必先在策略测试器验证。

MQL5 / C++
    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) 就能让文法多认一个取模运算符,编译过即证明宏展开没破链。

MQL5 / C++
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 分析层做实时语法流监控。

MQL5 / C++
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 解析,而是写个调试脚本把语法跑起来看树对不对。脚本职责很单纯:读预处理器交来的源文件,扫描器转成令牌流,解析器按根规则 expressionmatch 检查;勾上 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 主体解析。

MQL5 / C++
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 等分支,只留能用的:

MQL5 / C++
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 个练手实验。

MQL5 / C++
  _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 挂个回调就能跑,验证你自己的类继承图是否和设想一致。

MQL5 / C++
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 回测与实盘表现可能偏离,请自行验证。

MQL5 / C++
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。 外汇与贵金属图表上挂这类自定义对象,要注意多品种多周期切换时对象句柄可能失效,属于高风险操作,建议先在模拟盘验证。

MQL5 / C++
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 加载那个脚本,看一眼日志输出,比反复想“它该不该帮我找代码”实在得多。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到;把重复劳动交给小布,你专注决策,解析器跑出的类依赖图也能直接喂给看板。

常见问题

正则只能匹配文本模式,无法区分变量名出现在声明、调用还是注释中,也难以处理嵌套作用域与继承关系,解析器基于语法规则才能保留语义。
静态分析在编译前读源码结构,找潜在错误与依赖;动态分析在虚拟环境跑,追内存与变量值。本文只覆盖静态部分。
可以,解析出的类层次与质量度量可转成看板字段,小布盯盘的 AIGC 模块会按品种页聚合展示,省去手动梳理。
具备正确语法树后,做 MQL 到其它语言的 transpiling 是可行方向,本篇给出基础,完整转换见后续工具链。
Doxygen 类简单工具可凑合,lint 等强手段需深度适配,因 MQL 并非完整 C++,直接套会漏报 MQL 特有结构。