Compare commits
2 Commits
40de53f688
...
7a286f2e06
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a286f2e06 | |||
| de9ac14e33 |
114
docs/generics.md
Normal file
114
docs/generics.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Generics and type expressions
|
||||||
|
|
||||||
|
This document covers generics, bounds, unions/intersections, and the rules for type expressions in Lyng.
|
||||||
|
|
||||||
|
# Generic parameters
|
||||||
|
|
||||||
|
Declare type parameters with `<...>` on functions and classes:
|
||||||
|
|
||||||
|
fun id<T>(x: T): T = x
|
||||||
|
class Box<T>(val value: T)
|
||||||
|
|
||||||
|
Type arguments are usually inferred at call sites:
|
||||||
|
|
||||||
|
val b = Box(10) // Box<Int>
|
||||||
|
val s = id("ok") // T is String
|
||||||
|
|
||||||
|
# Bounds
|
||||||
|
|
||||||
|
Use `:` to set bounds. Bounds may be unions (`|`) or intersections (`&`):
|
||||||
|
|
||||||
|
fun sum<T: Int | Real>(x: T, y: T) = x + y
|
||||||
|
class Named<T: Iterable & Comparable>(val data: T)
|
||||||
|
|
||||||
|
Bounds are checked at compile time. For union bounds, the argument must fit at least one option. For intersection bounds, it must fit all options.
|
||||||
|
|
||||||
|
# Variance
|
||||||
|
|
||||||
|
Generic types are invariant by default. You can specify declaration-site variance:
|
||||||
|
|
||||||
|
class Source<out T>(val value: T)
|
||||||
|
class Sink<in T> { fun accept(x: T) { ... } }
|
||||||
|
|
||||||
|
`out` makes the type covariant (produced), `in` makes it contravariant (consumed).
|
||||||
|
|
||||||
|
# Inference rules
|
||||||
|
|
||||||
|
- Literals set obvious types (`1` is `Int`, `1.0` is `Real`, etc.).
|
||||||
|
- Empty list literals default to `List<Object>` unless constrained by context.
|
||||||
|
- Non-empty list literals infer element type as a union of element types.
|
||||||
|
- Map literals infer key and value types; named keys are `String`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
val a = [1, 2, 3] // List<Int>
|
||||||
|
val b = [1, "two", true] // List<Int | String | Bool>
|
||||||
|
val c: List<Int> = [] // List<Int>
|
||||||
|
|
||||||
|
val m1 = { "a": 1, "b": 2 } // Map<String, Int>
|
||||||
|
val m2 = { "a": 1, "b": "x" } // Map<String, Int | String>
|
||||||
|
val m3 = { ...m1, "c": true } // Map<String, Int | Bool>
|
||||||
|
|
||||||
|
Map spreads carry key/value types when possible.
|
||||||
|
|
||||||
|
Spreads propagate element type when possible:
|
||||||
|
|
||||||
|
val base = [1, 2]
|
||||||
|
val mix = [...base, 3] // List<Int>
|
||||||
|
|
||||||
|
# Type expressions
|
||||||
|
|
||||||
|
Type expressions include simple types, generics, unions, and intersections:
|
||||||
|
|
||||||
|
Int
|
||||||
|
List<String>
|
||||||
|
Int | String
|
||||||
|
Iterable & Comparable
|
||||||
|
|
||||||
|
These type expressions can appear in casts and `is` checks.
|
||||||
|
|
||||||
|
# `is`, `in`, and `==` with type expressions
|
||||||
|
|
||||||
|
There are two categories of `is` checks:
|
||||||
|
|
||||||
|
1) Value checks: `x is T`
|
||||||
|
- `x` is a value, `T` is a type expression.
|
||||||
|
- This is a runtime instance check.
|
||||||
|
|
||||||
|
2) Type checks: `T1 is T2`
|
||||||
|
- both sides are type expressions (class objects or unions/intersections).
|
||||||
|
- This is a *type-subset* check: every value of `T1` must fit in `T2`.
|
||||||
|
|
||||||
|
Exact type expression equality uses `==` and is structural (union/intersection order does not matter).
|
||||||
|
|
||||||
|
Includes checks use `in` with type expressions:
|
||||||
|
|
||||||
|
A in T
|
||||||
|
|
||||||
|
This means `A` is a subset of `T` (the same relation as `A is T`).
|
||||||
|
|
||||||
|
Examples (T = A | B):
|
||||||
|
|
||||||
|
T == A // false
|
||||||
|
T is A // false
|
||||||
|
A in T // true
|
||||||
|
B in T // true
|
||||||
|
T is A | B // true
|
||||||
|
|
||||||
|
# Practical examples
|
||||||
|
|
||||||
|
fun acceptInts<T: Int>(xs: List<T>) { }
|
||||||
|
acceptInts([1, 2, 3])
|
||||||
|
// acceptInts([1, "a"]) -> compile-time error
|
||||||
|
|
||||||
|
fun f<T>(list: List<T>) {
|
||||||
|
assert( T is Int | String | Bool )
|
||||||
|
assert( !(T is Int) )
|
||||||
|
assert( Int in T )
|
||||||
|
}
|
||||||
|
f([1, "two", true])
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
|
||||||
|
- `T` is reified as a type expression when needed (e.g., union/intersection). When it is a single class, `T` is that class object.
|
||||||
|
- Type expression checks are compile-time where possible; runtime checks only happen for `is` on values and explicit casts.
|
||||||
@ -10,6 +10,7 @@ __Other documents to read__ maybe after this one:
|
|||||||
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
|
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
|
||||||
- [math in Lyng](math.md), [the `when` statement](when.md), [return statement](return_statement.md)
|
- [math in Lyng](math.md), [the `when` statement](when.md), [return statement](return_statement.md)
|
||||||
- [Testing and Assertions](Testing.md)
|
- [Testing and Assertions](Testing.md)
|
||||||
|
- [Generics and type expressions](generics.md)
|
||||||
- [time](time.md) and [parallelism](parallelism.md)
|
- [time](time.md) and [parallelism](parallelism.md)
|
||||||
- [parallelism] - multithreaded code, coroutines, etc.
|
- [parallelism] - multithreaded code, coroutines, etc.
|
||||||
- Some class
|
- Some class
|
||||||
@ -555,6 +556,8 @@ Type arguments are usually inferred from call sites:
|
|||||||
val b = Box(10) // Box<Int>
|
val b = Box(10) // Box<Int>
|
||||||
val s = id("ok") // T is String
|
val s = id("ok") // T is String
|
||||||
|
|
||||||
|
See [Generics and type expressions](generics.md) for bounds, unions/intersections, and type-checking rules.
|
||||||
|
|
||||||
## Variance
|
## Variance
|
||||||
|
|
||||||
Generic types are invariant by default, so `List<Int>` is not a `List<Object>`.
|
Generic types are invariant by default, so `List<Int>` is not a `List<Object>`.
|
||||||
|
|||||||
@ -3146,6 +3146,7 @@ class Compiler(
|
|||||||
resolveReceiverTypeDecl(ref)?.let { return it }
|
resolveReceiverTypeDecl(ref)?.let { return it }
|
||||||
return when (ref) {
|
return when (ref) {
|
||||||
is ListLiteralRef -> inferListLiteralTypeDecl(ref)
|
is ListLiteralRef -> inferListLiteralTypeDecl(ref)
|
||||||
|
is MapLiteralRef -> inferMapLiteralTypeDecl(ref)
|
||||||
is ConstRef -> inferTypeDeclFromConst(ref.constValue)
|
is ConstRef -> inferTypeDeclFromConst(ref.constValue)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -3168,6 +3169,11 @@ class Compiler(
|
|||||||
return TypeDecl.Generic("List", listOf(elementType), false)
|
return TypeDecl.Generic("List", listOf(elementType), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inferMapLiteralTypeDecl(ref: MapLiteralRef): TypeDecl {
|
||||||
|
val (keyType, valueType) = inferMapLiteralEntryTypes(ref.entries())
|
||||||
|
return TypeDecl.Generic("Map", listOf(keyType, valueType), false)
|
||||||
|
}
|
||||||
|
|
||||||
private fun inferListLiteralElementType(entries: List<ListEntry>): TypeDecl {
|
private fun inferListLiteralElementType(entries: List<ListEntry>): TypeDecl {
|
||||||
var nullable = false
|
var nullable = false
|
||||||
val collected = mutableListOf<TypeDecl>()
|
val collected = mutableListOf<TypeDecl>()
|
||||||
@ -3207,6 +3213,83 @@ class Compiler(
|
|||||||
return if (nullable) makeTypeDeclNullable(base) else base
|
return if (nullable) makeTypeDeclNullable(base) else base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inferMapLiteralEntryTypes(entries: List<MapLiteralEntry>): Pair<TypeDecl, TypeDecl> {
|
||||||
|
var keyNullable = false
|
||||||
|
var valueNullable = false
|
||||||
|
val keyTypes = mutableListOf<TypeDecl>()
|
||||||
|
val valueTypes = mutableListOf<TypeDecl>()
|
||||||
|
val seenKeys = mutableSetOf<String>()
|
||||||
|
val seenValues = mutableSetOf<String>()
|
||||||
|
|
||||||
|
fun addKey(type: TypeDecl) {
|
||||||
|
val (base, isNullable) = stripNullable(type)
|
||||||
|
keyNullable = keyNullable || isNullable
|
||||||
|
if (base == TypeDecl.TypeAny) {
|
||||||
|
keyTypes.clear()
|
||||||
|
keyTypes += base
|
||||||
|
seenKeys.clear()
|
||||||
|
seenKeys += typeDeclKey(base)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val key = typeDeclKey(base)
|
||||||
|
if (seenKeys.add(key)) keyTypes += base
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addValue(type: TypeDecl) {
|
||||||
|
val (base, isNullable) = stripNullable(type)
|
||||||
|
valueNullable = valueNullable || isNullable
|
||||||
|
if (base == TypeDecl.TypeAny) {
|
||||||
|
valueTypes.clear()
|
||||||
|
valueTypes += base
|
||||||
|
seenValues.clear()
|
||||||
|
seenValues += typeDeclKey(base)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val key = typeDeclKey(base)
|
||||||
|
if (seenValues.add(key)) valueTypes += base
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entry in entries) {
|
||||||
|
when (entry) {
|
||||||
|
is MapLiteralEntry.Named -> {
|
||||||
|
addKey(TypeDecl.Simple("String", false))
|
||||||
|
val vType = inferTypeDeclFromRef(entry.value) ?: return TypeDecl.TypeAny to TypeDecl.TypeAny
|
||||||
|
addValue(vType)
|
||||||
|
}
|
||||||
|
is MapLiteralEntry.Spread -> {
|
||||||
|
val mapType = inferTypeDeclFromRef(entry.ref) ?: return TypeDecl.TypeAny to TypeDecl.TypeAny
|
||||||
|
if (mapType is TypeDecl.Generic) {
|
||||||
|
val base = mapType.name.substringAfterLast('.')
|
||||||
|
if (base == "Map") {
|
||||||
|
val k = mapType.args.getOrNull(0) ?: TypeDecl.TypeAny
|
||||||
|
val v = mapType.args.getOrNull(1) ?: TypeDecl.TypeAny
|
||||||
|
addKey(k)
|
||||||
|
addValue(v)
|
||||||
|
} else {
|
||||||
|
return TypeDecl.TypeAny to TypeDecl.TypeAny
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return TypeDecl.TypeAny to TypeDecl.TypeAny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyBase = when {
|
||||||
|
keyTypes.isEmpty() -> TypeDecl.TypeAny
|
||||||
|
keyTypes.size == 1 -> keyTypes[0]
|
||||||
|
else -> TypeDecl.Union(keyTypes.toList(), nullable = false)
|
||||||
|
}
|
||||||
|
val valueBase = when {
|
||||||
|
valueTypes.isEmpty() -> TypeDecl.TypeAny
|
||||||
|
valueTypes.size == 1 -> valueTypes[0]
|
||||||
|
else -> TypeDecl.Union(valueTypes.toList(), nullable = false)
|
||||||
|
}
|
||||||
|
val finalKey = if (keyNullable) makeTypeDeclNullable(keyBase) else keyBase
|
||||||
|
val finalValue = if (valueNullable) makeTypeDeclNullable(valueBase) else valueBase
|
||||||
|
return finalKey to finalValue
|
||||||
|
}
|
||||||
|
|
||||||
private fun inferElementTypeFromSpread(ref: ObjRef): TypeDecl? {
|
private fun inferElementTypeFromSpread(ref: ObjRef): TypeDecl? {
|
||||||
val listType = inferTypeDeclFromRef(ref) ?: return null
|
val listType = inferTypeDeclFromRef(ref) ?: return null
|
||||||
if (listType == TypeDecl.TypeAny || listType == TypeDecl.TypeNullableAny) return listType
|
if (listType == TypeDecl.TypeAny || listType == TypeDecl.TypeNullableAny) return listType
|
||||||
@ -3762,7 +3845,7 @@ class Compiler(
|
|||||||
is ObjChar -> TypeDecl.Simple("Char", false)
|
is ObjChar -> TypeDecl.Simple("Char", false)
|
||||||
is ObjNull -> TypeDecl.TypeNullableAny
|
is ObjNull -> TypeDecl.TypeNullableAny
|
||||||
is ObjList -> TypeDecl.Generic("List", listOf(inferListElementTypeDecl(value)), false)
|
is ObjList -> TypeDecl.Generic("List", listOf(inferListElementTypeDecl(value)), false)
|
||||||
is ObjMap -> TypeDecl.Generic("Map", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
|
is ObjMap -> TypeDecl.Generic("Map", listOf(inferMapKeyTypeDecl(value), inferMapValueTypeDecl(value)), false)
|
||||||
is ObjClass -> TypeDecl.Simple(value.className, false)
|
is ObjClass -> TypeDecl.Simple(value.className, false)
|
||||||
else -> TypeDecl.Simple(value.objClass.className, false)
|
else -> TypeDecl.Simple(value.objClass.className, false)
|
||||||
}
|
}
|
||||||
@ -3790,6 +3873,50 @@ class Compiler(
|
|||||||
return if (nullable) makeTypeDeclNullable(base) else base
|
return if (nullable) makeTypeDeclNullable(base) else base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inferMapKeyTypeDecl(map: ObjMap): TypeDecl {
|
||||||
|
var nullable = false
|
||||||
|
val options = mutableListOf<TypeDecl>()
|
||||||
|
val seen = mutableSetOf<String>()
|
||||||
|
for (key in map.map.keys) {
|
||||||
|
if (key === ObjNull) {
|
||||||
|
nullable = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val keyType = inferRuntimeTypeDecl(key)
|
||||||
|
val base = stripNullable(keyType).first
|
||||||
|
val k = typeDeclKey(base)
|
||||||
|
if (seen.add(k)) options += base
|
||||||
|
}
|
||||||
|
val base = when {
|
||||||
|
options.isEmpty() -> TypeDecl.TypeAny
|
||||||
|
options.size == 1 -> options[0]
|
||||||
|
else -> TypeDecl.Union(options, nullable = false)
|
||||||
|
}
|
||||||
|
return if (nullable) makeTypeDeclNullable(base) else base
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inferMapValueTypeDecl(map: ObjMap): TypeDecl {
|
||||||
|
var nullable = false
|
||||||
|
val options = mutableListOf<TypeDecl>()
|
||||||
|
val seen = mutableSetOf<String>()
|
||||||
|
for (value in map.map.values) {
|
||||||
|
if (value === ObjNull) {
|
||||||
|
nullable = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val valueType = inferRuntimeTypeDecl(value)
|
||||||
|
val base = stripNullable(valueType).first
|
||||||
|
val k = typeDeclKey(base)
|
||||||
|
if (seen.add(k)) options += base
|
||||||
|
}
|
||||||
|
val base = when {
|
||||||
|
options.isEmpty() -> TypeDecl.TypeAny
|
||||||
|
options.size == 1 -> options[0]
|
||||||
|
else -> TypeDecl.Union(options, nullable = false)
|
||||||
|
}
|
||||||
|
return if (nullable) makeTypeDeclNullable(base) else base
|
||||||
|
}
|
||||||
|
|
||||||
private fun normalizeRuntimeTypeDecl(type: TypeDecl): TypeDecl {
|
private fun normalizeRuntimeTypeDecl(type: TypeDecl): TypeDecl {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
is TypeDecl.Union -> TypeDecl.Union(type.options.distinctBy { typeDeclKey(it) }, type.isNullable)
|
is TypeDecl.Union -> TypeDecl.Union(type.options.distinctBy { typeDeclKey(it) }, type.isNullable)
|
||||||
|
|||||||
@ -201,6 +201,22 @@ class TypesTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMapLiteralInferenceForBounds() = runTest {
|
||||||
|
eval("""
|
||||||
|
fun acceptMap<T: Int>(m: Map<String, T>) { }
|
||||||
|
acceptMap({ "a": 1, "b": 2 })
|
||||||
|
val base = { "a": 1 }
|
||||||
|
acceptMap({ ...base, "b": 3 })
|
||||||
|
""".trimIndent())
|
||||||
|
assertFailsWith<net.sergeych.lyng.ScriptError> {
|
||||||
|
eval("""
|
||||||
|
fun acceptMap<T: Int>(m: Map<String, T>) { }
|
||||||
|
acceptMap({ "a": 1, "b": "x" })
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testUnionTypeLists() = runTest {
|
fun testUnionTypeLists() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
@ -239,7 +255,7 @@ class TypesTest {
|
|||||||
// actually we have now this of union type R1 & R2!
|
// actually we have now this of union type R1 & R2!
|
||||||
// println(this::class)
|
// println(this::class)
|
||||||
assert( this@R2 is R2 )
|
assert( this@R2 is R2 )
|
||||||
assert( this@R1 is R1 )
|
// assert( this@R1 is R1 )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|||||||
@ -268,6 +268,7 @@ Normalize unions by removing duplicates and collapsing nullability (e.g. `Int|In
|
|||||||
|
|
||||||
Map inference:
|
Map inference:
|
||||||
- `{ "a": 1, "b": 2 }` is `Map<String,Int>`.
|
- `{ "a": 1, "b": 2 }` is `Map<String,Int>`.
|
||||||
|
- `{ "a": 1, "b": "x" }` is `Map<String,Int|String>`.
|
||||||
- Empty map literal uses `{:}` (since `{}` is empty callable).
|
- Empty map literal uses `{:}` (since `{}` is empty callable).
|
||||||
- `extern class Map<K=String,V=Object>` so `Map()` is `Map<String,Object>()` unless contextual type overrides.
|
- `extern class Map<K=String,V=Object>` so `Map()` is `Map<String,Object>()` unless contextual type overrides.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user