Add type aliases for type expressions

This commit is contained in:
Sergey Chernov 2026-02-05 21:08:04 +03:00
parent 7a286f2e06
commit d82a9bb930
6 changed files with 227 additions and 1 deletions

View File

@ -12,6 +12,7 @@
- `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.
- 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.
## Bytecode frame-first migration plan

View File

@ -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).
# 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
- Literals set obvious types (`1` is `Int`, `1.0` is `Real`, etc.).

View File

@ -467,6 +467,13 @@ It cannot be combined with an explicit type annotation.
class A(x?) { ... } // 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:
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 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
Lyng uses flow analysis to narrow types inside branches:

View File

@ -129,6 +129,13 @@ class Compiler(
private val nameObjClass: MutableMap<String, ObjClass> = mutableMapOf()
private val slotTypeDeclByScopeId: MutableMap<Int, MutableMap<Int, 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 callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
@ -558,6 +565,59 @@ class Compiler(
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? {
for (i in slotPlanStack.indices.reversed()) {
if (!includeModule && i == 0) continue
@ -2743,7 +2803,112 @@ class Compiler(
}
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 {
@ -4499,6 +4664,12 @@ class Compiler(
parseEnumDeclaration()
}
"type" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseTypeAliasDeclaration()
}
"try" -> parseTryStatement()
"throw" -> parseThrowStatement(id.pos)
"when" -> parseWhenStatement()

View File

@ -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
fun multipleReceivers() = runTest {
eval("""

View File

@ -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 &...>
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.
Notes and open questions to answer in this spec: