OCaml + Menhir 编译/写作

14

对于OCaml,我完全是一个新手。我只是最近开始使用这种语言(大约两周前),但不幸的是,我被要求使用Menhir为一种虚构的语言编写一个语法分析器(解析器+词法分析器,其功能是接受或拒绝一个句子)。现在,我在互联网上找到了一些关于OCaml和Menhir的资料:

Menhir手册。

这个法国大学课程的网页。

在Sourceforge上Toss主页上的一个简短的Menhir教程。

derdon在github上的一个Menhir示例。

一本关于OCaml的书(其中有关于ocamllex+ocamlyacc的一些内容)

SooHyoung Oh的一个随机ocamllex教程。

以及Menhir源代码附带的示例。

(我不能放超过两个超链接,所以我不能直接链接您到我提到的一些网站。抱歉!)

所以,你可以看到,我一直在拼命寻找更多的材料来帮助我制作这个程序。不幸的是,我仍然无法掌握许多概念,因此我遇到了很多困难。
首先,我不知道如何正确编译我的程序。我一直在使用以下命令:
ocamlbuild -use-menhir -menhir "menhir --external-tokens Tokens" main.native

我的程序分为四个不同的文件:main.ml; lexer.mll; parser.mly; tokens.mly。 main.ml 是从以参数形式给定的文件系统中获取输入的部分。
let filename = Sys.argv.(1)

let () =
    let inBuffer = open_in filename in
    let lineBuffer = Lexing.from_channel inBuffer in
    try
        let acceptance = Parser.main Lexer.main lineBuffer in
        match acceptance with
            | true -> print_string "Accepted!\n"
            | false -> print_string "Not accepted!\n"
    with
        | Lexer.Error msg -> Printf.fprintf stderr "%s%!\n" msg
        | Parser.Error -> Printf.fprintf stderr "At offset %d: syntax error.\n%!" (Lexing.lexeme_start lineBuffer)

第二个文件是lexer.mll。

{
  open Tokens
  exception Error of string
}

rule main = parse
  | [' ' '\t']+
      { main lexbuf }
  | ['0'-'9']+ as integer
      { INT (int_of_string integer) }
  | "True"
      { BOOL true }
  | "False"
      { BOOL false }
  | '+'
      { PLUS }
  | '-'
      { MINUS }
  | '*'
      { TIMES }
  | '/'
      { DIVIDE }
  | "def"
      { DEF }
  | "int"
      { INTTYPE }
  | ['A'-'Z' 'a'-'z' '_']['0'-'9' 'A'-'Z' 'a'-'z' '_']* as s
      { ID (s) }
  | '('
      { LPAREN }
  | ')'
      { RPAREN }
  | '>'
      { LARGER }
  | '<'
      { SMALLER }
  | ">="
      { EQLARGER }
  | "<="
      { EQSMALLER }
  | "="
      { EQUAL }
  | "!="
      { NOTEQUAL }
  | '~'
      { NOT }
  | "&&"
      { AND }
  | "||"
      { OR }
  | '('
      { LPAREN }
  | ')'
      { RPAREN }
  | "writeint"
      { WRITEINT }
  | '\n'
      { EOL }
  | eof
      { EOF }
  | _
      { raise (Error (Printf.sprintf "At offset %d: unexpected character.\n" (Lexing.lexeme_start lexbuf))) }

第三个文件是 parser.mly。
%start <bool> main
%%

main:
| WRITEINT INT { true }

第四个是tokens.mly
%token <string> ID
%token <int> INT
%token <bool> BOOL
%token EOF EOL DEF INTTYPE LPAREN RPAREN WRITEINT
%token PLUS MINUS TIMES DIVIDE
%token LARGER SMALLER EQLARGER EQSMALLER EQUAL NOTEQUAL
%token NOT AND OR

%left OR
%left AND
%nonassoc NOT
%nonassoc LARGER SMALLER EQLARGER EQSMALLER EQUAL NOTEQUAL
%left PLUS MINUS
%left TIMES DIVIDE
%nonassoc LPAREN
%nonassoc ATTRIB

