サンプルでなんとなく理解するmllex

やむを得ず調べてしまったので動く状態のサンプルを書き留めておきます。(タイガー本で書いたヤツとか忘れたわー(ノ`´)ノミ┻┻)

mllexとは

mllexはlexのSML版です。トークンに対応する正規表現を並べておくと、それをストリームから切り出すレキサ(字句解析器)を生成してくれます。

SML/NJに付属するものとしてmllexが作られ、それをMLtonがforkして取り込んだようです。いくつか機能が追加されてるみたいですね。
SML/NJにはml-ulexというレキサジェネレータも付いてきますが別物です。こちらの方が後発で高機能なようです(が私は使ったことありません)。
SML#ディストリビューションにもmllexからfork(?)したsmllex*1が含まれており、これで構文のレキサを作っています。READMEを見る感じではバグフィックスが1つ入っただけでmllexと同じもののようです。v1.2.0を自分でビルドしたなら(src/ml-lex/smllex)にあるはず。

サンプル

いきなりサンプルから。
以下の例はML-Yaccの使い方(東北大学の資料)に大体合わせたlambda.lexです。

(* lambda.lex *)
datatype lexresult (* 読み出すトークンの型 *)
	= VAR of string
	| NUM of IntInf.int
	| OPER of string
	| LPAREN | RPAREN
	| LBRACKET | RBRACKET
	| LAM | DOT
	| EOF

fun toString (VAR id)    = concat["VAR(", id, ")"]
  | toString (NUM num)   = concat["NUM(", IntInf.toString num, ")"]
  | toString (OPER oper) = concat["OPER(", oper, ")"]
  | toString LPAREN   = "LPAREN"
  | toString RPAREN   = "RPAREN"
  | toString LBRACKET = "LBRACKET"
  | toString RBRACKET = "RBRACKET"
  | toString LAM      = "LAM"
  | toString DOT      = "DOT"
  | toString EOF      = "<EOF>"

(* トークンを読み出した行番号 *)
val pos = ref 0
(* ストリーム終端で構成する値を指定。後処理もあれば書く。 *)
val eof = fn () => EOF

%%
%structure LambdaLex
alpha=[A-Za-z];
prime=['];
digit=[0-9];
operator=[-+/*];
ws=[\ \t];

%%
\n              => (pos := (!pos) + 1; lex());
{ws}+           => (lex());
"\\"            => (LAM);
"."             => (DOT);
-?{digit}+      => (NUM(valOf (LargeInt.fromString yytext)));
"("             => (LPAREN);
")"             => (RPAREN);
"["             => (LBRACKET);
"]"             => (RBRACKET);
{alpha}{prime}* => (VAR yytext);
{operator}      => (OPER yytext);

何となくわかりそうですか? これが読み出そうとしているのは、関数適用を[](ブラケット)で囲う必要のあるラムダ式を構成するトークンです。(字句解析では文法は気にしないのでデタラメ順序でも読み出せますけど)


1つ目の %% の前に必要な幾つかの値と型を定義します。重要なのは lexresult 型で、これは生成するレキサが読み出す値の型として使います(名前はなんでもいいです)。
二つ目の %% の後ろには

<正規表現> => <SMLコード>

という記法で、切り出したいトークンを構築するSMLコードを指定します。この式は lexreult型 である必要があります。

真ん中のセクションで指定している %structure では生成するstructureの名前を決めることが出来ます(例ではLambdaLex)。指定しない場合はTokenという名前になります。
ここではどうでもいいんですが、yaccと連携するときには気にする必要が出てきます…。

使い方

smlsharpのreplで実行してみます。
上のファイル(lambda.lex)をsmllexに投げると lambda.lex.sml が生成されるので、それを使うだけです。

$ smllex lambda.lex (* lambda.lex.sml が作られる *)
$ smlsharp
$ use "lambda.lex.sml";
$ open LambdaLex;
# val lex = LambdaLex.makeLexer(fn _=>valOf(TextIO.inputLine TextIO.stdIn));
val lex = fn : unit -> LambdaLex.UserDeclarations.lexresult
(* 補助関数を定義 *)
# fun pp s=print(s^"\n");
# infixr 1 $ ; fun f $ a = f a;
# fun lexpp f = pp $ LambdaLex.UserDeclarations.toString $ f();
(* 使う *)
# lexpp lex;
(\x.y); (* <- 標準入力 *)
LPAREN
# lexpp lex;
LAM
# lexpp lex;
VAR(x)
# lexpp lex;
DOT
# lexpp lex;
VAR(y)      (* lexを()に適用するたびにトークンが出てくる *)
# lexpp lex;
RPAREN
# lexpp lex; (* 入力ストリームが終端に達すると例外を投げる *)
uncaught exception: LambdaLex.LexError
#

解説

ここで重要なのは makeLexer という関数だけです。

val LambdaLex.makeLexer
	: (int -> string) -> unit -> LambdaLex.UserDeclarations.lexresult

この関数には *与えられた文字数の文字列を読み出す* 関数を与えると、レキサが手に入ります。ただここで与えられる文字数はストリームへのヒントなだけで実際に何文字読んでもよさげです。
上の例では標準入力から一行読んでます。これね>

# val lex = LambdaLex.makeLexer(fn _=>valOf(TextIO.inputLine TextIO.stdIn));

で、得られたレキサ(()->lexresult)を () に適用するとそのたびにトークンが読み出されます。終わり。

おまけ

smllexはsmiファイルを吐いてくれないので、自分で定義しないとlexで生成した結果を他からコンパイルして使えません。
以下にsmiファイルを挙げます。

(* lambda.lex.smi *)
_require "basis.smi"
_require "ml-yacc-lib.smi"

structure LambdaLex =
struct
  structure UserDeclarations =
  struct
    datatype lexresult = DOT
                       | EOF
                       | LAM
                       | LBRACKET
                       | LPAREN
                       | NUM of intInf
                       | OPER of string
                       | RBRACKET
                       | RPAREN
                       | VAR of string
    val toString : lexresult -> string
  end
  exception LexError
  val makeLexer : (int -> string) -> unit -> UserDeclarations.lexresult
end

(あぁ、なんて短いんだろう…)

*1:名前が紛らわしすぎる…