Edited at

8 複数の引数に対応する

More than 1 year has passed since last update.


はじめに

以前の記事で単純な関数定義と呼び出しを追加しました。

そのときに関数の引数は1つにしか対応しませんでした。

今度は引数なしや2つ以上の引数へ対応します。


複数の引数対応でやりたいこと

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

例えば下のようなプログラムがあったら、add3()関数が定義され、標準出力に6と出力されることを目指します。

v = 0

function add3(a1, a2, a3) {
v = a1 + a2 + a3
}
add3(1,2,3)
println(v)

関数の戻り値や変数のスコープに対応しないのは、以前の記事のままです。


実装の仕方

実装の仕方について、字句解析(Lexer)、構文解析(Parser)、インタプリタ(Interpreter)と順に考えていきます。


字句解析(Lexer)への実装の仕方

これまでの実装の字句解析にはカンマ、,を解析する機能がないので、それを実装します。


構文解析(Parser)の実装の仕方

以前の記事で加えた、関数定義と関数呼び出しの構文解析の実装部分へ変更を加えます。

どちらも引数が1つの場合しか考慮していないので、

引数なしや複数の引数の場合も考慮するように、解析の実装を変更します。


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

こちらも以前の記事で加えた関数を表すクラスや、

それ使って関数を定義する部分、呼び出す部分へ変更を加えます。

どれも引数が1つの場合しか考慮していないので、

引数なしや複数の引数の場合も考慮するように変更します。


Javaで実装してみる

実装にうつります。

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

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


Lexer.java

Lexer.javaの実装です。

まずはカンマ、,を解析する機能の追加です。


Lexer.java

    private boolean isSymbolStart(char c) {

return c == ',';
}

private Token symbol() throws Exception {
Token t = new Token();
t.kind = "symbol";
t.value = Character.toString(next());
return t;
}


カンマ解析の呼び出し部分を追加してあげます。

Lexer.javaの実装は終了です。


Lexer.java

    public Token nextToken() throws Exception {

skipSpace();
if (isEOT()) {
return null;
} else if (isSignStart(c())) {
return sign();
} else if (isDigitStart(c())) {
return digit();
} else if (isIdentStart(c())) {
return ident();
} else if (isParenStart(c())) {
return paren();
} else if (isCurlyStart(c())) {
return curly();
} else if (isSymbolStart(c())) {
return symbol();
} else {
throw new Exception("Not a character for tokens");
}
}


Parser.java

関数定義の解析を行うメソッドfunc()を変更しました。

func()メッソド変更の説明の前に、Tokenクラス変更が関連するので

Tokenクラス変更の説明をします。

関数定義の解析結果は引数のtokenにまとめられます。

以前の記事で仮引数をあらわすparamというフィールド変数をTokenクラスに追加しました。

今回、複数引数へ対応するために、そのparamフィールドは廃止して、List<Token>型のparamsというフィールド変数をTokenクラスに追加します。

続いてfunc()メッソド変更の説明です。

<-- Updateとあるところが主な変更箇所です。

そこの構文解析は次のようになります。

(トークンのあとで、


  • すぐに)トークンがくるなら、引数はなし

  • すぐに)トークンがこないなら、まず引数は1つ以上はある

  • 引数は1つ以上あった上で、)トークンがこないなら引数が2つ以上続く

と解析します。


Parser.java

    private Token func(Token token) throws Exception {

token.kind = "func";
token.ident = ident();
consume("(");
token.params = new ArrayList<Token>();
if (!token().value.equals(")")) { // <-- Update
token.params.add(ident());
while (!token().value.equals(")")) {
consume(",");
token.params.add(ident());
}
}
consume(")");
consume("{");
token.block = block();
consume("}");
return token;
}

関数呼び出しを行う部分の構文解析の実装です。

<-- Updateとあるところが主な変更箇所で、実装の仕方は先ほどの関数定義の構文解析とほぼ同じ形になっているがわかるとおもいます。


Parser.java

    private Token bind(Token left, Token operator) throws Exception {

if (binaryKinds.contains(operator.kind)) {
operator.left = left;
int leftDegree = degree(operator);
if (rightAssocs.contains(operator.value)) {
leftDegree -= 1;
}
operator.right = expression(leftDegree);
return operator;
} else if (operator.kind.equals("paren") && operator.value.equals("(")) {
operator.left = left;
operator.params = new ArrayList<Token>();
if (!token().value.equals(")")) { // <-- Update
operator.params.add(expression(0));
while (!token().value.equals(")")) {
consume(",");
operator.params.add(expression(0));
}
}
consume(")");
return operator;
} else {
throw new Exception("The token cannot place there.");
}
}


Interpreter.java

Interpreter.javaの実装です。

関数を表すクラスの抽象クラスの変更です。

複数引数にあわせてinvoke()メソッドの引数を、

Object argからList<Object> argsへ変更しました。


Interpreter.java

    public static abstract class Func {

public String name;

abstract public Object invoke(List<Object> args) throws Exception;
}


抽象クラスの変更にあわせてPrintlnクラスのinvoke()メソッドも変更しました。


Interpreter.java

    public static class Println extends Func {

public Println() {
name = "println";
}

@Override
public Object invoke(List<Object> args) throws Exception {
Object arg = args.size() > 0 ? args.get(0) : null;
System.out.println(arg);
return null;
}
}


同じく抽象クラスの変更にあわせてDynamicFuncクラスのinvoke()メソッドも変更しました。

を変更します。

また複数引数を表現できるようにフィールド変数のparamを廃止して、List<Token>型のparamsを導入しました。


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;
}
}
context.body(block);
return null;
}
}


関数定義部分の変更です。

<-- Add<-- Updateが主な変更箇所です。

複数引数を処理するようにfor文での繰り返しになっています。


Interpreter.java

    public Object func(Token token) throws Exception {

String name = token.ident.value;
if (functions.containsKey(name)) {
throw new Exception("Name was used");
}
if (variables.containsKey(name)) {
throw new Exception("Name was used");
}
List<String> paramCheckList = new ArrayList<String>(); // <-- Add
for (Token p : token.params) {
String param = p.value;
if (paramCheckList.contains(param)) {
throw new Exception("Parameter name was used");
}
paramCheckList.add(param);
}
DynamicFunc func = new DynamicFunc();
func.context = this;
func.name = name;
func.params = token.params; // <-- Update
func.block = token.block;
functions.put(name, func);
return null;
}

以上の実装を使って下のプログラム

v = 0

function add3(a1, a2, a3) {
v = a1 + a2 + a3
}
add3(1,2,3)
println(v)

を実行し、変数vへ代入される値6を標準出力へプリントします。


Interpreter.java

    public static void main(String[] args) throws Exception {

String text = "";
text += "v = 0";
text += "function add3(a1, a2, a3) {";
text += " v = a1 + a2 + a3";
text += "}";
text += "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-8-multiple-arguments-r2/Calc/src/main/java

続きの記事があります。

戻り値に対応する

http://qiita.com/quwahara/items/1db9a5b880fd36dcfd3c