ドキュメントだけでなく実際の処理をRustを使って実装するようになってきた
Ergは構文がシンプルだが、パースでは複雑な処理をしている
そのためパーサーで簡単な実装を加えるだけで割と簡単に正常な構文もエラーになるなどの問題が発生する
それで自分が理解できているかの整理を兼ねて,Erg言語を開発してみたい人向けの内容を書いてみようと思った
Rustと再帰下降構文解析の話は省く
現在進行形(2022/1/25)でエラーの種類分けをしているのでもしかしたらエラーの位置だったり種類が変わっている可能性があるので注意
Lexerは軽く、Parserは以下のメソッドに絞って説明をする
- try_reduce_module()
- try_reduce_chunk()
- try_reduce_expr()
- try_reduce_bin_lhs()
- try_reduce_call_or_acc()
- try_reduce_acc_lhs()
- try_reduce_acc_chain()
- try_reduce_args()
Lexer
ファイルを文字列として取得し,Erg言語で使われるtoken
に分類する
ここで文字列や数値、Ergで使われる構文の記号などに分類する
コマンド実行方法
erg --mode lex
erg --mode lex <script.er>
cargo lex
cargo lex <script.er>
実際に分析してみると以下の様な出力を得られる
[token名 解析された文字, ..., EOF ]
最後に必ずEOF
が挿入される
>>> a = 1
[Symbol a, Equal =, NatLit 1, EOF ]
>>> add(x, y) = x + y
[Symbol add, LParen (, Symbol x, Comma ,, Symbol y, RParen ), Equal =, Symbol x, Plus +, Symbol y, EOF ]
ここで,例えば[ProcArrow =>
]が[Equal =
]、[Gre >
]に分割されたりすれば構文のバグになる
token
の種類やカテゴリは全てtoken.rs
に集約されているので、新しい記号などを追加する際にはそこに定義してから利用する
解析方法
これら構文解析は以下で処理される
impl Iterator for Lexer /*<'a>*/ {
type Item = LexResult<Token>;
fn next(&mut self) -> Option<Self::Item> {
...
}
...
}
スクリプトやREPLなどで得た文字列を全てVec<char>
に変換して,上記のnext()
で前から順番に解析していく
pub fn new(input: Input) -> Self {
let normed = normalize_newline(&input.read());
Lexer {
chars: normed.chars().collect::<Vec<char>>(),
...
}
}
文字列は消費せずに解析するスタート位置を更新してそこから解析する文字を見る
解析の際によく使うメソッドの一覧
fn emit_token() // ここでtoken化と解析済みの位置の更新
fn consume() // 解析する位置を一つ進める
fn peek_cur_ch() // 現在の位置の文字を見る
fn peek_next_ch() // 次の文字を見る
自分が実装した箇所だからわかりやすいので,例としてcomment
のところを見る
next()
なので返り値がOption<char>
となっている
それをErgの構文とマッチングさせている
今回のcomment
の場合だと#
と#[~]#
になる
fn next(&mut self) -> Option<Self::Item> {
...
if let Some('#') = self.peek_cur_ch() {
if let Some('[') = self.peek_next_ch() {
if let Err(e) = self.lex_multi_line_comment() {
return Some(Err(e));
}
} else if let Err(e) = self.lex_comment() {
return Some(Err(e));
}
}
...
現在の位置がself.peek_cur_ch()
でわかるのでそれで#
かどうかをマッチングをかける
次の文字が[
なら複数行のコメントになるのでself.lex_multi_line_comment()
で解析させる
[
ではないなら一行のコメントになるのでself.lex_comment()
で一行の解析をさせる
ここで大切なのは先に複数行のコメントかどうかを見るということ
一行コメントとして解析を始めてしまうと,それ以降の改行まで文字列が全てコメントとしてスキップされていくので,複数行コメントの構文になる[
を見逃すバグになってしまう
また、最近見つけたバグだとelse if
になっていなくて、複数行コメントが呼ばれた後にさらに追加で普通のコメントが呼ばれるようになってしまっていた
新しいtoken
を追加する際には順番に気を付けて実装をする
他にも複数文字列と一行の文字列のときに逆していたのを発見して直したりもした
"""
Error[#0000]: File <stdin>, line 1,
1 | """
: -
SyntaxError: the string is not closed by "
""
と"
で解析されており"""
で解析されていなかった
なのでこれも"""
を先にしないといけなかった
このように,順番がおかしい時にはそれはバグとなる
lex_multi_line_comment()
は以下の通り
fn lex_multi_line_comment(&mut self) -> LexResult<()> {
// エラーを起こした時に無視されるコメントを表示するため
let mut s = "".to_string();
let mut nest_level = 0;
while let Some(c) = self.peek_cur_ch() {
if let Some(next_c) = self.peek_next_ch() {
match (c, next_c) {
('#', '[') => {
nest_level += 1;
}
(']', '#') => {
nest_level -= 1;
if nest_level == 0 {
self.consume() // ]
self.consume() // #
return Ok(());
}
}
_ => {}
}
if c == '\n' {
self.lineno_token_starts += 1;
self.col_token_starts = 0;
}
}
if Self::is_bidi(self.peek_cur_ch().unwrap()) {
let comment = self.emit_token(Illegal, &s);
return Err(LexError::syntax_error(
0,
comment.loc(),
switch_lang!(
"japanese" => "不正なユニコード文字(双方向オーバーライド)がコメント中に使用されています",
"simplified_chinese" => "注释中使用了非法的unicode字符(双向覆盖)",
"traditional_chinese" => "註釋中使用了非法的unicode字符(雙向覆蓋)",
"english" => "invalid unicode character (bi-directional override) in comments",
),
None,
));
}
s.push(self.consume().unwrap());
}
if nest_level >= 0 {
return Ok(());
}
let comment = self.emit_token(Illegal, &s);
Err(LexError::syntax_error(
0,
comment.loc(),
switch_lang!(
"japanese" => "複数行コメントが]#で閉じられていません",
"simplified_chinese" => "未用]#号结束的多处评论",
"traditional_chinese" => "多條評論未用]#關閉",
"english" => "Multi-comment is not closed with ]#",
),
None,
))
}
やっていることは簡単で、]#
が来るまでループして見る位置をself.consume()
している
少し厄介なのは、#[
が再度コメント内で呼ばれたりしてそれの閉じるための]#
があればそれもコメントにしないといけないという問題がある
#[
#[
#[
コメント
]#
]#
]#
なので、その入子になっているかどうかをnest_level
で加算し,]#
で減算して0になったときにOk(())になるようにしている
それまでにNoneになったら解析エラーとして返す
このような関数がいくつかあり、最終的にnext()
がNoneで文字を全て読み終わったら解析が終了となり,これらがTokenStream
として上記の様な形でParser
に渡される
Parser
ここからが非常に厄介なので注意
Erg言語には予約語が一つもなく、組み込みの定数などを含め多くの文字(print, Class, True, etc.)がParseする段階ではどれもSymbol
扱いになる
なので、token種類と並びのみで関数であったり変数の宣言かどうかを判断している
このSymbol
の扱い方を覚えればとりあえずParserの難しいところは分かっていくと思う
a # 変数呼び出し、関数オブジェト呼び出し、高階関数オブジェクト呼び出しなどなど
.a # パブリック
::a # 明示的なプライベート
a + 1 # 二項演算
a.0 # タプル添え字アクセス
a.method() # メソッド呼び出し
a. # クラスアクセス(本来はaではなく大文字のA)
a:: # クラスアクセス(これも)
a::{data} # データパック宣言
a: Type # 型指定
a :> Type # 汎化型指定
a <: Type # 部分型指定
a|T: Type| # 型変数宣言
a[0] # 配列添え字アクセス
a x:= 1 # 関数キーワード引数呼び出し
a -> a # 無名関数宣言
a => a # 無名プロシージャ宣言
a = 1 # 変数代入
a x = x # 関数宣言
a = x -> x # 無名関数宣言と代入
a = x => x # 無名プロシージャ宣言と代入
a x = x # 関数宣言
a 1 # 関数呼び出し
a x->x = x # 高階関数宣言
a 1 1 # 高階関数呼び出し
a(1) 1 # こんなのとかもa(1)(1)
このように、Symbol
(この場合はa
)が始めに来たら次のtoken
によって様々に分岐が発生する
因みに、私が分かっている範囲での宣言なのでまだある
なので、Parserを修正するのにはErgの構文知識とParserのアルゴリズムの理解が必要になる
私もまだ完全には理解できていないので、試行錯誤しながら実装だったり修正をしている最中
Parserで使われている略語は大まかなのはこんな感じ
略語 | 単語 | 訳 |
---|---|---|
acc | accessor | アクセッサ― |
asc | ascription | 帰属 |
bin | binary | 二項 |
def | definition | 定義式 |
elem | element | 要素 |
expr | expression | 式 |
kw | Keyword | キーワード |
ident | identifier | 識別子 |
op | operator | 演算子 |
sig | signature | シグネチャ |
spec | specifier | 指定子 |
subr | subroutine | サブルーチン |
メソッド名や変数名などにこれらが良く使われているので参考までに
Parserコマンド
ここからデバッグコマンドを利用をすることができ、呼び出されたメソッドやエラーが発生した場所などを情報として取得できる
一応注意点としては重い処理になるため、大きいファイルでやるとかなり処理に時間がかかる
また、cargo install erg
でインストールした場合は再度erg install erg --features debug
が必要になる
degub
featureフラグとコマンドは--mode parse
を利用する
erg --mode parser
erg --mode parser <script.er>
cargo run --features debug -- --mode parse
cargo run --features debug -- --mode parse <script.er>
cargo dprs # ショートカットもある
cargo run --features debug -- --mode parse
で実行すると以下の様な結果が得られるようになる
>>> a = 1
[DEBUG] compiler\erg_parser\parse.rs:0307: the parsing process has started.
[DEBUG] compiler\erg_parser\parse.rs:0308: token stream: [Symbol a, Equal =, NatLit 1, EOF ]
[DEBUG] compiler\erg_parser\parse.rs:0334:
(1) entered try_reduce_module, cur: Symbol a
[DEBUG] compiler\erg_parser\parse.rs:1065:
(2) entered try_reduce_chunk, cur: Symbol a
[DEBUG] compiler\erg_parser\parse.rs:1605:
(3) entered try_reduce_bin_lhs, cur: Symbol a
[DEBUG] compiler\erg_parser\parse.rs:1797:
・ (4) entered try_reduce_call_or_acc, cur: Symbol a
[DEBUG] compiler\erg_parser\parse.rs:0526:
・ (5) entered try_reduce_acc_lhs, cur: Symbol a
[DEBUG] compiler\erg_parser\parse.rs:0548:
・ (4) exit try_reduce_acc_lhs, cur: Equal =
[DEBUG] compiler\erg_parser\parse.rs:1818:
・ (5) entered try_reduce_acc_chain, cur: Equal =
[DEBUG] compiler\erg_parser\parse.rs:1937:
・ (4) exit try_reduce_acc_chain, cur: Equal =
[DEBUG] compiler\erg_parser\parse.rs:0644:
・ (5) entered opt_reduce_args, cur: Equal =
[DEBUG] compiler\erg_parser\parse.rs:1811:
・ (4) exit try_reduce_call_or_acc, cur: Equal =
[DEBUG] compiler\erg_parser\parse.rs:1677:
(3) exit try_reduce_bin_lhs, cur: Equal =
[DEBUG] compiler\erg_parser\convert.rs:0013:
・ (4) entered convert_rhs_to_sig, cur: NatLit 1
[DEBUG] compiler\erg_parser\convert.rs:0078:
・ (5) entered convert_accessor_to_var_sig, cur: NatLit 1
[DEBUG] compiler\erg_parser\parse.rs:1367:
・ (4) entered try_reduce_expr, cur: NatLit 1
[DEBUG] compiler\erg_parser\parse.rs:1605:
・ (5) entered try_reduce_bin_lhs, cur: NatLit 1
[DEBUG] compiler\erg_parser\parse.rs:2404:
・ (6) entered try_reduce_lit, cur: NatLit 1
[DEBUG] compiler\erg_parser\parse.rs:2405:
・ (5) exit try_reduce_lit, cur: NatLit 1
[DEBUG] compiler\erg_parser\parse.rs:1630:
・ (4) exit try_reduce_bin_lhs, cur: EOF
[DEBUG] compiler\erg_parser\parse.rs:1536:
(3) exit try_reduce_expr, cur: EOF
[DEBUG] compiler\erg_parser\parse.rs:1099:
(2) exit try_reduce_chunk, cur: EOF
[DEBUG] compiler\erg_parser\parse.rs:0366:
(1) exit try_reduce_module, cur: EOF
[DEBUG] compiler\erg_parser\parse.rs:0321: the parsing process has completed (errs: 0).
[DEBUG] compiler\erg_parser\parse.rs:0322: AST:
::a =
1
::a =
1
再帰の深さ
と実行されたメソッド名
,解析しているtoken
が一行で表示されるようになる
最終的に脱糖衣された抽象構文木が出力されるようになっている
因みに・
一つで深さ四つ分になる
以下からは[DEBUG]は過剰なので省略する
基本的な解析の流れ
重要なメソッドはいくつかあるが,ほぼ必ず呼び出されるのは以下の通り
fn try_reduce_module() // ファイル(module)やREPLの一行とかがこれになる
fn try_reduce_chunk() // 一行やブロックがこの塊になる
fn try_reduce_expr() // ergでは基本的には式なのでこれが呼び出される
fn try_reduce_bin_lhs() // 一番小さな要素でchunk()やexpr()の始めにこれが呼ばれる
解析されていく感じとしては以下の様な感じだと思われる(まだ調べている最中)
再帰下降で左側から順番に解析がされていく
| a = (x, y) -> x + y
| lhs | rhs
| sym | lhs | rhs
| sym | arg, arg | rhs
| sym | arg, arg | lit binOp lit
try_reduce_module()
ファイルにある一つの行やブロック、REPLの一行がいわゆるmoduleにあたる
なのでこれが必ずParser
が起動したときにこれが呼ばれる
ここから呼ばれてSeparator(;
や\n
)の時にはスキップして毎度try_reduce_chunk()
を呼び出している
ここでtoken
がEOF
になればパースが完了になる
fn try_reduce_module(&mut self) -> ParseResult<Module> {
debug_call_info!(self);
let mut chunks = Module::empty();
loop {
match self.peek_kind() {
Some(Newline | Semi) => {
self.skip();
}
Some(EOF) => {
break;
}
Some(_) => {
if let Ok(expr) = self.try_reduce_chunk(true, false) {
chunks.push(expr);
// 一行の複数のchunkがある場合はエラー
if !self.cur_is(EOF) && !self.cur_category_is(TC::Separator) {
let err = self.skip_and_throw_invalid_chunk_err(
caused_by!(),
chunks.last().unwrap().loc(),
);
self.errs.push(err);
}
}
}
None => {
if !self.errs.is_empty() {
debug_exit_info!(self);
return Err(());
} else {
switch_unreachable!()
}
}
}
}
debug_exit_info!(self);
Ok(chunks)
}
try_reduce_chunk()
expr + defが一つのchunkらしい(まだ調べている最中)
ブロック単位や式全般がこのchunkとなり、大抵は一行だったりブロックだったりする
a = 1 # これ一行がchunk
b = 2 # これも一つのchunk
main!() =
print! "hi"
log "there" # ここまでのひとまとまりがblockのchunk
try_reduce_bin_lhs()
は後で説明する
ここで一つ目のtoken
を読み込んで以降の処理が決定するのでここの処理が非常に大切になる
let mut stack = Vec::<ExprOrOp>::new();
stack.push(ExprOrOp::Expr(
self.try_reduce_bin_lhs(false, in_brace)
.map_err(|_| self.stack_dec())?,
));
try_reduce_bin_lhs()
で始めのtoken
を読み込んでそれが次のトークンを示唆する場合には次の解析に移り,なければここに戻って来る
REPLだと基本的に一行とブロックがこのchunk
になるのでファイルで複数行にならなければmodule
と同様に一回しか呼ばれない
次に、loopで現在のトークンの解析を始めて、各メソッドでトークンを消費して再度ここに戻ってくるということをしている
これらスタックされた式、演算子は再度取り出されてsignatureとして変換される
ここで小さな要素群を塊の式に直している感じ
必ずここで一つはExprがスタックされるはずなので、次のループで場合によっては取り出して再度式に変換するなどをしている
実際のループ処理を見てみる
引数全般
self.peek()
で現在の要素を見てSymbol
かLiteral
だったらtry_reduce_args
が呼ばれる
引数が返ってきたら、スタックされているExprを取り出して引数を取るCall式
に変換する
// aやxとかの`Symbol`と 1, True, "sample"などの`Literal`
Some(arg) if arg.is(Symbol) || arg.category_is(TC::Literal) => {
let args = self.try_reduce_args(false).map_err(|_| self.stack_dec())?;
let obj = enum_unwrap!(stack.pop(), Some:(ExprOrOp::Expr:(_)));
stack.push(ExprOrOp::Expr(obj.call_expr(args)));
}
代入全般
=
が来たら一般的な代入表現に還元する
代入式はブロックを持ってくることもできるので,途中でNewline
かどうかを求めてtry_reduce_block
をやっていたりする
ブロックの時には複数の式が来る可能性があるが,ブロックじゃなければ一行の式になるのでそれを判断している
取り出したexpr(lhs)をシグネチャに変換している
これで右辺に式を持つ形に変換している
Some(op) if op.category_is(TC::DefOp) => {
let op = self.lpop(); // =が取り出される
// =の次に改行があればブロックになる
let is_multiline_block = self.cur_is(Newline);
let lhs = enum_unwrap!(stack.pop(), Some:(ExprOrOp::Expr:(_)));
// 代入式のシグネチャに式を直す lhs = rhs
let sig = self.convert_rhs_to_sig(lhs).map_err(|_| self.stack_dec())?;
// =は定義になるのでIDを付与する
self.counter.inc();
// ブロックだったら複数行の表現が可能なのでそちらに任せる
let block = if is_multiline_block {
self.try_reduce_block().map_err(|_| self.stack_dec())?
} else {
// precedence: `=` < `,`
let expr = self
.try_reduce_expr(true, false, false, false) // =で代入する式を求める
.map_err(|_| self.stack_dec())?;
Block::new(vec![expr])
};
// IDと代入するブロック(exprs)を代入式に変換する
let body = DefBody::new(op, block, self.counter); // lhs = block <-こんな感じにあるように変換
self.level -= 1;
return Ok(Expr::Def(Def::new(sig, body)));
}
無名関数・プロシージャ
=>
か->
が来たら無名関数・プロシージャに還元する
(x, y) -> log x + y
(x: Int, y: Int): Int => print! x + y
x -> x
!y => y
Parseする段階では副作用のあるなしを判断できないので、両方とも同じ処理をしている
これもブロックを定義できるのでNewlineかどうかを先に見る
その上で式をblockメソッドかexprメソッドで取得するようになっている
Some(op) if op.category_is(TC::LambdaOp) => {
let op = self.lpop(); // =>, ->のどちらか
let is_multiline_block = self.cur_is(Newline);
let lhs = enum_unwrap!(stack.pop(), Some:(ExprOrOp::Expr:(_)));
// 引数が左辺にあるはずなので,それを抽象構文木で右辺を取れる形に変換する
let sig = self
.convert_rhs_to_lambda_sig(lhs)
.map_err(|_| self.stack_dec(fn_name!()))?;
self.counter.inc();
let block = if is_multiline_block {
self.try_reduce_block()
.map_err(|_| self.stack_dec(fn_name!()))?
} else {
// precedence: `->` > `,`
let expr = self
.try_reduce_expr(false, false, false, false)
.map_err(|_| self.stack_dec(fn_name!()))?;
Block::new(vec![expr])
};
stack.push(ExprOrOp::Expr(Expr::Lambda(Lambda::new(
sig,
op,
block,
self.counter,
))));
}
型指定
:
が来た時には型指定(型帰属)に還元する
今までのはExprだが、これはOperatorになる
コロンがありかつ次が改行でなければ型を指定した形(var: Type
, var :> Type
, var <: Type
)になる
Some(op)
if (op.is(Colon) && !self.nth_is(1, Newline))
|| (op.is(SubtypeOf) || op.is(SupertypeOf)) =>
{
// "a": 1 (key-value pair)
if in_brace {
while stack.len() >= 3 {
collect_last_binop_on_stack(&mut stack);
}
break;
}
let op = self.lpop(); // colon
let lhs = enum_unwrap!(stack.pop(), Some:(ExprOrOp::Expr:(_)));
let t_spec = self // typeを求める
.try_reduce_expr(false, false, false, false)
.map_err(|_| self.stack_dec(fn_name!()))?,
// ただの式から型指定に変換する
let t_spec = Self::expr_to_type_spec(t_spec).map_err(|e| self.errs.push(e))?;
// 最終的に既にスタックされていた要素の型指定の形に変換する
let expr = lhs.type_asc_expr(op, t_spec);
stack.push(ExprOrOp::Expr(expr));
}
二項演算子
これもOperator
これは+-とかの二項演算子のパターン
掛け算や割り算などの優先順位を数値で受け取って評価をする
その上で今のBinOpの順番を決める
そのために、スタックの中身を取り出して変換する作業を別でやっている
Some(op) if op.category_is(TC::BinOp) => {
// 演算子を優先順位を計れるようにusizeに変換
let op_prec = op.kind.precedence();
if stack.len() >= 2 {
while let Some(ExprOrOp::Op(prev_op)) = stack.get(stack.len() - 2) { // "a * b + 1"とかの*を見る時
// 前の演算子の方が優先度が高ければ、そちらを優先するように変換する
if prev_op.category_is(TC::BinOp)
&& prev_op.kind.precedence() >= op_prec
{
// ここでstackの中身が消費される
collect_last_binop_on_stack(&mut stack);
} else {
break;
}
if stack.len() <= 1 {
break;
}
}
}
stack.push(ExprOrOp::Op(self.lpop())); // 今の演算子
stack.push(ExprOrOp::Expr(
self.try_reduce_bin_lhs(false, in_brace) // 右辺を求めてスタック
.map_err(|_| self.stack_dec(fn_name!()))?,
));
}
可視性プライベート
- 明示的なプライベート変数
- クラスのプライベート属性
- データパック
私が間違えているかもしれないのでここは注意
可視性パブリックのセクションでmethod
を使っているが,ここで使われていないのはミスかもしれない
Some(t) if t.is(DblColon) => {
let vis = self.lpop(); // ::
match self.lpop() {
symbol if symbol.is(Symbol) => {
// objが取り出せなかったらそもそも定義できないので構文エラーになる
let Some(ExprOrOp::Expr(obj)) = stack.pop() else {
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
};
// 引数のあるなしでアトリビュートかメソッドなどの呼び出しかを判断している
if let Some(args) = self
.opt_reduce_args(false)
.transpose()
.map_err(|_| self.stack_dec())?
{
let ident = Identifier::new(None, VarName::new(symbol));
let call = Call::new(obj, Some(ident), args);
stack.push(ExprOrOp::Expr(Expr::Call(call)));
} else {
let ident = Identifier::new(None, VarName::new(symbol));
stack.push(ExprOrOp::Expr(obj.attr_expr(ident)));
}
}
// ClassName::\n
// クラスのプライベート属性の宣言
line_break if line_break.is(Newline) => {
// maybeなのは無効なシンボルの可能性があるから(組み込み定数や小文字ならだめなど)
let maybe_class = enum_unwrap!(stack.pop(), Some:(ExprOrOp::Expr:(_)));
// クラス属性を定義している、methodとあるが属性の定義もある
let defs = self
.try_reduce_method_defs(maybe_class, vis)
.map_err(|_| self.stack_dec(fn_name!()))?;
let expr = Expr::Methods(defs);
assert_eq!(stack.len(), 0); // どうしてassert_eqしているか分かっていない
debug_exit_info!(self);
return Ok(expr);
}
// DataPack::{ここの中身}
l_brace if l_brace.is(LBrace) => {
let maybe_class = enum_unwrap!(stack.pop(), Some:(ExprOrOp::Expr:(_)));
self.restore(l_brace);
// これでレコードが来ないといけない
let container = self
.try_reduce_brace_container()
.map_err(|_| self.stack_dec(fn_name!()))?;
match container {
BraceContainer::Record(args) => { // ここでレコードを確定させる
let pack = DataPack::new(maybe_class, vis, args);
stack.push(ExprOrOp::Expr(Expr::DataPack(pack)));
}
other => { // 辞書型や集合型なら中身が違うのでエラー
let err = ParseError::invalid_data_class_container(
line!() as usize,
other.loc(),
&other.to_string(),
);
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
}
}
other => {
self.restore(other);
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
}
}
可視性パブリック
これもプライベートと似たようなことをしている
― パブリック変数
- パブリッククラス属性
ただ,DetaPack式はない
Some(t) if t.is(Dot) => {
let vis = self.lpop(); // .が取り出される
match self.lpop() {
// 可視性がパブリックの変数になる
symbol if symbol.is(Symbol) => {
let Some(ExprOrOp::Expr(obj)) = stack.pop() else {
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
};
if let Some(args) = self
.opt_reduce_args(false) // 引数があれば関数,なければ変数
.transpose() // Ok()とSome()をひっくり返す
.map_err(|_| self.stack_dec())?
{
let ident = Identifier::new(Some(vis), VarName::new(symbol)); // ここでパブリックを指定している
let call = Expr::Call(Call::new(obj, Some(ident), args));
stack.push(ExprOrOp::Expr(call));
} else {
let ident = Identifier::new(Some(vis), VarName::new(symbol));
stack.push(ExprOrOp::Expr(obj.attr_expr(ident)));
}
}
// クラスのパブリック属性が定義される
line_break if line_break.is(Newline) => {
// maybeなのは無効なシンボルの可能性があるから(組み込み定数や小文字ならだめなど)
let maybe_class = enum_unwrap!(stack.pop(), Some:(ExprOrOp::Expr:(_)));
let defs = self
.try_reduce_method_defs(maybe_class, vis)
.map_err(|_| self.stack_dec(fn_name!()))?;
return Ok(Expr::Methods(defs));
}
other => {
self.restore(other);
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
}
}
配列の添え字アクセス
配列は式なのでchunkで呼ばれることはないので,[index]
の添え字アクセスになる
Some(t) if t.is(LSqBr) => {
let Some(ExprOrOp::Expr(obj)) = stack.pop() else {
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
};
self.skip(); // [がスキップされる
let index = self
.try_reduce_expr(false, false, in_brace, false)
.map_err(|_| self.stack_dec())?;
let r_sqbr = self.lpop();
// ]ではなければ括弧で閉じられていないか別の式か何かが来ているのでエラー
if !r_sqbr.is(RSqBr) {
self.restore(r_sqbr);
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
let acc = Accessor::subscr(obj, index, r_sqbr);
stack.push(ExprOrOp::Expr(Expr::Accessor(acc)));
}
括弧なし引数
Tuple型で括弧なしで引数宣言するとき
print! 1, True, "sample"
windingが基本的にはfalseになっているが,moduleとblockの時にはtrueになる
// elem, ←ここから始まっている
Some(t) if t.is(Comma) && winding => {
// カンマが来るということは初めの要素が解析されたことが前提となる
// なので、式が来ることが期待されている
let first_elem = PosOrKwArg::Pos(PosArg::new(
enum_unwrap!(stack.pop(), Some:(ExprOrOp::Expr:(_))),
));
let tup = self
.try_reduce_nonempty_tuple(first_elem, false) // 最初の要素が必ずあるので,noneempty_setが呼ばれる
.map_err(|_| self.stack_dec())?;
stack.push(ExprOrOp::Expr(Expr::Tuple(tup)));
}
デフォルト引数
仮引数のデフォルト代入の構文
引数は全てTuple型なので最終的にTupleに変換されている
Some(t) if t.is(Walrus) && winding => {
let tuple = self
.try_reduce_default_parameters(&mut stack, in_brace)
.map_err(|_| self.stack_dec())?;
stack.push(ExprOrOp::Expr(Expr::Tuple(tuple)));
}
予約(反転)
^と&のビット演算子だと思うが定かではない
正直一番ここがわからない
恐らく,これら演算子が来た時点でchunkにならないから構文エラーなのだと思う
[edit(2023/02/10)]
予約しているトークンのことだった
Some(t) if t.category_is(TC::Reserved) => {
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
プレースホルダ―
一つの塊になったら終了で,スタックされている数が多ければそもそも塊になっていないはずなので塊に直すことをしている
ここで塊になるまでループが続く
コメントアウトされている2個の場合が気になるがどうなのだろうか
_ => {
if stack.len() <= 1 {
break;
}
// else if stack.len() == 2 { switch_unreachable!() }
else {
while stack.len() >= 3 {
collect_last_binop_on_stack(&mut stack);
}
}
}
chunkの終了
塊が一つだけのはずなのでそれを取り出している
match stack.pop() {
Some(ExprOrOp::Expr(expr)) if stack.is_empty() => {
debug_exit_info!(self);
Ok(expr)
}
Some(ExprOrOp::Expr(expr)) => {
let extra = stack.pop().unwrap();
let loc = match extra {
ExprOrOp::Expr(expr) => expr.loc(),
ExprOrOp::Op(op) => op.loc(),
};
// 余分なのはwarning行き
self.warns
.push(ParseError::compiler_bug(0, loc, fn_name!(), line!()));
debug_exit_info!(self);
Ok(expr)
}
// オペレータだけの場合はエラー、必ず二項演算になっているはず
Some(ExprOrOp::Op(op)) => {
self.errs
.push(ParseError::compiler_bug(0, op.loc(), fn_name!(), line!()));
debug_exit_info!(self);
Err(())
}
_ => switch_unreachable!(),
}
try_reduce_bin_lhs()
exprやchunkの塊の元になる要素に還元する
try_reduce_chunkやtry_reduce_exprでかならず初めにこれが呼ばれる
これを解析して初めの要素が何かで次の解析に移る
分かりやすいのが、Literalになると思う
数値や文字列といったのがそのままIntLit
とかStrLit
にここで変換される
>>> -1
(1) entered try_reduce_module, cur: IntLit -1
(2) entered try_reduce_chunk, cur: IntLit -1
(3) entered try_reduce_bin_lhs, cur: IntLit -1 <- ここ
・ (4) entered try_reduce_lit, cur: IntLit -1
>>> "sample"
(1) entered try_reduce_module, cur: StrLit "sample"
(2) entered try_reduce_chunk, cur: StrLit "sample"
(3) entered try_reduce_bin_lhs, cur: StrLit "sample" <- ここ
・ (4) entered try_reduce_lit, cur: StrLit "sample"
moduleからchunkへ行ってそこからlhs_binからのlitの流れになっている
このように小さなユニットに還元する
もう少し見るとこうなる
>>> a = 1
(1) entered try_reduce_module, cur: Symbol a
(2) entered try_reduce_chunk, cur: Symbol a
(3) entered try_reduce_bin_lhs, cur: Symbol a // <- ここ
・ (4) entered try_reduce_call_or_acc, cur: Symbol a
・ (5) entered try_reduce_acc_lhs, cur: Symbol a
・ (4) exit try_reduce_acc_lhs, cur: Equal =
・ (5) entered try_reduce_acc_chain, cur: Equal =
・ (4) exit try_reduce_acc_chain, cur: Equal =
・ (5) entered opt_reduce_args, cur: Equal =
・ (4) exit try_reduce_call_or_acc, cur: Equal =
(3) exit try_reduce_bin_lhs, cur: Equal =
・ (4) entered convert_rhs_to_sig, cur: NatLit 1
・ (5) entered convert_accessor_to_var_sig, cur: NatLit 1
・ (4) entered try_reduce_expr, cur: NatLit 1
・ (5) entered try_reduce_bin_lhs, cur: NatLit 1 // <- ここ
・ (6) entered try_reduce_lit, cur: NatLit 1
・ (5) exit try_reduce_lit, cur: NatLit 1
・ (4) exit try_reduce_bin_lhs, cur: EOF
(3) exit try_reduce_expr, cur: EOF
(2) exit try_reduce_chunk, cur: EOF
(1) exit try_reduce_module, cur: EOF
(3) try_reduce_bin_lhs, cur: Symbol a
のここから連続する要素を見ている感じ
Symbol
があるとcall_or_acc
とacc_chain
が呼ばれている
その中で数値などがあれば,またこれが呼ばれる(後半の方)
このようにtry_reduce_bin_lhs
が呼ばれるようになっている
実際の処理を見ていくが,ここでは最小の要素に直していく処理をしているので単純にメソッドが呼ばれてそれを返す処理が多い
気になるところがあればそれを取り上げるようにする
Lambdaの糖衣構文
Lambdaは->
と=>
だが,これらの糖衣構文としてdo
とdo!
がある
Some(t) if &t.inspect()[..] == "do" || &t.inspect()[..] == "do!" => {
let lambda = self.try_reduce_do_block().map_err(|_| self.stack_dec())?;
debug_exit_info!(self);
Ok(Expr::Lambda(lambda))
}
これらはSymbol
の中にある文字列なので、Rustでの文字列として読み取るためにinspect()[..]
をしている
[..]
がされている理由は、token
の文字列は以下のように定義されているので,Str
から&strとして取り出すためにこうなっている
pub type RcStr = std::rc::Rc<str>;
#[derive(Debug, Clone, Eq)]
pub enum Str {
Rc(RcStr),
Static(&'static str),
}
デコレータ
pythonと同じでデコレータは@
が使われる
複数の場合があるのでdecoratorsとなっている
Some(t) if t.is(AtSign) => {
// @以降のパースは別で処理をする
let decos = self.opt_reduce_decorators()?;
// デコレートされているクラスや関数の宣言を確認して違う式ならエラーとしている
let expr = self.try_reduce_chunk(false, in_brace)?;
let Some(mut def) = option_enum_unwrap!(expr, Expr::Def) else {
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
};
// var = something
// func(parms) = something
// どちらかになっているはずなのでそれを判断している
match def.sig {
// 関数(サブルーチン)
Signature::Subr(mut subr) => {
subr.decorators = decos;
let expr = Expr::Def(Def::new(Signature::Subr(subr), def.body));
debug_exit_info!(self);
Ok(expr)
}
// 変数
Signature::Var(var) => {
let mut last = def.body.block.pop().unwrap();
for deco in decos.into_iter() {
last = deco.into_expr().call_expr(Args::new(
vec![PosArg::new(last)],
vec![],
None,
));
}
def.body.block.push(last);
let expr = Expr::Def(Def::new(Signature::Var(var), def.body));
debug_exit_info!(self);
Ok(expr)
}
}
}
Tuple型
括弧が付いているTuple型
一行の場合と改行の場合の2通りある
(1, True, "sample")
(
1,
True,
"sample"
)
(
1, True, "sample"
)
ここでは形がどうなっているかを判断して結果を返すだけ
中身はexprであれば何でも取れるのがTuple型なので,そのままtry_reduce_expr()で中身を処理している
Some(t) if t.is(LParen) => {
let lparen = self.lpop(); // (を取り出す
while self.cur_is(Newline) {
self.skip();
}
// 改行で宣言されているかどうか
let line_break = if self.cur_is(Indent) {
self.skip();
true
} else {
false
};
// 空のTuple
if self.cur_is(RParen) {
let rparen = self.lpop(); // )を取り出す
let args = Args::new(vec![], vec![], Some((lparen, rparen)));
let unit = Tuple::Normal(NormalTuple::new(args));
debug_exit_info!(self);
return Ok(Expr::Tuple(unit));
}
// 中身の処理をexprに任せる(引数のboolはexprの方で説明する)
let mut expr = self
.try_reduce_expr(true, false, false, line_break)
.map_err(|_| self.stack_dec(fn_name!()))?;
// 最後の要素までが,exprで取り出される
// その後に空行あっても文法上は無視される
while self.cur_is(Newline) {
self.skip();
}
// Tupleの宣言時に改行で宣言した場合にはIndentが付くので,それのDedent
if self.cur_is(Dedent) {
self.skip();
}
let rparen = self.lpop(); // )を取り出す
if let Expr::Tuple(Tuple::Normal(tup)) = &mut expr {
tup.elems.paren = Some((lparen, rparen));
}
debug_exit_info!(self);
Ok(expr)
}
辞書型、レコード型、集合型
ここで{
が来た時に変換をしている
中身の処理はtry_reduce_brace_container()
にまかせて、返って式をそれぞれでマッチングをかけている
Some(t) if t.is(LBrace) => {
match self
.try_reduce_brace_container()
.map_err(|_| self.stack_dec(fn_name!()))?
{
BraceContainer::Dict(dic) => {
debug_exit_info!(self);
Ok(Expr::Dict(dic))
}
BraceContainer::Record(rec) => {
debug_exit_info!(self);
Ok(Expr::Record(rec))
}
BraceContainer::Set(set) => {
debug_exit_info!(self);
Ok(Expr::Set(set))
}
}
}
多相関数型?
恐らく多相関数型?のパース
f: |T: Type| T -> T
f: |Y, Z: Type, X <: Add(Y, O1), O1 <: Add(Z, O2), O2 <: Add(X, O3)| (X, Y, Z) -> O3
|~|
中身をtry_reduce_type_app_args()
でパースする
そして,残りのT->T
だったり(X, Y, Z) -> O3
をLambdaシグネチャに変換していると思われる
Some(t) if t.is(VBar) => {
// |type|これを判断しているはず
let type_args = self
.try_reduce_type_app_args()
.map_err(|_| self.stack_dec())?;
let bounds = self
.convert_type_args_to_bounds(type_args)
.map_err(|_| self.stack_dec())?;
let args = self.try_reduce_args(false).map_err(|_| self.stack_dec())?;
// |の後の引数のパース
let params = self
.convert_args_to_params(args)
.map_err(|_| self.stack_dec())?;
if !self.cur_category_is(TC::LambdaOp) {
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
// ここでシグネチャをlmabdaに変換する
let sig = LambdaSignature::new(params, None, bounds);
let op = self.lpop(); // ->をスキップ
// lambdaはブロックを持てるので,そのままblockの解析をする
let block = self.try_reduce_block().map_err(|_| self.stack_dec())?;
self.counter.inc();
debug_exit_info!(self);
let lambda = Lambda::new(sig, op, block, self.counter);
Ok(Expr::Lambda(lambda))
}
try_reduce_expr
Ergで使われる式(experesion)をパースするための起点となるメソッド
- windingは丸括弧なしのTuple
Tuple型は中身にそれぞれ異なる式を宣言することができるので、専用の引数を持っている
a = True, a => a, "sample", !a => print! a, (a, b, c)
みたいなこともできてしまう
- in_type_argsは型指定付き
これも基本的にはTuple型で関数などの仮引数を定義するときの条件
func(x: Int, y: Int) = somthing
みたいな引数と型を指定するときのTuple型の中身を処理するとき
- in_braceは
{}
の中身
辞書型時はkey: value
それぞれが式を取ることができる
ただし、これは{}
で囲まれているときだけなので、それを判断するため
- line_brakは改行で式を宣言しているかどうか
詳細はTuple型の方で説明をしている
fn try_reduce_expr(
&mut self,
winding: bool,
in_type_args: bool,
in_brace: bool,
line_break: bool,
) -> ParseResult<Expr> {
式は以下の通り
pub enum Expr {
Lit(Literal), // 1, "sapmle", True, 1e-1
Accessor(Accessor), // [1, 2, 3][i], (1, 2, 3).i, .func
Array(Array), // [1, 2, 3], ["True", "False", "Even"], [Nat; 10]
Tuple(Tuple), // (1, True, 1.0)
Dict(Dict), // {"a": 1, "b": 2, "c":3}
Set(Set), // {1, 2, 3}, {1; 10}
Record(Record), // {x; y}, {x = 1; y}, {x = 1; y = 1}
BinOp(BinOp), // +, -, *, //
UnaryOp(UnaryOp), // +, -, *, /, //, %
Call(Call), // func, proc!
DataPack(DataPack), // datapack::{recordのどれか}
Lambda(Lambda), // do, do!, ->, =>
TypeAsc(TypeAscription), // var: type, var :> type, var <: typeだと思う
Def(Def), // 基本的な=の式
Methods(Methods), // .method(parm) = somehting
ClassDef(ClassDef), // Class., Class::
PatchDef(PatchDef), // Patch(type)
ReDef(ReDef), //
/// for mapping to Python AST
Dummy(Dummy),
}
try_reduce_chunk
とtry_reduce_bin_lhs
と被るので省略
try_reduce_call_or_acc
try_reduce_bin_lhs()
にSymbol
,Dot(.)
,UBar(_)
が来た時に必ず呼ばれる重要なメソッド
ここでacc_lhs
とacc_chain
でtoken(この場合は基本的にSymbol)
がどのように連続しているかを見ている
a
a 1
a 1, True, "sample"
a.0
a[0]
a.attr
a.method(args)
# これらにの前に.が付いたバージョン
.a
.a 1
.a 1, True, "sample"
.a.0
.a[0]
.a.attr
.a.method(args)
# クラス
C.
attr = 1
method(parms) = something
>>> a
(1) entered try_reduce_module, cur: Symbol a
(2) entered try_reduce_chunk, cur: Symbol a
(3) entered try_reduce_bin_lhs, cur: Symbol a
(4) entered try_reduce_call_or_acc, cur: Symbol a <- ここ
(5) entered try_reduce_acc_lhs, cur: Symbol a
(5) entered try_reduce_acc_chain, cur: EOF
(5) entered opt_reduce_args, cur: EOF
>>> a 1
(1) entered try_reduce_module, cur: Symbol a
(2) entered try_reduce_chunk, cur: Symbol a
(3) entered try_reduce_bin_lhs, cur: Symbol a
(4) entered try_reduce_call_or_acc, cur: Symbol a <- ここ
(5) entered try_reduce_acc_lhs, cur: Symbol a
(5) entered try_reduce_acc_chain, cur: NatLit 1
(5) entered opt_reduce_args, cur: NatLit 1
(6) entered try_reduce_args, cur: NatLit 1
(7) entered try_reduce_arg, cur: NatLit 1
(8) entered try_reduce_expr, cur: NatLit 1
(9) entered try_reduce_bin_lhs, cur: NatLit 1
(10) entered try_reduce_lit, cur: NatLit 1
(6) entered opt_reduce_args, cur: EOF
関数の宣言か関数などへのアクセスかどうかを見ている
fn try_reduce_call_or_acc(&mut self, in_type_args: bool) -> ParseResult<Expr> {
debug_call_info!(self);
let acc = self
.try_reduce_acc_lhs()
.map_err(|_| self.stack_dec(fn_name!()))?;
let mut call_or_acc = self.try_reduce_acc_chain(acc, in_type_args)?;
while let Some(res) = self.opt_reduce_args(in_type_args) {
let args = res.map_err(|_| self.stack_dec(fn_name!()))?;
let (receiver, attr_name) = match call_or_acc {
Expr::Accessor(Accessor::Attr(attr)) => (*attr.obj, Some(attr.ident)),
other => (other, None),
};
let call = Call::new(receiver, attr_name, args);
call_or_acc = Expr::Call(call);
}
debug_exit_info!(self);
Ok(call_or_acc)
}
try_reuduce_acc_lhs
単純にパブリック・プライベート変数の確認をしているだけ
# private(local)
a = 1
f()
p!()
# public
.a = 1
.f()
.p!()
fn try_reduce_acc_lhs(&mut self) -> ParseResult<Accessor> {
debug_call_info!(self);
let acc = match self.peek_kind() {
Some(Symbol | UBar) => Accessor::local(self.lpop()), // 非公開変数化
Some(Dot) => {
let dot = self.lpop();
let maybe_symbol = self.lpop();
if maybe_symbol.is(Symbol) {
Accessor::public(dot, maybe_symbol) // 公開変数化
} else {
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
}
_ => { // これが来たらParserのバグ
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
};
debug_exit_info!(self);
Ok(acc)
}
try_reduce_acc_chain
先ほどのacc_lhs
で公開・非公開変数を求めて,ここでその次のtoken
を見て関数呼び出しかアクセッサ―かの確認している
長いので分割してこれも説明をしていく
配列の添え字アクセス
一般的な配列に対する添え字のアクセス
今のところ,一要素のアクセスしかできないが将来的には複数のアクセスが可能になるはず
a = [1, 2, 3, 4, 5]
one = a[0]
slice = a[2..3]
one_two = a[1, 2]
Some(t) if t.is(LSqBr) && obj.col_end() == t.col_begin() => {
let _l_sqbr = self.lpop();
// これでスライスや添え字が取り出される
// bin_lhsやlitじゃないのは恐らく上記のスライスとかに対応するためだと思う
let index = self
.try_reduce_expr(true, false, false, false)
.map_err(|_| self.stack_dec(fn_name!()))?;
let r_sqbr = if self.cur_is(RSqBr) {
self.lpop()
} else {
// ]が無ければ,閉じられていないからエラー
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
};
obj = Expr::Accessor(Accessor::subscr(obj, index, r_sqbr));
}
配列の中身の添え字アクセス
多分こういうのだと思う
a = [(1, 2), (3, 4)]
three = a[1].0
.0とかの数字はRatio
の整数省略だと解釈されてしまう
なのでここではRatioLitで明示的にアクセスするように直している
Some(t) if t.is(RatioLit) && obj.col_end() == t.col_begin() => {
let mut token = self.lpop();
token.content = Str::rc(&token.content[1..]); // 小数点を切り離してる
token.kind = NatLit; // 添え字に直す
token.col_begin += 1;
obj = obj.tuple_attr_expr(Literal::from(token));
}
可視性パブリックへのアクセス
obj.col_end()とt.col_begin()が呼ばれているのは改行でのアクセスかどうかを見るためだと思う
a.0
a.attr
C. # クラス定義
これも今現在は一行だけになっているが、複数行でできるようにする予定
(0..10)
.map x -> x * 3
.filter x -> x % 2 == 0 #[6, 12, 18, 24, 30]
Some(t) if t.is(Dot) && obj.col_end() == t.col_begin() => {
let vis = self.lpop();
match self.peek() {
// ojb.attrもしくはobj.method
Some(t) if t.is(Symbol) => {
let ident = Identifier::new(Some(vis), VarName::new(self.lpop()));
obj = obj.attr_expr(ident);
}
// tuple.iの添え字アクセス
Some(t) if t.is(NatLit) => {
let index = Literal::from(self.lpop());
obj = obj.tuple_attr_expr(index);
}
// クラスの定義なのでやり直す
Some(t) if t.is(Newline) => {
self.restore(vis);
break;
}
// それ以外は無効な構文なのでエラー
Some(other) => {
let err = ParseError::invalid_acc_chain(
line!() as usize,
other.loc(),
&other.inspect()[..],
);
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
// これが来たらParserのバグ
None => {
self.errs.push(self.unexpected_none(line!(), caused_by!()));
debug_exit_info!(self);
return Err(());
}
}
}
可視性プライベートへのアクセス
Some(t) if t.is(DblColon) && obj.col_end() == t.col_begin() => {
let vis = self.lpop();
let token = self.lpop();
match token.kind {
Symbol => { // private(local)属性アクセス
let ident = Identifier::new(None, VarName::new(token));
obj = obj.attr_expr(ident);
}
LBrace => { //
self.restore(token);
let args = self
.try_reduce_brace_container()
.map_err(|_| self.stack_dec(fn_name!()))?;
match args {
BraceContainer::Record(args) => {
obj = Expr::DataPack(DataPack::new(obj, vis, args));
}
other => {
let err = ParseError::simple_syntax_error(
line!() as usize,
other.loc(),
);
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
}
}
// MethodDefs
Newline => {
self.restore(token);
self.restore(vis);
break;
}
_ => {
self.restore(token);
let err = self.skip_and_throw_syntax_err(caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
}
}
引数
ここは一度ループが来たことが前提となっている
ojb.attr
で属性があり,それがojb.attr(args)
となっていたら引数を求めてメソッドの呼び出しに変換する
obj.attr
→obj.method(args)
Some(t) if t.is(LParen) && obj.col_end() == t.col_begin() => {
let args = self
.try_reduce_args(false)
.map_err(|_| self.stack_dec(fn_name!()))?;
let (receiver, attr_name) = match obj {
// 引数があるということは直前のobjに属性があり
// その属性にも識別子があるはずなのでそれを取り出す
Expr::Accessor(Accessor::Attr(attr)) => (*attr.obj, Some(attr.ident)),
other => (other, None),
};
// メソッド呼び出しに変換
let call = Call::new(receiver, attr_name, args);
obj = Expr::Call(call);
}
全称量化?
バーティカルバーはまだ正直わかっていないのでこれも推測になる
多分,全称量化のパースだと思っている
add|T <: Add| l: T, r: T = l + r
Some(t) if t.is(VBar) && !in_type_args => {
let type_args = self
.try_reduce_type_app_args()
.map_err(|_| self.stack_dec(fn_name!()))?;
obj = Expr::Accessor(Accessor::TypeApp(TypeApp::new(obj, type_args)));
}
後はloopをbreakして今までの内容を適用させたobjを返している
try_reduce_args
引数全般を解析するためのメソッド
Ergは数値だけではなく、関数も簡単に引数にすることができる
また,引数は一行で書くこともできるが,改行を使って書くこともできる
更に,実引数(argument)と仮引数(parameter)はパースする段階ではどちらかを判断できないため,同じ処理をすることになっていしまう
なので、すごく複雑な処理をしている
# argment
x = if True, 1, 2 # SingleCommaNoParen
x = if True: # Colon
1
2
# parameter
f(x, y, z) = something # SingleCommaWithParen
f x, y, z = something # SingleCommaNoParen
f( # MultiComma
x,
y,
z
)
これも長いので区切りながら見ていく
初めの処理
引数は必ずタプル型になる
しかし,タプル型は丸括弧を省略することができたり,改行で宣言できたりする(上記の例)
そのためのstyle
をEnum
で分けている
let mut lp = None; // left parenthese
let rp; // right parentheses
if self.cur_is(LParen) {
lp = Some(self.lpop());
}
// とりあえず一行で,丸括弧があるかどうかを見ている
// 後のループの処理で複数行になるかを決めている
let mut style = if lp.is_some() {
ArgsStyle::SingleCommaWithParen
} else {
ArgsStyle::SingleCommaNoParen
};
match self.peek_kind() {
// 引数無し
Some(RParen) => {
rp = Some(self.lpop());
debug_exit_info!(self);
return Ok(Args::new(vec![], vec![], Some((lp.unwrap(), rp.unwrap()))));
}
// 正直ここはわかっていない(空になるのはわかる)
// 配列やセット,辞書,レコード型は別で処理されているはずなので,
// Dedent以外は必要ない気がする
Some(RBrace | RSqBr | Dedent) => {
debug_exit_info!(self);
return Ok(Args::new(vec![], vec![], None));
}
// 丸括弧ありで改行を使った宣言
Some(Newline) if style.needs_parens() => {
self.skip();
if self.cur_is(Indent) {
self.skip();
}
style = ArgsStyle::MultiComma;
}
_ => {}
}
最初の引数
基本的にはtry_reduce_arg
を使って個々の引数をパースをしてく
ただ、初めの引数の処理をしてコロンスタイルの場合は改行が次に来るが,それ以外は一行での処理になるはず
そのために初めの引数を見ている
let mut args = match self
.try_reduce_arg(in_type_args)
.map_err(|_| self.stack_dec(fn_name!()))?
{
PosOrKwArg::Pos(arg) => Args::new(vec![arg], vec![], None),
PosOrKwArg::Kw(arg) => Args::new(vec![], vec![arg], None),
};
以降はループをして、最後の引数が来るまでこのargs
に引数をpush
している
実引数のエラー
これは最初のループでは来ることがない
二回目以降に来る可能性があり,その上でコロンスタイルだったり丸括弧が既にある場合にはエラーになる
if (True: 1, 2) # コロンがあるときには改行がされていたりするはず
if True: # 一回目のループでこれが処理される
:1 # 上記のコロンがあるときにこういうコロンが来ることがない
do: # これはできるが,doが必ずargで処理されるはず
log 2
Some(Colon) if style.is_colon() || lp.is_some() => {
self.skip();
let err = self.skip_and_throw_syntax_err(line!(), caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
コロン改行スタイル
以下のように改行がきたらブロックが期待される
更にその中でも式が来たり,様々な要素が来る可能性がある
それら要素は再度ループを回してarg
に任せる
if True:
do:
log 1
do:
log 2
Some(Colon) => {
self.skip(); // :をスキップ
style = ArgsStyle::Colon;
// 改行のみはスキップしている(上記の例)
while self.cur_is(Newline) {
self.skip();
}
// 改行が来たら次はブロックが期待されるのでインデントが必要になる
if !self.cur_is(Indent) {
let caused_by = caused_by!();
log!(err "error caused by: {caused_by}");
let err = ParseError::no_indention(
line!() as usize,
self.lpop().loc(),
switch_lang!(
"japanese" => "実引数",
"traditional_chinese" => "参数",
"simplified_chinese" => "参数",
"english" => "argument",
),
);
self.errs.push(err);
let caused_by = caused_by!();
log!(err "error caused by: {caused_by}");
self.next_expr();
debug_exit_info!(self);
return Err(());
}
self.skip();
}
カンマスタイル
こんな感じ
f 1, 2, 3
f(1, 2, 3)
f(
1,
2,
3
)
Some(Comma) => {
self.skip(); // ,はスキップ
// コロンスタイルの時カンマは来ない
if style.is_colon() || self.cur_is(Comma) {
let err = self.skip_and_throw_syntax_err(line!(), caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
// 改行スタイルなら改行は無視
if style.is_multi_comma() {
while self.cur_is(Newline) {
self.skip();
}
if self.cur_is(Dedent) {
self.skip();
}
}
// 括弧で始まったときにはこれが必ずtrueになっているはず
// そのうえで右の括弧}が来た時
if style.needs_parens() && self.cur_is(RParen) {
let rp = self.lpop();
let (pos_args, kw_args, _) = args.deconstruct();
args = Args::new(pos_args, kw_args, Some((lp.unwrap(), rp)));
break;
}
if !args.kw_is_empty() {
args.push_kw(
self.try_reduce_kw_arg(in_type_args)
.map_err(|_| self.stack_dec(fn_name!()))?,
);
} else {
// 引数は必ずポジショナル引数はキーワード引数になる
match self
.try_reduce_arg(in_type_args)
.map_err(|_| self.stack_dec(fn_name!()))?
{
PosOrKwArg::Pos(arg) => {
args.push_pos(arg);
}
PosOrKwArg::Kw(arg) => {
args.push_kw(arg);
}
}
}
}
引数パース終了
括弧があるときの引数の終了
Some(RParen) => {
if let Some(lp) = lp {
let rp = self.lpop();
let (pos_args, kw_args, _) = args.deconstruct();
args = Args::new(pos_args, kw_args, Some((lp, rp)));
} else {
// 括弧内で括弧を省略した実引数を取るとき
// e.g. f(g 1)
let (pos_args, kw_args, _) = args.deconstruct();
args = Args::new(pos_args, kw_args, None);
}
break;
}
改行スタイル
f(
1,
2
)
# or
if True:
1
2
Some(Newline) => {
if !style.is_colon() {
if style.is_multi_comma() {
self.skip();
while self.cur_is(Dedent) {
self.skip();
}
let rp = self.lpop();
if !rp.is(RParen) {
let err = self.skip_and_throw_syntax_err(line!(), caused_by!());
self.errs.push(err);
debug_exit_info!(self);
return Err(());
}
let (pos_args, kw_args, _) = args.deconstruct();
args = Args::new(pos_args, kw_args, Some((lp.unwrap(), rp)));
}
break;
}
let last = self.lpop();
if self.cur_is(Dedent) {
self.skip();
self.restore(last);
break;
}
}
一行引数のパース
一行の引数のパースがここで行われる
# キーワード実引数が来たらその後ろの引数は全てキーワード引数にしないといけない
f(True, "sample", x:=1, y:=2, z:=3)
キーワードがあるときには必ず,その後の引数もキーワード引数にする必要がある
それ以外は単純なポジショナル引数になるが,途中からキーワード引数をとることもできる
Some(_) if style.is_colon() => {
if !args.kw_is_empty() {
args.push_kw(
self.try_reduce_kw_arg(in_type_args)
.map_err(|_| self.stack_dec(fn_name!()))?,
);
} else {
match self
.try_reduce_arg(in_type_args)
.map_err(|_| self.stack_dec(fn_name!()))?
{
PosOrKwArg::Pos(arg) => {
args.push_pos(arg);
}
PosOrKwArg::Kw(arg) => {
args.push_kw(arg);
}
}
}
}
try_reduce_arg
try_redcuce_args
から呼ばれて、一つの引数だけをパースする
キーワード引数
パースする段階ではデフォルト仮引数とキーワード実引数は区別できない
f(x:=1, y:=[2]) # キーワード実引数
f(x:=1, y:=[1, 2, 3], z:={name="John", age=21})=
x
y
print! z # デフォルト引数
これらは右辺があるかどうかでしか判断が付けないので,ここではあくまでもキーワード引数として処理される
Some(Symbol) => {
// シンボルの次に:=が来た時
if self.nth_is(1, Walrus) {
let acc = self
.try_reduce_acc_lhs()
.map_err(|_| self.stack_dec(fn_name!()))?;
debug_power_assert!(self.cur_is(Walrus));
self.skip();
// 識別子が来ないといけない
let kw = if let Accessor::Ident(n) = acc {
n.name.into_token()
} else {
let err = ParseError::simple_syntax_error(0, acc.loc());
self.errs.push(err);
self.next_expr();
debug_exit_info!(self);
return Err(());
};
// キーワードに代入する式の解析
let expr = self
.try_reduce_expr(false, in_type_args, false, false)
.map_err(|_| self.stack_dec(fn_name!()))?;
debug_exit_info!(self);
Ok(PosOrKwArg::Kw(KwArg::new(kw, None, expr)))
} else {
関数が引数
シンボルが来て更に別のが来るのは関数を引数にするときなど
f x = x
g y = y
f(g(x))
f g 1
f g y:=1
関数を取れるのでここで式としてパースする
内容は上記のキーワード引数と同じ
let expr = self
.try_reduce_expr(false, in_type_args, false, false)
.map_err(|_| self.stack_dec(fn_name!()))?;
if self.cur_is(Walrus) {
self.skip();
let (kw, t_spec) = match expr {
Expr::Accessor(Accessor::Ident(n)) => (n.name.into_token(), None),
Expr::TypeAsc(tasc) => {
if let Expr::Accessor(Accessor::Ident(n)) = *tasc.expr {
let t_spec = TypeSpecWithOp::new(tasc.op, tasc.t_spec);
(n.name.into_token(), Some(t_spec))
} else {
let err = ParseError::simple_syntax_error(0, tasc.loc());
self.errs.push(err);
self.next_expr();
debug_exit_info!(self);
return Err(());
}
}
_ => {
let err = ParseError::simple_syntax_error(0, expr.loc());
self.errs.push(err);
self.next_expr();
debug_exit_info!(self);
return Err(());
}
};
let expr = self
.try_reduce_expr(false, in_type_args, false, false)
.map_err(|_| self.stack_dec(fn_name!()))?;
debug_exit_info!(self);
Ok(PosOrKwArg::Kw(KwArg::new(kw, t_spec, expr)))
} else {
debug_exit_info!(self);
Ok(PosOrKwArg::Pos(PosArg::new(expr)))
}
}
}
ポジショナル引数
いわゆる一般的な引数のパース
引数には式が持ってこれるのでそれを解析し、PosArg
として返す
f(1)
f {x = 1; y =2}
Some(_) => {
let expr = self
.try_reduce_expr(false, in_type_args, false, false)
.map_err(|_| self.stack_dec(fn_name!()))?;
debug_exit_info!(self);
Ok(PosOrKwArg::Pos(PosArg::new(expr)))
}
終わり
いくつかわからないところがあったので何とかわかるように努めたい
以下の内容について大雑把であるが説明をしてきた
- Lexer
- lex_multi_line_comment()
- Parser
- try_reduce_module()
- try_reduce_chunk()
- try_reduce_bin_lhs()
- try_reduce_expr()
- try_reduce_call_or_acc()
- try_reduce_acc_lhs()
- try_reduce_arg()
- try_reduce_args()
今回はパースの幹になるところの説明を行ったのでこれをしっかりと理解できればdebugの情報を読み取れるようになると思う
後はこれらから呼び出されるtry_reudce系のメソッドをより詳しく見ていけばより詳しく理解できると思う
パースでまだできていないところ
これを読んでちょっと興味が出た人向けの内容を軽く紹介する
しかし、多分初心者の人は難しいとは思うので質問をすることをお勧めする
配列の改行宣言
割と簡単だと思う
他のセット型やレコード型などなどでできているので、これらを参考にするとできると思う
try_reduce_elems
のところ
a = [1, # 現在のパーサーではエラー
2]
↓
a = [
2, 1,
3, 2,
4, 3,
]
バックスラッシュによる途中改行
簡単だと思う
# x + y + z
x \
+ y\
+ z
簡易乗算
これも簡単だと思う
x = 1
assert 2x + 1 == 3 # 2*x + 1
assert 2(x+1) == 4 # 2*(x + 1)
assert 2.2x == 2.2
1m / 1s # (1*m)/(1*s)
改行によるメソッドチェーン
恐らくこれも簡単だと思う
Issueは作っていないがこれもできれば嬉しい機能なのでぜひ誰か
# 途中改行(現状)
(0..10)\
.map x -> x + 1\
.filter x % 2 == 0\
# 改行
(0..10)
.map x -> x + 1
.filter x % 2 = 1
配列の連続アクセス
これもIssueが作られていないが一応APIにはあるので載せておく
a = [1, 2, 3, 4]
a[1, 2] # 現在エラー
↓
a[1, 2] # [2, 3]
内包表記
難しいので、もしチャレンジしたい人はGithubかDiscord質問をした方が良い
[expr | (name <- iter) + (predicate)] # セット型や辞書型も同様にできる
[i*2 | i <- [0, 1, 2]] # [0, 2, 4]
篩型
これはパースの実装の難しさは普通くらいだと思う
ただ、抽象木(ast)を作成する必要があるのでその分だけ難しいと思う
{name: Type | predicate}
{I: Int | I >= 0}