はじめに
以前の記事で複数の引数に対応しました。
続けて関数の戻り値に対応したいと思います。
関数の戻り値対応でやりたいこと
簡単にやりたいことを確認します。
例えば下のようなプログラムがあったら、add3()
関数呼び出しの戻り値が返されて変数v
に代入されます。
標準出力には変数v
の値6
が出力されることを目指します。
function add3(a1, a2, a3) {
return a1 + a2 + a3
}
v = add3(1,2,3)
println(v)
変数のスコープに対応しないのは、以前の記事のままです。
実装の仕方
実装の仕方について、構文解析(Parser)、インタプリタ(Interpreter)と順に考えていきます。
字句解析(Lexer)はとくに変更しません。
構文解析(Parser)の実装の仕方
return
の構文解析を実装するにあたり、return
の構文に注意する必要があります。
return
の構文はreturn 1
のように、戻り値が指定される場合もあれば、
return
だけ指定され戻り値が指定されない場合もあります。
return
だけの指定が構文として正しいのなら、
return 1
と書かれた場合に、
それは戻り値を指定された場合の構文ともとれるし、
1
がreturn
とは独立した構文であると考えて、
戻り値が指定されない場合の構文ともとれます。
このどっちともとれる状況を簡単に解決するために、手抜きな方法をとります。
その方法はreturn
トークンのあとに}
トークンがきたら、
戻り値が指定されない場合のreturn
とみなします。
逆に}
トークンがこないなら、戻り値が指定される場合のreturn
とみなします。
戻り値が指定されない場合の解析は変数名と同じように扱い、
戻り値が指定される場合の解析は、- 1
のような単項演算子と同じように扱います。
インタプリタ(Interpreter)の実装の仕方
インタプリタでは式を逐次実行するメソッドbody()
へ変更を加えます。
body()
の中でreturn
トークンにであったら、
逐次実行を中断して呼び出し元へ戻るようにします。
またreturn
は関数内でしか呼び出しが許されません。
関数内でのreturn
呼び出しかの判定も行います。
Javaで実装してみる
実装にうつります。
構文解析(Parser)、インタプリタ(Interpreter)について、
変更と追加をしたところを順にみていきます。
Parser.java
Parser.javaの実装です。
トークンの意味に対して、どう動作するかの定義を追加します。
return
は予約語となるので、
<-- Update
とあるところにreturn
を追加しました。
public Parser() {
degrees = new HashMap<>();
degrees.put("(", 80);
degrees.put("*", 60);
degrees.put("/", 60);
degrees.put("+", 50);
degrees.put("-", 50);
degrees.put("=", 10);
factorKinds = Arrays.asList(new String[] { "digit", "ident" });
binaryKinds = Arrays.asList(new String[] { "sign" });
rightAssocs = Arrays.asList(new String[] { "=" });
unaryOperators = Arrays.asList(new String[] { "+", "-" });
reserved = Arrays.asList(new String[] { "function", "return" }); // <-- Update
}
解析を行う部分の変更です。
return
の解析を行うif文を<-- Add
がある箇所へ加えました。
return
トークンの次のトークンが、閉じの波カッコの種類を表すeob
でなければ、
値を返すreturn
と判断し、戻り値になるトークンをleft
フィールドへ保持します。
private Token lead(Token token) throws Exception {
if (token.kind.equals("ident") && token.value.equals("function")) {
return func(token);
} else if (token.kind.equals("ident") && token.value.equals("return")) { // <-- Add
token.kind = "ret";
if (!token().kind.equals("eob")) {
token.left = expression(0);
}
return token;
} else if (factorKinds.contains(token.kind)) {
return token;
} else if (unaryOperators.contains(token.value)) {
token.kind = "unary";
token.left = expression(70);
return token;
} else if (token.kind.equals("paren") && token.value.equals("(")) {
Token expr = expression(0);
consume(")");
return expr;
} else {
throw new Exception("The token cannot place there.");
}
}
Interpreter.java
Interpreter.javaの実装です。
式を逐次実行するメソッドbody()
への変更です。
body()
は戻り値を返さないvoid
型でしたが、戻り値を返せるようにObject
型に変更しました。
body()
のシグニチャーにboolean[] ret
を加えました。
ret
の役割りは2つです。
1つめはret
がnull
でないなら、return
が可能な状況であることを表します。
2つめはreturn
にであったら、呼び出し元にもreturn
にであったことを伝播します。
ret
をboolean
の配列型にしたのは、呼び出し元に値を返すためです。
本来の配列を使う目的ではない、邪道な使い方をしています。
C#のref
キーワードをつけた引数のような働きを、簡単にさせたくてそうしました。
for
の中で最初に、逐次実行するトークンがreturn
かを判定します。
return
でないなら、いままでのbody()
と同じ処理です。
return
ならreturn
可能な状況か判定します。
ret
へreturn
にであったことを呼び出し元に伝えるためにtrue
を代入します。
戻り値があるreturn
の場合は、戻り値あたるトークンをexpression()
で実行します。
public Object body(List<Token> body, boolean[] ret) throws Exception {
for (Token exprs : body) {
if (exprs.kind.equals("ret")) {
if (ret == null) {
throw new Exception("Can not return");
}
ret[0] = true;
if (exprs.left == null) {
return null;
} else {
return expression(exprs.left);
}
} else {
expression(exprs);
}
}
return null;
}
DynamicFunc
クラスのinvoke()
メソッドの変更です。
<-- Update
が変更箇所です。
body()
メソッドのシグニチャーと戻り値の変更に対応しています。
引数ret
にインスタンスを代入して、return
できる状況であることを表しています。
body()
の戻り値をそのままinvoke()
の戻り値にします。
public static class DynamicFunc extends Func {
public Interpreter context;
public List<Token> params;
public List<Token> block;
@Override
public Object invoke(List<Object> args) throws Exception {
for (int i = 0; i < params.size(); ++i) {
Token param = params.get(i);
Variable v = context.variable(context.ident(param));
if (i < args.size()) {
v.value = context.value(args.get(i));
} else {
v.value = null;
}
}
boolean[] ret = new boolean[1]; // <-- Update
return context.body(block, ret); // <-- Update
}
}
run()
メソッドの変更です。
<-- Update
が変更箇所です。
body()
メソッドのシグニチャーと戻り値の変更に対応しています。
run()
が呼び出されている状況は、関数の中ではありません。
その状況はreturn
できない状況なので仮引数ret
はnull
を指定します。
public Map<String, Variable> run() throws Exception {
body(body, null); // <-- Update
return variables;
}
以上の実装を使って下のプログラム
function add3(a1, a2, a3) {
return a1 + a2 + a3
}
v = add3(1,2,3)
println(v)
を実行し、変数v
へ代入される値6
を標準出力へプリントします。
public static void main(String[] args) throws Exception {
String text = "";
text += "function add3(a1, a2, a3) {";
text += " return a1 + a2 + a3";
text += "}";
text += "v = add3(1,2,3)";
text += "println(v)";
List<Token> tokens = new Lexer().init(text).tokenize();
List<Token> blk = new Parser().init(tokens).block();
new Interpreter().init(blk).run();
// --> 6
}
実装は以上です。
ありがとうございました。
おわりに
ソースの全文はこちらで公開しています。
Calc
https://github.com/quwahara/Calc/tree/article-9-return-r2/Calc/src/main/java
続きの記事があります。
if文に対応する
http://qiita.com/quwahara/items/96a68cdee4f2a0452836