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 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 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 `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
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 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:
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])
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
See also: [Testing and Assertions](Testing.md)

View File

@ -209,6 +209,9 @@ class Compiler(
if (plan.slots.containsKey(name)) continue
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
scopeSeedNames.add(name)
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
val instance = record.value as? ObjInstance
if (instance != null && nameObjClass[name] == null) {
nameObjClass[name] = instance.objClass
@ -285,6 +288,12 @@ class Compiler(
record.type == ObjRecord.Type.Delegated
)
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,
initializer = initStmt,
isTransient = false,
typeDecl = null,
slotIndex = slotIndex,
scopeId = scopeId,
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? {
val seedRecord = findSeedScopeRecord(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),
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.Intersection -> TypeDecl.Intersection(decl.options.map { transform(it) }, decl.isNullable)
else -> decl
@ -1515,6 +1545,7 @@ class Compiler(
declareSlotNameIn(plan, "$~", isMutable = true, isDelegated = false)
}
seedScope?.let { seedNameObjClassFromScope(it) }
seedScope?.let { seedNameTypeDeclFromScope(it) }
seedNameObjClassFromScope(importManager.rootScope)
if (shouldSeedDefaultStdlib()) {
val stdlib = importManager.prepareImport(start, "lyng.stdlib", null)
@ -2211,6 +2242,7 @@ class Compiler(
stmt.visibility,
init,
stmt.isTransient,
stmt.typeDecl,
stmt.slotIndex,
stmt.scopeId,
stmt.pos,
@ -3475,6 +3507,10 @@ class Compiler(
val ret = expandTypeAliases(type.returnType, pos, seen)
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 -> {
val options = type.options.map { expandTypeAliases(it, pos, seen) }
TypeDecl.Union(options, type.isNullable)
@ -3560,6 +3596,10 @@ class Compiler(
val ret = substituteTypeAliasTypeVars(type.returnType, bindings)
TypeDecl.Function(receiver, params, ret, type.nullable)
}
is TypeDecl.Ellipsis -> {
val elem = substituteTypeAliasTypeVars(type.elementType, bindings)
TypeDecl.Ellipsis(elem, type.nullable)
}
is TypeDecl.Union -> {
val options = type.options.map { substituteTypeAliasTypeVars(it, bindings) }
TypeDecl.Union(options, type.isNullable)
@ -3578,6 +3618,7 @@ class Compiler(
TypeDecl.TypeAny -> TypeDecl.TypeNullableAny
TypeDecl.TypeNullableAny -> type
is TypeDecl.Function -> type.copy(nullable = true)
is TypeDecl.Ellipsis -> type.copy(nullable = true)
is TypeDecl.TypeVar -> type.copy(nullable = true)
is TypeDecl.Union -> type.copy(nullable = true)
is TypeDecl.Intersection -> type.copy(nullable = true)
@ -3634,14 +3675,41 @@ class Compiler(
fun parseParamTypes(): List<Pair<TypeDecl, MiniTypeRef>> {
val params = mutableListOf<Pair<TypeDecl, MiniTypeRef>>()
var seenEllipsis = false
cc.skipWsTokens()
if (cc.peekNextNonWhitespace().type == Token.Type.RPAREN) {
cc.nextNonWhitespace()
return params
}
while (true) {
val (paramDecl, paramMini) = parseTypeExpressionWithMini()
params += paramDecl to paramMini
cc.skipWsTokens()
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()
when (sep.type) {
Token.Type.COMMA -> continue
@ -3952,6 +4020,7 @@ class Compiler(
is TypeDecl.Simple -> typeDecl.name
is TypeDecl.Generic -> typeDecl.name
is TypeDecl.Function -> "Callable"
is TypeDecl.Ellipsis -> "${typeDeclName(typeDecl.elementType)}..."
is TypeDecl.TypeVar -> typeDecl.name
is TypeDecl.Union -> typeDecl.options.joinToString(" | ") { typeDeclName(it) }
is TypeDecl.Intersection -> typeDecl.options.joinToString(" & ") { typeDeclName(it) }
@ -4129,6 +4198,7 @@ class Compiler(
val nullable = type.isNullable
val base = if (!nullable) type else when (type) {
is TypeDecl.Function -> type.copy(nullable = false)
is TypeDecl.Ellipsis -> type.copy(nullable = false)
is TypeDecl.TypeVar -> type.copy(nullable = false)
is TypeDecl.Union -> 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.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>"
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.Union -> "U:${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(
paramType: TypeDecl,
argType: TypeDecl,
@ -4739,10 +4948,11 @@ class Compiler(
TypeDecl.TypeAny, TypeDecl.TypeNullableAny -> true
is TypeDecl.Union -> 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) {
is TypeDecl.Union -> bound.options.any { 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 boundClass = resolveTypeDeclObjClass(bound) ?: return false
argClass == boundClass || argClass.allParentsSet.contains(boundClass)
@ -5122,6 +5332,8 @@ class Compiler(
receiverTypeName
)
} else {
checkFunctionTypeCallArity(left, args, left.pos())
checkFunctionTypeCallTypes(left, args, left.pos())
checkGenericBoundsAtCall(left.name, args, left.pos())
CallRef(left, args, detectedBlockArgument, isOptional)
}
@ -5144,6 +5356,8 @@ class Compiler(
receiverTypeName
)
} else {
checkFunctionTypeCallArity(left, args, left.pos())
checkFunctionTypeCallTypes(left, args, left.pos())
checkGenericBoundsAtCall(left.name, args, left.pos())
CallRef(left, args, detectedBlockArgument, isOptional)
}
@ -7643,6 +7857,7 @@ class Compiler(
is TypeDecl.Simple -> type.name
is TypeDecl.Generic -> type.name
is TypeDecl.Function -> "Callable"
is TypeDecl.Ellipsis -> return resolveTypeDeclObjClass(type.elementType)
is TypeDecl.TypeVar -> return null
is TypeDecl.Union -> return null
is TypeDecl.Intersection -> return null
@ -8206,12 +8421,18 @@ class Compiler(
}
nameObjClass[name] = initObjClass
}
val declaredType = if (varTypeDecl == TypeDecl.TypeAny || varTypeDecl == TypeDecl.TypeNullableAny) {
null
} else {
varTypeDecl
}
return VarDeclStatement(
name,
isMutable,
visibility,
initialExpression,
isTransient,
declaredType,
slotIndex,
scopeId,
start,

View File

@ -655,6 +655,7 @@ open class Scope(
isOverride: Boolean = false,
isTransient: Boolean = false,
callSignature: CallSignature? = null,
typeDecl: TypeDecl? = null,
fieldId: Int? = null,
methodId: Int? = null
): ObjRecord {
@ -667,6 +668,7 @@ open class Scope(
isOverride = isOverride,
isTransient = isTransient,
callSignature = callSignature,
typeDecl = typeDecl,
memberName = name,
fieldId = fieldId,
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");
* 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 nullable: Boolean = false
) : 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 Union(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.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
@ -25,6 +26,7 @@ class VarDeclStatement(
val visibility: Visibility,
val initializer: Statement?,
val isTransient: Boolean,
val typeDecl: TypeDecl?,
val slotIndex: Int?,
val scopeId: Int?,
private val startPos: Pos,

View File

@ -5730,7 +5730,8 @@ class BytecodeCompiler(
stmt.name,
stmt.isMutable,
stmt.visibility,
stmt.isTransient
stmt.isTransient,
stmt.typeDecl
)
)
builder.emit(Opcode.DECL_LOCAL, declId, localSlot)
@ -5757,7 +5758,8 @@ class BytecodeCompiler(
stmt.name,
stmt.isMutable,
stmt.visibility,
stmt.isTransient
stmt.isTransient,
stmt.typeDecl
)
)
builder.emit(Opcode.DECL_LOCAL, declId, scopeSlot)
@ -5775,7 +5777,8 @@ class BytecodeCompiler(
stmt.name,
stmt.isMutable,
stmt.visibility,
stmt.isTransient
stmt.isTransient,
stmt.typeDecl
)
)
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");
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.ArgsDeclaration
import net.sergeych.lyng.Pos
import net.sergeych.lyng.TypeDecl
import net.sergeych.lyng.Visibility
import net.sergeych.lyng.obj.ListLiteralRef
import net.sergeych.lyng.obj.Obj
@ -68,6 +70,7 @@ sealed class BytecodeConst {
val isMutable: Boolean,
val visibility: Visibility,
val isTransient: Boolean,
val typeDecl: TypeDecl?,
) : BytecodeConst()
data class DelegatedDecl(
val name: String,

View File

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

View File

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

View File

@ -39,6 +39,7 @@ data class ObjRecord(
/** The receiver object to resolve this member against (for instance fields/methods). */
var receiver: Obj? = null,
val callSignature: net.sergeych.lyng.CallSignature? = null,
val typeDecl: net.sergeych.lyng.TypeDecl? = null,
val memberName: String? = null,
val fieldId: 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");
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
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('.'))
}
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.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) {
is TypeDecl.Union -> l.options.all { 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) {
is TypeDecl.Union -> r.options.any { 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 rightClass = resolveTypeDeclClass(scope, r) ?: return false
leftClass == rightClass || leftClass.allParentsSet.contains(rightClass)
@ -171,6 +174,7 @@ private fun stripNullable(type: TypeDecl): TypeDecl {
} else {
when (type) {
is TypeDecl.Function -> type.copy(nullable = false)
is TypeDecl.Ellipsis -> type.copy(nullable = false)
is TypeDecl.TypeVar -> type.copy(nullable = false)
is TypeDecl.Union -> 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.TypeNullableAny -> type
is TypeDecl.Function -> type.copy(nullable = true)
is TypeDecl.Ellipsis -> type.copy(nullable = true)
is TypeDecl.TypeVar -> type.copy(nullable = true)
is TypeDecl.Union -> 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.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>"
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.Union -> "U:${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
}
is TypeDecl.Function -> scope["Callable"]?.value as? ObjClass
is TypeDecl.Ellipsis -> resolveTypeDeclClass(scope, type.elementType)
is TypeDecl.TypeVar -> {
val bound = scope[type.name]?.value
when (bound) {

View File

@ -213,4 +213,18 @@ class BridgeBindingTest {
""".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 net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.eval
import kotlin.test.Test
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())
}
}