From d82302dd01f7024d6d83924e4c69efed92ad8718 Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 19 Feb 2026 11:12:22 +0300 Subject: [PATCH] Add support for variadic function types and ellipsis in type declarations, stricter lamda types compile-time checks and object binding for Kotlin bridges --- docs/declaring_arguments.md | 5 + docs/embedding.md | 39 +++ docs/tutorial.md | 12 + .../kotlin/net/sergeych/lyng/Compiler.kt | 227 +++++++++++++++++- .../kotlin/net/sergeych/lyng/Scope.kt | 2 + .../kotlin/net/sergeych/lyng/TypeDecl.kt | 6 +- .../net/sergeych/lyng/VarDeclStatement.kt | 2 + .../lyng/bytecode/BytecodeCompiler.kt | 9 +- .../sergeych/lyng/bytecode/BytecodeConst.kt | 5 +- .../lyng/bytecode/BytecodeStatement.kt | 1 + .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 6 +- .../kotlin/net/sergeych/lyng/obj/ObjRecord.kt | 1 + .../net/sergeych/lyng/obj/ObjTypeExpr.kt | 11 +- .../commonTest/kotlin/BridgeBindingTest.kt | 14 ++ lynglib/src/commonTest/kotlin/TypesTest.kt | 59 +++++ 15 files changed, 387 insertions(+), 12 deletions(-) diff --git a/docs/declaring_arguments.md b/docs/declaring_arguments.md index 92faaa9..3f157e6 100644 --- a/docs/declaring_arguments.md +++ b/docs/declaring_arguments.md @@ -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: diff --git a/docs/embedding.md b/docs/embedding.md index 59cf657..7eac8ee 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -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(0).value + val b = args.requiredArg(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. diff --git a/docs/tutorial.md b/docs/tutorial.md index 73259e7..90be4e5 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 6169c45..26587f4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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> { val params = mutableListOf>() + 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, + 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() + 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, + pos: Pos + ) { + val decl = (resolveReceiverTypeDecl(target) as? TypeDecl.Function) + ?: seedTypeDeclFromRef(target) as? TypeDecl.Function + ?: return + val paramList = mutableListOf() + 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, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 278c1ea..78e4590 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt index 6c63233..5edd68c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt @@ -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, val nullable: Boolean = false) : TypeDecl(nullable) data class Intersection(val options: List, val nullable: Boolean = false) : TypeDecl(nullable) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt index 26f318e..26dc457 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt @@ -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, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 3d7201c..7b5505d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt index 467017f..90b8fce 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt @@ -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, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index 26ca319..e16e534 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -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, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index 5a16616..c668c17 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -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) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt index 26ebd87..e6f3216 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt @@ -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, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjTypeExpr.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjTypeExpr.kt index 61b7e7d..00494cc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjTypeExpr.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjTypeExpr.kt @@ -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) { diff --git a/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt b/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt index ca44b87..385ec85 100644 --- a/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt +++ b/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt @@ -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()) +// } } diff --git a/lynglib/src/commonTest/kotlin/TypesTest.kt b/lynglib/src/commonTest/kotlin/TypesTest.kt index aef0618..7924e8e 100644 --- a/lynglib/src/commonTest/kotlin/TypesTest.kt +++ b/lynglib/src/commonTest/kotlin/TypesTest.kt @@ -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 { + 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 { + scope.eval(""" + fun testTooFew() { + l2(1) + } + """.trimIndent()) + } + assertFailsWith { + scope.eval(""" + fun testWrongHead() { + l2("x", "y") + } + """.trimIndent()) + } + assertFailsWith { + 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()) + } }