Add support for variadic function types and ellipsis in type declarations, stricter lamda types compile-time checks and object binding for Kotlin bridges

This commit is contained in:
Sergey Chernov 2026-02-19 11:12:22 +03:00
parent e7c1adb2c5
commit d82302dd01
15 changed files with 387 additions and 12 deletions

View File

@ -34,6 +34,11 @@ Valid examples:
Ellipsis are used to declare variadic arguments. It basically means "all the arguments available here". It means, ellipsis argument could be in any part of the list, being, end or middle, but there could be only one ellipsis argument and it must not have default value, its default value is always `[]`, en empty list. Ellipsis are used to declare variadic arguments. It basically means "all the arguments available here". It means, ellipsis argument could be in any part of the list, being, end or middle, but there could be only one ellipsis argument and it must not have default value, its default value is always `[]`, en empty list.
Ellipsis can also appear in **function types** to denote a variadic position:
var f: (Int, Object..., String)->Real
var anyArgs: (...)->Int // shorthand for (Object...)->Int
Ellipsis argument receives what is left from arguments after processing regular one that could be before or after. Ellipsis argument receives what is left from arguments after processing regular one that could be before or after.
Ellipsis could be a first argument: Ellipsis could be a first argument:

View File

@ -225,6 +225,45 @@ Notes:
- Use [LyngClassBridge] to bind by name/module, or by an already resolved `ObjClass`. - Use [LyngClassBridge] to bind by name/module, or by an already resolved `ObjClass`.
- Use `ObjInstance.data` / `ObjClass.classData` to attach Kotlin‑side state when needed. - Use `ObjInstance.data` / `ObjClass.classData` to attach Kotlin‑side state when needed.
### 6.5a) Bind Kotlin implementations to declared Lyng objects
For `extern object` declarations, bind implementations to the singleton instance using `ModuleScope.bindObject`.
This mirrors class binding but targets an already created object instance.
```lyng
// Lyng side (in a module)
extern object HostObject {
extern fun add(a: Int, b: Int): Int
extern val status: String
extern var count: Int
}
```
```kotlin
// Kotlin side (binding)
val moduleScope = importManager.createModuleScope(Pos.builtIn, "bridge.obj")
moduleScope.bindObject("HostObject") {
classData = "OK"
init { _ -> data = 0L }
addFun("add") { _, _, args ->
val a = args.requiredArg<ObjInt>(0).value
val b = args.requiredArg<ObjInt>(1).value
ObjInt.of(a + b)
}
addVal("status") { _, _ -> ObjString(classData as String) }
addVar(
"count",
get = { _, inst -> ObjInt.of((inst as ObjInstance).data as Long) },
set = { _, inst, value -> (inst as ObjInstance).data = (value as ObjInt).value }
)
}
```
Notes:
- Members must be marked `extern` so the compiler emits ABI slots for Kotlin bindings.
- You can also bind by name/module via `LyngObjectBridge.bind(...)`.
### 6.6) Preferred: Kotlin reflection bridge for call‑by‑name ### 6.6) Preferred: Kotlin reflection bridge for call‑by‑name
For Kotlin code that needs dynamic access to Lyng variables, functions, or members, use the bridge resolver. For Kotlin code that needs dynamic access to Lyng variables, functions, or members, use the bridge resolver.

View File

@ -518,6 +518,13 @@ Examples:
fun inc(x=0) = x + 1 // (Int)->Int fun inc(x=0) = x + 1 // (Int)->Int
fun maybe(flag) { if(flag) 1 else null } // ()->Int? fun maybe(flag) { if(flag) 1 else null } // ()->Int?
Function types are written as `(T1, T2, ...)->R`. You can include ellipsis in function *types* to
express a variadic position:
var fmt: (String, Object...)->String
var f: (Int, Object..., String)->Real
var anyArgs: (...)->Int // shorthand for (Object...)->Int
Untyped locals are allowed, but their type is fixed on the first assignment: Untyped locals are allowed, but their type is fixed on the first assignment:
var x var x
@ -735,6 +742,11 @@ one could be with ellipsis that means "the rest pf arguments as List":
assert( { a, b...-> [a,...b] }(100, 1, 2, 3) == [100, 1, 2, 3]) assert( { a, b...-> [a,...b] }(100, 1, 2, 3) == [100, 1, 2, 3])
void void
Type-annotated lambdas can use variadic *function types* as well:
val f: (Int, Object..., String)->Real = { a, rest..., b -> 0.0 }
val anyArgs: (...)->Int = { -> 0 }
### Using lambda as the parameter ### Using lambda as the parameter
See also: [Testing and Assertions](Testing.md) See also: [Testing and Assertions](Testing.md)

View File

