4.1 KiB
What's New in Lyng 1.3 (vs 1.2.* / master)
This is a programmer-focused summary of what changed since the 1.2.* line on master. It highlights new language and type-system features, runtime/IDE improvements, and how to migrate code safely.
Highlights
- Generics are now a first-class part of the type system, with bounds, variance, unions, and intersections.
- Type aliases and type-expression checks (
T1 is T2,A in T) enable richer static modeling. - Nested declarations inside classes, plus lifted enum entries via
enum E*. - Stepped ranges (
step) including iterable open-ended and real ranges. - Runtime and compiler speedups: more bytecode coverage, direct slot access, call-site caching.
Language and type system
Generics, bounds, and variance
You can declare generic functions/classes with <...>, restrict them with bounds, and control variance.
fun id<T>(x: T): T = x
class Box<out T>(val value: T)
fun sum<T: Int | Real>(x: T, y: T) = x + y
class Named<T: Iterable & Comparable>(val data: T)
Type aliases and type expressions
Type aliases can name any type expression, including unions and intersections.
type Num = Int | Real
type Maybe<T> = T?
Type expressions can be checked directly:
fun f<T>(xs: List<T>) {
assert( T is Int | String | Bool ) // type-subset check
assert( Int in T ) // same relation as `Int is T`
}
Value checks remain x is T. Type expression equality uses == and is structural.
Nullable shorthand for parameters
Untyped parameters and constructor args can use x? as shorthand for x: Object?:
class A(x?) { ... }
fun f(x?) { x == null }
List/map literal inference
The compiler now infers element and key/value types from literals and spreads. Mixed element types produce unions.
val a = [1, 2, 3] // List<Int>
val b = [1, "two", true] // List<Int | String | Bool>
val m = { "a": 1, "b": "x" } // Map<String, Int | String>
Compile-time member access only
Member access is resolved at compile time. On unknown types, only Object members are visible; other members require an explicit cast.
fun f(x) { // x: Object
x.toString() // ok
x.size() // compile-time error
(x as List).size()
}
This removes runtime name-resolution fallbacks and makes errors deterministic.
Nested declarations and lifted enums
Classes, objects, enums, and type aliases can be declared inside another class and accessed by qualifier. Enums can lift entries into the outer namespace with *.
class A {
class B(x?)
object Inner { val foo = "bar" }
type Alias = B
enum E* { One, Two }
}
val b = A.B()
assertEquals(A.One, A.E.One)
Stepped ranges
Ranges now support step, and open-ended/real ranges are iterable only with an explicit step.
(1..5 step 2).toList() // [1,3,5]
(0.0..1.0 step 0.25).toList() // [0,0.25,0.5,0.75,1.0]
(0.. step 1).take(3).toList() // [0,1,2]
Tooling and performance
- Bytecode compiler/VM coverage expanded (loops, expressions, calls), improving execution speed and consistency.
- Direct frame-slot access and scoped slot addressing reduce lookup overhead, including in closures.
- Call-site caching and numeric fast paths reduce hot-loop overhead.
- IDE tooling updated for the new type system and nested declarations; MiniAst-based completion work continues.
Migration guide (from 1.2.*)
- Replace dynamic member access on unknown types
- If you relied on runtime name resolution, add explicit casts or annotate types so the compiler can resolve members.
- Adopt new type-system constructs where helpful
- Consider
typealiases for complex unions/intersections. - Prefer generic signatures over
Objectwhen the API is parametric.
- Update range iteration where needed
- Use
stepfor open-ended or real ranges you want to iterate.
- Nullable shorthand is optional
- If you used untyped nullable params, you can keep
x(Object) or switch tox?(Object?) for clarity.
References
docs/generics.mddocs/Range.mddocs/OOP.mddocs/BytecodeSpec.md