はじめに
以前の記事で単純な関数定義と呼び出しを追加しました。
そのときに関数の引数は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の実装です。
まずはカンマ、,
を解析する機能の追加です。
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の実装は終了です。
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つ以上続く
と解析します。
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
とあるところが主な変更箇所で、実装の仕方は先ほどの関数定義の構文解析とほぼ同じ形になっているがわかるとおもいます。
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
へ変更しました。
public static abstract class Func {
public String name;
abstract public Object invoke(List<Object> args) throws Exception;
}
抽象クラスの変更にあわせてPrintln
クラスのinvoke()
メソッドも変更しました。
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
を導入しました。
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文での繰り返しになっています。
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
を標準出力へプリントします。
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