はじめに
以前の記事で関数式に対応しました。
関数式の対応で、値として扱う型がInteger
のみから、関数式も値として扱うようになりました。
同じよう要領で文字列も値として扱えるように対応します。
文字列対応でやりたいこと
文字列対応でやりたいことを確認します。
例えば下のようなプログラムがあります。
"
で囲まれた文字列は文字列リテラルとします。
文字列がif文などの条件で論理値として扱われる場合は、長さ0の文字列を偽とします。
文字列は+
で連結できるようにします。
プログラム最後のprintln()
メソッド呼び出しではHello world!
が出力されることを目指します。
var object = ""
if (!object) {
object = "world"
}
println("Hello " + object + "!")
実装の仕方
実装の仕方について、字句解析(Lexer)、構文解析(Parser)、インタプリタ(Interpreter)と順に考えていきます。
字句解析(Lexer)の実装の仕方
"
で囲まれた文字列リテラルを解析する機能がないので追加します。
構文解析(Parser)の実装の仕方
文字列リテラルは数値リテラルと同じように扱えばよいので、数値リテラルに準じて定義を追加してあげます。
インタプリタ(Interpreter)の実装の仕方
String
もInteger
と同様に値として扱えるようにしていきます。
Integer
だけしか考慮していなかった単項演算子と二項演算子の処理を、
String
も扱えるように変更します。
Javaで実装してみる
実装にうつります。
字句解析(Lexer)、構文解析(Parser)、インタプリタ(Interpreter)について、
変更と追加をしたところを順にみていきます。
Lexer.java
Lexer.javaの実装です。
"
で囲まれた文字列を字句解析する機能を追加します。
isStringStart()
メソッドを追加します。
"
囲みの開始を検出します。
private boolean isStringStart(char c) {
return c == '"';
}
string()
メソッドを追加します。
"
囲みの終わりまでを1つのトークンへ解析します。
下のJSON の string を参考に実装します。
http://json.org/json-ja.html
参考にある 4 Hexadecimal digits の実装は端折りました。
文字列リテラルを表すkind
はstring
にします。
private Token string() throws Exception {
StringBuilder b = new StringBuilder();
next();
while (c() != '"') {
if (c() != '\\') {
b.append(next());
} else {
next();
char c = c();
if (c == '"') {
b.append('"');
next();
} else if (c == '\\') {
b.append('\\');
next();
} else if (c == '/') {
b.append('/');
next();
} else if (c == 'b') {
b.append('\b');
next();
} else if (c == 'f') {
b.append('\f');
next();
} else if (c == 'n') {
b.append('\n');
next();
} else if (c == 'r') {
b.append('\r');
next();
} else if (c == 't') {
b.append('\t');
next();
} else {
throw new Exception("string error");
}
}
}
next();
Token t = new Token();
t.kind = "string";
t.value = b.toString();
return t;
}
nextToken()
メソッドを変更します。
// Add
のところへ、追加したメソッドの呼び出しを追加します。
これで文字列リテラルのトークンへ分解できます。
public Token nextToken() throws Exception {
skipSpace();
if (isEOT()) {
return null;
} else if (isSignStart(c())) {
return sign();
} else if (isDigitStart(c())) {
return digit();
// Add
} else if (isStringStart(c())) {
return string();
} 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
Parser.javaの実装です。
トークンの意味に対して、どう動作するかの定義を追加します。
// Update
のところへ"string"
を追加しました。
文字列リテラルは数字リテラルや変数名などと同じように、
それ以上分解できない構文なので、factorの分類に加えます。
factorの構文解析は実装済みなので、構文解析(Parser)の実装はこれで終わりです。
public Parser() {
degrees = new HashMap<>();
degrees.put("(", 80);
degrees.put("*", 60);
degrees.put("/", 60);
degrees.put("+", 50);
degrees.put("-", 50);
degrees.put("==", 40);
degrees.put("!=", 40);
degrees.put("<", 40);
degrees.put("<=", 40);
degrees.put(">", 40);
degrees.put(">=", 40);
degrees.put("&&", 30);
degrees.put("||", 30);
degrees.put("=", 10);
// Update
factorKinds = Arrays.asList(new String[] { "digit", "ident", "string" });
binaryKinds = Arrays.asList(new String[] { "sign" });
rightAssocs = Arrays.asList(new String[] { "=" });
unaryOperators = Arrays.asList(new String[] { "+", "-", "!" });
reserved = Arrays.asList(new String[] { "function", "return", "if", "else", "while", "break", "var" });
}
Interpreter.java
Interpreter.javaの実装です。
expression()
メソッドの変更です。
式を表すトークンの意味(kind)によって分岐する処理です。
// Add
の下へ文字列リテラルを表すトークンstring
のための分岐を追加しました。
string
トークンの処理は単純にそのvalue
を返すだけです。
public Object expression(Token expr) throws Exception {
if (expr.kind.equals("digit")) {
return digit(expr);
// Add
} else if (expr.kind.equals("string")) {
return string(expr);
} else if (expr.kind.equals("ident")) {
return ident(expr);
} else if (expr.kind.equals("func")) {
return func(expr);
} else if (expr.kind.equals("fexpr")) {
return fexpr(expr);
} else if (expr.kind.equals("paren")) {
return invoke(expr);
} else if (expr.kind.equals("sign") && expr.value.equals("=")) {
return assign(expr);
} else if (expr.kind.equals("unary")) {
return unaryCalc(expr);
} else if (expr.kind.equals("sign")) {
return calc(expr);
} else {
throw new Exception("Expression error");
}
}
public String string(Token token) {
return token.value;
}
value()
メソッドの変更です。
value()
メソッドは、引数が1
のような値そのものだったらその値を返し、
変数だったら、変数が保持する値を返します。
引数がString
だった場合にも対応しました。
public Object value(Object value) throws Exception {
if (value instanceof Integer) {
return value;
} else if (value instanceof String) {
return value;
} else if (value instanceof Func) {
return value;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return value(v.value);
}
throw new Exception("right value error");
}
integer()
メソッドの変更です。
integer()
メソッドは引数がInteger
であることを保証します。
引数がString
だった場合にInteger
への変換を暗黙的に試みるように変更しました。
public Integer integer(Object value) throws Exception {
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof String) {
return Integer.decode((String) value);
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return integer(v.value);
}
throw new Exception("right value error");
}
string()
メソッドを追加しました。
string()
メソッドは引数がString
であることを保証します。
引数がInteger
だった場合にString
へ暗黙的に変換します。
public String string(Object value) throws Exception {
if (value instanceof String) {
return (String) value;
} else if (value instanceof Integer) {
return value.toString();
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return string(v.value);
}
throw new Exception("right value error");
}
unaryCalc()
メソッドの変更です。
unaryCalc()
メソッドは単項演算子を処理するメソッドです。
変更前はInteger
のみに対応していました。
String
へ対応するため、value()
メソッドの呼び出し結果によって、
Integer
だったらInteger
用の単項演算子を処理するunaryCalcInteger()
メソッドを呼び出します。
String
だったらString
用の単項演算子を処理するunaryCalcString()
メソッドを呼び出します。
public Object unaryCalc(Token expr) throws Exception {
Object value = value(expression(expr.left));
if (value instanceof Integer) {
return unaryCalcInteger(expr.value, (Integer) value);
} else if (value instanceof String) {
return unaryCalcString(expr.value, (String) value);
} else {
throw new Exception("unaryCalc error");
}
}
public Object unaryCalcInteger(String sign, Integer left) throws Exception {
if (sign.equals("+")) {
return left;
} else if (sign.equals("-")) {
return -left;
} else if (sign.equals("!")) {
return toInteger(!isTrue(left));
} else {
throw new Exception("unaryCalcInteger error");
}
}
public Object unaryCalcString(String sign, String left) throws Exception {
if (sign.equals("!")) {
return toInteger(!isTrue(left));
} else {
throw new Exception("unaryCalcString error");
}
}
calc()
メソッドの変更です。
calc()
メソッドは二項演算子を処理するメソッドです。
変更前はInteger
のみに対応していました。
String
へ対応するため、左辺のvalue()
メソッドの呼び出し結果によって、
Integer
だったらInteger
用の二項演算子を処理するcalcInteger()
メソッドを呼び出します。
String
だったらString
用の二項演算子を処理するcalcString()
メソッドを呼び出します。
右辺は暗黙の変換をされることがあります。
public Object calc(Token expr) throws Exception {
Object left = value(expression(expr.left));
Object right = value(expression(expr.right));
Integer ileft = null;
String sleft = null;
if (left instanceof Integer) {
ileft = (Integer) left;
} else if (left instanceof String) {
sleft = (String) left;
}
if (ileft != null) {
return calcInteger(expr.value, ileft, right);
} else if (sleft != null) {
return calcString(expr.value, sleft, right);
} else {
throw new Exception("calc error");
}
}
public Object calcInteger(String sign, Integer left, Object right) throws Exception {
if (sign.equals("+")) {
return left + integer(right);
} else if (sign.equals("-")) {
return left - integer(right);
} else if (sign.equals("*")) {
return left * integer(right);
} else if (sign.equals("/")) {
return left / integer(right);
} else if (sign.equals("==")) {
return toInteger(left == integer(right));
} else if (sign.equals("!=")) {
return toInteger(left != integer(right));
} else if (sign.equals("<")) {
return toInteger(left < integer(right));
} else if (sign.equals("<=")) {
return toInteger(left <= integer(right));
} else if (sign.equals(">")) {
return toInteger(left > integer(right));
} else if (sign.equals(">=")) {
return toInteger(left >= integer(right));
} else if (sign.equals("&&")) {
if (!isTrue(left)) {
return left;
}
return right;
} else if (sign.equals("||")) {
if (isTrue(left)) {
return left;
}
return right;
} else {
throw new Exception("calcIteger error");
}
}
public Object calcString(String sign, String left, Object right) throws Exception {
if (sign.equals("+")) {
return left + string(right);
} else if (sign.equals("==")) {
return toInteger(left.equals(string(right)));
} else if (sign.equals("!=")) {
return toInteger(!left.equals(string(right)));
} else if (sign.equals("<")) {
return toInteger(left.compareTo(string(right)) < 0);
} else if (sign.equals("<=")) {
return toInteger(left.compareTo(string(right)) <= 0);
} else if (sign.equals(">")) {
return toInteger(left.compareTo(string(right)) > 0);
} else if (sign.equals(">=")) {
return toInteger(left.compareTo(string(right)) >= 0);
} else if (sign.equals("&&")) {
if (!isTrue(left)) {
return left;
}
return right;
} else if (sign.equals("||")) {
if (isTrue(left)) {
return left;
}
return right;
} else {
throw new Exception("calcString error");
}
}
isTrue()
メソッドの変更です。
String
を論理値として処理できるように変更しました。
長さ0の文字列を偽と判定するようにしました。
public boolean isTrue(Object value) throws Exception {
if (value instanceof Integer) {
return 0 != ((Integer) value);
} else if (value instanceof String) {
return !"".equals(value);
} else if (value instanceof Func) {
return true;
} else {
return false;
}
}
以上の実装を使って下のプログラム
var object = ""
if (!object) {
object = "world"
}
println("Hello " + object + "!")
を実行し、
プログラム最後のprintln()
メソッド呼び出しでHello world!
を出力します。
public static void main(String[] args) throws Exception {
String text = "";
text += "var object = \"\"";
text += "if (!object) {";
text += " object = \"world\"";
text += "}";
text += "println(\"Hello \" + object + \"!\")";
List<Token> tokens = new Lexer().init(text).tokenize();
List<Token> blk = new Parser().init(tokens).block();
new Interpreter().init(blk).run();
// --> Hello world!
}
実装は以上です。
ありがとうございました。
おわりに
ソースの全文はこちらで公開しています。
Calc
https://github.com/quwahara/Calc/tree/article-15-string-r3/Calc/src/main/java
続きの記事があります。
メソッド呼び出しに対応する
http://qiita.com/quwahara/items/f1bddefe984c8d233e02