I would like to support an array of basic data structures. This article is Supports strings. When it corresponds to a character string, I want to be able to handle a substring of the character string. Here, I would like to be able to call a method so that a substring can be extracted from the string.
Confirm what you want to do with method call support.
For example, there is the following program.
After assigning a string to the variable hw
, we aim to be able to call the instance methodsubstring ()
of the variable hw
.
The println ()
method call at the end of the program aims to output a substring of the variable hw
, Hello
.
var hw = "Hello world!"
var h = hw.substring(0, 5)
println(h)
What you don't do is not support static method calls.
We will consider how to implement it in the order of lexical analysis (Lexer), parser (Parser), and interpreter (Interpreter).
Since there is no function to parse .
, add it.
The .
token will be added by adding the function of Lexer.
Add the ability to parse it.
The .
token can be parsed like a binary operator, so change it that way.
We will implement the instance method call so that it can be treated as if it were an already implemented function call.
In a function call, the token to the left of the token that triggers the function call is There was only one token for the function or variable name. The method call
(the token to the left of thetoken is The
.token and its left and right tokens are also involved in the call. Therefore, the
.` token and the tokens on the left and right of it can be treated as one piece of information.
Introduce a class that puts them together.
We will make it possible to handle the collected classes in the same way as function calls.
Move on to implementation. About lexical analysis (Lexer), parser (Parser), interpreter (Interpreter) Let's take a look at the changes and additions in order.
Lexer.java
An implementation of Lexer.java.
Add a lexical analysis function for .
.
Add the ʻisDotStart ()method. Detects
.`.
Lexer.java
private boolean isDotStart(char c) {
return c == '.';
}
Add the dot ()
method.
Parse .
into a token.
The kind
that represents .
should be dot
.
Lexer.java
private Token dot() throws Exception {
Token t = new Token();
t.kind = "dot";
t.value = Character.toString(next());
return t;
}
Modify the nextToken ()
method.
Add the call to the added method to // Add
.
Now you can decompose .
into tokens.
Lexer.java
public Token nextToken() throws Exception {
skipSpace();
if (isEOT()) {
return null;
} else if (isSignStart(c())) {
return sign();
// Add
} else if (isDotStart(c())) {
return dot();
} else if (isDigitStart(c())) {
return digit();
} 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");
}
}
That's all for changing Lexer.java.
Parser.java
An implementation of Parser.java.
Add a definition of how it works for the meaning of the token.
Added the degree of ordering of .
to // Update 1
.
.
Is better than arithmetic operators like+
and*
Because it strongly connects the left and right tokens,
It is 80, which is larger than the degree of +
and *
.
Added " dot "
to // Update 2
.
The syntax of .
is equivalent to the binary operator,
Since parsing can be done in the same way as binary operators,
So that it is subject to parsing of binary operators
Added " dot "
to binary Kinds
.
Parser.java
public Parser() {
degrees = new HashMap<>();
// Update 1
degrees.put(".", 80);
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);
factorKinds = Arrays.asList(new String[] { "digit", "ident", "string" });
// Update 2
binaryKinds = Arrays.asList(new String[] { "sign", "dot" });
rightAssocs = Arrays.asList(new String[] { "=" });
unaryOperators = Arrays.asList(new String[] { "+", "-", "!" });
reserved = Arrays.asList(new String[] { "function", "return", "if", "else", "while", "break", "var" });
}
The parsing process itself does not make any changes or additions. This is because it is subject to parsing of existing binary operators in addition to the definition. That's all for changing Parser.java.
Interpreter.java
An implementation of Interpreter.java.
ʻExpression () method change. It is a process that branches according to the meaning (kind) of the token that represents the expression. Added a branch under
// Add for the token
dotto represent
.`.
Interpreter.java
public Object expression(Token expr) throws Exception {
if (expr.kind.equals("digit")) {
return digit(expr);
} 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);
// Add
} else if (expr.kind.equals("dot")) {
return dot(expr);
} else {
throw new Exception("Expression error");
}
}
Added the dot ()
method.
Called on the branch added above.
The dot ()
method is a class that pairs the left and right sides of .
Returns an instance of Dotted
.
The left side of .
assigns the return value of thevalue ()
method, which guarantees that it is a value.
This is to ensure that the left side of .
is always an instance of the method call.
Interpreter.java
public Object dot(Token token) throws Exception {
Dotted d = new Dotted();
d.left = value(expression(token.left));
d.right = token.right;
return d;
}
public static class Dotted {
public Object left;
public Token right;
}
This is a modification of the func ()
method.
The func ()
method guarantees that the argument value
is callable like a function.
Added the processing when the Dotted
added above is passed by the argument value
under // Add
.
If the argument is Dotted
, it returns an instance of the MethodFunc
class.
The MethodFunc
class inherits from the callable abstract classFunc
and
The information required for method call is stored in the field variable.
Interpreter.java
public Func func(Object value) throws Exception {
if (value instanceof Func) {
return (Func) value;
// Add
} else if (value instanceof Dotted) {
Dotted d = (Dotted) value;
MethodFunc mf = new MethodFunc();
mf.name = d.right.value;
mf.class_ = d.left.getClass();
mf.target = d.left;
return mf;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return func(v.value);
} else {
throw new Exception("Not a function");
}
}
Added the MethodFunc
class.
It is returned by the func ()
method.
Make an instance method call with the ʻinvoke` method of this class.
To make an instance method call, you need to get the information that represents the method.
Method information can be obtained from the information that represents the class.
The class_
field variable is the information for that class.
You can get the method information from the class information, Of the multiple methods defined in the class You need to select one piece of information about the method you want to call.
In the ʻinvoke method, in order to select the information of the method you want to call, Use the method name and the type information of the argument of the ʻinvoke
method to narrow down.
If the corresponding method information cannot be found or narrowed down to one, an error occurs.
The method of narrowing down is very simple and is different from the implementation in the Java compiler. I made a comment with a little detailed explanation.
Interpreter.java
public static class MethodFunc extends Func {
//Represents the type of method call target
public Class<?> class_;
//Represents the instance for which the method is called
public Object target;
@Override
public Object invoke(List<Object> args) throws Exception {
//Make a list of argument types from arguments
List<Class<?>> aClasses = argClasses(args);
//A list of method information that the type to call the method has
//List only those with the same name as this MethodFunc
List<Method> mByName = methodByName(class_.getMethods(), name);
//A list of method information narrowed down by name,
//The list of argument types is limited to those with assignable signatures.
List<Method> mByAssignable = methodByAssignable(mByName, aClasses);
//As a result of narrowing down, an error occurs if there is no corresponding method information
if (mByAssignable.size() == 0) {
throw new Exception("MethodFunc.invoke error");
}
Method method;
if (mByAssignable.size() == 1) {
//As a result of narrowing down, if there is only one applicable method information,
//That is the method information to be called
method = mByAssignable.get(0);
} else {
//As a result of narrowing down, if there are two or more applicable method information, further narrow down.
//A list of method information narrowed down by assignable signatures,
//Narrow down the list of argument types to those with exact matching signatures.
List<Method> mByAbsolute = methodByAbsolute(mByAssignable, aClasses);
//As a result of narrowing down, an error will occur if the corresponding method information does not become one.
if (mByAbsolute.size() != 1) {
throw new Exception("MethodFunc.invoke error");
}
//As a result of narrowing down, if there is only one applicable method information,
//That is the method information to be called
method = mByAbsolute.get(0);
}
//Make a method call using only one method information
Object val = method.invoke(target, args.toArray());
return val;
}
public List<Class<?>> argClasses(List<Object> args) {
List<Class<?>> classes = new ArrayList<Class<?>>();
int psize = args.size();
for (int i = 0; i < psize; ++i) {
Object a = args.get(i);
if (a != null) {
classes.add(a.getClass());
} else {
classes.add(null);
}
}
return classes;
}
public List<Method> methodByName(Method[] methods, String name) {
List<Method> ms = new ArrayList<Method>();
for (Method m : methods) {
if (m.getName().equals(name)) {
ms.add(m);
}
}
return ms;
}
public List<Method> methodByAssignable(List<Method> methods, List<Class<?>> aClasses) {
List<Method> candidates = new ArrayList<Method>();
int aSize = aClasses.size();
for (Method m : methods) {
Class<?>[] pTypes = m.getParameterTypes();
if (pTypes.length != aSize) {
continue;
}
Boolean allAssignable = true;
for (int i = 0; i < aSize; ++i) {
Class<?> c = pTypes[i];
Class<?> cc = toBoxClass(c);
Class<?> ac = aClasses.get(i);
if (ac != null) {
Class<?> acc = toBoxClass(ac);
allAssignable &= cc.isAssignableFrom(acc);
}
if (!allAssignable) {
break;
}
}
if (allAssignable) {
candidates.add(m);
}
}
return candidates;
}
public List<Method> methodByAbsolute(List<Method> candidates, List<Class<?>> aClasses) {
List<Method> screened = new ArrayList<Method>();
int aSize = aClasses.size();
for (int i = 0; i < aSize; ++i) {
Class<?> ac = aClasses.get(i);
if (ac == null) {
return screened;
}
}
for (Method m : candidates) {
Class<?>[] pTypes = m.getParameterTypes();
Boolean allEquals = true;
for (int i = 0; i < aSize; ++i) {
Class<?> c = pTypes[i];
Class<?> ac = aClasses.get(i);
allEquals &= c == ac;
if (!allEquals) {
break;
}
}
if (allEquals) {
screened.add(m);
}
}
return screened;
}
}
public static Class<?> toBoxClass(Class<?> c) {
Class<?> bc;
if (c == boolean.class) {
bc = Boolean.class;
} else if (c == char.class) {
bc = Character.class;
} else if (c == byte.class) {
bc = Byte.class;
} else if (c == short.class) {
bc = Short.class;
} else if (c == int.class) {
bc = Integer.class;
} else if (c == long.class) {
bc = Long.class;
} else if (c == float.class) {
bc = Float.class;
} else if (c == double.class) {
bc = Double.class;
} else {
bc = c;
}
return bc;
}
The program below using the above implementation
var hw = "Hello world!"
var h = hw.substring(0, 5)
println(h)
And
Print Hello
with theprintln ()
method call at the end of the program.
Interpreter.java
public static void main(String[] args) throws Exception {
String text = "";
text += "var hw = \"Hello world!\"";
text += "var h = hw.substring(0, 5)";
text += "println(h)";
List<Token> tokens = new Lexer().init(text).tokenize();
List<Token> blk = new Parser().init(tokens).block();
new Interpreter().init(blk).run();
// --> Hello
}
That's all for the implementation. Thank you very much.
The full source is available here.
Calc https://github.com/quwahara/Calc/tree/article-16-method-call/Calc/src/main/java
There is a continuation article.
** Corresponds to the array ** http://qiita.com/quwahara/items/b4f821a797a146c8d873
Recommended Posts