From c5bf4e5039ef07b598cc54f71b6fd974b1b83fde Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 3 Feb 2026 09:09:04 +0300 Subject: [PATCH] Add variance-aware type params and generic delegates --- .../kotlin/net/sergeych/lyng/CodeContext.kt | 5 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 87 ++++++++++----- .../kotlin/net/sergeych/lyng/TypeDecl.kt | 7 ++ lynglib/stdlib/lyng/root.lyng | 105 +++++++++--------- notes/new_lyng_type_system_spec.md | 7 +- 5 files changed, 131 insertions(+), 80 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt index c6b38f6..75c6168 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt @@ -23,9 +23,12 @@ sealed class CodeContext { val name: String, val implicitThisMembers: Boolean = false, val implicitThisTypeName: String? = null, - val typeParams: Set = emptySet() + val typeParams: Set = emptySet(), + val typeParamDecls: List = emptyList() ): CodeContext() class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() { + var typeParams: Set = emptySet() + var typeParamDecls: List = emptyList() val pendingInitializations = mutableMapOf() val declaredMembers = mutableSetOf() val memberOverrides = mutableMapOf() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 06bef14..dda200f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -439,11 +439,56 @@ class Compiler( } private fun currentTypeParams(): Set { + val result = mutableSetOf() for (ctx in codeContexts.asReversed()) { - val fn = ctx as? CodeContext.Function ?: continue - if (fn.typeParams.isNotEmpty()) return fn.typeParams + when (ctx) { + is CodeContext.Function -> result.addAll(ctx.typeParams) + is CodeContext.ClassBody -> result.addAll(ctx.typeParams) + else -> {} + } } - return emptySet() + return result + } + + private fun parseTypeParamList(): List { + if (cc.peekNextNonWhitespace().type != Token.Type.LT) return emptyList() + val typeParams = mutableListOf() + cc.nextNonWhitespace() + while (true) { + val varianceToken = cc.peekNextNonWhitespace() + val variance = when (varianceToken.value) { + "in" -> { + cc.nextNonWhitespace() + TypeDecl.Variance.In + } + "out" -> { + cc.nextNonWhitespace() + TypeDecl.Variance.Out + } + else -> TypeDecl.Variance.Invariant + } + val idTok = cc.requireToken(Token.Type.ID, "type parameter name expected") + var bound: TypeDecl? = null + var defaultType: TypeDecl? = null + if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) { + bound = parseTypeExpressionWithMini().first + } + if (cc.skipTokenOfType(Token.Type.ASSIGN, isOptional = true)) { + defaultType = parseTypeExpressionWithMini().first + } + typeParams.add(TypeDecl.TypeParam(idTok.value, variance, bound, defaultType)) + val sep = cc.nextNonWhitespace() + when (sep.type) { + Token.Type.COMMA -> continue + Token.Type.GT -> break + Token.Type.SHR -> { + cc.pushPendingGT() + break + } + else -> sep.raiseSyntax("expected ',' or '>' in type parameter list") + } + } + return typeParams } private fun lookupSlotLocation(name: String, includeModule: Boolean = true): SlotLocation? { @@ -3702,6 +3747,9 @@ class Compiler( resolutionSink?.declareSymbol(nameToken.value, SymbolKind.CLASS, isMutable = false, pos = nameToken.pos) return inCodeContext(CodeContext.ClassBody(nameToken.value, isExtern = isExtern)) { val classCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody + val typeParamDecls = parseTypeParamList() + classCtx?.typeParamDecls = typeParamDecls + classCtx?.typeParams = typeParamDecls.map { it.name }.toSet() val constructorArgsDeclaration = if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) parseArgsDeclaration(isClassDeclaration = true) @@ -3731,15 +3779,19 @@ class Compiler( val baseSpecs = mutableListOf() if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) { do { - val baseId = cc.requireToken(Token.Type.ID, "base class name expected") - resolutionSink?.reference(baseId.value, baseId.pos) + val (baseDecl, _) = parseSimpleTypeExpressionWithMini() + val baseName = when (baseDecl) { + is TypeDecl.Simple -> baseDecl.name + is TypeDecl.Generic -> baseDecl.name + else -> throw ScriptError(cc.currentPos(), "base class name expected") + } var argsList: List? = null // Optional constructor args of the base — parse and ignore for now (MVP), just to consume tokens if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) { // Parse args without consuming any following block so that a class body can follow safely argsList = parseArgsNoTailBlock() } - baseSpecs += BaseSpec(baseId.value, argsList) + baseSpecs += BaseSpec(baseName, argsList) } while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true)) } @@ -4360,24 +4412,8 @@ class Compiler( declareLocalName(extensionWrapperName, isMutable = false) } - val typeParams = mutableSetOf() - if (cc.peekNextNonWhitespace().type == Token.Type.LT) { - cc.nextNonWhitespace() - while (true) { - val idTok = cc.requireToken(Token.Type.ID, "type parameter name expected") - typeParams.add(idTok.value) - val sep = cc.nextNonWhitespace() - when (sep.type) { - Token.Type.COMMA -> continue - Token.Type.GT -> break - Token.Type.SHR -> { - cc.pushPendingGT() - break - } - else -> sep.raiseSyntax("expected ',' or '>' in type parameter list") - } - } - } + val typeParamDecls = parseTypeParamList() + val typeParams = typeParamDecls.map { it.name }.toSet() val argsDeclaration: ArgsDeclaration = if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) { @@ -4441,7 +4477,8 @@ class Compiler( name, implicitThisMembers = implicitThisMembers, implicitThisTypeName = extTypeName, - typeParams = typeParams + typeParams = typeParams, + typeParamDecls = typeParamDecls ) ) { cc.labels.add(name) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt index 8f40238..1294944 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt @@ -22,6 +22,7 @@ package net.sergeych.lyng // this is highly experimental and subject to complete redesign // very soon sealed class TypeDecl(val isNullable:Boolean = false) { + enum class Variance { In, Out, Invariant } // ?? data class Function( val receiver: TypeDecl?, @@ -30,6 +31,12 @@ sealed class TypeDecl(val isNullable:Boolean = false) { val nullable: Boolean = false ) : TypeDecl(nullable) data class TypeVar(val name: String, val nullable: Boolean = false) : TypeDecl(nullable) + data class TypeParam( + val name: String, + val variance: Variance = Variance.Invariant, + val bound: TypeDecl? = null, + val defaultType: TypeDecl? = null + ) object TypeAny : TypeDecl() object TypeNullableAny : TypeDecl(true) diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index bb66718..8dcfbf4 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -1,7 +1,6 @@ package lyng.stdlib -// desired type: FlowBuilder.()->void (function types not yet supported in type grammar) -extern fun flow(builder) +extern fun flow(builder: FlowBuilder.()->Void): Flow /* Built-in exception type. */ extern class Exception @@ -10,10 +9,10 @@ extern class NotImplementedException extern class Delegate // Built-in math helpers (implemented in host runtime). -extern fun abs(x) -extern fun ln(x) -extern fun pow(x, y) -extern fun sqrt(x) +extern fun abs(x: Object): Real +extern fun ln(x: Object): Real +extern fun pow(x: Object, y: Object): Real +extern fun sqrt(x: Object): Real // Last regex match result, updated by =~ / !~. var $~ = null @@ -22,7 +21,7 @@ var $~ = null Wrap a builder into a zero-argument thunk that computes once and caches the result. The first call invokes builder() and stores the value; subsequent calls return the cached value. */ -fun cached(builder) { +fun cached(builder: ()->T): ()->T { var calculated = false var value = null { @@ -30,13 +29,13 @@ fun cached(builder) { value = builder() calculated = true } - value + value as T } } /* Filter elements of this iterable using the provided predicate and provide a flow of results. Coudl be used to map infinte flows, etc. */ -fun Iterable.filterFlow(predicate): Flow { +fun Iterable.filterFlow(predicate: (T)->Bool): Flow { val list = this flow { for( item in list ) { @@ -50,8 +49,8 @@ fun Iterable.filterFlow(predicate): Flow { /* Filter this iterable and return List of elements */ -fun Iterable.filter(predicate) { - var result: List = List() +fun Iterable.filter(predicate: (T)->Bool): List { + var result: List = List() for( item in this ) if( predicate(item) ) result += item result } @@ -59,7 +58,7 @@ fun Iterable.filter(predicate) { /* Count all items in this iterable for which predicate returns true */ -fun Iterable.count(predicate): Int { +fun Iterable.count(predicate: (T)->Bool): Int { var hits = 0 this.forEach { if( predicate(it) ) hits++ @@ -70,24 +69,24 @@ fun Iterable.count(predicate): Int { filter out all null elements from this collection (Iterable); flow of non-null elements is returned */ -fun Iterable.filterFlowNotNull(): Flow { +fun Iterable.filterFlowNotNull(): Flow { filterFlow { it != null } } /* Filter non-null elements and collect them into a List */ -fun Iterable.filterNotNull(): List { +fun Iterable.filterNotNull(): List { filter { it != null } } /* Skip the first N elements of this iterable. */ -fun Iterable.drop(n) { +fun Iterable.drop(n: Int): List { var cnt = 0 filter { cnt++ >= n } } /* Return the first element or throw if the iterable is empty. */ -val Iterable.first get() { +val Iterable.first: Object get() { val i: Iterator = iterator() if( !i.hasNext() ) throw NoSuchElementException() i.next().also { i.cancelIteration() } @@ -97,7 +96,7 @@ val Iterable.first get() { Return the first element that matches the predicate or throws NuSuchElementException */ -fun Iterable.findFirst(predicate) { +fun Iterable.findFirst(predicate: (T)->Bool): T { for( x in this ) { if( predicate(x) ) break x @@ -108,7 +107,7 @@ fun Iterable.findFirst(predicate) { /* return the first element matching the predicate or null */ -fun Iterable.findFirstOrNull(predicate) { +fun Iterable.findFirstOrNull(predicate: (T)->Bool): T? { for( x in this ) { if( predicate(x) ) break x @@ -118,7 +117,7 @@ fun Iterable.findFirstOrNull(predicate) { /* Return the last element or throw if the iterable is empty. */ -val Iterable.last get() { +val Iterable.last: Object get() { var found = false var element = null for( i in this ) { @@ -130,7 +129,7 @@ val Iterable.last get() { } /* Emit all but the last N elements of this iterable. */ -fun Iterable.dropLast(n) { +fun Iterable.dropLast(n: Int): Flow { val list = this val buffer = RingBuffer(n) flow { @@ -143,17 +142,17 @@ fun Iterable.dropLast(n) { } /* Return the last N elements of this iterable as a buffer/list. */ -fun Iterable.takeLast(n) { - val buffer = RingBuffer(n) +fun Iterable.takeLast(n: Int): RingBuffer { + val buffer: RingBuffer = RingBuffer(n) for( item in this ) buffer += item buffer } /* Join elements into a string with a separator (separator parameter) and optional transformer. */ -fun Iterable.joinToString(separator=" ", transformer=null) { +fun Iterable.joinToString(separator: String=" ", transformer: (T)->Object = { it }): String { var result = null for( part in this ) { - val transformed = transformer?(part)?.toString() ?: part.toString() + val transformed = transformer(part).toString() if( result == null ) result = transformed else result += separator + transformed } @@ -161,7 +160,7 @@ fun Iterable.joinToString(separator=" ", transformer=null) { } /* Return true if any element matches the predicate. */ -fun Iterable.any(predicate): Bool { +fun Iterable.any(predicate: (T)->Bool): Bool { for( i in this ) { if( predicate(i) ) break true @@ -169,12 +168,12 @@ fun Iterable.any(predicate): Bool { } /* Return true if all elements match the predicate. */ -fun Iterable.all(predicate): Bool { +fun Iterable.all(predicate: (T)->Bool): Bool { !any { !predicate(it) } } /* Sum all elements; returns null for empty collections. */ -fun Iterable.sum() { +fun Iterable.sum(): T? { val i: Iterator = iterator() if( i.hasNext() ) { var result = i.next() @@ -185,7 +184,7 @@ fun Iterable.sum() { } /* Sum mapped values of elements; returns null for empty collections. */ -fun Iterable.sumOf(f) { +fun Iterable.sumOf(f: (T)->R): R? { val i: Iterator = iterator() if( i.hasNext() ) { var result = f(i.next()) @@ -196,7 +195,7 @@ fun Iterable.sumOf(f) { } /* Minimum value of the given function applied to elements of the collection. */ -fun Iterable.minOf( lambda ) { +fun Iterable.minOf(lambda: (T)->R): R { val i: Iterator = iterator() var minimum = lambda( i.next() ) while( i.hasNext() ) { @@ -207,7 +206,7 @@ fun Iterable.minOf( lambda ) { } /* Maximum value of the given function applied to elements of the collection. */ -fun Iterable.maxOf( lambda ) { +fun Iterable.maxOf(lambda: (T)->R): R { val i: Iterator = iterator() var maximum = lambda( i.next() ) while( i.hasNext() ) { @@ -218,18 +217,18 @@ fun Iterable.maxOf( lambda ) { } /* Return elements sorted by natural order. */ -fun Iterable.sorted() { +fun Iterable.sorted(): List { sortedWith { a, b -> a <=> b } } /* Return elements sorted by the key selector. */ -fun Iterable.sortedBy(predicate) { +fun Iterable.sortedBy(predicate: (T)->R): List { sortedWith { a, b -> predicate(a) <=> predicate(b) } } /* Return a shuffled copy of the iterable as a list. */ -fun Iterable.shuffled() { - val list: List = toList() +fun Iterable.shuffled(): List { + val list: List = toList() list.shuffle() list } @@ -238,8 +237,8 @@ fun Iterable.shuffled() { Returns a single list of all elements from all collections in the given collection. @return List */ -fun Iterable.flatten() { - var result: List = List() +fun Iterable.flatten(): List { + var result: List = List() forEach { i -> i.forEach { result += it } } @@ -250,8 +249,8 @@ fun Iterable.flatten() { Returns a single list of all elements yielded from results of transform function being invoked on each element of original collection. */ -fun Iterable.flatMap(transform): List { - val mapped: List = map(transform) +fun Iterable.flatMap(transform: (T)->Iterable): List { + val mapped: List> = map(transform) mapped.flatten() } @@ -268,26 +267,26 @@ override fun List.toString() { } /* Sort list in-place by key selector. */ -fun List.sortBy(predicate) { +fun List.sortBy(predicate: (T)->R): Void { sortWith { a, b -> predicate(a) <=> predicate(b) } } /* Sort list in-place by natural order. */ -fun List.sort() { +fun List.sort(): Void { sortWith { a, b -> a <=> b } } /* Print this exception and its stack trace to standard output. */ -fun Exception.printStackTrace() { +fun Exception.printStackTrace(): Void { println(this) for( entry in stackTrace ) println("\tat "+entry.toString()) } /* Compile this string into a regular expression. */ -val String.re get() = Regex(this) +val String.re: Regex get() = Regex(this) -fun TODO(message=null) { +fun TODO(message: Object?=null): Void { throw "not implemented" } @@ -306,21 +305,21 @@ enum DelegateAccess { Implementing this interface is optional as Lyng uses dynamic dispatch, but it is recommended for documentation and clarity. */ -interface Delegate { +interface Delegate { /* Called when a delegated 'val' or 'var' is read. */ - fun getValue(thisRef, name) = TODO("delegate getter is not implemented") + fun getValue(thisRef: ThisRefType, name: String): T = TODO("delegate getter is not implemented") /* Called when a delegated 'var' is written. */ - fun setValue(thisRef, name, newValue) = TODO("delegate setter is not implemented") + fun setValue(thisRef: ThisRefType, name: String, newValue: T): Void = TODO("delegate setter is not implemented") /* Called when a delegated function is invoked. */ - fun invoke(thisRef, name, args...) = TODO("delegate invoke is not implemented") + fun invoke(thisRef: ThisRefType, name: String, args...): Object = TODO("delegate invoke is not implemented") /* Called once during initialization to configure or validate the delegate. Should return the delegate object to be used (usually 'this'). */ - fun bind(name, access, thisRef) = this + fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object = this } /* @@ -338,19 +337,19 @@ fun with(self: T, block: T.()->R): R { The provided creator lambda is called once on the first access to compute the value. Can only be used with 'val' properties. */ -class lazy(creatorParam) : Delegate { - private val creator = creatorParam +class lazy(creatorParam: Object.()->T) : Delegate { + private val creator: Object.()->T = creatorParam private var value = Unset - override fun bind(name, access, thisRef) { + override fun bind(name: String, access: DelegateAccess, thisRef: Object): Object { if (access.toString() != "DelegateAccess.Val") throw "lazy delegate can only be used with 'val'" this } - override fun getValue(thisRef, name) { + override fun getValue(thisRef: Object, name: String): T { if (value == Unset) value = with(thisRef,creator) - value + value as T } } diff --git a/notes/new_lyng_type_system_spec.md b/notes/new_lyng_type_system_spec.md index 2ef5655..8206253 100644 --- a/notes/new_lyng_type_system_spec.md +++ b/notes/new_lyng_type_system_spec.md @@ -203,7 +203,12 @@ square("3.14") - Generics runtime model: Are type params reified via hidden Class args always, or only when used (T::class, T is ...)? How does this interact with Kotlin interop? -I think we can omit if not used. For kotlin interop: if the class has at least one `extern` symbol, that means native implementation, we always include type parameters, to kotlin implementation can rely on it. +Type params are erased by default. Hidden `Class` args are only injected when a type parameter is used in a reified way (`T::class`, `T is`, `is T`, `as T`) or when the class has at least one `extern` symbol (so host implementations can rely on them). Otherwise `T` is compile-time only and runtime uses `Object`. + +- Variance syntax: + - Declaration-site only, Kotlin-style: `out` (covariant) and `in` (contravariant). + - Example: `class Box`, `class Sink`. + - Bounds remain `T: A & B` or `T: A | B`. - Member access rules: If a variable is Object (dynamic), is member access a compile-time error, or allowed with fallback (which we are trying to remove)? If error, do we require explicit cast first?