Edited at

15 文字列に対応する

More than 1 year has passed since last update.


はじめに

以前の記事で関数式に対応しました。

関数式の対応で、値として扱う型がIntegerのみから、関数式も値として扱うようになりました。

同じよう要領で文字列も値として扱えるように対応します。


文字列対応でやりたいこと

文字列対応でやりたいことを確認します。

例えば下のようなプログラムがあります。

"で囲まれた文字列は文字列リテラルとします。

文字列がif文などの条件で論理値として扱われる場合は、長さ0の文字列を偽とします。

文字列は+で連結できるようにします。

プログラム最後のprintln()メソッド呼び出しではHello world!が出力されることを目指します。

var object = ""

if (!object) {
object = "world"
}
println("Hello " + object + "!")


実装の仕方

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


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

"で囲まれた文字列リテラルを解析する機能がないので追加します。


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

文字列リテラルは数値リテラルと同じように扱えばよいので、数値リテラルに準じて定義を追加してあげます。


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

StringIntegerと同様に値として扱えるようにしていきます。

Integerだけしか考慮していなかった単項演算子と二項演算子の処理を、

Stringも扱えるように変更します。


Javaで実装してみる

実装にうつります。

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

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


Lexer.java

Lexer.javaの実装です。

"で囲まれた文字列を字句解析する機能を追加します。

isStringStart()メソッドを追加します。

"囲みの開始を検出します。


Lexer.java

    private boolean isStringStart(char c) {

return c == '"';
}

string()メソッドを追加します。

"囲みの終わりまでを1つのトークンへ解析します。

下のJSON の string を参考に実装します。

http://json.org/json-ja.html

参考にある 4 Hexadecimal digits の実装は端折りました。

文字列リテラルを表すkindstringにします。


Lexer.java

    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のところへ、追加したメソッドの呼び出しを追加します。

これで文字列リテラルのトークンへ分解できます。


Lexer.java

    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)の実装はこれで終わりです。


Parser.java

    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を返すだけです。


Interpreter.java

    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だった場合にも対応しました。


Interpreter.java

    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への変換を暗黙的に試みるように変更しました。


Interpreter.java

    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へ暗黙的に変換します。


Interpreter.java

    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()メソッドを呼び出します。


Interpreter.java

    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()メソッドを呼び出します。

右辺は暗黙の変換をされることがあります。


Interpreter.java

    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の文字列を偽と判定するようにしました。


Interpreter.java

    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!を出力します。


Interpreter.java

    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