@ -209,6 +209,9 @@ class Compiler(
if (plan.slots.containsKey(name)) continue if (plan.slots.containsKey(name)) continue
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated) declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
scopeSeedNames.add(name) scopeSeedNames.add(name)
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
val instance = record.value as? ObjInstance val instance = record.value as? ObjInstance
if (instance != null && nameObjClass[name] == null) { if (instance != null && nameObjClass[name] == null) {
nameObjClass[name] = instance.objClass nameObjClass[name] = instance.objClass
@ -285,6 +288,12 @@ class Compiler(
record.type == ObjRecord.Type.Delegated record.type == ObjRecord.Type.Delegated
) )
scopeSeedNames.add(name) scopeSeedNames.add(name)
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
if (record.typeDecl != null) {
slotTypeDeclByScopeId.getOrPut(plan.id) { mutableMapOf() }[slotIndex] = record.typeDecl
}
} }
} }
@ -843,6 +852,7 @@ class Compiler(
visibility = Visibility.Public, visibility = Visibility.Public,
initializer = initStmt, initializer = initStmt,
isTransient = false, isTransient = false,
typeDecl = null,
slotIndex = slotIndex, slotIndex = slotIndex,
scopeId = scopeId, scopeId = scopeId,
startPos = nameToken.pos, startPos = nameToken.pos,
@ -1180,6 +1190,25 @@ class Compiler(
} }
} }
private fun seedNameTypeDeclFromScope(scope: Scope) {
var current: Scope? = scope
while (current != null) {
for ((name, record) in current.objects) {
if (!record.visibility.isPublic) continue
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
}
for ((name, slotIndex) in current.slotNameToIndexSnapshot()) {
val record = current.getSlotRecord(slotIndex)
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
}
current = current.parent
}
}
private fun resolveImportBinding(name: String, pos: Pos): ImportBindingResolution? { private fun resolveImportBinding(name: String, pos: Pos): ImportBindingResolution? {
val seedRecord = findSeedScopeRecord(name)?.takeIf { it.visibility.isPublic } val seedRecord = findSeedScopeRecord(name)?.takeIf { it.visibility.isPublic }
val rootRecord = importManager.rootScope.objects[name]?.takeIf { it.visibility.isPublic } val rootRecord = importManager.rootScope.objects[name]?.takeIf { it.visibility.isPublic }
@ -1387,6 +1416,7 @@ class Compiler(
returnType = transform(decl.returnType), returnType = transform(decl.returnType),
nullable = decl.isNullable nullable = decl.isNullable
) )
is TypeDecl.Ellipsis -> TypeDecl.Ellipsis(transform(decl.elementType), decl.isNullable)
is TypeDecl.Union -> TypeDecl.Union(decl.options.map { transform(it) }, decl.isNullable) is TypeDecl.Union -> TypeDecl.Union(decl.options.map { transform(it) }, decl.isNullable)
is TypeDecl.Intersection -> TypeDecl.Intersection(decl.options.map { transform(it) }, decl.isNullable) is TypeDecl.Intersection -> TypeDecl.Intersection(decl.options.map { transform(it) }, decl.isNullable)
else -> decl else -> decl
@ -1515,6 +1545,7 @@ class Compiler(
declareSlotNameIn(plan, "$~", isMutable = true, isDelegated = false) declareSlotNameIn(plan, "$~", isMutable = true, isDelegated = false)
} }
seedScope?.let { seedNameObjClassFromScope(it) } seedScope?.let { seedNameObjClassFromScope(it) }
seedScope?.let { seedNameTypeDeclFromScope(it) }
seedNameObjClassFromScope(importManager.rootScope) seedNameObjClassFromScope(importManager.rootScope)
if (shouldSeedDefaultStdlib()) { if (shouldSeedDefaultStdlib()) {
val stdlib = importManager.prepareImport(start, "lyng.stdlib", null) val stdlib = importManager.prepareImport(start, "lyng.stdlib", null)
@ -2211,6 +2242,7 @@ class Compiler(
stmt.visibility, stmt.visibility,
init, init,
stmt.isTransient, stmt.isTransient,
stmt.typeDecl,
stmt.slotIndex, stmt.slotIndex,
stmt.scopeId, stmt.scopeId,
stmt.pos, stmt.pos,
@ -3475,6 +3507,10 @@ class Compiler(
val ret = expandTypeAliases(type.returnType, pos, seen) val ret = expandTypeAliases(type.returnType, pos, seen)
TypeDecl.Function(receiver, params, ret, type.nullable) TypeDecl.Function(receiver, params, ret, type.nullable)
} }
is TypeDecl.Ellipsis -> {
val elem = expandTypeAliases(type.elementType, pos, seen)
TypeDecl.Ellipsis(elem, type.nullable)
}
is TypeDecl.Union -> { is TypeDecl.Union -> {
val options = type.options.map { expandTypeAliases(it, pos, seen) } val options = type.options.map { expandTypeAliases(it, pos, seen) }
TypeDecl.Union(options, type.isNullable) TypeDecl.Union(options, type.isNullable)
@ -3560,6 +3596,10 @@ class Compiler(
val ret = substituteTypeAliasTypeVars(type.returnType, bindings) val ret = substituteTypeAliasTypeVars(type.returnType, bindings)
TypeDecl.Function(receiver, params, ret, type.nullable) TypeDecl.Function(receiver, params, ret, type.nullable)
} }
is TypeDecl.Ellipsis -> {
val elem = substituteTypeAliasTypeVars(type.elementType, bindings)
TypeDecl.Ellipsis(elem, type.nullable)
}
is TypeDecl.Union -> { is TypeDecl.Union -> {
val options = type.options.map { substituteTypeAliasTypeVars(it, bindings) } val options = type.options.map { substituteTypeAliasTypeVars(it, bindings) }
TypeDecl.Union(options, type.isNullable) TypeDecl.Union(options, type.isNullable)
@ -3578,6 +3618,7 @@ class Compiler(
TypeDecl.TypeAny -> TypeDecl.TypeNullableAny TypeDecl.TypeAny -> TypeDecl.TypeNullableAny
TypeDecl.TypeNullableAny -> type TypeDecl.TypeNullableAny -> type
is TypeDecl.Function -> type.copy(nullable = true) is TypeDecl.Function -> type.copy(nullable = true)
is TypeDecl.Ellipsis -> type.copy(nullable = true)
is TypeDecl.TypeVar -> type.copy(nullable = true) is TypeDecl.TypeVar -> type.copy(nullable = true)
is TypeDecl.Union -> type.copy(nullable = true) is TypeDecl.Union -> type.copy(nullable = true)
is TypeDecl.Intersection -> type.copy(nullable = true) is TypeDecl.Intersection -> type.copy(nullable = true)
@ -3634,14 +3675,41 @@ class Compiler(
fun parseParamTypes(): List<Pair<TypeDecl, MiniTypeRef>> { fun parseParamTypes(): List<Pair<TypeDecl, MiniTypeRef>> {
val params = mutableListOf<Pair<TypeDecl, MiniTypeRef>>() val params = mutableListOf<Pair<TypeDecl, MiniTypeRef>>()
var seenEllipsis = false
cc.skipWsTokens() cc.skipWsTokens()
if (cc.peekNextNonWhitespace().type == Token.Type.RPAREN) { if (cc.peekNextNonWhitespace().type == Token.Type.RPAREN) {
cc.nextNonWhitespace() cc.nextNonWhitespace()
return params return params
} }
while (true) { while (true) {
val (paramDecl, paramMini) = parseTypeExpressionWithMini() cc.skipWsTokens()
params += paramDecl to paramMini val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ELLIPSIS) {
val ell = cc.nextNonWhitespace()
if (seenEllipsis) {
ell.raiseSyntax("function type can contain only one ellipsis")
}
seenEllipsis = true
val paramDecl = TypeDecl.Ellipsis(TypeDecl.TypeAny)
val mini = MiniTypeName(
MiniRange(ell.pos, ell.pos),
listOf(MiniTypeName.Segment("Object", MiniRange(ell.pos, ell.pos))),
nullable = false
)
params += paramDecl to mini
} else {
val (paramDecl, paramMini) = parseTypeExpressionWithMini()
val finalDecl = if (cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true)) {
if (seenEllipsis) {
cc.current().raiseSyntax("function type can contain only one ellipsis")
}
seenEllipsis = true
TypeDecl.Ellipsis(paramDecl)
} else {
paramDecl
}
params += finalDecl to paramMini
}
val sep = cc.nextNonWhitespace() val sep = cc.nextNonWhitespace()
when (sep.type) { when (sep.type) {
Token.Type.COMMA -> continue Token.Type.COMMA -> continue
@ -3952,6 +4020,7 @@ class Compiler(
is TypeDecl.Simple -> typeDecl.name is TypeDecl.Simple -> typeDecl.name
is TypeDecl.Generic -> typeDecl.name is TypeDecl.Generic -> typeDecl.name
is TypeDecl.Function -> "Callable" is TypeDecl.Function -> "Callable"
is TypeDecl.Ellipsis -> "${typeDeclName(typeDecl.elementType)}..."
is TypeDecl.TypeVar -> typeDecl.name is TypeDecl.TypeVar -> typeDecl.name
is TypeDecl.Union -> typeDecl.options.joinToString(" | ") { typeDeclName(it) } is TypeDecl.Union -> typeDecl.options.joinToString(" | ") { typeDeclName(it) }
is TypeDecl.Intersection -> typeDecl.options.joinToString(" & ") { typeDeclName(it) } is TypeDecl.Intersection -> typeDecl.options.joinToString(" & ") { typeDeclName(it) }
@ -4129,6 +4198,7 @@ class Compiler(
val nullable = type.isNullable val nullable = type.isNullable
val base = if (!nullable) type else when (type) { val base = if (!nullable) type else when (type) {
is TypeDecl.Function -> type.copy(nullable = false) is TypeDecl.Function -> type.copy(nullable = false)
is TypeDecl.Ellipsis -> type.copy(nullable = false)
is TypeDecl.TypeVar -> type.copy(nullable = false) is TypeDecl.TypeVar -> type.copy(nullable = false)
is TypeDecl.Union -> type.copy(nullable = false) is TypeDecl.Union -> type.copy(nullable = false)
is TypeDecl.Intersection -> type.copy(nullable = false) is TypeDecl.Intersection -> type.copy(nullable = false)
@ -4145,6 +4215,7 @@ class Compiler(
is TypeDecl.Simple -> "S:${type.name}" is TypeDecl.Simple -> "S:${type.name}"
is TypeDecl.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>" is TypeDecl.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>"
is TypeDecl.Function -> "F:(${type.params.joinToString(",") { typeDeclKey(it) }})->${typeDeclKey(type.returnType)}" is TypeDecl.Function -> "F:(${type.params.joinToString(",") { typeDeclKey(it) }})->${typeDeclKey(type.returnType)}"
is TypeDecl.Ellipsis -> "E:${typeDeclKey(type.elementType)}"
is TypeDecl.TypeVar -> "V:${type.name}" is TypeDecl.TypeVar -> "V:${type.name}"
is TypeDecl.Union -> "U:${type.options.joinToString("|") { typeDeclKey(it) }}" is TypeDecl.Union -> "U:${type.options.joinToString("|") { typeDeclKey(it) }}"
is TypeDecl.Intersection -> "I:${type.options.joinToString("&") { typeDeclKey(it) }}" is TypeDecl.Intersection -> "I:${type.options.joinToString("&") { typeDeclKey(it) }}"
@ -4671,6 +4742,144 @@ class Compiler(
} }
} }
private fun checkFunctionTypeCallArity(
target: ObjRef,
args: List<ParsedArgument>,
pos: Pos
) {
val decl = (resolveReceiverTypeDecl(target) as? TypeDecl.Function)
?: seedTypeDeclFromRef(target) as? TypeDecl.Function
?: return
if (args.any { it.isSplat }) return
val actual = args.size
val receiverCount = if (decl.receiver != null) 1 else 0
val paramList = mutableListOf<TypeDecl>()
decl.receiver?.let { paramList += it }
paramList += decl.params
val ellipsisIndex = paramList.indexOfFirst { it is TypeDecl.Ellipsis }
if (ellipsisIndex < 0) {
val expected = paramList.size
if (actual != expected) {
throw ScriptError(pos, "expected $expected arguments, got $actual")
}
return
}
val headCount = ellipsisIndex
val tailCount = paramList.size - ellipsisIndex - 1
val minArgs = headCount + tailCount
if (actual < minArgs) {
throw ScriptError(pos, "expected at least $minArgs arguments, got $actual")
}
}
private fun seedTypeDeclFromRef(ref: ObjRef): TypeDecl? {
val name = when (ref) {
is LocalVarRef -> ref.name
is LocalSlotRef -> ref.name
is FastLocalVarRef -> ref.name
else -> null
} ?: return null
seedScope?.getLocalRecordDirect(name)?.typeDecl?.let { return it }
return seedScope?.get(name)?.typeDecl
}
private fun checkFunctionTypeCallTypes(
target: ObjRef,
args: List<ParsedArgument>,
pos: Pos
) {
val decl = (resolveReceiverTypeDecl(target) as? TypeDecl.Function)
?: seedTypeDeclFromRef(target) as? TypeDecl.Function
?: return
val paramList = mutableListOf<TypeDecl>()
decl.receiver?.let { paramList += it }
paramList += decl.params
if (paramList.isEmpty()) return
val ellipsisIndex = paramList.indexOfFirst { it is TypeDecl.Ellipsis }
fun argTypeDecl(arg: ParsedArgument): TypeDecl? {
val stmt = arg.value as? ExpressionStatement ?: return null
val ref = stmt.ref
return inferTypeDeclFromRef(ref)
?: inferObjClassFromRef(ref)?.let { TypeDecl.Simple(it.className, false) }
}
fun typeDeclSubtypeOf(arg: TypeDecl, param: TypeDecl): Boolean {
if (param == TypeDecl.TypeAny || param == TypeDecl.TypeNullableAny) return true
val (argBase, argNullable) = stripNullable(arg)
val (paramBase, paramNullable) = stripNullable(param)
if (argNullable && !paramNullable) return false
if (paramBase == TypeDecl.TypeAny) return true
if (paramBase is TypeDecl.TypeVar) return true
if (argBase is TypeDecl.TypeVar) return true
if (paramBase is TypeDecl.Simple && (paramBase.name == "Object" || paramBase.name == "Obj")) return true
if (argBase is TypeDecl.Ellipsis) return typeDeclSubtypeOf(argBase.elementType, paramBase)
if (paramBase is TypeDecl.Ellipsis) return typeDeclSubtypeOf(argBase, paramBase.elementType)
return when (argBase) {
is TypeDecl.Union -> argBase.options.all { typeDeclSubtypeOf(it, paramBase) }
is TypeDecl.Intersection -> argBase.options.any { typeDeclSubtypeOf(it, paramBase) }
else -> when (paramBase) {
is TypeDecl.Union -> paramBase.options.any { typeDeclSubtypeOf(argBase, it) }
is TypeDecl.Intersection -> paramBase.options.all { typeDeclSubtypeOf(argBase, it) }
else -> {
val argClass = resolveTypeDeclObjClass(argBase) ?: return false
val paramClass = resolveTypeDeclObjClass(paramBase) ?: return false
argClass == paramClass || argClass.allParentsSet.contains(paramClass)
}
}
}
}
fun fail(argPos: Pos, expected: TypeDecl, got: TypeDecl) {
throw ScriptError(argPos, "argument type ${typeDeclName(got)} does not match ${typeDeclName(expected)}")
}
if (ellipsisIndex < 0) {
val limit = minOf(paramList.size, args.size)
for (i in 0 until limit) {
val arg = args[i]
val argType = argTypeDecl(arg) ?: continue
val paramType = paramList[i]
if (!typeDeclSubtypeOf(argType, paramType)) {
fail(arg.pos, paramType, argType)
}
}
return
}
val headCount = ellipsisIndex
val tailCount = paramList.size - ellipsisIndex - 1
val ellipsisType = paramList[ellipsisIndex] as TypeDecl.Ellipsis
val argCount = args.size
val headLimit = minOf(headCount, argCount)
for (i in 0 until headLimit) {
val arg = args[i]
val argType = argTypeDecl(arg) ?: continue
val paramType = paramList[i]
if (!typeDeclSubtypeOf(argType, paramType)) {
fail(arg.pos, paramType, argType)
}
}
val tailStartArg = maxOf(headCount, argCount - tailCount)
for (i in tailStartArg until argCount) {
val arg = args[i]
val paramType = paramList[paramList.size - (argCount - i)]
val argType = argTypeDecl(arg) ?: continue
if (!typeDeclSubtypeOf(argType, paramType)) {
fail(arg.pos, paramType, argType)
}
}
val ellipsisArgEnd = argCount - tailCount
for (i in headCount until ellipsisArgEnd) {
val arg = args[i]
val argType = if (arg.isSplat) {
val stmt = arg.value as? ExpressionStatement
val ref = stmt?.ref
ref?.let { inferElementTypeFromSpread(it) }
} else {
argTypeDecl(arg)
} ?: continue
if (!typeDeclSubtypeOf(argType, ellipsisType.elementType)) {
fail(arg.pos, ellipsisType.elementType, argType)
}
}
}
private fun collectTypeVarBindings( private fun collectTypeVarBindings(
paramType: TypeDecl, paramType: TypeDecl,
argType: TypeDecl, argType: TypeDecl,
@ -4739,10 +4948,11 @@ class Compiler(
TypeDecl.TypeAny, TypeDecl.TypeNullableAny -> true TypeDecl.TypeAny, TypeDecl.TypeNullableAny -> true
is TypeDecl.Union -> argType.options.all { typeDeclSatisfiesBound(it, bound) } is TypeDecl.Union -> argType.options.all { typeDeclSatisfiesBound(it, bound) }
is TypeDecl.Intersection -> argType.options.all { typeDeclSatisfiesBound(it, bound) } is TypeDecl.Intersection -> argType.options.all { typeDeclSatisfiesBound(it, bound) }
is TypeDecl.Ellipsis -> typeDeclSatisfiesBound(argType.elementType, bound)
else -> when (bound) { else -> when (bound) {
is TypeDecl.Union -> bound.options.any { typeDeclSatisfiesBound(argType, it) } is TypeDecl.Union -> bound.options.any { typeDeclSatisfiesBound(argType, it) }
is TypeDecl.Intersection -> bound.options.all { typeDeclSatisfiesBound(argType, it) } is TypeDecl.Intersection -> bound.options.all { typeDeclSatisfiesBound(argType, it) }
is TypeDecl.Simple, is TypeDecl.Generic, is TypeDecl.Function -> { is TypeDecl.Simple, is TypeDecl.Generic, is TypeDecl.Function, is TypeDecl.Ellipsis -> {
val argClass = resolveTypeDeclObjClass(argType) ?: return false val argClass = resolveTypeDeclObjClass(argType) ?: return false
val boundClass = resolveTypeDeclObjClass(bound) ?: return false val boundClass = resolveTypeDeclObjClass(bound) ?: return false
argClass == boundClass || argClass.allParentsSet.contains(boundClass) argClass == boundClass || argClass.allParentsSet.contains(boundClass)
@ -5122,6 +5332,8 @@ class Compiler(
receiverTypeName receiverTypeName
) )
} else { } else {
checkFunctionTypeCallArity(left, args, left.pos())
checkFunctionTypeCallTypes(left, args, left.pos())
checkGenericBoundsAtCall(left.name, args, left.pos()) checkGenericBoundsAtCall(left.name, args, left.pos())
CallRef(left, args, detectedBlockArgument, isOptional) CallRef(left, args, detectedBlockArgument, isOptional)
} }
@ -5144,6 +5356,8 @@ class Compiler(
receiverTypeName receiverTypeName
) )
} else { } else {
checkFunctionTypeCallArity(left, args, left.pos())
checkFunctionTypeCallTypes(left, args, left.pos())
checkGenericBoundsAtCall(left.name, args, left.pos()) checkGenericBoundsAtCall(left.name, args, left.pos())
CallRef(left, args, detectedBlockArgument, isOptional) CallRef(left, args, detectedBlockArgument, isOptional)
} }
@ -7643,6 +7857,7 @@ class Compiler(
is TypeDecl.Simple -> type.name is TypeDecl.Simple -> type.name
is TypeDecl.Generic -> type.name is TypeDecl.Generic -> type.name
is TypeDecl.Function -> "Callable" is TypeDecl.Function -> "Callable"
is TypeDecl.Ellipsis -> return resolveTypeDeclObjClass(type.elementType)
is TypeDecl.TypeVar -> return null is TypeDecl.TypeVar -> return null
is TypeDecl.Union -> return null is TypeDecl.Union -> return null
is TypeDecl.Intersection -> return null is TypeDecl.Intersection -> return null
@ -8206,12 +8421,18 @@ class Compiler(
} }
nameObjClass[name] = initObjClass nameObjClass[name] = initObjClass
} }
val declaredType = if (varTypeDecl == TypeDecl.TypeAny || varTypeDecl == TypeDecl.TypeNullableAny) {
null
} else {
varTypeDecl
}
return VarDeclStatement( return VarDeclStatement(
name, name,
isMutable, isMutable,
visibility, visibility,
initialExpression, initialExpression,
isTransient, isTransient,
declaredType,
slotIndex, slotIndex,
scopeId, scopeId,
start, start,

View File

@ -655,6 +655,7 @@ open class Scope(
isOverride: Boolean = false, isOverride: Boolean = false,
isTransient: Boolean = false, isTransient: Boolean = false,
callSignature: CallSignature? = null, callSignature: CallSignature? = null,
typeDecl: TypeDecl? = null,
fieldId: Int? = null, fieldId: Int? = null,
methodId: Int? = null methodId: Int? = null
): ObjRecord { ): ObjRecord {
@ -667,6 +668,7 @@ open class Scope(
isOverride = isOverride, isOverride = isOverride,
isTransient = isTransient, isTransient = isTransient,
callSignature = callSignature, callSignature = callSignature,
typeDecl = typeDecl,
memberName = name, memberName = name,
fieldId = fieldId, fieldId = fieldId,
methodId = methodId methodId = methodId

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,6 +30,10 @@ sealed class TypeDecl(val isNullable:Boolean = false) {
val returnType: TypeDecl, val returnType: TypeDecl,
val nullable: Boolean = false val nullable: Boolean = false
) : TypeDecl(nullable) ) : TypeDecl(nullable)
data class Ellipsis(
val elementType: TypeDecl,
val nullable: Boolean = false
) : 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 Union(val options: List<TypeDecl>, val nullable: Boolean = false) : TypeDecl(nullable) data class Union(val options: List<TypeDecl>, val nullable: Boolean = false) : TypeDecl(nullable)
data class Intersection(val options: List<TypeDecl>, val nullable: Boolean = false) : TypeDecl(nullable) data class Intersection(val options: List<TypeDecl>, val nullable: Boolean = false) : TypeDecl(nullable)

View File

@ -12,6 +12,7 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package net.sergeych.lyng package net.sergeych.lyng
@ -25,6 +26,7 @@ class VarDeclStatement(
val visibility: Visibility, val visibility: Visibility,
val initializer: Statement?, val initializer: Statement?,
val isTransient: Boolean, val isTransient: Boolean,
val typeDecl: TypeDecl?,
val slotIndex: Int?, val slotIndex: Int?,
val scopeId: Int?, val scopeId: Int?,
private val startPos: Pos, private val startPos: Pos,

View File

@ -5730,7 +5730,8 @@ class BytecodeCompiler(
stmt.name, stmt.name,
stmt.isMutable, stmt.isMutable,
stmt.visibility, stmt.visibility,
stmt.isTransient stmt.isTransient,
stmt.typeDecl
) )
) )
builder.emit(Opcode.DECL_LOCAL, declId, localSlot) builder.emit(Opcode.DECL_LOCAL, declId, localSlot)
@ -5757,7 +5758,8 @@ class BytecodeCompiler(
stmt.name, stmt.name,
stmt.isMutable, stmt.isMutable,
stmt.visibility, stmt.visibility,
stmt.isTransient stmt.isTransient,
stmt.typeDecl
) )
) )
builder.emit(Opcode.DECL_LOCAL, declId, scopeSlot) builder.emit(Opcode.DECL_LOCAL, declId, scopeSlot)
@ -5775,7 +5777,8 @@ class BytecodeCompiler(
stmt.name, stmt.name,
stmt.isMutable, stmt.isMutable,
stmt.visibility, stmt.visibility,
stmt.isTransient stmt.isTransient,
stmt.typeDecl
) )
) )
builder.emit(Opcode.DECL_LOCAL, declId, value.slot) builder.emit(Opcode.DECL_LOCAL, declId, value.slot)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2026 Sergey S. Chernov * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -12,12 +12,14 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package net.sergeych.lyng.bytecode package net.sergeych.lyng.bytecode
import net.sergeych.lyng.ArgsDeclaration import net.sergeych.lyng.ArgsDeclaration
import net.sergeych.lyng.Pos import net.sergeych.lyng.Pos
import net.sergeych.lyng.TypeDecl
import net.sergeych.lyng.Visibility import net.sergeych.lyng.Visibility
import net.sergeych.lyng.obj.ListLiteralRef import net.sergeych.lyng.obj.ListLiteralRef
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
@ -68,6 +70,7 @@ sealed class BytecodeConst {
val isMutable: Boolean, val isMutable: Boolean,
val visibility: Visibility, val visibility: Visibility,
val isTransient: Boolean, val isTransient: Boolean,
val typeDecl: TypeDecl?,
) : BytecodeConst() ) : BytecodeConst()
data class DelegatedDecl( data class DelegatedDecl(
val name: String, val name: String,

View File

@ -208,6 +208,7 @@ class BytecodeStatement private constructor(
stmt.visibility, stmt.visibility,
stmt.initializer?.let { unwrapDeep(it) }, stmt.initializer?.let { unwrapDeep(it) },
stmt.isTransient, stmt.isTransient,
stmt.typeDecl,
stmt.slotIndex, stmt.slotIndex,
stmt.scopeId, stmt.scopeId,
stmt.pos, stmt.pos,

View File

@ -2380,7 +2380,8 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() {
decl.isMutable, decl.isMutable,
decl.visibility, decl.visibility,
isTransient = decl.isTransient, isTransient = decl.isTransient,
type = ObjRecord.Type.Other type = ObjRecord.Type.Other,
typeDecl = decl.typeDecl
) )
) )
return return
@ -2392,7 +2393,8 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() {
decl.isMutable, decl.isMutable,
decl.visibility, decl.visibility,
isTransient = decl.isTransient, isTransient = decl.isTransient,
type = ObjRecord.Type.Other type = ObjRecord.Type.Other,
typeDecl = decl.typeDecl
) )
val moduleScope = frame.scope as? ModuleScope val moduleScope = frame.scope as? ModuleScope
if (moduleScope != null) { if (moduleScope != null) {

View File

@ -39,6 +39,7 @@ data class ObjRecord(
/** The receiver object to resolve this member against (for instance fields/methods). */ /** The receiver object to resolve this member against (for instance fields/methods). */
var receiver: Obj? = null, var receiver: Obj? = null,
val callSignature: net.sergeych.lyng.CallSignature? = null, val callSignature: net.sergeych.lyng.CallSignature? = null,
val typeDecl: net.sergeych.lyng.TypeDecl? = null,
val memberName: String? = null, val memberName: String? = null,
val fieldId: Int? = null, val fieldId: Int? = null,
val methodId: Int? = null, val methodId: Int? = null,

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2026 Sergey S. Chernov * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -12,6 +12,7 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
@ -63,6 +64,7 @@ internal fun matchesTypeDecl(scope: Scope, value: Obj, typeDecl: TypeDecl): Bool
if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name.substringAfterLast('.')) if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name.substringAfterLast('.'))
} }
is TypeDecl.Function -> value.isInstanceOf("Callable") is TypeDecl.Function -> value.isInstanceOf("Callable")
is TypeDecl.Ellipsis -> matchesTypeDecl(scope, value, typeDecl.elementType)
is TypeDecl.Union -> typeDecl.options.any { matchesTypeDecl(scope, value, it) } is TypeDecl.Union -> typeDecl.options.any { matchesTypeDecl(scope, value, it) }
is TypeDecl.Intersection -> typeDecl.options.all { matchesTypeDecl(scope, value, it) } is TypeDecl.Intersection -> typeDecl.options.all { matchesTypeDecl(scope, value, it) }
} }
@ -90,10 +92,11 @@ internal fun typeDeclIsSubtype(scope: Scope, left: TypeDecl, right: TypeDecl): B
return when (l) { return when (l) {
is TypeDecl.Union -> l.options.all { typeDeclIsSubtype(scope, it, r) } is TypeDecl.Union -> l.options.all { typeDeclIsSubtype(scope, it, r) }
is TypeDecl.Intersection -> l.options.any { typeDeclIsSubtype(scope, it, r) } is TypeDecl.Intersection -> l.options.any { typeDeclIsSubtype(scope, it, r) }
is TypeDecl.Ellipsis -> typeDeclIsSubtype(scope, l.elementType, r)
else -> when (r) { else -> when (r) {
is TypeDecl.Union -> r.options.any { typeDeclIsSubtype(scope, l, it) } is TypeDecl.Union -> r.options.any { typeDeclIsSubtype(scope, l, it) }
is TypeDecl.Intersection -> r.options.all { typeDeclIsSubtype(scope, l, it) } is TypeDecl.Intersection -> r.options.all { typeDeclIsSubtype(scope, l, it) }
is TypeDecl.Simple, is TypeDecl.Generic, is TypeDecl.Function -> { is TypeDecl.Simple, is TypeDecl.Generic, is TypeDecl.Function, is TypeDecl.Ellipsis -> {
val leftClass = resolveTypeDeclClass(scope, l) ?: return false val leftClass = resolveTypeDeclClass(scope, l) ?: return false
val rightClass = resolveTypeDeclClass(scope, r) ?: return false val rightClass = resolveTypeDeclClass(scope, r) ?: return false
leftClass == rightClass || leftClass.allParentsSet.contains(rightClass) leftClass == rightClass || leftClass.allParentsSet.contains(rightClass)
@ -171,6 +174,7 @@ private fun stripNullable(type: TypeDecl): TypeDecl {
} else { } else {
when (type) { when (type) {
is TypeDecl.Function -> type.copy(nullable = false) is TypeDecl.Function -> type.copy(nullable = false)
is TypeDecl.Ellipsis -> type.copy(nullable = false)
is TypeDecl.TypeVar -> type.copy(nullable = false) is TypeDecl.TypeVar -> type.copy(nullable = false)
is TypeDecl.Union -> type.copy(nullable = false) is TypeDecl.Union -> type.copy(nullable = false)
is TypeDecl.Intersection -> type.copy(nullable = false) is TypeDecl.Intersection -> type.copy(nullable = false)
@ -186,6 +190,7 @@ private fun makeNullable(type: TypeDecl): TypeDecl {
TypeDecl.TypeAny -> TypeDecl.TypeNullableAny TypeDecl.TypeAny -> TypeDecl.TypeNullableAny
TypeDecl.TypeNullableAny -> type TypeDecl.TypeNullableAny -> type
is TypeDecl.Function -> type.copy(nullable = true) is TypeDecl.Function -> type.copy(nullable = true)
is TypeDecl.Ellipsis -> type.copy(nullable = true)
is TypeDecl.TypeVar -> type.copy(nullable = true) is TypeDecl.TypeVar -> type.copy(nullable = true)
is TypeDecl.Union -> type.copy(nullable = true) is TypeDecl.Union -> type.copy(nullable = true)
is TypeDecl.Intersection -> type.copy(nullable = true) is TypeDecl.Intersection -> type.copy(nullable = true)
@ -200,6 +205,7 @@ private fun typeDeclKey(type: TypeDecl): String = when (type) {
is TypeDecl.Simple -> "S:${type.name}" is TypeDecl.Simple -> "S:${type.name}"
is TypeDecl.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>" is TypeDecl.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>"
is TypeDecl.Function -> "F:(${type.params.joinToString(",") { typeDeclKey(it) }})->${typeDeclKey(type.returnType)}" is TypeDecl.Function -> "F:(${type.params.joinToString(",") { typeDeclKey(it) }})->${typeDeclKey(type.returnType)}"
is TypeDecl.Ellipsis -> "E:${typeDeclKey(type.elementType)}"
is TypeDecl.TypeVar -> "V:${type.name}" is TypeDecl.TypeVar -> "V:${type.name}"
is TypeDecl.Union -> "U:${type.options.joinToString("|") { typeDeclKey(it) }}" is TypeDecl.Union -> "U:${type.options.joinToString("|") { typeDeclKey(it) }}"
is TypeDecl.Intersection -> "I:${type.options.joinToString("&") { typeDeclKey(it) }}" is TypeDecl.Intersection -> "I:${type.options.joinToString("&") { typeDeclKey(it) }}"
@ -216,6 +222,7 @@ private fun resolveTypeDeclClass(scope: Scope, type: TypeDecl): ObjClass? {
direct ?: scope[type.name.substringAfterLast('.')]?.value as? ObjClass direct ?: scope[type.name.substringAfterLast('.')]?.value as? ObjClass
} }
is TypeDecl.Function -> scope["Callable"]?.value as? ObjClass is TypeDecl.Function -> scope["Callable"]?.value as? ObjClass
is TypeDecl.Ellipsis -> resolveTypeDeclClass(scope, type.elementType)
is TypeDecl.TypeVar -> { is TypeDecl.TypeVar -> {
val bound = scope[type.name]?.value val bound = scope[type.name]?.value
when (bound) { when (bound) {

View File

@ -213,4 +213,18 @@ class BridgeBindingTest {
""".trimIndent() """.trimIndent()
) )
} }
// @Test
// fun testGlobalBindingsProperty() = runTest {
// eval("""
// val D: ()->Void = dynamic {
// get { name ->
// {
// args -> "name: "+name+" args="+args
// }
// }
// }
// assertEquals("name: foo args=[42,bar]", D.foo(42, "bar"))
// """.trimIndent())
// }
} }

View File

@ -16,6 +16,8 @@
*/ */
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.eval import net.sergeych.lyng.eval
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
@ -302,4 +304,61 @@ class TypesTest {
} }
""") """)
} }
@Test
fun testLambdaTypes1() = runTest {
val scope = Script.newScope()
// declare: ok
scope.eval("""
var l1: (Int,String)->String
""".trimIndent())
// this should be Lyng compile time exception
assertFailsWith<ScriptError> {
scope.eval("""
fun test() {
// compiler should detect that l1 us called with arguments that does not match
// declare type (Int,String)->String:
l1()
}
""".trimIndent())
}
}
@Test
fun testLambdaTypesEllipsis() = runTest {
val scope = Script.newScope()
scope.eval("""
var l2: (Int,Object...,String)->Real
var l4: (Int,String...,String)->Real
var l3: (...)->Int
""".trimIndent())
assertFailsWith<ScriptError> {
scope.eval("""
fun testTooFew() {
l2(1)
}
""".trimIndent())
}
assertFailsWith<ScriptError> {
scope.eval("""
fun testWrongHead() {
l2("x", "y")
}
""".trimIndent())
}
assertFailsWith<ScriptError> {
scope.eval("""
fun testWrongEllipsis() {
l4(1, 2, "x")
}
""".trimIndent())
}
scope.eval("""
fun testOk1() { l2(1, "x") }
fun testOk2() { l2(1, 2, 3, "x") }
fun testOk3() { l3() }
fun testOk4() { l3(1, true, "x") }
fun testOk5() { l4(1, "a", "b", "x") }
""".trimIndent())
}
} }