【Android 15 タブレット 初登場】Bmax I10 Plus アンドロイド 15 タブレット 10インチ、12GB+128GB+1TB拡張、WidevineL1 Netflix対応、8コアCPU T606 タブレット、6000mAh+Type-C充電+5GWiFi+BT5.0、OTG+顔認識+無線投影+画面分割+FMラジオ、Android 15 タブレット 10インチ wi-fiモデル
¥16,900 (2025年4月28日 13:11 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)タッチペン 【2025年革新版 全機種対応】タブレット ペン スタイラスペン スマホ Type-C充電 超高精度 極細 12g超軽量 3つ交換用ペン先付き 互換ペン 電量表示/磁気吸着機能対応 軽量 耐摩 耐久 iPad・iPhone・Android・スマホ・タブレット用ペンシル 日本語取扱説明書付き
¥2,099 (2025年4月28日 13:05 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)
この記事では、正規表現や言語処理系のしくみ、型レベルプログラミング等について理解しながら、TypeScript の型システム上で動作する正規表現処理系をつくることを目指します。この記事の内容を完了すると、以下のような型をつくることができます。
type EmailRegExp = TRegExp"^.+@.+\\..+$">;
type ValidEmail = TRegExpTest"piyo@hiyoko.com", EmailRegExp>;
type InvalidEmail = TRegExpTest"piyo.com", EmailRegExp>;
今回の実装内容は こちら で動作確認することができます。
本記事は以下の前提知識を必要とします。
- TypeScript の型, 型演算
- 基本的な型
- Generics
- Conditional Type
- infer
- Mapped Types
- Template Literal Types
- 再帰への慣れ
ここでは、正規表現の概要と言語処理系のしくみについて紹介します。
正規表現でできること
正規表現 (regular expression) は、複数パターンの文字列を一つの文字列で表現するための枠組みです。例えば、正規表現 pi|yo
は、文字列 pi
または yo
を表現します。繰り返し回数を指定できる 数量子 (quantifier) を用いることで、無限パターンの文字列を表現することもできます。
// `pi` または `yo` の1回以上の繰り返し
(pi|yo)+ -> pi, yo, piyo, yopi, pipiyo, piyopiyo, ...
今回は以下の要素を実装します。ここで、expr
などはある一つの正規表現とします。
※(キャプチャ)グループやエスケープなど、一般的な正規表現と仕様が一部異なる要素があります。
言語のつくりかた
正規表現やプログラミング言語などのコンピュータが解釈できる形式言語は、シンタックス (統語論, 構文論, syntax) とセマンティクス (意味論, semantics) から成り立ちます。逆に言うと、これらをちゃんと定義するとプログラミング言語をつくったことになります。シンタックスはざっくりどのような文字列がプログラムを表現するか?という「文法」の定義で、セマンティクスはざっくりどんな処理が実行されるか?という「意味」の定義です。
言語処理系
ある文字列が与えられたときに、それを特定言語のシンタックス・セマンティクスに基づいて解釈し、なんらかの処理を行うソフトウェアのことを言語処理系といい、特にセマンティクス部分の処理方式によって、インタプリタとコンパイラに大別されます。インタプリタでもコンパイラでも、与えられた文字列をシンタックスに基づいて解釈する字句解析・構文解析は共通の機能であり、解釈した結果は 抽象構文木 (abstract syntax tree, AST) というデータ構造で表現されます。
正規表現処理系における構文解析以降の実装方法としては、主に DFA 型と VM 型があります。DFA 型は、抽象構文木から DFA (決定性有限オートマトン) を構成します。VM 型は、抽象構文木を仮想マシン用の命令列にコンパイルして実行します。今回は、DFA 型のうち、特に NFA (非決定性有限オートマトン) バージョンのものを実装します。(NFA の方がかんたん)
今回実装する処理系で行う以下の処理フローについて、次節以降で詳しく説明します。
- 字句解析: 正規表現を表す文字列 → トークン列
- 構文解析: トークン列 → 抽象構文木
- NFA 構築: 抽象構文木 → NFA
- NFA 実行: (NFA, 対象文字列) → 受理結果
字句解析
字句解析 (lexical analysis) とは、与えられた文字列を、解釈対象の言語において意味のある最小単位 (トークン) に分割する処理です。正規表現におけるトークンは、先に挙げた演算子、数量子、文字リテラルなどがそれにあたります。今回の実装範囲では、エスケープ以外は基本的に入力文字 1 文字とトークンが一対一対応しますが、一般的なプログラミング言語でよくある予約語 if
, else
, for
, class
などの複数文字からなるキーワードも一つのトークンとして扱われます。
トークン化の例
(pi|yo)+ -> [LPAREN, CHAR(p), CHAR(i), UNION, CHAR(y), CHAR(o), RPAREN, PLUS]
構文解析
構文解析 (parsing, syntactic analysis) とは、字句解析で出力されたトークン列から抽象構文木を構成する処理です。抽象構文木の “雰囲気” は以下のような感じです。
if (x > y) {
max = x;
} else {
max = y;
}
トークン列からこのような構文木を構成するには、文法規則、つまりどのようなトークン列が正しい言語か?を定義する必要があります。文法の定義方法の一つとして、BNF (バッカス・ナウア記法, Backus–Naur form) があります。今回実装する正規表現の BNF は以下のような感じです。
regexp> ::= union-expr>
union-expr> ::= concat-expr> UNION union-expr> | concat-expr>
concat-expr> ::= plus-expr> concat-expr> | plus-expr>
plus-expr> ::= factor-expr> PLUS | star-expr>
star-expr> ::= factor-expr> STAR | option-expr>
option-expr> ::= factor-expr> OPTION | factor-expr>
factor-expr> ::= LPAREN union-expr> RPAREN | DOT | CHAR(x)
::=
で定義されるのが一つの規則で、左辺の記号から右辺の記号列を導くことができます。紛らわしいですが、ここでの |
は正規表現ではなく BNF の記法で、|
で分割されたいずれかの記号列を導くことができる、という意味になります。例えば、規則
から生成される記号列は以下のいずれかです。
-
から生成される記号列 +UNION
+
から生成される記号列 -
から生成される記号列
規則
から始めて、規則 (非終端記号) がなくなるまで連続的に規則を適用することで生成されるトークン列 (終端記号列) は、すべて (今回定義する) 正規表現です。
試しに、文字列 (ab|c)*
に対応する以下のトークン列を
から導いてみましょう!
LPAREN CHAR(a) CHAR(b) UNION CHAR(c) RPAREN STAR
(ab|c)\* を表すトークン列の導出
-
: トップレベルの記号 -
: 規則
を適用して
に変換 -
: 規則
の右側を適用 -
: 規則
の右側 -
: 規則
の右 -
:STAR
の左 -
LPAREN
:RPAREN STAR
の 1 番目 -
LPAREN
:UNION RPAREN STAR
の左 -
LPAREN
:UNION RPAREN STAR
の左 -
LPAREN
:UNION RPAREN STAR
の右 -
LPAREN
:UNION RPAREN STAR
の右 -
LPAREN
:UNION RPAREN STAR
の右 -
LPAREN CHAR(a)
:UNION RPAREN STAR
の 3 番目 -
LPAREN CHAR(a)
:UNION RPAREN STAR
の右 -
LPAREN CHAR(a)
:UNION RPAREN STAR
の右 -
LPAREN CHAR(a)
:UNION RPAREN STAR
の右 -
LPAREN CHAR(a)
:UNION RPAREN STAR
の右 -
LPAREN CHAR(a) CHAR(b) UNION
:RPAREN STAR
の 3 番目 - (省略)
->
までずっと右 -
LPAREN CHAR(a) CHAR(b) UNION CHAR(c) RPAREN STAR
:
の 3 番目
できました!
このトークン列から生成される AST は以下のようになります。
有限オートマトン
オートマトン (automaton) とは、ざっくり「状態」と「遷移規則」をもった抽象的な計算モデルです。特に、状態数が有限のものを 有限オートマトン (finite automaton) といいます。本来は数学的記号を導入して形式的に定義したほうがよいのですが、具体例を示してしまった方が分かりやすいので、まずは以下の例をみてみましょう。
3 つの状態と各状態からの遷移 (文字) からできています。また、特別な状態として初期状態 (start) と受理状態 (accept) があります。ある文字列が与えられたとき、初期状態から始めて文字を 1 文字ずつ読み込みながら状態を遷移していき、すべての文字を読み込んだ時点で受理状態にいたらその文字は受理されます。初期状態は必ず一つ、受理状態は複数あっても構いません。
この有限オートマトンは a
, b
からなる文字列が与えられたとき、それが a
を偶数個含むかどうかを判定することができます。abab
が与えられたときの判定処理は以下のような感じです。
- 初期状態
s0
からスタート - 1 文字目が
a
なので、s0
からa
で遷移できる状態s1
に移動 - 2 文字目が
b
なので、s1
からb
で遷移できる状態s1
に移動 - 3 文字目が
a
なので、s1
からa
で遷移できる状態s2
に移動 - 4 文字目が
b
なので、s2
からb
で遷移できる状態s2
に移動 - すべての入力文字列を読んだ時点で受理状態
s2
にいるので、この文字列は受理される
状態を解釈すると、s1
が「これまでに読んでいる文字列のうち a
の数が奇数個」、s2
が 「これまでに読んでいる文字列のうち a
の数が偶数個」となっています。
このように、有限オートマトンは「ある文字の集合を決めたときにその文字からなる文字列がある性質を満たすかどうか?」を判定することができます。
正規表現と有限オートマトンの対応
正規表現は複数パターンの文字列を一つの文字列で表現したもので、ある文字列がそのパターンに一致するか判定したりすることができます。任意の正規表現について、それに対応する有限オートマトンに変換できれば判定処理が実現できそうです。ここでは、各正規表現に対応する有限オートマトンの構成方法について考えます。
リテラル文字
リテラル文字トークン CHAR(a)
に対応するオートマトンは以下の通りです。初期状態からはじめて、文字 a
を受け取ったら受理状態に遷移する。簡単ですね!
… ところで、a
以外の文字列が来たときはどうなるのでしょうか?
遷移先が 0 個 (未定義)であったり、2 個以上だったりするものを 非決定性有限オートマトン (nondeterministic finite automaton, NFA) といいます。それに対して、入力に対して遷移先が必ず 1 つに定まっているものを 決定性有限オートマトン (deterministic finite automaton, DFA) といいます。先ほどの a
が偶数個か判定するやつは (入力文字が a
, b
のみという前提のもとで) DFA です。
連接
連接は、2 つの正規表現 expr1
, expr2
があるとき、それらをその順で並べたもの expr1expr2
でした。まず前提として、expr1
, expr2
それぞれに対応する NFA ができていると仮定します。ここで、初期状態と受理状態以外はどうでもいいので省略しています。
これらを利用して連接に対応する NFA を構成する方法を考えます。そのために、ここで イプシロン遷移 (ε-遷移) というものを導入します。簡単にいうと「ある入力に対して、いくらでも遷移してもいいし、全くしなくてもいい」遷移規則です。例えば、以下のような NFA を考えたとき、 s0
にいる状態で入力 a
を受け取ったときは、 ε
を経て a
の遷移で s2
に移動したあと、状態 s2
に留まることも、 s3
へ遷移することもできます。よって、 NFA の動作をシミュレートする際は、現在いる複数状態を管理しながら並列実行する必要があります。
このイプシロン遷移を用いると、先程の連接に対応する NFA は以下のように構成することができます。
expr1
のすべての受理状態について、ε-遷移で expr2
の初期状態に移動できるようにしています。NFA の直列つなぎ的な感じですね。2 つの NFA を合成してできた新たな NFA について、初期状態は s0
、受理状態は t1
となります。
これで連接については OK です。以降の正規表現について、対応した NFA を考えるための道具は既にそろっているので、ぜひご自身で変換方法を考えてみてください。
論理和
次に、2 つの正規表現 expr1
, expr2
の論理和 expr1|expr2
に対応する NFA を構成する方法を考えます。新たに初期状態 u0
を用意して、expr1
, expr2
それぞれの初期状態へ ε-遷移で移動できるようにします。受理状態は、元の 2 つの NFA のすべての受理状態の集合 (s1
, s2
, t1
) になります。こちらは並列つなぎ的な感じですね。
これで論理和に対応する NFA を構成することができました。
数量子 +
数量詞 +
は「直前の正規表現の 1 回以上の繰り返し」でした。expr+
に対応する NFA を考えるために、まず expr
に対応する NFA ができていると仮定します。
繰り返しに対応するには、すべての受理状態から初期状態への ε-遷移を追加します。これで、受理状態にいる状態からさらに文字列を読む場合に状態を初期状態にリセットすることができます。
これで expr+
に対応する NFA を構成することができました。
数量子 *
数量詞 *
は「直前の正規表現の 0 回以上の繰り返し」でした。0 回でも OK ということは、初期状態の時点で受理されるので、先ほどの expr+
に対応する NFA について、初期状態を受理状態に追加するとよさそうです。
これで expr*
に対応する NFA を構成することができました。
数量子 ?
数量詞 ?
は「直前の正規表現の 0 または 1 回の出現」でした。*
と同じ考え方で、expr
に対応する NFA について初期状態を受理状態に含めるだけで OK ですね。
これで expr?
に対応する NFA を構成することができました。
以上で、正規表現処理系をつくるための理論的側面をざっとおさえることができました。
ここでは、TypeScript の型機能のみを用いた正規表現処理系を実装していきます。
Lexer
まず、字句解析モジュール lexer
を実装していきましょう!
はじめに、トークンに関連する型を定義します。
token.ts
type Token = { type: string };
type TokenLParen = Token & { type: "LPAREN" };
type TokenRParen = Token & { type: "RPAREN" };
type TokenUnion = Token & { type: "UNION" };
type TokenPlus = Token & { type: "PLUS" };
type TokenStar = Token & { type: "STAR" };
type TokenOption = Token & { type: "OPTION" };
type TokenDot = Token & { type: "DOT" };
type TokenCharC extends string> = Token & { type: "CHAR"; char: C };
type TokenAnchorStart = Token & { type: "ANCHOR_START" };
type TokenAnchorEnd = Token & { type: "ANCHOR_END" };
まずトークンの基底型として string
型の type
プロパティを持ったオブジェクト型である Token
を定義します。次に、TokenLParen
では LPAREN
といった具合に、 type
で特定の文字列を指定することで具体的なトークンを指定します。ただし、リテラル文字に対応する TokenChar
については、指定された文字を保持しておく必要があるため、型パラメータ S
をとって char
プロパティの型とします。
また、利便性のために正規表現文字とトークン型のマッピングを表現した型 TokenMap
を定義しておきます。
token.ts
type TokenMap = {
"(": TokenLParen;
")": TokenRParen;
"|": TokenUnion;
"+": TokenPlus;
"*": TokenStar;
"?": TokenOption;
".": TokenDot;
"^": TokenAnchorStart;
$: TokenAnchorEnd;
};
次に、メインとなる字句解析型 Lexer
の定義です。
lexer.ts
type LexerS extends string> = LexerInternalS>;
type LexerInternal
S extends string,
Tokens extends Token[] = []
> = S extends `${infer Head}${infer Tail}`
? Head extends keyof TokenMap
? LexerInternalTail, [...Tokens, TokenMap[Head]]>
: Head extends "\\"
? Tail extends `${infer EscapedChar}${infer EscapedTail}`
? LexerInternalEscapedTail, [...Tokens, TokenCharEscapedChar>]>
: never
: Head extends string
? LexerInternalTail, [...Tokens, TokenCharHead>]>
: never
: {
tokens: Tokens;
};
まず LexerInternal
の型引数について、S
は正規表現として解釈する文字列、Tokens
は字句解析結果のトークン列 Token[]
です。パラメータ Tokens
はあくまでも LexerInternal
の再帰処理のためのものなので、ラップした型 Lexer
を定義し、外側からはこちらを使ってもらうようにします。
LexerInternal
では、Template Literal Types によって S
の文字列を先頭から読み込んでいき、例えば (
に一致したときは、「残りの文字列 Tail
」と「TokenLParen
を追加したトークン列」を入力とする LexerInternal
を返します。このように、再帰的に次に続く文字列を処理しながらトークン列 Tokens
を生成します。各正規表現記号にマッチするかどうかを書き連ねる代わりにここで TokenMap
を用いています。
S
の先頭 Head
がエスケープ \
に一致する場合はその次の文字を EscapedChar
としてリテラル文字トークン TokenChar
を追加します。以上のどれにも当てはまらない場合は、先頭文字 Head
に対応する文字列トークン TokenChar
を追加します。
これで、字句解析型 Lexer
ができました!
以下のように、正規表現文字列を与えると、それに対応するトークン列に変換されます。
type Result = Lexer"(a|b)+">;
Parser
次に、トークン列から抽象構文木を構成する構文解析型 Parser
を作成します。
まず、抽象構文木を表す型を用意していきます。
ASTNode
ast.ts
type ASTNode = { id: string; type: string };
抽象構文木のノード基底型 ASTNode
は、ノード固有の識別子 id
とノードの種類 type
を持つオブジェクト型とし、それぞれ string
型とします。
ASTNodeRoot
具体的なノードについて、まずは構文木の根ノードとなる ASTNodeRoot
型を作ります。
ast.ts
type ASTNodeRootId = "#";
type ASTNodeRootChild extends ASTNode = ASTNode> = ASTNode & {
id: ASTNodeRootId;
type: "ROOT";
child: Child;
};
パラメータ Child
は子となるノード ASTNode
です。id
はなんでもいいのですが、ここでは文字 #
としています。
ASTNodeUnion
次に、論理和 |
ノード型 ASTNodeUnion
の定義です。
ast.ts
type ASTNodeUnionIdParentId extends ASTNode["id"]> = `${ParentId}u`;
type ASTNodeUnionIdLParentId extends ASTNode["id"]> = `${ParentId}u-l`;
type ASTNodeUnionIdRParentId extends ASTNode["id"]> = `${ParentId}u-r`;
type ASTNodeUnion
ParentId extends ASTNode["id"] = ASTNode["id"],
LChild extends ASTNode = ASTNode,
RChild extends ASTNode = ASTNode
> = ASTNode & {
id: ASTNodeUnionIdParentId>;
type: "UNION";
left: LChild;
right: RChild;
};
各パラメータについて、ParentId
は親ノードの識別子、LChild
は右の子ノード、RChild
は左の子ノードです。ASTNodeUnion
の識別子 ASTNodeUnionId
は、親ノードの識別子 ParentId
に u
を付けたものとします。また、左右の子ノードが同じ type
を持つ場合にそれらを区別するために、子ノードから見た親ノードの左右での識別子 ASTNodeUnionIdL/R
を別で定義しておきます。
ASTNodeConcat
ast.ts
type ASTNodeConcatIdParentId extends ASTNode["id"]> = `${ParentId}c`;
type ASTNodeConcatIdLParentId extends ASTNode["id"]> = `${ParentId}c-l`;
type ASTNodeConcatIdRParentId extends ASTNode["id"]> = `${ParentId}c-r`;
type ASTNodeConcat
ParentId extends ASTNode["id"] = ASTNode["id"],
LChild extends ASTNode = ASTNode,
RChild extends ASTNode = ASTNode
> = ASTNode & {
id: ASTNodeConcatIdParentId>;
type: "CONCAT";
left: LChild;
right: RChild;
};
連接に対応するノードの型です。論理和のものとほぼ同じです。
ASTNodePlus
ast.ts
type ASTNodePlusIdParentId extends ASTNode["id"]> = `${ParentId}p`;
type ASTNodePlus
ParentId extends ASTNode["id"] = ASTNode["id"],
Child extends ASTNode = ASTNode
> = ASTNode & {
id: ASTNodePlusIdParentId>;
type: "PLUS";
child: Child;
};
数量詞 +
に対応するノードの型です。識別子は親ノードの識別子に p
を付け加えたもので、パラメータとして親ノードの識別子 ParentId
と子ノードの型 Child
を取ってプロパティ child
を付け加えます。ここで、Child
は +
の対象となる正規表現に対応するノードを想定しています。
ASTNodeStar
ast.ts
type ASTNodeStarIdParentId extends ASTNode["id"]> = `${ParentId}s`;
type ASTNodeStar
ParentId extends ASTNode["id"] = ASTNode["id"],
Child extends ASTNode = ASTNode
> = ASTNode & {
id: ASTNodeStarIdParentId>;
type: "STAR";
child: Child;
};
数量詞 *
に対応するノードの型です。数量詞 +
のものとほぼ同じです。
ASTNodeOption
ast.ts
type ASTNodeOptionIdParentId extends ASTNode["id"]> = `${ParentId}o`;
type ASTNodeOption
ParentId extends ASTNode["id"] = ASTNode["id"],
Child extends ASTNode = ASTNode
> = ASTNode & {
id: ASTNodeOptionIdParentId>;
type: "OPTION";
child: Child;
};
数量詞 ?
に対応するノードの型です。こちらも、数量詞 +
のものとほぼ同じです。
ASTNodeDot
ast.ts
type ASTNodeDotIdParentId extends ASTNode["id"]> = `${ParentId}d`;
type ASTNodeDotParentId extends ASTNode["id"] = ASTNode["id"]> = ASTNode & {
id: ASTNodeDotIdParentId>;
type: "DOT";
};
ワイルドカード .
に対応するノードの型です。任意の 1 文字を表すので子要素はありません。
ASTNodeChar
ast.ts
type ASTNodeCharId
ParentId extends ASTNode["id"],
Char extends string
> = `${ParentId}c-${Char}-`;
type ASTNodeChar
ParentId extends ASTNode["id"] = ASTNode["id"],
Char extends string = string
> = ASTNode & {
id: ASTNodeCharIdParentId, Char>;
type: "CHAR";
char: Char;
};
リテラル文字に対応するノードの型です。正規表現のリテラル文字に相当する文字列リテラル型 Char
を型パラメータに取って、Char
型のプロパティ char
を付け加えます。識別子は親ノードの識別子に c-${C}-
を付け加えたものとします。
ASTNodeError
ast.ts
type ASTNodeParseError = ASTNode & {
id: "#error";
type: "ERROR";
};
構文解析時にうまくパースできなかった場合のノードの型です。
抽象構文木に関する型の定義は以上です。
Parser
次に、抽象構文木を構築する構文解析型 Parser
をつくっていきます。ここでは、今回実装する正規表現の BNF が核となるので再掲しておきます。
regexp> ::= union-expr>
union-expr> ::= concat-expr> UNION union-expr> | concat-expr>
concat-expr> ::= plus-expr> concat-expr> | plus-expr>
plus-expr> ::= factor-expr> PLUS | star-expr>
star-expr> ::= factor-expr> STAR | option-expr>
option-expr> ::= factor-expr> OPTION | factor-expr>
factor-expr> ::= LPAREN union-expr> RPAREN | DOT | CHAR(x)
ParseUnionExpr
まず、規則
に基づいてパースを行う ParseUnionExpr
型を定義します。
type ParseUnionExpr
ParentId extends string,
Tokens extends Token[],
LeftTokens extends Token[] = []
> = Tokens extends [infer Head extends Token, ...infer Tail extends Token[]]
? [
ParseConcatExprASTNodeUnionIdLParentId>, LeftTokens>,
Head,
ParseUnionExprASTNodeUnionIdRParentId>, Tail>
] extends [
infer ParseConcatExprResult extends ASTNode,
TokenUnion,
infer ParseUnionExprResult extends ASTNode
]
? [ParseConcatExprResult, ParseUnionExprResult] extends
| [ASTNodeParseError, unknown]
| [unknown, ASTNodeParseError]
? ParseUnionExprParentId, Tail, [...LeftTokens, Head]>
: ASTNodeUnionParentId, ParseConcatExprResult, ParseUnionExprResult>
: ParseUnionExprParentId, Tail, [...LeftTokens, Head]>
: ParseConcatExprParentId, LeftTokens>;
Tokens
のトークンを先頭から順に見ていき、現在注目しているトークン Head
について、Head
が TokenUnion
の場合に、 Head
よりも左のトークン列 LeftTokens
を ConcatExpr
としてパース、Head
よりも右のトークン列 Tail
を UnionExpr
としてパースします。パースに成功した (どちらもエラーノード ASTNodeParseError
でない) 場合は、ノード ASTNodeUnion
をつくって返します。パースに失敗した場合は、次のトークンに注目した状態の ParseUnionExpr
を返します。
最後のトークンまで読み込んだ状態 (Tokens
がすべて LeftTokens
に移った状態) になったときは
でパースできなかったということなので、
として解釈してパースするために ParseConcatExpr
を返します。
ParseConcatExpr
規則
に基づいてパースを行う ParseConcatExpr
型を定義します。先ほどの ParseUnionExpr
では、着目するトークンを進めながら
が適用できるかどうか確認していましたが、ここでは
が適用できるかどうかを確認しています。
type ParseConcatExpr
ParentId extends string,
Tokens extends Token[],
LeftTokens extends Token[] = []
> = Tokens extends [infer Head extends Token, ...infer Tail extends Token[]]
? [
ParsePlusExprASTNodeConcatIdLParentId>, [...LeftTokens, Head]>,
ParseConcatExprASTNodeConcatIdRParentId>, Tail>
] extends [
infer ParsePlusExprResult extends ASTNode,
infer ParseConcatExprResult extends ASTNode
]
? [ParsePlusExprResult, ParseConcatExprResult] extends
| [ASTNodeParseError, unknown]
| [unknown, ASTNodeParseError]
? ParseConcatExprParentId, Tail, [...LeftTokens, Head]>
: ASTNodeConcatParentId, ParsePlusExprResult, ParseConcatExprResult>
: ParseConcatExprParentId, Tail, [...LeftTokens, Head]>
: ParsePlusExprParentId, LeftTokens>;
ParseUnionExpr
とほぼ同じです。
ParsePlusExpr
規則
に基づいてパースを行う ParsePlusExpr
型を定義します。
type ParsePlusExpr
ParentId extends string,
Tokens extends Token[]
> = Tokens extends [...infer FactorTokens extends Token[], TokenPlus]
? ParseFactorExpr
ASTNodePlusIdParentId>,
FactorTokens
> extends infer ParseFactorExprResult extends ASTNode
? ParseFactorExprResult extends ASTNodeParseError
? ParseStarExprParentId, Tokens>
: ASTNodePlusParentId, ParseFactorExprResult>
: ParseStarExprParentId, Tokens>
: ParseStarExprParentId, Tokens>;
受け取ったトークン列 Tokens
の末尾のトークンが TokenPlus
の場合は、規則
の左側
を適用可能なので、末尾以外のトークン列 FactorTokens
を ParseFactorExpr
(後述) でパースします。パースに成功した場合は、ノード ASTNodePlus
をつくってかえします。パースに失敗した場合は、規則
を適用するために ParseStarExpr
(後述) を返します。
ParseStarExpr
type ParseStarExpr
ParentId extends string,
Tokens extends Token[]
> = Tokens extends [...infer FactorTokens extends Token[], TokenStar]
? ParseFactorExpr
ASTNodeStarIdParentId>,
FactorTokens
> extends infer ParseFactorExprResult extends ASTNode
? ParseFactorExprResult extends ASTNodeParseError
? ParseOptionExprParentId, Tokens>
: ASTNodeStarParentId, ParseFactorExprResult>
: ParseOptionExprParentId, Tokens>
: ParseOptionExprParentId, Tokens>;
規則
に基づいてパースを行う ParseStarExpr
型を定義します。ParsePlusExpr
とほぼ同じなので、説明は省きます。
ParseOptionExpr
type ParseOptionExpr
ParentId extends string,
Tokens extends Token[]
> = Tokens extends [...infer FactorTokens extends Token[], TokenOption]
? ParseFactorExpr
ASTNodeOptionIdParentId>,
FactorTokens
> extends infer ParseFactorExprResult extends ASTNode
? ParseFactorExprResult extends ASTNodeParseError
? ParseFactorExprParentId, Tokens>
: ASTNodeOptionParentId, ParseFactorExprResult>
: ParseFactorExprParentId, Tokens>
: ParseFactorExprParentId, Tokens>;
規則
に基づいてパースを行う ParseOptionExpr
型を作成します。こちらも ParsePlusExpr
とほぼ同じなので、説明は省きます。
ParseFactorExpr
type ParseFactorExpr
ParentId extends string,
Tokens extends Token[]
> = Tokens extends [
TokenLParen,
...infer UnionTokens extends Token[],
TokenRParen
]
? UnionTokens extends []
? ASTNodeCharParentId, "">
: ParseUnionExprParentId, UnionTokens>
: Tokens extends [TokenDot]
? ASTNodeDotParentId>
: Tokens extends [TokenCharinfer Char extends string>]
? ASTNodeCharParentId, Char>
: ASTNodeParseError;
規則
に基づいてパースを行う ParseFactorExpr
型を定義します。
まず、与えられたトークン列が (UnionExpr)
に合致するか確認します。トークン列の先頭が (
, 末尾が )
となっている場合は、その間のトークン列 UnionTokens
を ParseUnionExpr
に渡し、規則
に基づいたパース処理を委ねます。
(UnionExpr)
に合致しない場合は、トークン列が Dot
に合致するか確認します。合致する場合は、ワイルドカード .
に対応するノード ASTNodeDot
を返します。
Dot
に合致しない場合は、Char
に合致するかどうか確認します。合致する場合は、リテラル文字ノード ASTNodeChar
を返します。
以上のいずれにも合致しない場合は、エラー状態を表すノード ASTNodeParseError
を返します。
Parser
最後に構文解析型 Parser を作成します。
parser.ts
type ParserTokens extends Token[]> =
ParseOptionTokens> extends infer ParseOptionResult extends {
tokens: Token[];
option: TRegExpOption;
}
? {
ast: ASTNodeRoot
ParseUnionExprASTNodeRootId, ParseOptionResult["tokens"]>
>;
option: ParseOptionResult["option"];
}
: never;
type ParseOption
Tokens extends Token[],
Option extends TRegExpOption = {}
> = Tokens extends [TokenAnchorStart, ...infer Rest extends Token[]]
? ParseOptionRest, Option & { anchorStart: true }>
: Tokens extends [...infer Rest extends Token[], TokenAnchorEnd]
? ParseOptionRest, Option & { anchorEnd: true }>
: {
tokens: Tokens;
option: Option;
};
今回の実装ではアンカー (^
,$
) をオプションとして扱い、構文木とは分離させます。具体的な処理の説明は省きますが、まず Parser
のパラメータとして与えられたトークン列を ParseOption
でパースし、残りのトークン列を ParseUnionExpr
でパースします。そして、その結果を子ノードとしてもつ ASTNodeRoot
とオプションをもったオブジェクト型を返します。
tregexp.ts
type TRegExpOption = {
readonly anchorStart?: boolean;
readonly anchorEnd?: boolean;
};
以上で、構文解析型 Parser
をつくることができました!
与えられたトークン列に対して、規則に基づいた構文木が生成されます。
NFA
ここでは、与えられた抽象構文木からその正規表現を受理する NFA を構成する処理を型レベルで実装します。まずは、NFA そのものの型を定義しましょう。
type NFAState = string;
type EpsChar = null;
type NFATransition = {
from: NFAState;
to: NFAState;
char: string | EpsChar;
};
type NFA = {
states: NFAState[];
transitions: NFATransition[];
start: NFAState;
accept: NFAState[];
};
型定義の通り、有限オートマトンの定義は (文字集合)、状態集合、遷移規則集合、初期状態、受理状態集合からなります。
NFABuilder
次に、NFA を構築する型 NFABuilder
を定義します。構築方法は 正規表現と有限オートマトンの対応 で紹介されているものに基づいています。
NFABuilder, NFABuilderInternal
まずはトップレベルの NFABuilder
の定義からです。
nfa-builder.ts
type NFABuilderNode extends ASTNodeRoot> = {
nfa: NFABuilderInternalNode>;
};
type NFABuilderInternalNode extends ASTNode> = Node extends ASTNodeRoot
? NFABuilderInternalNode["child"]>
: Node extends ASTNodeUnion
? NFABuilderUnionNode>
: Node extends ASTNodeConcat
? NFABuilderConcatNode>
: Node extends ASTNodePlus
? NFABuilderPlusNode>
: Node extends ASTNodeStar
? NFABuilderStarNode>
: Node extends ASTNodeOption
? NFABuilderOptionNode>
: Node extends ASTNodeDot
? NFABuilderDotNode>
: Node extends ASTNodeChar
? NFABuilderCharNode>
: never;
パラメータとして与えられた抽象構文木 ASTNodeRoot
から NFA
を返します。NFABuilderInternal
では AST のノードの種別に応じて別々の Builder を返します。
NFABuilderUnion
nfa-builder.ts
type NFABuilderUnion
Node extends ASTNodeUnion,
LNFA extends NFA = NFABuilderInternalNode["left"]>,
RNFA extends NFA = NFABuilderInternalNode["right"]>
> = {
states: [...LNFA["states"], ...RNFA["states"], `${Node["id"]}@0`];
transitions: [
...LNFA["transitions"],
...RNFA["transitions"],
{ from: `${Node["id"]}@0`; to: LNFA["start"]; char: EpsChar },
{ from: `${Node["id"]}@0`; to: RNFA["start"]; char: EpsChar }
];
start: `${Node["id"]}@0`;
accept: [...LNFA["accept"], ...RNFA["accept"]];
};
まず、NFABuilderInternal
側から与えられた ASTNodeUnion
を Node
として、左右のノード Node["left"]
, Node["right"]
について再帰的に NFABuilderInternal
を適用して NFA
を構築したものをそれぞれ LNFA
, RNFA
とします。これで、前に述べた構築方法の前提ができました。
2 つの NFA
があったときそれらの論理和 NFA を構成するには、新たに初期状態を用意して、そこから各 NFA の初期状態への ε-遷移をつくるとよいのでした。よって、以下のような NFA をつくるとよさそうです。
- states:
LNFA
,RNFA
の全ての状態 + 新しい状態${Node["id"]}@0
を状態集合とする。状態の識別子はなんでもよいが、論理和ノードの識別子 +@0
とする。 - transitions:
LNFA
,RNFA
の全ての遷移 + 状態${Node["id"]}@0
からLNFA
,RNFA
の初期状態への ε-遷移を遷移集合とする。 - start:
${Node["id"]}@0
を初期状態とする。 - accept:
LNFA
,RNFA
の全ての受理状態を受理状態集合とする。
NFABuilderConcat
nfa-builder.ts
type NFABuilderConcat
Node extends ASTNodeConcat,
LNFA extends NFA = NFABuilderInternalNode["left"]>,
RNFA extends NFA = NFABuilderInternalNode["right"]>
> = {
states: [...LNFA["states"], ...RNFA["states"]];
transitions: [
...LNFA["transitions"],
...RNFA["transitions"],
...TransitionsFromStatesLNFA["accept"], RNFA["start"], EpsChar>
];
start: LNFA["start"];
accept: RNFA["accept"];
};
基本的はつくりは NFABuilderUnion
と同じです。以下の NFA をつくっています。
- states:
LNFA
,RNFA
の全ての状態 - transitions:
LNFA
,RNFA
の全ての遷移 +LNFA
の全ての受理状態からRNFA
の初期状態への ε-遷移 - start:
LNFA
の初期状態 - accept:
RNFA
の全ての受理状態
ここで、TransitionsFromStates
型は複数の集合 FromStates
と単一の集合 ToState
、ある文字列 Char
をパラメータとして、FromStates
の各状態から ToState
への文字 Char
による遷移の集合を返します。
nfa-builder.ts
type TransitionsFromStates
FromStates extends NFAState[],
ToState extends NFAState,
Char extends NFATransition["char"]
> = {
[Index in keyof FromStates]: {
from: FromStates[Index];
to: ToState;
char: Char;
};
};
NFABuilderPlus
nfa-builder.ts
type NFABuilderPlus
Node extends ASTNodePlus,
CNFA extends NFA = NFABuilderInternalNode["child"]>
> = {
states: CNFA["states"];
transitions: [
...CNFA["transitions"],
...TransitionsFromStatesCNFA["accept"], CNFA["start"], EpsChar>
];
start: CNFA["start"];
accept: CNFA["accept"];
};
まず、NFABuilderInternal
側から与えられた ASTNodeUnion
を Node
として、Node
の子要素 (expr+
の expr
に対応するノード) に対して NFABuilderInternal
を適用して得られた NFA
を CNFA
とします。
続いて、以下のような NFA を返します。
- states:
CNFA
の全ての状態 - transitions:
CNFA
の全ての遷移 +CNFA
の全ての受理状態からCNFA
の初期状態への ε-遷移 - start:
CNFA
の初期状態 - accept:
CNFA
の全ての受理状態
NFABuilderStar
nfa-builder.ts
type NFABuilderStar
Node extends ASTNodeStar,
CNFA extends NFA = NFABuilderInternalNode["child"]>
> = {
states: CNFA["states"];
transitions: [
...CNFA["transitions"],
...TransitionsFromStatesCNFA["accept"], CNFA["start"], EpsChar>
];
start: CNFA["start"];
accept: [...CNFA["accept"], CNFA["start"]];
};
NFABuilderPlus
とほぼ同じです。以下の NFA を返します。
- states:
CNFA
の全ての状態 - transitions:
CNFA
の全ての遷移 +CNFA
の全ての受理状態からCNFA
の初期状態への ε-遷移 - start:
CNFA
の初期状態 - accept:
CNFA
の全ての受理状態 +CNFA
の初期状態
NFABuilderOption
nfa-builder.ts
type NFABuilderOption
Node extends ASTNodeOption,
CNFA extends NFA = NFABuilderInternalNode["child"]>
> = {
states: CNFA["states"];
transitions: CNFA["transitions"];
start: CNFA["start"];
accept: [...CNFA["accept"], CNFA["start"]];
};
こちらも NFABuilderPlus
と同じつくりです。以下の NFA を返します。
- states:
CNFA
の全ての状態 - transitions:
CNFA
の全ての遷移 - start:
CNFA
の初期状態 - accept:
CNFA
の全ての受理状態 +CNFA
の初期状態
NFABuilderDot
nfa-builder.ts
type NFABuilderDotNode extends ASTNodeDot> = {
states: [`${Node["id"]}@0`, `${Node["id"]}@1`];
transitions: [
{
from: `${Node["id"]}@0`;
to: `${Node["id"]}@1`;
char: string;
}
];
start: `${Node["id"]}@0`;
accept: [`${Node["id"]}@1`];
};
ワイルドカードノード型に対応する NFA を構築する型です。与えられたワイルドカードノードの識別子をもとに新たな状態を 2 つ (@0
, @1
) つくり、以下の NFA を返します。
- states:
@0
,@1
- transitions:
@0
から@1
への任意の文字 (string
) による遷移 - start:
@0
- accept:
@1
NFABuilderChar
nfa-builder.ts
type NFABuilderCharNode extends ASTNodeChar> = {
states: [`${Node["id"]}@0`, `${Node["id"]}@1`];
transitions: [
{
from: `${Node["id"]}@0`;
to: `${Node["id"]}@1`;
char: Node["char"];
}
];
start: `${Node["id"]}@0`;
accept: [`${Node["id"]}@1`];
};
リテラル文字ノード型に対応する NFA を構築する型です。与えられたリテラル文字ノードの識別子をもとに新たな状態を 2 つ (@0
, @1
) つくり、以下の NFA を返します。
- states:
@0
,@1
- transitions:
@0
から@1
へのASTNodeChar
で指定された文字 (Node["char"]
) による遷移 - start:
@0
- accept:
@1
以上で、NFA の構築に関する型の定義が完了しました!
NFAExec
次に、入力文字列をもとに NFA の動作をシミュレートする型 NFAExec
を定義していきます。
NextStatesFromSingleState
まずは、遷移集合 Transitions
に基づいて、ある状態 FromState
から文字 Char
で遷移できる状態を列挙する型 NextStatesFromSingleState
をつくります。
nfa-exec.ts
type NextStatesFromSingleState
FromState extends NFAState,
Char extends string | EpsChar,
Transitions extends NFATransition[]
> = NextStatesFromSingleStateInternalFromState, Char, Transitions>;
type NextStatesFromSingleStateInternal
FromState extends NFAState,
Char extends string | EpsChar,
Transitions extends NFATransition[],
Result extends NFAState[] = []
> = Transitions extends [
infer Head extends NFATransition,
...infer Tail extends NFATransition[]
]
? [FromState, Char] extends [Head["from"], Head["char"]]
? NextStatesFromSingleStateInternal
FromState,
Char,
Tail,
[...Result, Head["to"]]
>
: NextStatesFromSingleStateInternalFromState, Char, Tail, Result>
: UniqueResult>;
NextStatesFromSingleStateInternal
は前述の 3 つのパラメータと、遷移可能な状態集合 Result
をパラメータとし、最終的な Result
を返します。
まず、Transitions
を展開し、先頭の遷移 Head
に着目します。指定された FromState
が Head
の遷移元状態 Head["from"]
かつ、指定された Char
が Head
の遷移文字 Head["char"]
である場合は遷移が可能なので Head
の遷移先状態 Head["to"]
を Result
に含めて再帰的に処理した NextStatesFromSingleStateInternal
を返します。
Transitions
の全ての遷移のチェックが終わったら Result
について Unique
を適用したものを返します。ここで Unique
は以下のように、ある配列型から重複要素を取り除いた配列型を返します。例えば Unique
は [1, 2, 3]
となります。
unique.ts
type UniqueT extends unknown[]> = UniqueInternalT>;
type UniqueInternal
T extends unknown[],
Result extends unknown[] = []
> = T extends [infer Head, ...infer Tail]
? Head extends Result[number]
? UniqueInternalTail, Result>
: UniqueInternalTail, [...Result, Head]>
: Result;
NextStatesFromStates
つぎに、遷移集合 Transitions
に基づいて、ある状態集合 FromStates
から文字 Char
で遷移できる状態を列挙する型 NextStatesFromStates
をつくります。先ほどの NextStatesFromSingleState
の遷移元が複数のバージョンです。
nfa-exec.ts
type NextStatesFromStates
FromStates extends NFAState[],
Char extends string | EpsChar,
Transitions extends NFATransition[]
> = NextStatesFromStatesInternalFromStates, Char, Transitions>;
type NextStatesFromStatesInternal
FromStates extends NFAState[],
Char extends string | EpsChar,
Transitions extends NFATransition[],
Result extends NFAState[] = []
> = FromStates extends [
infer Head extends NFAState,
...infer Tail extends NFAState[]
]
? NextStatesFromSingleState
Head,
Char,
Transitions
> extends infer NextStates extends NFAState[]
? NextStatesFromStatesInternal
Tail,
Char,
Transitions,
[...Result, ...NextStates]
>
: NextStatesFromStatesInternalTail, Char, Transitions, Result>
: UniqueResult>;
FromStates
の先頭要素状態 Head
を遷移元として NextStatesFromSingleState
を適用し、遷移可能な状態集合があれば Result
に追加して残りの遷移元集合 Tail
について再帰的に処理します。全ての FromStates
について処理が完了したら Unique
を返します。
EpsilonClosure
ここで、新たな用語「イプシロン閉包 (ε-閉包)」を導入します。ε-閉包とは、ある状態から ε-遷移のみで遷移可能な状態の集合のことをいいます。ある状態集合 FromStates
の ε-閉包を返す型 EpsilonClosure
を定義します。
nfa-exec.ts
type EpsilonClosure
FromStates extends NFAState[],
Transitions extends NFATransition[]
> = EpsilonClosureInternalFromStates, Transitions>;
type EpsilonClosureInternal
FromStates extends NFAState[],
Transitions extends NFATransition[],
Result extends NFAState[] = FromStates
> = FromStates extends [
infer Head extends NFAState,
...infer Tail extends NFAState[]
]
? NextStatesFromSingleState
Head,
EpsChar,
Transitions
> extends infer ReachableStates extends NFAState[]
? EpsilonClosureInternal
[...Tail, ...DiffReachableStates, Result>],
Transitions,
[...Result, ...ReachableStates]
>
: EpsilonClosureInternalTail, Transitions, Result>
: UniqueResult>;
FromStates
の先頭要素 Head
を遷移元として一度の ε-遷移で到達可能な状態集合を NextStatesFromSingleState
で取得します。この結果を ReachableStates
とします。ReachableStates
を遷移元状態集合に追加して再度 NextStatesFromSingleState
を適用したいのですが、そのまま含めると重複した状態が含まれている際に状態が減らないので Result
との差集合を求めて、一度探索した状態を除外する必要があります。
FromStates
の初期値から複数の ε-遷移で到達可能な状態が全て求まると FromStates
が空になります。空になったら Unique
を返します。
二つの配列型についての差集合を求める Diff
は以下のように定義できます。
diff.ts
type DiffS extends unknown[], T extends unknown[]> = DiffInternalS, T>;
type DiffInternal
S extends unknown[],
T extends unknown[],
Result extends unknown[] = []
> = S extends [infer Head, ...infer Tail]
? ExistsT, Head> extends true
? DiffInternalTail, T, Result>
: DiffInternalTail, T, [...Result, Head]>
: Result;
加えて、Exists, Equal 型も使用しています。ここでは説明を省略します。
exists.ts
type ExistsT extends unknown[], U> = T extends [
infer Head,
...infer Tail
]
? EqualHead, U> extends true
? true
: ExistsTail, U>
: false;
equal.ts
type EqualX, Y> = (T>() => T extends X ? 1 : 2) extends
T
>() => T extends Y ? 1 : 2
? true
: false;
NextStatesWithEpsilonClosure
ある状態集合 FromStates
と遷移集合 Transitions
があるとき、FromStates
の ε-閉包を遷移元状態集合としてある文字 Char
で遷移可能な状態集合の ε-閉包を求める型です。具体的には、NFA の並列実行の特定時点である状態集合が求まっている場合に、入力文字を読み込んで次の状態集合を求める型です。
nfa-exec.ts
type NextStatesWithEpsilonClosure
FromStates extends NFAState[],
Char extends string,
Transitions extends NFATransition[]
> = EpsilonClosure
NextStatesFromStates
EpsilonClosureFromStates, Transitions>,
Char,
Transitions
>,
Transitions
>;
NFAExec, NFAExecInternal
最後に、NFA を実行する型 NFAExec
,NFAExecInternal
を定義します。
nfa-exec.ts
type NFAExec
InputNFA extends NFA,
InputString extends string
> = NFAExecInternalInputNFA, InputString>;
type NFAExecInternal
InputNFA extends NFA,
InputString extends string,
States extends NFAState[] = [InputNFA["start"]],
Accepted extends boolean[] = [],
Histories extends NFAState[][] = []
> = InputString extends `${infer Head}${infer Tail}`
? NFAExecInternal
InputNFA,
Tail,
NextStatesWithEpsilonClosureStates, Head, InputNFA["transitions"]>,
[...Accepted, IsAcceptedStates, InputNFA>],
[...Histories, States]
>
: {
accepted: [...Accepted, IsAcceptedStates, InputNFA>];
histories: [...Histories, States];
};
type NFAExecResult = {
accepted: boolean[];
histories: NFAState[][];
};
type IsAcceptedStates extends NFAState[], InputNFA extends NFA> = Intersection
States,
InputNFA["accept"]
> extends []
? false
: true;
NFAExecInternal
はパラメータとして NFA NFA
、入力文字列 InputString
を受け取り、InputString
の先頭文字 Head
を順次読み取りながら NextStatesWithEpsilonClosure
によって次の状態集合を求めて NFAExecInternal
を再帰的に適用します。現在の状態集合は States
に格納されており、 各ステップについて受理されたかどうかとその時点での状態集合はそれぞれ Accepted
, Histories
に格納されます。
以下のような NFA に対して 文字列 aaa
を入力した場合の Accepted
, Histories
はこのような感じになります。
{
accepted: [false, false, true, false];
histories: [["q0"], ["q1", "q2"], ["q3", "q5"], ["q6"]];
}
accepted
をみると、各入力文字を読み込んだ時点でそれが受理されるかどうかが分かります。上の例では、二文字目まで読み込んだ文字列 aa
が受理されていることが分かります。また、["q3", "q5"]
に受理状態 q5
が含まれていることも確認できます。
以上で、NFA の構築・実行のための型を定義することができました。
TRegExp
これまでに定義した型を組み合わせた集大成の型 TRegExp
を定義します。この型はパラメータとして正規表現文字列 RegExpString
を受け取り、字句解析型 Lexer
、構文解析型 Parser
、NFA 構築型 NFABuilder
を順次適用して NFA
を得ます。
tregexp.ts
type TRegExpRegExpString extends string> =
TRegExpInternalRegExpString>;
type TRegExpInternal
RegExpString extends string,
LexerRes extends LexerResult = LexerRegExpString>,
Tokens extends Token[] = LexerRes["tokens"],
ParserRes extends ParserResult = ParserTokens>,
Option extends TRegExpOption = ParserRes["option"],
AST extends ASTNodeRoot = ParserRes["ast"],
NFA extends NFABuilderAST>["nfa"] = NFABuilderAST>["nfa"]
> = {
readonly string: RegExpString;
readonly tokens: Tokens;
readonly ast: AST;
readonly option: Option;
readonly nfa: NFA;
};
TRegExpTest
TRegExp
で構築された NFA
をもとに入力文字列 InputString
が受理されるかをチェックする型 TRegExpTest
を定義します。オプションについての処理の説明は省略します。核となるのは NFAExec
が返す Result
の処理で、入力境界アサーションの有無によって以下のように処理を分岐させます。
- アサーション無し: 入力文字列の各文字を先頭とみなしてそれぞれについて
NFAExec
を実行し、いずれかのaccepted
列に受理状態が含まれていればtrue
を返す。つまり、入力文字列の任意の部分文字列についてNFA
を実行しています。 - 入力開始境界アサーション
^
あり: 入力文字列についてそのままNFAExec
を実行する。 - 入力末尾境界アサーション
$
あり:accepted
列の末尾がtrue
の場合にのみtrue
を返す。
tregexp-test.ts
type TRegExpTest
InputString extends string,
InputTRegExp
> = InputTRegExp extends TRegExpinfer _>
? TRegExpTestInternalInputString, InputTRegExp>
: never;
type TRegExpTestInternal
InputString extends string,
InputTRegExp extends TRegExpstring>,
InputNFA extends InputTRegExp["nfa"] = InputTRegExp["nfa"],
Option extends TRegExpOption = InputTRegExp["option"]
> = Option["anchorStart"] extends true
? NFAExecInputNFA, InputString> extends infer Result extends NFAExecResult
? Option["anchorEnd"] extends true
? EqualLastResult["accepted"]>, true>
: ExistsResult["accepted"], true>
: never
: TRegExpTestWithoutAnchorStartInputString, InputNFA, Option>;
type TRegExpTestWithoutAnchorStart
InputString extends string,
InputNFA extends NFA,
Option extends TRegExpOption
> = InputString extends `${infer Head}${infer Tail}`
? NFAExecInputNFA, InputString> extends infer Result extends NFAExecResult
? (
Option["anchorEnd"] extends true
? EqualLastResult["accepted"]>, true>
: ExistsResult["accepted"], true>
) extends true
? true
: TRegExpTestWithoutAnchorStartTail, InputNFA, Option>
: never
: false;
これらの TRegExp
, TRegExpTest
型を用いると、本記事冒頭で示した動作が実現できます。
type EmailRegExp = TRegExp"^.+@.+\\..+$">;
type ValidEmail = TRegExpTest"piyo@hiyoko.com", EmailRegExp>;
type InvalidEmail = TRegExpTest"piyo.com", EmailRegExp>;
正規表現をはじめとする形式言語の定義方法からはじめて、TypeScript の型システム上で動作する正規表現処理系のつくりかたを解説しました。本記事を通して型レベルプログラミングのおもしろさを少しでも感じていただけたら幸いです。今回の実装内容は こちら (再掲) で動作確認することができます。