Sources
The project can be cloned from github repository.
The revision described in this post is 62a99fe34f540f5cae7a48386b66d23e4b879046.
Why do I need named arguments?
In java (like in most languages) the method call arguments are identified by indexes. This seems reasonable for a methods with small amount of parameters and preferably different types. Unfortunately there are many methods that neither have small amount of parameters nor different types.
If you’ve ever done some game programing you proably came across functions like this:
Rect createRectangle(int x1,int y1,int x2, int y2) //createRectangle signature
I am more than sure you called it with wrong arguments order at least once.
Do you see the problem? The function has plenty parameters each the same type. It is very easy to forget what is the order - the compiler doesn’t care as long as types match.
Wouldn’t it be awesome if you could explicitly specify a parameter without relying on the indexes? That’s where named arguments come in:
createRectangle(25,50,-25,-50) //method invokation without named parameters :(
createRectangle(x1->25,x2->-25,y1->50,y2->-50) //method invokation with named parameters :)
The benefits from using named arguments are:
- The order of arguments is unrestricted
- The code is more readable
- No need to jump between files to compare call with signature
Grammar changes
functionCall : functionName '('argument? (',' argument)* ')';
argument : expression //unnamed argument
| name '->' expression ; //named argument
The function call can have one, or more (splitted by ‘,’ character) arguments.
The rule argument
comes in two flavours (unnamed and named).
Mixing named and unnamed arguments is not allowed.
Reordering arguments
As described in Creating JVM language [PART 7] - Methods , method parsing process is divided into two steps. First it finds all the signatures (declarations), and once it’s done it starts parsing the bodies. It is guaranteed that during parsing method bodies all the signatures are already available.
Using that characteristics the idea is to “transform” named call to unnamed call by getting parameters indexes from signature:
- Look for a parameter name in the signature that matches the argument name
- Get parameter index
- If the argument is at different index than a parameter reorder it.
In the example above the x2 would be swapped with y1.
public class ExpressionVisitor extends EnkelBaseVisitor<Expression> {
//other stuff
@Override
public Expression visitFunctionCall(@NotNull EnkelParser.FunctionCallContext ctx) {
String funName = ctx.functionName().getText();
FunctionSignature signature = scope.getSignature(funName);
List<EnkelParser.ArgumentContext> argumentsCtx = ctx.argument();
//Create comparator that compares arguments based on their index in signature
Comparator<EnkelParser.ArgumentContext> argumentComparator = (arg1, arg2) -> {
if(arg1.name() == null) return 0; //If the argument is not named skip
String arg1Name = arg1.name().getText();
String arg2Name = arg2.name().getText();
return signature.getIndexOfParameter(arg1Name) - signature.getIndexOfParameter(arg2Name);
};
List<Expression> arguments = argumentsCtx.stream() //parsed arguments (wrong order)
.sorted(argumentComparator) //Order using created comparator
.map(argument -> argument.expression().accept(this)) //Map parsed arguments into expressions
.collect(toList());
return new FunctionCall(signature, arguments);
}
}
That way the component responsible for generting bytecode does not distinct named and unnamed arguments. It only sees FunctionCall as a collection of arguments (properly ordered) and a signature. No modifications to bytecode generation are therefore needed.
Example
The following Enkel class:
NamedParamsTest {
main(string[] args) {
createRect(x1->25,x2->-25,y1->50,y2->-50)
}
createRect (int x1,int y1,int x2, int y2) {
print "Created rect with x1=" + x1 + " y1=" + y1 + " x2=" + x2 + " y2=" + y2
}
}
gets compiled into following bytecode:
kuba@kuba-laptop:~/repos/Enkel-JVM-language$ javap -c NamedParamsTest.class
public class NamedParamsTest {
public static void main(java.lang.String[]);
Code:
0: bipush 25 //x1 (1 index in call)
2: bipush 50 //y1 (3 index in call)
4: bipush -25 //x2 (2 index in call)
6: bipush -50 //y2 (4 index in call)
8: invokestatic #10 // Method createRect:(IIII)V
11: return
public static void createRect(int, int, int, int);
Code:
//normal printing code
}
As you can see the y1 and x2 arguments were swapped as expected.
The output is:
Created rect with x1=25 y1=50 x2=-25 y2=-50
- enkel (21) ,
- jvm (22) ,
- asm (17) ,
- antlr (36) ,
- antlr4 (18) ,
- antlr (36) ,
- java (24) ,
- language (19)