Update Summary: v0.22.0 to v0.25.0
After a long pause, it’s time for an update on Dyvil again! This time, four new releases have been made available with plenty of new features and changes to the language, compiler, library and REPL. These are the most important ones that will be discussed in this post:
- Expression Juxtaposition
- The Exponentiation Operator
**
- Implicit Conversion Methods
- Enhanced Angle Brackets
- Virtual Static Methods
- Enhanced Overload Resolution and Ambiguity Errors
- Overhauled
import
Declarations
Expression Juxtaposition
Dyvil update v0.22.0 introduced a major change to expression parsing. In short, the rules for what is an infix operator have been changed in favor of Apply Calls. The term Juxtaposition means that two expressions can be placed directly next to each other. This will be treated like an Apply Call by the compiler. Consider the following snippet:
let i = 10
println -i
Before this update, it was parsed as a binary operator -
with the operands println
and 1
. Unless a variable with
the name println
of a numeric type was in scope, this used to fail with a compilation error.
The new parser treats this expression like this:
println(-(i))
This correctly passes the resolution phase and prints -10
at runtime.
More generally, a binary operator can now have either of two forms:
expr + expr
// or
expr+expr
What matters is not the amount of whitespace between the tokens, but whether or not there is whitespace or not (whitespace refers to any number of spaces, tabs or comments). Other forms are parsed as prefix or postfix operators, respectively:
expr+ expr // --> (expr+)(expr) --> expr.`+`.apply(expr)
expr +expr // --> expr(+expr) --> expr.apply(+(expr))
The Exponentiation Operator **
Dyvil v0.23.0 added a new operator for all numeric data types (primitives, BigInteger
and BigDecimal
).
The operator is right-associative and takes precedence over multiplicative operators.
2 ** 3 // = 8
3 ** 2 ** 2 // = 3 ** (2 ** 2) = 3 ** 4 = 27
2 * 3 ** 2 // = 2 * (3 ** 2) = 2 * 9 = 18
9 ** 0.5 // = √ 9 = 3
1.5 ** 2 // = 2.25
Implicit Conversion Methods
The same update also introduces a completely new feature: Implicit Conversion Methods. They use the implicit
modifier
and can perform conversions of a value from one type to another without having to be explicitly called. An implicit
method is always static
, can only take one parameter but may have generic type arguments. The scoping rules are the
same as for infix
and extension
methods. The compiler will always try to use upcasts before looking for conversions.
implicit func i2s(int i): String = i.toString // implicit conversion int -> String
String s = 10 // conversion method implicitly called
// translates to:
String s = i2s(10)
Implicit Conversion Methods are especially useful when you want to make a type conform to an interface, but can’t change it’s definition:
interface DebugPrintable
{
func debugString: String
}
implicit func int2debug(int i): DebugPrintable = _.toString
implicit func str2debug(String s): DebugPrintable = s => s
DebugPrintable dp = "abc"
println dp.debugString // prints 'abc'
dp = 20
println dp.debugString // prints '20'
Enhanced Angle Brackets
Dyvil v0.24.0 builds upon the new expression rules by enhancing the syntax for generic method calls and parameterized
types. In addition to that, the this
, super
, type
and class
expressions now allow the use of Angle Brackets.
The following types and expressions are now legal:
type<List<+int>>
type<List<_>>
type<List<@annotated int>>
class<String>
class<int>
foo<-int>
foo<_>
foo<@annotated int>
foo<int> + foo<String>
this<Outer>.bar
super<Parent>.baz
Virtual Static Methods
In the same release, a new method dispatch type has been introduced: Virtual Static Methods. Given a reified type
parameter T
whose upper bound supports some static method foo
, it is possible to call that method using T
. This
can be shown with an example:
interface Foo
{
static String foo() = "A"
}
class Bar implements Foo
{
static String foo() = "B"
}
class Baz implements Foo
{
static String foo() = "C"
}
func printFoo<@Reified T>() = println T.foo
printFoo<Foo> // prints 'A'
printFoo<Bar> // prints 'B'
printFoo<Baz> // prints 'C'
A possible application of this pattern is overridable constructors defined in interfaces:
interface IntConstructible
{
static IntConstructible apply(int value) = null
}
case class Age(int value) implements IntConstructible // case class implicitly has apply(int) method
class Wrapper implements IntConstructible
{
int value
private init(int value) { this.value = value }
public static Wrapper apply(int value) = new Wrapper(value)
}
func create<@Reified T extends IntConstructible>(int fromInt): T = T(value)
Age age = create<Age>(fromInt: 10)
Wrapper wrapper = create<Wrapper>(fromInt: 10)
Enhanced Overload Resolution and Ambiguity Errors
In the updates v0.23.0 and v0.25.0, several changes have been made to the method overload resolution system. Without
going into too much depth, the main changes are overload by type arguments, compiler errors for ambiguous overloads,
the @OverloadPriority
annotation and a way to export these to Java. The following example code covers all changes:
@OverloadPriority
@BytecodeName("f_ints")
func f(List<int> ints): int = ints.reduce(_ + _)
@BytecodeName("f_strings")
func f(List<String> strings): int = strings.reduce((acc, s) => acc + s.length)
These two definitions allow you to overload the method call based on the element type of the List that is passed.
List<int> ints = [ 1, 2, 3 ]
List<String> strings = [ "a", "bc", "def", "ghij" ]
println(f(ints)) // calls the first overload and prints '6'
println(f(strings)) // calls the second overload and prints '10'
Coming from Java, you might notice that it is normally not possible to overload the methods like this, due to erasure. As a quick recap, erasure practically converts this code into the below pseudo-bytecode:
int f(List ints) { ... }
int f(List strings) { ... }
The problem is that both methods have the exact same signature: f(Ldyvil/lang/List;)I
. This is rejected by the JVM,
so we need a workaround. The solution is the @BytecodeName
annotation. It tells the compiler to use the argument as
the method name in the bytecode. In Dyvil code, the method will still be accessible under the name f
. In the bytecode,
and subsequently when used from Java code, the name will be f_ints
or f_strings
respectively.
The @BytecodeName
annotation can be automatically generated by the Dyvil compiler, but it will emit a warning as this
can be inconsistent and may lead to subtle bugs.
You may be wondering that would happen if you passed something that could be either a List<int>
or List<String>
, for
example the null
literal. The compiler would normally emit an error, because the call would be ambiguous. This is
where the @OverloadPriority
annotation shines: It allows you to specify which overload should take precedence in case
of an ambigous call. In this case, the compiler would produce no error and use the f(List<int>)
overload (although the
implementation would cause an error at runtime).
Overhauled import
Declarations
In Dyvil v0.25.0, import
declarations and their cousins using
and include
have been completely overhauled.
They now allow you to specify what kinds of declarations you want to make available in the current scope.
The syntax for this is fairly straight-forward:
import class dyvil.util.Version
import func dyvil.math.MathUtils.min
import class header foo.bar.Baz
import <kind>* <qualifiedName>
Any of the following declaration kinds are legal:
func
var / let / const
class
type
operator
header
package
You may use multiple keywords for different declarations with the same name (e.g. a class
and a header
).
In addition to that, you may also use the following keywords, which can have special effects:
static - alias for 'var func'
implicit - makes an (implicit) method available as such
inline (header) - 'inline's the import declaration of the header
The keywords are not required to be used after the import
keyword. You may also use them within multi-imports:
import dyvil.lang.{ class Null, header Lang }
While include
statements are now deprecated and map to import inline
, using
statements now map to import static
inline header
. This means you can now use using
where include
used to be necessary:
using dyvil.Collections
using dyvil.Strings