ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

FSharpCompiler.Yacc 入门示例

2021-02-02 17:33:59  阅读:212  来源: 互联网

标签:yacc DIGIT FSharpCompiler 示例 rest Yacc let yield Interior


本文向大家推荐一个yacc语法自动构建器,FSharpCompiler.YaccFSharpCompiler.Parsing前者是解析器生成工具,后者是解析器的依赖项。顾名思义,这个编译器是专门为F#语言使用的。这个文件位于https://github.com/xp44mm/FSharpCompiler.Docs/tree/master/FSharpCompiler.Docs大家有问题,可以去提交issue,将会及时得到更正或补充。

龙书示例4.69:

E -> E + T | T
T -> T * F | F
F -> ( E ) | digit

新建一个文本文件,输入以下内容:

line   : expr "\n"
       ;
expr   : expr "+" term
       | term
       ;
term   : term "*" factor
       | factor
       ;
factor : "(" expr ")"
       | DIGIT
       ;

这个文件可以任何的扩展名,建议使用*.yacc作为扩展名。表示这个文件是yacc语法规范文件。

说明:

语法符号说明:

语法符号字面量,为双引号包围的字符串字面量。转义规则同JSON语法。

语法符号不可以为空字符串"",空字符串被yacc内部用作特殊符号。

当语法符号匹配正则表达式\w+时,可以省略包围的引号,用标识符表示法。比如,"EXP"可以简写为EXP.

空白仅用于分隔语法符号。

翻译规则说明:

<head> : <body>_1
       | <body>_2
         ...
       | <body>_n
       ;

第一个产生式的左侧符号是开始符号。

空白仅用于分隔语法符号。

无需输入语义动作,语义动作已经内置为构造语法树。

新建一个FSharp xUnit测试项目:

安装NuGet包:

Install-Package FSharpCompiler.Yacc
Install-Package FSharpCompiler.Parsing
Install-Package FSharp.xUnit
Install-Package FSharp.Literals
Install-Package FSharp.Idioms

前两个包是编译器的主包,其余的是各种常用功能的函数库。本文为节约篇幅,直接使用库函数,而不展开代码。

把上面输入的规则文件添加到项目中,为了方便。

将文件中的内容读取到变量中:

let path = Path.Combine(__SOURCE_DIRECTORY__, @"E69.yacc")
let text = File.ReadAllText(path)

解析输入文本:

let yaccFile = YaccFile.parse text
let yacc = ParseTable.create(yaccFile.mainRules, yaccFile.precedences)

生成数据:

    [<Fact>]
    member this.``generate parse table``() =
        let result =
            [
                "let rules = " + Render.stringify yacc.rules
                "let kernelSymbols = " + Render.stringify yacc.kernelSymbols
                "let parsingTable = " + Render.stringify yacc.parsingTable
            ] |> String.concat System.Environment.NewLine
        output.WriteLine(result)

把输出的结果,复制到一个单独的模块文件中比如:

module E69ParseTable

let rules = set [...]
let kernelSymbols = Map.ofList [...]
let parsingTable = set [...]

我们可能经常升级改动输入文件,这是验证输出文件和输入文件是一致的方法:

    [<Fact>]
    member this.``validate parse table``() =
        Should.equal yacc.rules E69ParseTable.rules
        Should.equal yacc.kernelSymbols E69ParseTable.kernelSymbols
        Should.equal yacc.parsingTable E69ParseTable.parsingTable

这里需要添加NuGet程序包FSharp.xUnit,包里面只有一个方法Should.equal x y

这就是yacc的基本用法。下一步,我们将用生成的结果,构造一个解析器。

首先定义解析器的输入,输入为一个F#可区分联合的符记序列,我们需要知道,语法分析其中那些符号是终结符号

    [<Fact>]
    member this.``terminals``() =
        let grammar = Grammar.from yaccFile.mainRules
        let terminals = grammar.symbols - grammar.nonterminals
        let result = Render.stringify terminals
        output.WriteLine(result)

说明Grammar是yacc包中带的工具类型,可以获取文法的常用性质,这里是获知文法中所有的终结符号集合。Render.stringify位于FSharp.Literals中,用于打印F#数据字面量,可以理解为数据源代码。

打印的终结符号集合结果如下:

set ["\n";"(";")";"*";"+";"DIGIT"]

当然,有熟练的人可以自己计算文法中的那些符号是终结符号。接下来,我们根据此结果编写Token类型:

type ExpToken =
    | EOL
    | LPAREN
    | RPAREN
    | STAR
    | PLUS
    | DIGIT of int

F#可区分联合的标签名首字母必须大写,且是合法的标识,所以无法做到Token的标签和终结符号完全相同。还需要定义一个成员,用于把Token数据映射到终结符号:

    member this.getTag() =
        match this with
        | EOL -> "\n"
        | LPAREN -> "("
        | RPAREN -> ")"
        | STAR -> "*"
        | PLUS -> "+"
        | DIGIT _ -> "DIGIT"

