语法分析

手写语法分析使用递归下降分析法算符优先分析法

BNF

语法分析对应上下文无关文法。定义时一般用BNF描述出来。lua的BNF大致如下

chunk ::= block

block ::= {stat [";"]}

stat ::=
     "do" block "end" |
     "while" exp "do" block "end" |
     "if" exp "then" block {"elseif" exp "then" block} ["else" block] "end" |
     "for" Name "=" exp "," exp ["," exp] "do" block "end" |
     "for" namelist "in" explist "do" block "end" |
     "function" funcname funcbody |
     "local" "function" Name funcbody |
     "local" namelist ["=" explist] |
     "return" [explist] |
     "break" |
     varlist "=" explist |
     Name {tableindex | funccall} funccall

namelist ::= Name {"," Name}

varlist ::= var {"," var}

var ::= Name [{tableindex | funccall} tableindex]

funcname ::= Name {"." Name} [":" Name]

funcbody ::= "(" [parlist] ")" block "end"

parlist ::= Name {"," Name} ["," "..."] | "..."

explist ::= {exp ","} exp

tableconstructor ::= "{" [fieldlist] "}"

fieldlist ::= field {fieldsep field} [fieldsep]

field ::= "[" exp "]" "=" exp | Name "=" exp | exp

fieldsep ::= "," | ";"

exp ::= mainexp | exp binop exp

mainexp ::= nil | false | true | Number | String |
     "..." | function | tableconstructor |
     prefixexp |
     unop exp|

function ::= "function" funcbody

prefixexp ::= (Name | "(" exp ")") {tableindex | funccall}

tableindex ::= "[" exp "]" | "." Name

funccall ::= args | ":" Name args

args ::=  "(" [explist] ")" | tableconstructor

binop ::= "+" | "-" | "*" | "/" | "^" | "%" | ".." |
     "<" | "<=" | ">" | ">=" | "==" | "~=" |
     "and" | "or"

unop ::= "-" | "not" | "#"

这个语法定义经过整理,便于手写解析器。

递归下降解析

递归下降分析法用于处理常规语句。上面的BNF,经过特殊设计,读取至多两个前缀单词就可以确定选什么解析函数,可以算是LL(2)文法了。
代码实现比较无脑,差不多是一个BNF语句对应一个解析函数和一个语法树结点类型。读取前缀单词,看看应该选择什么解析函数。

以stat为例伪代码

Block ParseBlock()
{
    var block = new Block();
    for (; ; )
    {
        SyntaxTree statement = null;
        var token_ahead = LookAhead();
        switch (token_ahead.m_type)
        {
            case (int)';':
                NextToken(); continue;
            case (int)TokenType.DO:
                statement = ParseDoStatement(); break;
            case (int)TokenType.WHILE:
                statement = ParseWhileStatement(); break;
            case (int)TokenType.IF:
                statement = ParseIfStatement(); break;
            case (int)TokenType.FOR:
                statement = ParseForStatement(); break;
            case (int)TokenType.FOREACH:
                statement = ParseForEachStatement(); break;
            case (int)TokenType.FUNCTION:
                statement = ParseFunctionStatement(); break;
            case (int)TokenType.LOCAL:
                statement = ParseLocalStatement(); break;
            case (int)TokenType.RETURN:
                statement = ParseReturnStatement(); break;
            case (int)TokenType.BREAK:
                statement = ParseBreakStatement(); break;
            case (int)TokenType.CONTINUE:
                statement = ParseContinueStatement(); break;
            default:
                statement = ParseOtherStatement();
                break;
        }
        if (statement == null)
            break;
        block.statements.Add(statement);
    }
    return block;
}

一个特殊的地方,最后两个stat,赋值语句和函数调用语句不容易做区分。
他们的开头结构类似,都是Name {tableindex | funccall},区别是函数调用语句的结尾是funccall,赋值语句不是。
也就是需要比较后缀结构,但是结构是一样的,用同一个函数解析,检查结果类型就行了。

bool IsVar(SyntaxTree t)
{
    return t is TableAccess || t is Terminator;
}

SyntaxTree ParseOtherStatement()
{
    // lua做了限制,其他语句只有两种,assign statement and func call
    SyntaxTree exp;
    if (LookAhead().m_type == (int)TokenType.NAME)
    {
        exp = ParsePrefixExp();
        if(IsVar(exp))
        {
            // assign statement
            var assign_statement = new AssignStatement();
            assign_statement.var_list.Add(exp);
            while(LookAhead().m_type != (int)'=')
            {
                if (NextToken().m_type != (int)',')
                    throw new ParserException("expect ',' to split var-list");
                if (LookAhead().m_type != (int)TokenType.NAME)
                    throw new ParserException("expect 'id' to start var");
                exp = ParsePrefixExp();
                if (!IsVar(exp))
                    throw new ParserException("expect var here");
                assign_statement.var_list.Add(exp);
            }
            NextToken();// skip '='
            assign_statement.exp_list = ParseExpList();
            return assign_statement;
        }
        else
        {
            Debug.Assert(exp is FuncCall);
            return exp;
        }
    }
    else
    {
        if (IsMainExp())
            throw new ParserException("incomplete statement");
        return null;
    }
}

算符优先分析法

上面的BNF有个地方是递归下降方法解析不了的,文法定义里存在递归结构,基本所有的语言都有这类表达式语句。

exp ::= mainexp | exp binop exp

这儿做了相当的简化,mainexp包含了"(" exp ")" | unop exp,简化后表达式就是最简单的二元表达式了。
二元表达式是最基本的表达式了,算符优先分析法可以很好的解析这种语法结构。
文字简述过程:
设置两个栈,操作数栈和操作符栈。
扫描表达式,遇到操作数直接入栈,遇到操作符时,做下优先级判断确定下一步操作。
如果操作符优先级高于栈顶操作符,则入栈;否则,弹出栈顶操作符并从操作数栈里弹出两个操作数,组合成二元表达式,当成新的操作数入栈,并继续这个过程,直到当前操作符入栈。
写代码时有个特别的技巧,可以用函数调用栈代替操作数栈和操作符栈。

        SyntaxTree ParseExp(int left_priority = 0)
        {
            var exp = ParseMainExp();
            while(true)
            {
                int right_priority = GetOpPriority(LookAhead());
                if (left_priority < right_priority ||(left_priority == right_priority && IsRightAssociation(LookAhead())))
                {
                    // C++的函数参数执行顺序没有明确定义,方便起见,不在函数参数里搞出两个有依赖的函数调用,方便往C++里迁移
                    var op = NextToken();
                    exp = new BinaryExpression(exp, op, ParseExp(right_priority));
                }
                else
                {
                    return exp;
                }
            }
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,326评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,228评论 1 304
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,979评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,489评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,894评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,900评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,075评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,803评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,565评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,778评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,255评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,582评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,254评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,151评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,952评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,035评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,839评论 2 277

推荐阅读更多精彩内容