%{
type token =
  | ID of (string)
  | INT
  | BOOL
  | DEF
  | INTTYPE
  | LPAREN
  | RPAREN
  | WRITEINT
  | PLUS
  | MINUS
  | TIMES
  | DIVIDE
  | LARGER
  | SMALLER
  | EQLARGER
  | EQSMALLER
  | EQUAL
  | NOTEQUAL
  | NOT
  | AND
  | OR
  | EOF
  | EOL
%}

%%

现在,我知道这里有很多未使用的符号,但我打算在我的解析器中使用它们。无论我对文件进行多少更改,编译器都会在我的面前崩溃。我尝试了我能想到的一切,但似乎没有什么作用。是什么导致ocamlbuild以大量未绑定构造函数和未定义起始符号的错误而爆炸?我应该使用哪个命令来正确编译程序?在哪里可以找到有意义的材料来学习Menhir?
3个回答

10
更简单的方法是去掉Parser/Tokens分离。正如Thomas所指出的那样,不需要声明“type token = ...”,因为它会自动从%token指令中由Menhir生成。 因此,可以将parser.mly定义为:
%start <bool> main

%token <string> ID
%token <int> INT
%token <bool> BOOL
%token EOF EOL DEF INTTYPE LPAREN RPAREN WRITEINT
%token PLUS MINUS TIMES DIVIDE
%token LARGER SMALLER EQLARGER EQSMALLER EQUAL NOTEQUAL
%token NOT AND OR

%left OR
%left AND
%nonassoc NOT
%nonassoc LARGER SMALLER EQLARGER EQSMALLER EQUAL NOTEQUAL
%left PLUS MINUS
%left TIMES DIVIDE
%nonassoc LPAREN
%nonassoc ATTRIB
%%

main:
| WRITEINT INT { true }

并且 lexer.mll 如下:

{
  open Parser
  exception Error of string
}

[...] (* rest of the code not shown here *)

然后删除tokens.mly文件,并使用以下命令编译

ocamlbuild -use-menhir main.native

它一切都运行良好。


确实,只有一个 mly 更简单。我没有在我的答案中提出这个解决方案,因为我假设 @Lopson 想要使用 menhir 的“解析单元的分离编译”功能。 - Thomas
谢谢大家的帮助,你们不知道你们的帖子对我有多宝贵!终于,事情开始变得有些意义了。 - Lopson

7

首先,在 tokens.mly 中,您不需要重复标记:

%token <string> ID
%token <int> INT
%token <bool> BOOL
%token EOF EOL DEF INTTYPE LPAREN RPAREN WRITEINT
%token PLUS MINUS TIMES DIVIDE
%token LARGER SMALLER EQLARGER EQSMALLER EQUAL NOTEQUAL
%token NOT AND OR

%left OR
%left AND
%nonassoc NOT
%nonassoc LARGER SMALLER EQLARGER EQSMALLER EQUAL NOTEQUAL
%left PLUS MINUS
%left TIMES DIVIDE
%nonassoc LPAREN
%nonassoc ATTRIB

%%

然后,我不知道传递给ocamlbuild的魔法选项,也不太了解menhir,但是据我理解,您需要将所有.mly打包成一个解析器单元:

menhir tokens.mly parser.mly -base parser

接着,如果你在 lexer.mll 文件中用 Parser 代替任何一个 Token,那么运行 ocamlbuild -no-hygiene main.byte 应该是可行的。不过可能还有更巧妙的方法。


1
我遇到了同样的问题,除此之外,解析器还需要当前目录之外的模块。我无法找出如何调用ocamlbuild以指定必须从3个mly文件构建parser.{ml,mli},因此我只是制作了一个makefile:
  • 将模块.cmi从_build复制到当前目录中(以满足menhir --infer)
  • 调用menhir
  • 删除已复制的模块以满足ocamlbuild
  • 然后调用ocamlbuild
我对此并不满意,所以我对任何更好的替代方案都感兴趣,但如果你确实要以最小的努力完成项目,那么我想这就是走的路。
编辑: 实际上,没有必要复制和删除编译的模块,只需在第二步传递选项给menhir即可: menhir --ocamlc "ocamlc -I \"../_build/modules/\"" --infer --base parser 可悲的是,这仍意味着解析器生成将基于先前编译的模块,因此可以预期会有一个不必要的(且失败的)第一次编译。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接