Edited at

9 戻り値に対応する

More than 1 year has passed since last update.


はじめに

以前の記事で複数の引数に対応しました。

続けて関数の戻り値に対応したいと思います。


関数の戻り値対応でやりたいこと

簡単にやりたいことを確認します。

例えば下のようなプログラムがあったら、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と書かれた場合に、

それは戻り値を指定された場合の構文ともとれるし、

1returnとは独立した構文であると考えて、

戻り値が指定されない場合の構文ともとれます。

このどっちともとれる状況を簡単に解決するために、手抜きな方法をとります。

その方法はreturnトークンのあとに}トークンがきたら、

戻り値が指定されない場合のreturnとみなします。

逆に}トークンがこないなら、戻り値が指定される場合のreturnとみなします。

戻り値が指定されない場合の解析は変数名と同じように扱い、

戻り値が指定される場合の解析は、- 1のような単項演算子と同じように扱います。


インタプリタ(Interpreter)の実装の仕方

インタプリタでは式を逐次実行するメソッドbody()へ変更を加えます。

body()の中でreturnトークンにであったら、

逐次実行を中断して呼び出し元へ戻るようにします。

またreturnは関数内でしか呼び出しが許されません。

関数内でのreturn呼び出しかの判定も行います。


Javaで実装してみる

実装にうつります。

構文解析(Parser)、インタプリタ(Interpreter)について、

変更と追加をしたところを順にみていきます。


Parser.java

Parser.javaの実装です。

トークンの意味に対して、どう動作するかの定義を追加します。

returnは予約語となるので、

<-- Updateとあるところにreturnを追加しました。


Parser.java

    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フィールドへ保持します。


Parser.java

    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つめはretnullでないなら、returnが可能な状況であることを表します。

2つめはreturnにであったら、呼び出し元にもreturnにであったことを伝播します。

retbooleanの配列型にしたのは、呼び出し元に値を返すためです。

本来の配列を使う目的ではない、邪道な使い方をしています。

C#のrefキーワードをつけた引数のような働きを、簡単にさせたくてそうしました。

forの中で最初に、逐次実行するトークンがreturnかを判定します。

returnでないなら、いままでのbody()と同じ処理です。

returnならreturn可能な状況か判定します。

retreturnにであったことを呼び出し元に伝えるためにtrueを代入します。

戻り値があるreturnの場合は、戻り値あたるトークンをexpression()で実行します。


Interpreter.java

    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()の戻り値にします。


Interpreter.java

    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できない状況なので仮引数retnullを指定します。


Interpreter.java

    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を標準出力へプリントします。


Interpreter.java

    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