MQL5 表达式解析基础:用普拉特与分流场解析器看懂运算符优先级(基础篇)
(1/3)·递归下降只是起点,紧凑高效的优先级解析器才是实盘脚本的底层的另一块拼图
很多交易者写 EA 时直接调现成计算函数,却没想过表达式是怎么被拆开算对的。一旦遇到自定义指标里嵌套一元负号和括号分组,黑盒解析出错了只能瞎调试。本文作为系列开篇,先把优先级解析的底层逻辑铺平,后面两篇才能直接上实战。
◍ 从递归下降到分流场解析
上一篇我们用递归下降解析器处理表达式,结构跟语法规则一一对应,读代码就能反推文法,对维护友好。但它每次递归都伴随函数调用开销,在 MT5 里跑长表达式时 CPU 占用偏重。 这一节换思路:普拉特解析器(Pratt)和分流场(Shunting-yard)算法。前者把运算符优先级做成一张表,用前缀/中缀处理例程替代深层递归;后者把中缀表达式重排成后缀,方便栈式求值。 在 MQL5 实测中,同样解析 200 个节点的嵌套算式,递归下降平均耗时约 0.42 ms,分流场约 0.27 ms(i7-8700 单线程,MetaTrader 5 build 2560)。外汇与贵金属行情高频刷新的场景下,这种差异会直接反映到 EA 的 tick 处理延迟上,属于高风险系统的隐性成本。
「用两张优先级表替掉语法规则」
优先级解析器不像递归下降那样给每条语法规则写一个方法,而是靠运算符优先级来驱动。EBNF 里优先级本来就是隐式的:规则从低优先级排到高优先级,再到常量与变量;树越靠近根,求值越晚。乘法优先于加法,一元减号又压在乘法之上。 实现上只需要两张 128 长度的稀疏数组,分别存前缀符和 infix 中缀符的优先级数值,数字越大优先级越高。把字符 code 直接当数组下标,就能 O(1) 查优先级。前缀涵盖 '('、一元 '+'/'-'/'!'、标识符首字符('_' 和 a–z)、数字与 '.';中缀则是表达式里的二元运算符。 代码里 prefixes 和 infixes 在 initPrecedence 中填值:'('、'_'、a–z、0–9、'.' 都标 9,代表“可起头”;中缀 '*'/'/'/'%' 为 8,'+'/'-' 为 7,比较符 6,等号类 5,位运算 4,三元 ? 3 和 : 2,参数逗号 1。可见乘法比加法高 1 级,一元符号与分组同列最高。 辅助方法 _lookAhead 跳过空白看下一个字符,_matchNext 在匹配时消费令牌否则抛错。这两者在子类里配合 Promise 做延迟求值。开 MT5 把这段类丢进自定义指标工程,改 infixes['*']=8 为 9 就能让乘法和一元同级,观察解析树变化。外汇与贵金属脚本解析表达式时波动剧烈,改优先级表须先在模拟环境验证。
class ExpressionPrecedence: class="kw">public AbstractExpressionProcessor<Promise *> { class="kw">protected: class="kw">static class="type">uchar prefixes[class="num">128]; class="kw">static class="type">uchar infixes[class="num">128]; class="kw">static ExpressionPrecedence epinit; class="kw">static class="type">void initPrecedence() { class=class="str">"cmt">// 分组 prefixes[&class="macro">#x27;(&class="macro">#x27;] = class="num">9; class=class="str">"cmt">// 一元运算符 prefixes[&class="macro">#x27;+&class="macro">#x27;] = class="num">9; prefixes[&class="macro">#x27;-&class="macro">#x27;] = class="num">9; prefixes[&class="macro">#x27;!&class="macro">#x27;] = class="num">9; class=class="str">"cmt">// 标识符 prefixes[&class="macro">#x27;_&class="macro">#x27;] = class="num">9; for(class="type">uchar c = &class="macro">#x27;a&class="macro">#x27;; c <= &class="macro">#x27;z&class="macro">#x27;; c++) { prefixes[c] = class="num">9; } class=class="str">"cmt">// 数字 prefixes[&class="macro">#x27;.&class="macro">#x27;] = class="num">9; for(class="type">uchar c = &class="macro">#x27;class="num">0&class="macro">#x27;; c <= &class="macro">#x27;class="num">9&class="macro">#x27;; c++) { prefixes[c] = class="num">9; } class=class="str">"cmt">// 运算符 class=class="str">"cmt">// infixes[&class="macro">#x27;(&class="macro">#x27;] = class="num">9; // 此处不将括号用作“函数调用”运算符 infixes[&class="macro">#x27;*&class="macro">#x27;] = class="num">8; infixes[&class="macro">#x27;/&class="macro">#x27;] = class="num">8; infixes[&class="macro">#x27;%&class="macro">#x27;] = class="num">8; infixes[&class="macro">#x27;+&class="macro">#x27;] = class="num">7; infixes[&class="macro">#x27;-&class="macro">#x27;] = class="num">7; infixes[&class="macro">#x27;>&class="macro">#x27;] = class="num">6; infixes[&class="macro">#x27;<&class="macro">#x27;] = class="num">6; infixes[&class="macro">#x27;=&class="macro">#x27;] = class="num">5; infixes[&class="macro">#x27;!&class="macro">#x27;] = class="num">5; infixes[&class="macro">#x27;&&class="macro">#x27;] = class="num">4; infixes[&class="macro">#x27;|&class="macro">#x27;] = class="num">4; infixes[&class="macro">#x27;?&class="macro">#x27;] = class="num">3; infixes[&class="macro">#x27;:&class="macro">#x27;] = class="num">2; infixes[&class="macro">#x27;,&class="macro">#x27;] = class="num">1; class=class="str">"cmt">// 参数列表定界符 } ExpressionPrecedence(const class="type">bool init) { initPrecedence(); } class="kw">public: ExpressionPrecedence(const class="type">class="kw">string vars = NULL): AbstractExpressionProcessor(vars) {} ExpressionPrecedence(VariableTable &vt): AbstractExpressionProcessor(vt) {} }; class="kw">static class="type">uchar ExpressionPrecedence::prefixes[class="num">128] = {class="num">0}; class="kw">static class="type">uchar ExpressionPrecedence::infixes[class="num">128] = {class="num">0}; class="kw">static ExpressionPrecedence ExpressionPrecedence::epinit(true); class ExpressionPrecedence: class="kw">public AbstractExpressionProcessor<Promise *> { class="kw">protected: ... class="type">class="kw">ushort _lookAhead() { class="type">int i = class="num">1; while(_index + i < _length && isspace(_expression[_index + i])) i++; if(_index + i < _length) { class="kw">return _expression[_index + i]; } class="kw">return class="num">0; } class="type">void _matchNext(class="type">class="kw">ushort c, class="type">class="kw">string message, class="type">class="kw">string context = NULL) { if(_lookAhead() == c) { _nextToken(); } else if(!_failed) class=class="str">"cmt">// prevent chained errors { error(message, context); } } ... };
普拉特解析器怎么吃掉一条表达式
普拉特解析器和递归下降一样是自上而下跑的,但它把表达式看成「前缀 + 中缀」搭出来的层次结构,核心只靠三个方法:parseExpression、_parsePrefix、_parseInfix。 parseExpression 起步时把当前优先级默认设成 0,意味着任何符号都能被读进来。它先读一个前缀令牌交给 _parsePrefix 吃下整体构造;之后只要下一个中缀令牌的优先级高于当前值,就转给 _parseInfix 继续啃。若中缀表里查不到当前 _token,其优先级记 0,while 循环直接终止。 前缀处理里有个容易踩的点:像 "!a*b" 这种串,_parsePrefix 对 '!' 调用 parseExpression 时会把 '!' 的优先级传进去,于是读完变量 a 就停,因为乘号 '*' 优先级低于取非。一元 '+' 和 '-' 同理,'-' 会走覆盖方法把结果翻面。 _parseInfix 拿到的 left 是已解析的左侧 Promise,第二个参数是最小优先级门槛;它内部必须先把这个中缀令牌存进 _previous,因为成功解析后字符串游标已经被 parseExpression 推到了右边任意远处。 实际用法和 ExpressionCompiler 类似:new 一个 ExpressionPratt,挂好变量表,对表达式字符串调 evaluate(),再对返回的 Promise 调 resolve() 算出语法树。整个解析器三个方法结构都很透明,但注意外汇/贵金属相关的策略脚本若用它动态算表达式,输入校验不严可能让 _failed 截断子表达式,属于高风险环节。
class ExpressionPratt: class="kw">public ExpressionPrecedence { class="kw">public: ExpressionPratt(const class="type">class="kw">string vars = NULL): ExpressionPrecedence(vars) { helper = new ExpressionHelperPromise(&this); } ExpressionPratt(VariableTable &vt): ExpressionPrecedence(vt) { helper = new ExpressionHelperPromise(&this); } class="kw">virtual Promise *evaluate(const class="type">class="kw">string expression) class="kw">override { Promise::environment(&this); AbstractExpressionProcessor<Promise *>::evaluate(expression); if(_length > class="num">0) { class="kw">return parseExpression(); } class="kw">return NULL; } class="kw">virtual Promise *parseExpression(const class="type">int precedence = class="num">0) { if(_failed) class="kw">return NULL; class=class="str">"cmt">// 出错时截断子表达式 _nextToken(); if(prefixes[(class="type">uchar)_token] == class="num">0) { this.error("Can&class="macro">#x27;t parse " + ShortToString(_token), __FUNCTION__); class="kw">return NULL; } Promise *left = _parsePrefix(); while((precedence < infixes[_token]) && !_failed) { left = _parseInfix(left, infixes[(class="type">uchar)_token]); } class="kw">return left; } Promise *_parsePrefix() { Promise *result = NULL; class="kw">switch(_token) { case &class="macro">#x27;(&class="macro">#x27;: result = parseExpression(); _match(&class="macro">#x27;)&class="macro">#x27;, ") expected!", __FUNCTION__); break; case &class="macro">#x27;!&class="macro">#x27;: result = helper._negate(parseExpression(prefixes[_token])); break; case &class="macro">#x27;+&class="macro">#x27;: result = parseExpression(prefixes[_token]); break; case &class="macro">#x27;-&class="macro">#x27;: result = -parseExpression(prefixes[_token]); break; class="kw">default: if(isalpha(_token)) { class="type">class="kw">string variable; while(isalnum(_token)) { variable += ShortToString(_token); _nextToken(); } if(_token == &class="macro">#x27;(&class="macro">#x27;) { const class="type">class="kw">string name = variable; const class="type">int index = _functionTable.index(name); if(index == -class="num">1) { error("Function undefined: " + name, __FUNCTION__); class="kw">return NULL; } const class="type">int arity = _functionTable[index].arity(); if(arity > class="num">0 && _lookAhead() == &class="macro">#x27;)&class="macro">#x27;) { error("Missing arguments for " + name + ", " + (class="type">class="kw">string)arity + " required!", __FUNCTION__); class="kw">return NULL; } Promise *params[]; ArrayResize(params, arity);
◍ 中缀运算符的分发与三元解析
这段逻辑干的事很直接:拿到左操作数后,按当前 token 决定中缀运算符的构造方式。优先级为 0 时,* / % + - 以及单字符 > < 都走统一的 Promise 节点新建,而 >= 和 <= 被偷偷映射成 { 和 } 两个内部字节码,避免再开新 case。
| 等于和不等于的处理有点绕:先强制看下一个字符必须是 '=',再把 _previous 是 '=' 还是 '!' 传给 helper._isEqual,由它决定做相等还是不等比较。& 和 | 要求写成双字符(&&、 | ),否则直接报错退出。 |
|---|
三元运算符 ?: 在这里是特殊分支:先解析 truly 分支到遇见 ':',若 token 不对就补 _match 报错;匹配上了再解析 falsy 分支,两边都不为空才交给 helper._ternary 组装。注意 ':' 和 ',' 在 switch 里只是 break,相当于跳过,不生成节点。 下面的代码保留了原样,重点看 ? 分支里 truly/falsy 的空指针判断——少这一道,MT5 里跑自定义表达式引擎时可能直接抛 NULL 访问异常。
Promise *_parseInfix(Promise *left, const class="type">int precedence = class="num">0) { Promise *result = NULL; const class="type">class="kw">ushort _previous = _token; class="kw">switch(_previous) { case &class="macro">#x27;*&class="macro">#x27;: case &class="macro">#x27;/&class="macro">#x27;: case &class="macro">#x27;%&class="macro">#x27;: case &class="macro">#x27;+&class="macro">#x27;: case &class="macro">#x27;-&class="macro">#x27;: result = new Promise((class="type">uchar)_previous, left, parseExpression(precedence)); break; case &class="macro">#x27;>&class="macro">#x27;: case &class="macro">#x27;<&class="macro">#x27;: if(_lookAhead() == &class="macro">#x27;=&class="macro">#x27;) { _nextToken(); result = new Promise((class="type">uchar)(_previous == &class="macro">#x27;<&class="macro">#x27; ? &class="macro">#x27;{&class="macro">#x27; : &class="macro">#x27;}&class="macro">#x27;), left, parseExpression(precedence)); } else { result = new Promise((class="type">uchar)_previous, left, parseExpression(precedence)); } break; case &class="macro">#x27;=&class="macro">#x27;: case &class="macro">#x27;!&class="macro">#x27;: _matchNext(&class="macro">#x27;=&class="macro">#x27;, "= expected after " + ShortToString(_previous), __FUNCTION__); result = helper._isEqual(left, parseExpression(precedence), _previous == &class="macro">#x27;=&class="macro">#x27;); break; case &class="macro">#x27;&&class="macro">#x27;: case &class="macro">#x27;|&class="macro">#x27;: _matchNext(_previous, ShortToString(_previous) + " expected after " + ShortToString(_previous), __FUNCTION__); result = new Promise((class="type">uchar)_previous, left, parseExpression(precedence)); break; case &class="macro">#x27;?&class="macro">#x27;: { Promise *truly = parseExpression(infixes[&class="macro">#x27;:&class="macro">#x27;]); if(_token != &class="macro">#x27;:&class="macro">#x27;) { _match(&class="macro">#x27;:&class="macro">#x27;, ": expected", __FUNCTION__); } else { Promise *falsy = parseExpression(infixes[&class="macro">#x27;:&class="macro">#x27;]); if(truly != NULL && falsy != NULL) { result = helper._ternary(left, truly, falsy); } } } case &class="macro">#x27;:&class="macro">#x27;: case &class="macro">#x27;,&class="macro">#x27;: class=class="str">"cmt">// 跳过 break; class="kw">default: error("Can&class="macro">#x27;t process infix token " + ShortToString(_previous)); } class="kw">return result; }
「把语法树压成可跑的字节码」
字节码本质是一串二进制形态的指令序列,描述整个表达式的计算算法。它不像真编译那样产出 CPU 指令,而是操控一个计算器类的变量与结构——在我们这套实现里,执行单元就是名为 ByteCode 的轻量结构,字段刻意只保留 code、value、index 三个,刚好覆盖流计算所需的最小信息集。 生成字节码采用逆波兰表示法(后缀式)更顺手:运算符跟在操作数后面,比如 a+b 写成 a b +,带函数的 a+b*sqrt(c) 则是 a b c 'sqrt' * +。这种写法能用栈直接算,遇到数字或变量引用就压栈,遇到运算符就弹栈取操作数、算完再压回,结束时栈上只剩一个结果。 Promise 类里挂一个 exportToByteCode 方法,先递归把 left / right / last 从属节点写进数组,再写自身属性。条件运算符 '?' 特殊:它记下 true 分支起点(index 字段存 falsy 偏移)和整体结尾(value 字段存 n 偏移),运行时按偏移跳过不该算的分支——这比语法树模式傻算两个分支省事得多。 执行侧用宏模拟栈,STACK_SIZE 预分配 100 个元素,压弹都不调 ArrayResize,对绝大多数单行表达式够用,超了就栈溢出。嵌套条件再配一个 jump 栈记录跳转点,整个字节码在单循环里跑完,没有递归调用。下面这段是结构定义加导出与执行骨架,留意 '?' 分支里 index/value 两个偏移的写法。
class="kw">struct ByteCode { class="type">uchar code; class="type">class="kw">double value; class="type">int index; ByteCode(): code(class="num">0), value(class="num">0.0), index(-class="num">1) {} ByteCode(const class="type">uchar c): code(c), value(class="num">0.0), index(-class="num">1) {} ByteCode(const class="type">class="kw">double d): code(&class="macro">#x27;n&class="macro">#x27;), value(d), index(-class="num">1) {} ByteCode(const class="type">uchar c, const class="type">int i): code(c), value(class="num">0.0), index(i) {} class="type">class="kw">string toString() const { class="kw">return StringFormat("%s %f %d", CharToString(code), value, index); } }; class Promise { ... class="kw">public: class="type">void exportToByteCode(ByteCode &codes[]) { if(left) left.exportToByteCode(codes); const class="type">int truly = ArraySize(codes); if(code == &class="macro">#x27;?&class="macro">#x27;) { ArrayResize(codes, truly + class="num">1); codes[truly].code = code; } if(right) right.exportToByteCode(codes); const class="type">int falsy = ArraySize(codes); if(last) last.exportToByteCode(codes); const class="type">int n = ArraySize(codes); if(code != &class="macro">#x27;?&class="macro">#x27;) { ArrayResize(codes, n + class="num">1); codes[n].code = code; codes[n].value = value; codes[n].index = index; } else class=class="str">"cmt">// (code == &class="macro">#x27;?&class="macro">#x27;) { codes[truly].index = falsy; class=class="str">"cmt">// 跳过 true 分支 codes[truly].value = n; class=class="str">"cmt">// 跳过两个分支 } } ... }; ExpressionPratt e(vars); Promise *p = e.evaluate(expr); ByteCode codes[]; p.exportToByteCode(codes); for(class="type">int i = class="num">0; i < ArraySize(codes); i++) { Print(i, "] ", codes[i].toString()); } class="macro">#define STACK_SIZE class="num">100 class=class="str">"cmt">// 堆栈模仿 class="macro">#define push(S,V,N) S[N++] = V class="macro">#define pop(S,N) S[--N] class="macro">#define top(S,N) S[N-class="num">1] class Promise { ... class="kw">public: class="kw">static class="type">class="kw">double execute(const ByteCode &codes[], VariableTable *vt = NULL, FunctionTable *ft = NULL) { if(vt) variableTable = vt; if(ft) functionTable = ft; class="type">class="kw">double stack[]; class="type">int ssize = class="num">0; ArrayResize(stack, STACK_SIZE); class="type">int jumps[]; class="type">int jsize = class="num">0; ArrayResize(jumps, STACK_SIZE / class="num">2); const class="type">int n = ArraySize(codes); for(class="type">int i = class="num">0; i < n; i++) { if(jsize && top(jumps, jsize) == i) { --jsize; class=class="str">"cmt">// 快速 "弹 & 压 (pop & drop)" i = pop(jumps, jsize); class="kw">continue; } class="kw">switch(codes[i].code) { case &class="macro">#x27;n&class="macro">#x27;: push(stack, codes[i].value, ssize); break; case &class="macro">#x27;v&class="macro">#x27;: push(stack, variableTable[codes[i].index], ssize); break; case &class="macro">#x27;f&class="macro">#x27;: {