定义了语法的输入类型Token,这时就可以写解析器了。

let parser =
    SyntacticParser(
        E69ParseTable.rules,
        E69ParseTable.kernelSymbols,
        E69ParseTable.parsingTable)

let parseTokens tokens =
    parser.parse(tokens,fun (tok:ExpToken) -> tok.getTag())

SyntacticParser 位于FSharpCompiler.Parsing中的类型,其输入正是前面我们使用Yacc得到的数据,对于一个文法,我们只需要一个实例,应该把它的构造代码放在解析器外面。parser.parse是生成抽象语法树,第一个参数是词法符记的序列,第二个参数用来向解析器提供终结符号,getTag去除了词法符记的语义数据,只保留解析器所需要的文本数据,当树节点解析成功时,根据终结符号在序列中的位置取出词法符记的语义数据。绑定到抽象语法树相应的节点上。

此时,我们可以测试这个函数:

    [<Fact>]
    member this.``parse tokens``() =
        let tokens = [DIGIT 1;PLUS;DIGIT 2;STAR;LPAREN;DIGIT 4;PLUS;DIGIT 3;RPAREN;EOL]
        let tree = E69.Parser.parseTokens tokens
        let result = Render.stringify tree
        output.WriteLine(result)

可以得到结果:

        let y = Interior("line",[
            Interior("expr",[
                Interior("expr",[
                    Interior("term",[
                        Interior("factor",[
                            Terminal(DIGIT 1)])])]);
                Terminal PLUS;
                Interior("term",[
                    Interior("term",[
                        Interior("factor",[
                            Terminal(DIGIT 2)])]);
                    Terminal STAR;
                    Interior("factor",[
                        Terminal LPAREN;
                        Interior("expr",[
                            Interior("expr",[
                                Interior("term",[Interior("factor",[Terminal(DIGIT 4)])])]);
                            Terminal PLUS;
                            Interior("term",[
                                Interior("factor",[Terminal(DIGIT 3)])])]);
                        Terminal RPAREN])])]);
            Terminal EOL])

返回一个抽象语法树对象,这个树用可区分联合递归表达,此对象在FSharpCompiler.Parsing包中定义:

type ParseTree<'tok> =
    | Interior of symbol:string * children: list<ParseTree<'tok>>
    | Terminal of 'tok

这个数据结构是自解释的,Interior是内部节点,非终结符号,Terminal是终结符号。

如何从普通文本得到词法符记序列,利用.net库中的字符串和正则表达式方法,很容易就可以写一个词法分析器,把它作为词法符记类型的静态构造函数是很自然的:

    static member from (text:string) =
        let rec loop (inp:string) =
            seq {
                match inp with
                | "" -> ()

                | Prefix @"[\s-[\n]]+" (_,rest) // 空白
                    -> yield! loop rest

                | Prefix @"\n" (_,rest) -> //换行
                    yield EOL
                    yield! loop rest

                | PrefixChar '(' rest ->
                    yield LPAREN
                    yield! loop rest

                | PrefixChar ')' rest ->
                    yield RPAREN
                    yield! loop rest

                | PrefixChar '*' rest ->
                    yield STAR
                    yield! loop rest

                | PrefixChar '+' rest ->
                    yield PLUS
                    yield! loop rest

                | Prefix @"\d+" (lexeme,rest) ->
                    yield  DIGIT(Int32.Parse lexeme)
                    yield! loop rest

                | never -> failwith never
            }
        loop text

这里用到了活动模式,位于FSharp.Literals.StringUtils模块中,需要安装FSharp.Literals包。活动模式Prefix后面跟着一个正则表达式,匹配输入字符串的最开头的部分,则成功。输入的正则表达式不需要使用^进行定位。活动模式PrefixChar 带有一个字符参数,如果输入字符串的首字符匹配,则成功。

测试这个函数:

    [<Fact>]
    member this.``tokenize``() =
        let inp = "1+2*(4+3)" + System.Environment.NewLine
        let tokens = E69.ExpToken.from inp

        let result = Render.stringify (List.ofSeq tokens)
        output.WriteLine(result)

如上所属最后两行是打印输出结果的功能。第一行是输入,第二行得到函数的输出:

[DIGIT 1;PLUS;DIGIT 2;STAR;LPAREN;DIGIT 4;PLUS;DIGIT 3;RPAREN;EOL]

这个实例的源代码在https://github.com/xp44mm/FSharpCompiler.Docs上。

标签:yacc,DIGIT,FSharpCompiler,示例,rest,Yacc,let,yield,Interior
来源: https://www.cnblogs.com/cuishengli/p/14363253.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有