Add variance-aware type params and generic delegates

This commit is contained in:
Sergey Chernov 2026-02-03 09:09:04 +03:00
parent c9da0b256f
commit c5bf4e5039
5 changed files with 131 additions and 80 deletions

View File

@ -23,9 +23,12 @@ sealed class CodeContext {
val name: String, val name: String,
val implicitThisMembers: Boolean = false, val implicitThisMembers: Boolean = false,
val implicitThisTypeName: String? = null, val implicitThisTypeName: String? = null,
val typeParams: Set<String> = emptySet() val typeParams: Set<String> = emptySet(),
val typeParamDecls: List<TypeDecl.TypeParam> = emptyList()
): CodeContext() ): CodeContext()
class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() { class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() {
var typeParams: Set<String> = emptySet()
var typeParamDecls: List<TypeDecl.TypeParam> = emptyList()
val pendingInitializations = mutableMapOf<String, Pos>() val pendingInitializations = mutableMapOf<String, Pos>()
val declaredMembers = mutableSetOf<String>() val declaredMembers = mutableSetOf<String>()
val memberOverrides = mutableMapOf<String, Boolean>() val memberOverrides = mutableMapOf<String, Boolean>()

View File

@ -439,11 +439,56 @@ class Compiler(
} }
private fun currentTypeParams(): Set<String> { private fun currentTypeParams(): Set<String> {
val result = mutableSetOf<String>()
for (ctx in codeContexts.asReversed()) { for (ctx in codeContexts.asReversed()) {
val fn = ctx as? CodeContext.Function ?: continue when (ctx) {
if (fn.typeParams.isNotEmpty()) return fn.typeParams is CodeContext.Function -> result.addAll(ctx.typeParams)
is CodeContext.ClassBody -> result.addAll(ctx.typeParams)
else -> {}
} }
return emptySet() }
return result
}
private fun parseTypeParamList(): List<TypeDecl.TypeParam> {
if (cc.peekNextNonWhitespace().type != Token.Type.LT) return emptyList()
val typeParams = mutableListOf<TypeDecl.TypeParam>()
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? { 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) resolutionSink?.declareSymbol(nameToken.value, SymbolKind.CLASS, isMutable = false, pos = nameToken.pos)
return inCodeContext(CodeContext.ClassBody(nameToken.value, isExtern = isExtern)) { return inCodeContext(CodeContext.ClassBody(nameToken.value, isExtern = isExtern)) {
val classCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody val classCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody
val typeParamDecls = parseTypeParamList()
classCtx?.typeParamDecls = typeParamDecls
classCtx?.typeParams = typeParamDecls.map { it.name }.toSet()
val constructorArgsDeclaration = val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(isClassDeclaration = true) parseArgsDeclaration(isClassDeclaration = true)
@ -3731,15 +3779,19 @@ class Compiler(
val baseSpecs = mutableListOf<BaseSpec>() val baseSpecs = mutableListOf<BaseSpec>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) { if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
do { do {
val baseId = cc.requireToken(Token.Type.ID, "base class name expected") val (baseDecl, _) = parseSimpleTypeExpressionWithMini()
resolutionSink?.reference(baseId.value, baseId.pos) 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<ParsedArgument>? = null var argsList: List<ParsedArgument>? = null
// Optional constructor args of the base — parse and ignore for now (MVP), just to consume tokens // Optional constructor args of the base — parse and ignore for now (MVP), just to consume tokens
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) { if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
// Parse args without consuming any following block so that a class body can follow safely // Parse args without consuming any following block so that a class body can follow safely
argsList = parseArgsNoTailBlock() argsList = parseArgsNoTailBlock()
} }
baseSpecs += BaseSpec(baseId.value, argsList) baseSpecs += BaseSpec(baseName, argsList)
} while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true)) } while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true))
} }
@ -4360,24 +4412,8 @@ class Compiler(
declareLocalName(extensionWrapperName, isMutable = false) declareLocalName(extensionWrapperName, isMutable = false)
} }
val typeParams = mutableSetOf<String>() val typeParamDecls = parseTypeParamList()
if (cc.peekNextNonWhitespace().type == Token.Type.LT) { val typeParams = typeParamDecls.map { it.name }.toSet()
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 argsDeclaration: ArgsDeclaration = val argsDeclaration: ArgsDeclaration =
if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) { if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
@ -4441,7 +4477,8 @@ class Compiler(
name, name,
implicitThisMembers = implicitThisMembers, implicitThisMembers = implicitThisMembers,
implicitThisTypeName = extTypeName, implicitThisTypeName = extTypeName,
typeParams = typeParams typeParams = typeParams,
typeParamDecls = typeParamDecls
) )
) { ) {
cc.labels.add(name) cc.labels.add(name)

View File

@ -22,6 +22,7 @@ package net.sergeych.lyng
// this is highly experimental and subject to complete redesign // this is highly experimental and subject to complete redesign
// very soon // very soon
sealed class TypeDecl(val isNullable:Boolean = false) { sealed class TypeDecl(val isNullable:Boolean = false) {
enum class Variance { In, Out, Invariant }
// ?? // ??
data class Function( data class Function(
val receiver: TypeDecl?, val receiver: TypeDecl?,
@ -30,6 +31,12 @@ sealed class TypeDecl(val isNullable:Boolean = false) {
val nullable: Boolean = false val nullable: Boolean = false
) : TypeDecl(nullable) ) : TypeDecl(nullable)
data class TypeVar(val name: String, 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 TypeAny : TypeDecl()
object TypeNullableAny : TypeDecl(true) object TypeNullableAny : TypeDecl(true)

View File

@ -1,7 +1,6 @@
package lyng.stdlib package lyng.stdlib
// desired type: FlowBuilder.()->void (function types not yet supported in type grammar) extern fun flow(builder: FlowBuilder.()->Void): Flow
extern fun flow(builder)
/* Built-in exception type. */ /* Built-in exception type. */
extern class Exception extern class Exception
@ -10,10 +9,10 @@ extern class NotImplementedException
extern class Delegate extern class Delegate
// Built-in math helpers (implemented in host runtime). // Built-in math helpers (implemented in host runtime).
extern fun abs(x) extern fun abs(x: Object): Real
extern fun ln(x) extern fun ln(x: Object): Real
extern fun pow(x, y) extern fun pow(x: Object, y: Object): Real
extern fun sqrt(x) extern fun sqrt(x: Object): Real
// Last regex match result, updated by =~ / !~. // Last regex match result, updated by =~ / !~.
var $~ = null var $~ = null
@ -22,7 +21,7 @@ var $~ = null
Wrap a builder into a zero-argument thunk that computes once and caches the result. 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. The first call invokes builder() and stores the value; subsequent calls return the cached value.
*/ */
fun cached(builder) { fun cached<T>(builder: ()->T): ()->T {
var calculated = false var calculated = false
var value = null var value = null
{ {
@ -30,13 +29,13 @@ fun cached(builder) {
value = builder() value = builder()
calculated = true calculated = true
} }
value value as T
} }
} }
/* Filter elements of this iterable using the provided predicate and provide a flow /* Filter elements of this iterable using the provided predicate and provide a flow
of results. Coudl be used to map infinte flows, etc. of results. Coudl be used to map infinte flows, etc.
*/ */
fun Iterable.filterFlow(predicate): Flow { fun Iterable.filterFlow<T>(predicate: (T)->Bool): Flow<T> {
val list = this val list = this
flow { flow {
for( item in list ) { for( item in list ) {
@ -50,8 +49,8 @@ fun Iterable.filterFlow(predicate): Flow {
/* /*
Filter this iterable and return List of elements Filter this iterable and return List of elements
*/ */
fun Iterable.filter(predicate) { fun Iterable.filter<T>(predicate: (T)->Bool): List<T> {
var result: List = List() var result: List<T> = List()
for( item in this ) if( predicate(item) ) result += item for( item in this ) if( predicate(item) ) result += item
result result
} }
@ -59,7 +58,7 @@ fun Iterable.filter(predicate) {
/* /*
Count all items in this iterable for which predicate returns true Count all items in this iterable for which predicate returns true
*/ */
fun Iterable.count(predicate): Int { fun Iterable.count<T>(predicate: (T)->Bool): Int {
var hits = 0 var hits = 0
this.forEach { this.forEach {
if( predicate(it) ) hits++ if( predicate(it) ) hits++
@ -70,24 +69,24 @@ fun Iterable.count(predicate): Int {
filter out all null elements from this collection (Iterable); flow of filter out all null elements from this collection (Iterable); flow of
non-null elements is returned non-null elements is returned
*/ */
fun Iterable.filterFlowNotNull(): Flow { fun Iterable.filterFlowNotNull<T>(): Flow<T> {
filterFlow { it != null } filterFlow { it != null }
} }
/* Filter non-null elements and collect them into a List /* Filter non-null elements and collect them into a List
*/ */
fun Iterable.filterNotNull(): List { fun Iterable.filterNotNull<T>(): List<T> {
filter { it != null } filter { it != null }
} }
/* Skip the first N elements of this iterable. */ /* Skip the first N elements of this iterable. */
fun Iterable.drop(n) { fun Iterable.drop<T>(n: Int): List<T> {
var cnt = 0 var cnt = 0
filter { cnt++ >= n } filter { cnt++ >= n }
} }
/* Return the first element or throw if the iterable is empty. */ /* Return the first element or throw if the iterable is empty. */
val Iterable.first get() { val Iterable.first: Object get() {
val i: Iterator = iterator() val i: Iterator = iterator()
if( !i.hasNext() ) throw NoSuchElementException() if( !i.hasNext() ) throw NoSuchElementException()
i.next().also { i.cancelIteration() } i.next().also { i.cancelIteration() }
@ -97,7 +96,7 @@ val Iterable.first get() {
Return the first element that matches the predicate or throws Return the first element that matches the predicate or throws
NuSuchElementException NuSuchElementException
*/ */
fun Iterable.findFirst(predicate) { fun Iterable.findFirst<T>(predicate: (T)->Bool): T {
for( x in this ) { for( x in this ) {
if( predicate(x) ) if( predicate(x) )
break x break x
@ -108,7 +107,7 @@ fun Iterable.findFirst(predicate) {
/* /*
return the first element matching the predicate or null return the first element matching the predicate or null
*/ */
fun Iterable.findFirstOrNull(predicate) { fun Iterable.findFirstOrNull<T>(predicate: (T)->Bool): T? {
for( x in this ) { for( x in this ) {
if( predicate(x) ) if( predicate(x) )
break x break x
@ -118,7 +117,7 @@ fun Iterable.findFirstOrNull(predicate) {
/* Return the last element or throw if the iterable is empty. */ /* Return the last element or throw if the iterable is empty. */
val Iterable.last get() { val Iterable.last: Object get() {
var found = false var found = false
var element = null var element = null
for( i in this ) { for( i in this ) {
@ -130,7 +129,7 @@ val Iterable.last get() {
} }
/* Emit all but the last N elements of this iterable. */ /* Emit all but the last N elements of this iterable. */
fun Iterable.dropLast(n) { fun Iterable.dropLast<T>(n: Int): Flow<T> {
val list = this val list = this
val buffer = RingBuffer(n) val buffer = RingBuffer(n)
flow { flow {
@ -143,17 +142,17 @@ fun Iterable.dropLast(n) {
} }
/* Return the last N elements of this iterable as a buffer/list. */ /* Return the last N elements of this iterable as a buffer/list. */
fun Iterable.takeLast(n) { fun Iterable.takeLast<T>(n: Int): RingBuffer<T> {
val buffer = RingBuffer(n) val buffer: RingBuffer<T> = RingBuffer(n)
for( item in this ) buffer += item for( item in this ) buffer += item
buffer buffer
} }
/* Join elements into a string with a separator (separator parameter) and optional transformer. */ /* Join elements into a string with a separator (separator parameter) and optional transformer. */
fun Iterable.joinToString(separator=" ", transformer=null) { fun Iterable.joinToString<T>(separator: String=" ", transformer: (T)->Object = { it }): String {
var result = null var result = null
for( part in this ) { for( part in this ) {
val transformed = transformer?(part)?.toString() ?: part.toString() val transformed = transformer(part).toString()
if( result == null ) result = transformed if( result == null ) result = transformed
else result += separator + transformed else result += separator + transformed
} }
@ -161,7 +160,7 @@ fun Iterable.joinToString(separator=" ", transformer=null) {
} }
/* Return true if any element matches the predicate. */ /* Return true if any element matches the predicate. */
fun Iterable.any(predicate): Bool { fun Iterable.any<T>(predicate: (T)->Bool): Bool {
for( i in this ) { for( i in this ) {
if( predicate(i) ) if( predicate(i) )
break true break true
@ -169,12 +168,12 @@ fun Iterable.any(predicate): Bool {
} }
/* Return true if all elements match the predicate. */ /* Return true if all elements match the predicate. */
fun Iterable.all(predicate): Bool { fun Iterable.all<T>(predicate: (T)->Bool): Bool {
!any { !predicate(it) } !any { !predicate(it) }
} }
/* Sum all elements; returns null for empty collections. */ /* Sum all elements; returns null for empty collections. */
fun Iterable.sum() { fun Iterable.sum<T>(): T? {
val i: Iterator = iterator() val i: Iterator = iterator()
if( i.hasNext() ) { if( i.hasNext() ) {
var result = i.next() var result = i.next()
@ -185,7 +184,7 @@ fun Iterable.sum() {
} }
/* Sum mapped values of elements; returns null for empty collections. */ /* Sum mapped values of elements; returns null for empty collections. */
fun Iterable.sumOf(f) { fun Iterable.sumOf<T,R>(f: (T)->R): R? {
val i: Iterator = iterator() val i: Iterator = iterator()
if( i.hasNext() ) { if( i.hasNext() ) {
var result = f(i.next()) 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. */ /* Minimum value of the given function applied to elements of the collection. */
fun Iterable.minOf( lambda ) { fun Iterable.minOf<T,R>(lambda: (T)->R): R {
val i: Iterator = iterator() val i: Iterator = iterator()
var minimum = lambda( i.next() ) var minimum = lambda( i.next() )
while( i.hasNext() ) { while( i.hasNext() ) {
@ -207,7 +206,7 @@ fun Iterable.minOf( lambda ) {
} }
/* Maximum value of the given function applied to elements of the collection. */ /* Maximum value of the given function applied to elements of the collection. */
fun Iterable.maxOf( lambda ) { fun Iterable.maxOf<T,R>(lambda: (T)->R): R {
val i: Iterator = iterator() val i: Iterator = iterator()
var maximum = lambda( i.next() ) var maximum = lambda( i.next() )
while( i.hasNext() ) { while( i.hasNext() ) {
@ -218,18 +217,18 @@ fun Iterable.maxOf( lambda ) {
} }
/* Return elements sorted by natural order. */ /* Return elements sorted by natural order. */
fun Iterable.sorted() { fun Iterable.sorted<T>(): List<T> {
sortedWith { a, b -> a <=> b } sortedWith { a, b -> a <=> b }
} }
/* Return elements sorted by the key selector. */ /* Return elements sorted by the key selector. */
fun Iterable.sortedBy(predicate) { fun Iterable.sortedBy<T,R>(predicate: (T)->R): List<T> {
sortedWith { a, b -> predicate(a) <=> predicate(b) } sortedWith { a, b -> predicate(a) <=> predicate(b) }
} }
/* Return a shuffled copy of the iterable as a list. */ /* Return a shuffled copy of the iterable as a list. */
fun Iterable.shuffled() { fun Iterable.shuffled<T>(): List<T> {
val list: List = toList() val list: List<T> = toList()
list.shuffle() list.shuffle()
list list
} }
@ -238,8 +237,8 @@ fun Iterable.shuffled() {
Returns a single list of all elements from all collections in the given collection. Returns a single list of all elements from all collections in the given collection.
@return List @return List
*/ */
fun Iterable.flatten() { fun Iterable.flatten<T>(): List<T> {
var result: List = List() var result: List<T> = List()
forEach { i -> forEach { i ->
i.forEach { result += it } 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 Returns a single list of all elements yielded from results of transform function being
invoked on each element of original collection. invoked on each element of original collection.
*/ */
fun Iterable.flatMap(transform): List { fun Iterable.flatMap<T,R>(transform: (T)->Iterable<R>): List<R> {
val mapped: List = map(transform) val mapped: List<Iterable<R>> = map(transform)
mapped.flatten() mapped.flatten()
} }
@ -268,26 +267,26 @@ override fun List.toString() {
} }
/* Sort list in-place by key selector. */ /* Sort list in-place by key selector. */
fun List.sortBy(predicate) { fun List.sortBy<T,R>(predicate: (T)->R): Void {
sortWith { a, b -> predicate(a) <=> predicate(b) } sortWith { a, b -> predicate(a) <=> predicate(b) }
} }
/* Sort list in-place by natural order. */ /* Sort list in-place by natural order. */
fun List.sort() { fun List.sort<T>(): Void {
sortWith { a, b -> a <=> b } sortWith { a, b -> a <=> b }
} }
/* Print this exception and its stack trace to standard output. */ /* Print this exception and its stack trace to standard output. */
fun Exception.printStackTrace() { fun Exception.printStackTrace(): Void {
println(this) println(this)
for( entry in stackTrace ) for( entry in stackTrace )
println("\tat "+entry.toString()) println("\tat "+entry.toString())
} }
/* Compile this string into a regular expression. */ /* 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" throw "not implemented"
} }
@ -306,21 +305,21 @@ enum DelegateAccess {
Implementing this interface is optional as Lyng uses dynamic dispatch, Implementing this interface is optional as Lyng uses dynamic dispatch,
but it is recommended for documentation and clarity. but it is recommended for documentation and clarity.
*/ */
interface Delegate { interface Delegate<T,ThisRefType=Void> {
/* Called when a delegated 'val' or 'var' is read. */ /* 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. */ /* 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. */ /* 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. Called once during initialization to configure or validate the delegate.
Should return the delegate object to be used (usually 'this'). 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<T,R>(self: T, block: T.()->R): R {
The provided creator lambda is called once on the first access to compute the value. The provided creator lambda is called once on the first access to compute the value.
Can only be used with 'val' properties. Can only be used with 'val' properties.
*/ */
class lazy(creatorParam) : Delegate { class lazy<T>(creatorParam: Object.()->T) : Delegate<T,Object> {
private val creator = creatorParam private val creator: Object.()->T = creatorParam
private var value = Unset 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'" if (access.toString() != "DelegateAccess.Val") throw "lazy delegate can only be used with 'val'"
this this
} }
override fun getValue(thisRef, name) { override fun getValue(thisRef: Object, name: String): T {
if (value == Unset) if (value == Unset)
value = with(thisRef,creator) value = with(thisRef,creator)
value value as T
} }
} }

View File

@ -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? - 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<out T>`, `class Sink<in T>`.
- 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? - 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?