Add type aliases for type expressions
This commit is contained in:
parent
7a286f2e06
commit
d82a9bb930
@ -12,6 +12,7 @@
|
|||||||
- `void` is a singleton of class `Void` (syntax sugar for return type).
|
- `void` is a singleton of class `Void` (syntax sugar for return type).
|
||||||
- Object members are always allowed even on unknown types; non-Object members require explicit casts. Remove `inspect` from Object and use `toInspectString()` instead.
|
- Object members are always allowed even on unknown types; non-Object members require explicit casts. Remove `inspect` from Object and use `toInspectString()` instead.
|
||||||
- Type expression checks: `x is T` is value instance check; `T1 is T2` is type-subset; `A in T` means `A` is subset of `T`; `==` is structural type equality.
|
- Type expression checks: `x is T` is value instance check; `T1 is T2` is type-subset; `A in T` means `A` is subset of `T`; `==` is structural type equality.
|
||||||
|
- Type aliases: `type Name = TypeExpr` (generic allowed) expand to their underlying type expressions; no nominal distinctness.
|
||||||
- Do not reintroduce bytecode fallback opcodes (e.g., `GET_NAME`, `EVAL_*`, `CALL_FALLBACK`) or runtime name-resolution fallbacks; all symbol resolution must stay compile-time only.
|
- Do not reintroduce bytecode fallback opcodes (e.g., `GET_NAME`, `EVAL_*`, `CALL_FALLBACK`) or runtime name-resolution fallbacks; all symbol resolution must stay compile-time only.
|
||||||
|
|
||||||
## Bytecode frame-first migration plan
|
## Bytecode frame-first migration plan
|
||||||
|
|||||||
@ -32,6 +32,20 @@ Generic types are invariant by default. You can specify declaration-site varianc
|
|||||||
|
|
||||||
`out` makes the type covariant (produced), `in` makes it contravariant (consumed).
|
`out` makes the type covariant (produced), `in` makes it contravariant (consumed).
|
||||||
|
|
||||||
|
# Type aliases
|
||||||
|
|
||||||
|
Type aliases name type expressions (including unions/intersections):
|
||||||
|
|
||||||
|
type Num = Int | Real
|
||||||
|
type AB = A & B
|
||||||
|
|
||||||
|
Aliases can be generic and can use bounds and defaults:
|
||||||
|
|
||||||
|
type Maybe<T> = T?
|
||||||
|
type IntList<T: Int> = List<T>
|
||||||
|
|
||||||
|
Aliases expand to their underlying type expressions. They can be used anywhere a type expression is expected.
|
||||||
|
|
||||||
# Inference rules
|
# Inference rules
|
||||||
|
|
||||||
- Literals set obvious types (`1` is `Int`, `1.0` is `Real`, etc.).
|
- Literals set obvious types (`1` is `Int`, `1.0` is `Real`, etc.).
|
||||||
|
|||||||
@ -467,6 +467,13 @@ It cannot be combined with an explicit type annotation.
|
|||||||
class A(x?) { ... } // x: Object?
|
class A(x?) { ... } // x: Object?
|
||||||
fun f(x?) { x == null } // x: Object?
|
fun f(x?) { x == null } // x: Object?
|
||||||
|
|
||||||
|
Type aliases name type expressions and can be generic:
|
||||||
|
|
||||||
|
type Num = Int | Real
|
||||||
|
type Maybe<T> = T?
|
||||||
|
|
||||||
|
Aliases expand to their underlying type expressions. See `docs/generics.md` for details.
|
||||||
|
|
||||||
`void` is a singleton value of the class `Void`. `Void` can be used as an explicit return type:
|
`void` is a singleton value of the class `Void`. `Void` can be used as an explicit return type:
|
||||||
|
|
||||||
fun log(msg): Void { println(msg); void }
|
fun log(msg): Void { println(msg); void }
|
||||||
@ -503,6 +510,8 @@ Empty list/map literals default to `List<Object>` and `Map<Object,Object>` until
|
|||||||
val xs = [] // List<Object>
|
val xs = [] // List<Object>
|
||||||
val ys: List<Int> = [] // List<Int>
|
val ys: List<Int> = [] // List<Int>
|
||||||
|
|
||||||
|
Map literals infer key/value types from entries; named keys are `String`. See `docs/generics.md` for details.
|
||||||
|
|
||||||
## Flow analysis
|
## Flow analysis
|
||||||
|
|
||||||
Lyng uses flow analysis to narrow types inside branches:
|
Lyng uses flow analysis to narrow types inside branches:
|
||||||
|
|||||||
@ -129,6 +129,13 @@ class Compiler(
|
|||||||
private val nameObjClass: MutableMap<String, ObjClass> = mutableMapOf()
|
private val nameObjClass: MutableMap<String, ObjClass> = mutableMapOf()
|
||||||
private val slotTypeDeclByScopeId: MutableMap<Int, MutableMap<Int, TypeDecl>> = mutableMapOf()
|
private val slotTypeDeclByScopeId: MutableMap<Int, MutableMap<Int, TypeDecl>> = mutableMapOf()
|
||||||
private val nameTypeDecl: MutableMap<String, TypeDecl> = mutableMapOf()
|
private val nameTypeDecl: MutableMap<String, TypeDecl> = mutableMapOf()
|
||||||
|
private data class TypeAliasDecl(
|
||||||
|
val name: String,
|
||||||
|
val typeParams: List<TypeDecl.TypeParam>,
|
||||||
|
val body: TypeDecl,
|
||||||
|
val pos: Pos
|
||||||
|
)
|
||||||
|
private val typeAliases: MutableMap<String, TypeAliasDecl> = mutableMapOf()
|
||||||
private val methodReturnTypeDeclByRef: MutableMap<ObjRef, TypeDecl> = mutableMapOf()
|
private val methodReturnTypeDeclByRef: MutableMap<ObjRef, TypeDecl> = mutableMapOf()
|
||||||
private val callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
|
private val callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
|
||||||
private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
|
private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
|
||||||
@ -558,6 +565,59 @@ class Compiler(
|
|||||||
return typeParams
|
return typeParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun parseTypeAliasDeclaration(): Statement {
|
||||||
|
if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
|
||||||
|
throw ScriptError(cc.currentPos(), "type alias is not allowed in class body")
|
||||||
|
}
|
||||||
|
val nameToken = cc.requireToken(Token.Type.ID, "type alias name expected")
|
||||||
|
val name = nameToken.value
|
||||||
|
if (typeAliases.containsKey(name)) {
|
||||||
|
throw ScriptError(nameToken.pos, "type alias $name already declared")
|
||||||
|
}
|
||||||
|
if (resolveTypeDeclObjClass(TypeDecl.Simple(name, false)) != null) {
|
||||||
|
throw ScriptError(nameToken.pos, "type alias $name conflicts with existing class")
|
||||||
|
}
|
||||||
|
val typeParams = parseTypeParamList()
|
||||||
|
val uniqueParams = typeParams.map { it.name }.toSet()
|
||||||
|
if (uniqueParams.size != typeParams.size) {
|
||||||
|
throw ScriptError(nameToken.pos, "type alias $name has duplicate type parameters")
|
||||||
|
}
|
||||||
|
val typeParamNames = uniqueParams
|
||||||
|
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.add(typeParamNames)
|
||||||
|
val body = try {
|
||||||
|
cc.skipWsTokens()
|
||||||
|
val eq = cc.nextNonWhitespace()
|
||||||
|
if (eq.type != Token.Type.ASSIGN) {
|
||||||
|
throw ScriptError(eq.pos, "type alias $name expects '='")
|
||||||
|
}
|
||||||
|
parseTypeExpressionWithMini().first
|
||||||
|
} finally {
|
||||||
|
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.removeLast()
|
||||||
|
}
|
||||||
|
val alias = TypeAliasDecl(name, typeParams, body, nameToken.pos)
|
||||||
|
typeAliases[name] = alias
|
||||||
|
declareLocalName(name, isMutable = false)
|
||||||
|
resolutionSink?.declareSymbol(name, SymbolKind.LOCAL, isMutable = false, pos = nameToken.pos)
|
||||||
|
pendingDeclDoc = null
|
||||||
|
|
||||||
|
val aliasExpr = net.sergeych.lyng.obj.TypeDeclRef(body, nameToken.pos)
|
||||||
|
val initStmt = ExpressionStatement(aliasExpr, nameToken.pos)
|
||||||
|
val slotPlan = slotPlanStack.lastOrNull()
|
||||||
|
val slotIndex = slotPlan?.slots?.get(name)?.index
|
||||||
|
val scopeId = slotPlan?.id
|
||||||
|
return VarDeclStatement(
|
||||||
|
name = name,
|
||||||
|
isMutable = false,
|
||||||
|
visibility = Visibility.Public,
|
||||||
|
initializer = initStmt,
|
||||||
|
isTransient = false,
|
||||||
|
slotIndex = slotIndex,
|
||||||
|
scopeId = scopeId,
|
||||||
|
startPos = nameToken.pos,
|
||||||
|
initializerObjClass = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun lookupSlotLocation(name: String, includeModule: Boolean = true): SlotLocation? {
|
private fun lookupSlotLocation(name: String, includeModule: Boolean = true): SlotLocation? {
|
||||||
for (i in slotPlanStack.indices.reversed()) {
|
for (i in slotPlanStack.indices.reversed()) {
|
||||||
if (!includeModule && i == 0) continue
|
if (!includeModule && i == 0) continue
|
||||||
@ -2743,7 +2803,112 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTypeExpressionWithMini(): Pair<TypeDecl, MiniTypeRef> {
|
private fun parseTypeExpressionWithMini(): Pair<TypeDecl, MiniTypeRef> {
|
||||||
return parseTypeUnionWithMini()
|
val start = cc.currentPos()
|
||||||
|
val (decl, mini) = parseTypeUnionWithMini()
|
||||||
|
return expandTypeAliases(decl, start) to mini
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expandTypeAliases(type: TypeDecl, pos: Pos, seen: MutableSet<String> = mutableSetOf()): TypeDecl {
|
||||||
|
return when (type) {
|
||||||
|
is TypeDecl.Simple -> expandTypeAliasReference(type.name, emptyList(), type.isNullable, pos, seen) ?: type
|
||||||
|
is TypeDecl.Generic -> {
|
||||||
|
val expandedArgs = type.args.map { expandTypeAliases(it, pos, seen) }
|
||||||
|
expandTypeAliasReference(type.name, expandedArgs, type.isNullable, pos, seen)
|
||||||
|
?: TypeDecl.Generic(type.name, expandedArgs, type.isNullable)
|
||||||
|
}
|
||||||
|
is TypeDecl.Function -> {
|
||||||
|
val receiver = type.receiver?.let { expandTypeAliases(it, pos, seen) }
|
||||||
|
val params = type.params.map { expandTypeAliases(it, pos, seen) }
|
||||||
|
val ret = expandTypeAliases(type.returnType, pos, seen)
|
||||||
|
TypeDecl.Function(receiver, params, ret, type.nullable)
|
||||||
|
}
|
||||||
|
is TypeDecl.Union -> {
|
||||||
|
val options = type.options.map { expandTypeAliases(it, pos, seen) }
|
||||||
|
TypeDecl.Union(options, type.isNullable)
|
||||||
|
}
|
||||||
|
is TypeDecl.Intersection -> {
|
||||||
|
val options = type.options.map { expandTypeAliases(it, pos, seen) }
|
||||||
|
TypeDecl.Intersection(options, type.isNullable)
|
||||||
|
}
|
||||||
|
else -> type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expandTypeAliasReference(
|
||||||
|
name: String,
|
||||||
|
args: List<TypeDecl>,
|
||||||
|
isNullable: Boolean,
|
||||||
|
pos: Pos,
|
||||||
|
seen: MutableSet<String>
|
||||||
|
): TypeDecl? {
|
||||||
|
val alias = typeAliases[name] ?: typeAliases[name.substringAfterLast('.')] ?: return null
|
||||||
|
if (!seen.add(alias.name)) throw ScriptError(pos, "circular type alias: ${alias.name}")
|
||||||
|
val bindings = buildTypeAliasBindings(alias, args, pos)
|
||||||
|
val substituted = substituteTypeAliasTypeVars(alias.body, bindings)
|
||||||
|
val expanded = expandTypeAliases(substituted, pos, seen)
|
||||||
|
seen.remove(alias.name)
|
||||||
|
return if (isNullable) makeTypeDeclNullable(expanded) else expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildTypeAliasBindings(
|
||||||
|
alias: TypeAliasDecl,
|
||||||
|
args: List<TypeDecl>,
|
||||||
|
pos: Pos
|
||||||
|
): Map<String, TypeDecl> {
|
||||||
|
val params = alias.typeParams
|
||||||
|
val resolvedArgs = if (args.size >= params.size) {
|
||||||
|
if (args.size > params.size) {
|
||||||
|
throw ScriptError(pos, "type alias ${alias.name} expects ${params.size} type arguments")
|
||||||
|
}
|
||||||
|
args.toMutableList()
|
||||||
|
} else {
|
||||||
|
val filled = args.toMutableList()
|
||||||
|
for (param in params.drop(args.size)) {
|
||||||
|
val fallback = param.defaultType
|
||||||
|
?: throw ScriptError(pos, "type alias ${alias.name} expects ${params.size} type arguments")
|
||||||
|
filled.add(fallback)
|
||||||
|
}
|
||||||
|
filled
|
||||||
|
}
|
||||||
|
val bindings = mutableMapOf<String, TypeDecl>()
|
||||||
|
for ((index, param) in params.withIndex()) {
|
||||||
|
val arg = resolvedArgs[index]
|
||||||
|
val bound = param.bound
|
||||||
|
if (bound != null && arg !is TypeDecl.TypeVar && !typeDeclSatisfiesBound(arg, bound)) {
|
||||||
|
throw ScriptError(pos, "type alias ${alias.name} type argument ${param.name} does not satisfy bound")
|
||||||
|
}
|
||||||
|
bindings[param.name] = arg
|
||||||
|
}
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun substituteTypeAliasTypeVars(type: TypeDecl, bindings: Map<String, TypeDecl>): TypeDecl {
|
||||||
|
return when (type) {
|
||||||
|
is TypeDecl.TypeVar -> {
|
||||||
|
val bound = bindings[type.name] ?: return type
|
||||||
|
if (type.isNullable) makeTypeDeclNullable(bound) else bound
|
||||||
|
}
|
||||||
|
is TypeDecl.Simple -> type
|
||||||
|
is TypeDecl.Generic -> {
|
||||||
|
val args = type.args.map { substituteTypeAliasTypeVars(it, bindings) }
|
||||||
|
TypeDecl.Generic(type.name, args, type.isNullable)
|
||||||
|
}
|
||||||
|
is TypeDecl.Function -> {
|
||||||
|
val receiver = type.receiver?.let { substituteTypeAliasTypeVars(it, bindings) }
|
||||||
|
val params = type.params.map { substituteTypeAliasTypeVars(it, bindings) }
|
||||||
|
val ret = substituteTypeAliasTypeVars(type.returnType, bindings)
|
||||||
|
TypeDecl.Function(receiver, params, ret, type.nullable)
|
||||||
|
}
|
||||||
|
is TypeDecl.Union -> {
|
||||||
|
val options = type.options.map { substituteTypeAliasTypeVars(it, bindings) }
|
||||||
|
TypeDecl.Union(options, type.isNullable)
|
||||||
|
}
|
||||||
|
is TypeDecl.Intersection -> {
|
||||||
|
val options = type.options.map { substituteTypeAliasTypeVars(it, bindings) }
|
||||||
|
TypeDecl.Intersection(options, type.isNullable)
|
||||||
|
}
|
||||||
|
else -> type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeTypeDeclNullable(type: TypeDecl): TypeDecl {
|
private fun makeTypeDeclNullable(type: TypeDecl): TypeDecl {
|
||||||
@ -4499,6 +4664,12 @@ class Compiler(
|
|||||||
parseEnumDeclaration()
|
parseEnumDeclaration()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"type" -> {
|
||||||
|
pendingDeclStart = id.pos
|
||||||
|
pendingDeclDoc = consumePendingDoc()
|
||||||
|
parseTypeAliasDeclaration()
|
||||||
|
}
|
||||||
|
|
||||||
"try" -> parseTryStatement()
|
"try" -> parseTryStatement()
|
||||||
"throw" -> parseThrowStatement(id.pos)
|
"throw" -> parseThrowStatement(id.pos)
|
||||||
"when" -> parseWhenStatement()
|
"when" -> parseWhenStatement()
|
||||||
|
|||||||
@ -239,6 +239,32 @@ class TypesTest {
|
|||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTypeAliases() = runTest {
|
||||||
|
eval("""
|
||||||
|
type Num = Int | Real
|
||||||
|
type AB = A & B
|
||||||
|
class A
|
||||||
|
class B
|
||||||
|
class C: A, B
|
||||||
|
val c = C()
|
||||||
|
assert( c is AB )
|
||||||
|
assert( 1 is Num )
|
||||||
|
assert( !(true is Num) )
|
||||||
|
val v: Num = 1.5
|
||||||
|
assert( v is Num )
|
||||||
|
|
||||||
|
type Maybe<T> = T?
|
||||||
|
fun f<T>(x: Maybe<T>) = x
|
||||||
|
assertEquals(null, f(null))
|
||||||
|
assertEquals(1, f(1))
|
||||||
|
|
||||||
|
type IntList<T: Int> = List<T>
|
||||||
|
fun accept<T: Int>(xs: IntList<T>) { }
|
||||||
|
accept([1,2,3])
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun multipleReceivers() = runTest {
|
fun multipleReceivers() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
|
|||||||
@ -106,6 +106,11 @@ fun x10(x: Int & String) {
|
|||||||
```
|
```
|
||||||
but in fact some strange programmer can create `class T: String, Int` so we won't check it for sanity, except that we certainly disallow <T: Void &...>
|
but in fact some strange programmer can create `class T: String, Int` so we won't check it for sanity, except that we certainly disallow <T: Void &...>
|
||||||
|
|
||||||
|
Type aliases:
|
||||||
|
- `type Name = TypeExpr`
|
||||||
|
- can be generic: `type Maybe<T> = T?`
|
||||||
|
- aliases expand to their underlying type expressions (no nominal distinctness).
|
||||||
|
|
||||||
Instead of template expansions, we might provide explicit `inline` later.
|
Instead of template expansions, we might provide explicit `inline` later.
|
||||||
|
|
||||||
Notes and open questions to answer in this spec:
|
Notes and open questions to answer in this spec:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user