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