本文所有代码在vs2013下通过编译
从词法分析到正则表达式(1)
完整源码
花了两天完成模式匹配器,虽然过程坎坷,不过当时自信心膨胀,觉得自己可以搞定一切,而自己所写的程序在自己看来也是挺厉害的……
自然而然地,就想到能不能把程序的界面再升级一下,毕竟模式就是通过正则表达式的手段构建的,再写一个函数将正则表达式翻译成模式应该不难,所以就着手去做了。
首先设计所用的正则表达式,除了基本的三个操作 star(*), connect(隐含), either(|)
,还需要通配符 _
,和转义字符 /
(转义字和母语C++的转义字相同简直是失败的不可救药……)。
三个组合手段之间有优先级
* > connect > |
为了改变组合的优先级,括号 ()
也是不可或缺的。
一开始的正则表达式的解释函数 reg_exp
其实相当简单,有一个储存模式的栈,顺序逐个读取字符
_
就调用 is_any_char
, is_any_char
很好实现 not_one_of("")
*
就从模式栈中取出一个模式,用 star
修饰后放回去 |
reg_exp
,得到 right
connect
连接起来,得到 left
(最初居然没注意到要把整个栈串起来) either(left, right)
入栈 (
就递归调用 reg_exp
,结果入栈 )
就把栈用 connect
串起来,返回 /
就再读一个字符,入栈 其实可以写成迭代形式的,稍微想了一下,这个其实跟《数据结构》栈那一章所实现的计算器很像,应该可以用同样的方法去除递归。不过真动手的时候,瞬间迷茫,于是作罢。
函数写好以后就迫不及待地去写了这种语言的第一个表达式,最简单的那种,字符字面量
…… ……
写不出来,因为不支持否定。
于是如果要求除了 /
之外的字符,就要用 |
把整个字母表除 /
的字符串起来……
在上面的解释器里加入否定:
!
,再读一个字符,用 is_not_char
修饰,入栈 经过不断的失败,特别是认识到和母语用同一个转义字的愚蠢后,我终于写出了能运行的字符字面量表达式
'(!//|//_)'
实际在c++里面是
"'(!////|////_)'"
接下来把它拓展一下就可以用到字符串字面量上了……不对,又写不出来了……
这次还是否定,因为在字符串字面量里要同时否定两个字符 /
和 "
, !
不够用啊
在解释器的否定里加入:
!
后是 (
,则读取到 )
,对读取的串用 not_one_of
修饰,入栈 实际上没有上面这步,因为整个过程中充满了BUG,又穿插着上课,所以思维不太连贯,当时因为某个现在想不起来的BUG彻底绝望了,放弃了继续打补丁。最终决定重写整个正则表达式解析模块。
首先要搞清楚一个概念性的问题,整个表达式的特殊符号分成三类
它们在不同的时候被处理
另一个棘手的问题是 connect
,因为它是隐含的,不在原始表达式里,但是如果希望用和计算器一样的算法去计算它,就要把它加上。
综上,我对原始表达式增加了一个预处理步骤,将正则表达式自身的单元分割工作和后来的语义识别分开。
预处理的输出是 list<RegToken>
, RegToken
即带类型的单元串。
顺带地,预处理还根据相邻Token的类型在 list
中插入 connect
的Token。
list<RegToken> preprocess(const string &primary) { //break to tokens and add connect list<RegToken> acc; string::const_iterator iter = primary.begin(); string::const_iterator end = primary.end(); while (iter != end) { acc.push_back(read_and_tag(iter)); } list<RegToken>::iterator fst = acc.begin(), snd = acc.begin(); ++snd; for (; snd != acc.end(); fst = snd, ++snd) { if (should_insert_connect(fst->second, snd->second)) { acc.insert(snd, RegToken("", Op_conn)); } } return acc; }
接下来是从 list<RegToken>
到模式的组合过程,跟中缀表达式的计算如出一辄
void zip(stack<Pattern> &operands, stack<RegTag> &ops, RegTag op_now) { while (ops.size() > 0 && (op_cmp(ops.top(), op_now) > 0)) { bool test = (op_cmp(ops.top(), op_now) > 0); switch (ops.top()) { case Op_star : zip_star(operands); break; case Op_or : zip_or(operands); break; case Op_conn : zip_conn(operands); break; default : throw "zip : op error"; break; } ops.pop(); } if (op_now == Op_right) { assert(ops.top() == Op_left); ops.pop(); } else { ops.push(op_now); } } Pattern calculator(list<RegToken>::const_iterator begin, const list<RegToken>::const_iterator end) { stack<Pattern> operands; stack<RegTag> ops; while (begin != end) { if (begin->second == Clause) { operands.push(cal_clause(begin->first)); } else { zip(operands, ops, begin->second); } ++begin; } assert(operands.size() == 1 && ops.size() == 0); return operands.top(); }
于是,只剩下将普通字符和拓展格式映射到对应得模式构造器上了。
在某节课的走神时,突然《SICP》 4.3节那个神奇的语言模型进入脑海,当时瞬间大脑充血,满脑子都是实现这个模型的好处(非确定性计算,之前star的问题直接不存在)。
不过下课坐在电脑前才发现自己对于如何实现完全没有一点概念,探索了一天,一筹莫展。
第二天的上课例行走神,思路又神奇的回到了这个问题,居然有了思路(只是其中的一点点,毕竟C++不是Scheme)
现在想想,当时完全是大脑充血,加上对自己在干什么不是很清楚。
事后说起来其实相当简单,就是想把递归转成迭代。
经历了很长时间的思考与失败,终于形成了以下的模型
新的Pattern形式
class AbstractPatternClass { public : virtual Context exec(Cursor &code, std::shared_ptr<AbstractPatternClass> succ, Context fail) = 0; }; typedef std::shared_ptr<AbstractPatternClass> Pattern;
Context
,上下文类有两类子类,
Cursor
一个 Current
模式(类比于PC寄存器) 一个 succ
模式 一个 fail
上下文 succ和fail代表了计算的两个走向
注意到fail是一个上下文,不需要当前上下文给出运行的信息,而succ是一个模式,它的运行需要指定一些额外的信息以形成自己的上下文。
初始(待匹配)模式现在被视为一种不变的结构,即代表了一个正则表达式的机器。
我们通过给初始模式三个参数(资源游标 cursor
,成功指示符 succ
,失败指示符 fail
)以启动机器
机器的一个运行时状态由一个上下文表示,而运行本身就是上下文的转换
当一次计算顺利进行后,运行的下一步就是由succ指定。
而当计算失败,我们在fail里保存了之前某些需要做出选择的地方的其他选择,所以我们跳过去(替换上下文),继续寻找希望。
succ和fail各自有一个终点,也就是在最初启动时给出的那两个。
只要我们给出字符串是有限长的而机器又不会不正常地改变游标,机器就总会运行到两个终点中的一个。(终于找到了希望 VS 彻底没希望了)
template<typename Predicate> struct CharAtom : public AbstractPatternClass { private : Predicate pred; public : CharAtom(Predicate pred) : pred(pred) { } Context exec(cursor_succ_fail) { if (!cursor.is_end() && pred(cursor.get_char())) { cursor.step_fore(); return make_run_ctx(cursor, succ, get_epsilon(), fail); } else { return fail; } } }; struct CharIsPredicate { private : const char target; public : CharIsPredicate(const char &target) : target(target) { } bool operator () (const char &c) { return c == target; } }; struct EpsilonAtom : public AbstractPatternClass { public : Context exec(cursor_succ_fail) { return make_run_ctx(cursor, succ, undefined_pattern, fail); } };
因为抵达基本元素时,如果成功,当前模式被消耗掉,则只剩下succ了,则用epsilon做为其后继(占位)。
而epsilon将undefined_pattern(它会直接返回一个特殊上下文)作为填充,希望永远不会运行至此。(只要初始succ不会返回一个运行状态的上下文)
struct StarPatternClass : public AbstractPatternClass { private : Pattern pattern; public : StarPatternClass(Pattern pattern) : pattern(pattern) { } Context exec(cursor_succ_fail) { // try from the most longest star-pattern Context directly_go_to_succ = make_run_ctx(cursor, get_epsilon(), succ, fail); return make_run_ctx(cursor, pattern, connect(star(pattern), succ), directly_go_to_succ); } };
组合模式是真正表现Context跳转的地方,star模式会首先将匹配拓展到最大,并在这一过程中扩充失败链(栈),如果后继匹配发生失败,则会(通过返回fail)退回到短一次的匹配成功处继续尝试后继。
通过star其实就可以看出来整个匹配模型其实只是某种形式的递归转迭代+栈(失败链)。也就是说跟之前的版本没有本质差别。
领悟到这一点的时候还是有些失望。
不过下面的函数还是让我有些激动
bool run_loop(Context ctx) { while (ctx->get_state() == ctx->Running) { ctx = run_ctx_step_next(ctx); } assert(ctx->get_state() != ctx->Undefined); return ctx->get_state() == ctx->HaltHapply; }
这就是这一整套机制的表层
bool RegExp::Machine::match_all(const string &source) const { struct CursorEndPredicate : public CursorPredicate { public: bool operator () (Cursor cursor) { return cursor.is_end(); } }; Context beginning = make_run_ctx(Cursor(source), pattern, get_terminate(new CursorEndPredicate), get_halt_sadly()); return run_loop(beginning); }
operator ()
,其实问题很明显了,不过我一直没有意识到,最后耐下心来读msdn才发现binary_function本身确实没有重载 operator ()
,辜负了我的满心信任……