diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 2ce1c4e..95e69f7 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.5.9" +version = "1.5.10-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index 1151615..f3cea2a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -509,5 +509,15 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) val isTransient: Boolean = false, val annotationSpecs: List = emptyList(), val annotations: List = emptyList(), - ) + /** Exact source text of the default expression, when parsed from Lyng source. */ + val defaultSource: String? = null, + ) { + /** Type used at call sites, where ellipsis describes repeated arguments. */ + val signatureType: TypeDecl + get() = if (isEllipsis) TypeDecl.Ellipsis(type) else type + + /** Type visible inside the callable after repeated arguments are collected. */ + val localType: TypeDecl + get() = if (isEllipsis) TypeDecl.Generic("List", listOf(type), false) else type + } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 97acdb3..181193e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -19,6 +19,7 @@ package net.sergeych.lyng import net.sergeych.lyng.Compiler.Companion.compile import net.sergeych.lyng.bytecode.* +import net.sergeych.lyng.highlight.offsetOf import net.sergeych.lyng.miniast.* import net.sergeych.lyng.obj.* import net.sergeych.lyng.pacman.ImportManager @@ -3671,7 +3672,12 @@ class Compiler( val effectiveType = if (param.isEllipsis) TypeDecl.Ellipsis(rawType) else rawType lambdaParamTypeDecls += effectiveType if (effectiveType != TypeDecl.TypeAny && effectiveType != TypeDecl.TypeNullableAny) { - seedLambdaParamType(param.name, rawType) + val localType = if (param.isEllipsis) { + TypeDecl.Generic("List", listOf(rawType), false) + } else { + rawType + } + seedLambdaParamType(param.name, localType) } } } else { @@ -3753,7 +3759,7 @@ class Compiler( val returnClass = inferReturnClassFromStatement(body) val paramKnownClasses = mutableMapOf() argsDeclaration?.params?.forEach { param -> - val cls = resolveTypeDeclObjClass(param.type) ?: return@forEach + val cls = resolveTypeDeclObjClass(param.localType) ?: return@forEach paramKnownClasses[param.name] = cls } val returnLabels = label?.let { setOf(it) } ?: emptySet() @@ -4289,10 +4295,12 @@ class Compiler( } var defaultValue: Obj? = null - cc.ifNextIs(Token.Type.ASSIGN) { + var defaultSource: String? = null + cc.ifNextIs(Token.Type.ASSIGN) { assignment -> val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected default value expression") defaultValue = wrapBytecode(expr) + defaultSource = extractDefaultArgumentSource(assignment.pos) } val isEllipsis = cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true) result += ArgsDeclaration.Item( @@ -4305,7 +4313,8 @@ class Compiler( effectiveAccess, visibility, isTransient, - annotationSpecs = annotationSpecs + annotationSpecs = annotationSpecs, + defaultSource = defaultSource, ) // important: valid argument list continues with ',' and ends with '->' or ')' @@ -4343,6 +4352,48 @@ class Compiler( return ArgsDeclaration(result, endTokenType) } + /** + * Extracts a default expression from its original source. Token positions for strings point + * inside their quotes, so reconstructing the range from the first/last expression token loses + * delimiters. Scan the source instead, respecting nested groups and quoted literals. + */ + private fun extractDefaultArgumentSource(assignmentPos: Pos): String { + val source = assignmentPos.source + val text = source.text + val start = source.offsetOf(assignmentPos) + 1 + var index = start + var round = 0 + var square = 0 + var curly = 0 + var quote: Char? = null + var escaped = false + while (index < text.length) { + val ch = text[index] + val activeQuote = quote + if (activeQuote != null) { + if (escaped) escaped = false + else if (ch == '\\') escaped = true + else if (ch == activeQuote) quote = null + index++ + continue + } + when (ch) { + '"', '\'', '`' -> quote = ch + '(' -> round++ + ')' -> if (round == 0 && square == 0 && curly == 0) break else round-- + '[' -> square++ + ']' -> square-- + '{' -> curly++ + '}' -> curly-- + ',' -> if (round == 0 && square == 0 && curly == 0) break + '.' -> if (round == 0 && square == 0 && curly == 0 && + text.startsWith("...", index)) break + } + index++ + } + return text.substring(start, index).trim() + } + @Suppress("unused") private fun parseTypeDeclaration(): TypeDecl { return parseTypeDeclarationWithMini().first @@ -8666,7 +8717,7 @@ class Compiler( constructorArgsDeclaration?.params?.forEach { param -> if (param.accessType != null) { - val declClass = resolveTypeDeclObjClass(param.type) + val declClass = resolveTypeDeclObjClass(param.localType) if (declClass != null) { classFieldTypesByName.getOrPut(qualifiedName) { mutableMapOf() }[param.name] = declClass } @@ -8684,8 +8735,8 @@ class Compiler( val classParamTypeDeclMap = slotTypeDeclByScopeId.getOrPut(classSlotPlan.id) { mutableMapOf() } for (param in ctorDecl.params) { val slot = classSlotPlan.slots[param.name]?.index ?: continue - classParamTypeDeclMap[slot] = param.type - resolveTypeDeclObjClass(param.type)?.let { classParamTypeMap[slot] = it } + classParamTypeDeclMap[slot] = param.localType + resolveTypeDeclObjClass(param.localType)?.let { classParamTypeMap[slot] = it } } } val ctorForcedLocalSlots = LinkedHashMap() @@ -9984,7 +10035,7 @@ class Compiler( classMemberTypeDeclByName.getOrPut(parentContext.name) { mutableMapOf() }[name] = TypeDecl.Function( receiver = receiverTypeDecl, contextReceivers = contextReceiverTypeDecls, - params = argsDeclaration.params.map { it.type }, + params = argsDeclaration.params.map { it.signatureType }, returnType = returnTypeDecl ?: TypeDecl.TypeAny, nullable = false ) @@ -10087,19 +10138,19 @@ class Compiler( argsDeclaration = wrapDefaultArgsBytecode(argsDeclaration, forcedLocalSlots, paramSlotPlan.id) val capturePlan = CapturePlan(paramSlotPlan, isFunction = true, propagateToParentFunction = false) val rangeParamNames = argsDeclaration.params - .filter { isRangeType(it.type) } + .filter { isRangeType(it.localType) } .map { it.name } .toSet() val paramTypeMap = slotTypeByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() } val paramTypeDeclMap = slotTypeDeclByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() } for (param in argsDeclaration.params) { - val cls = resolveTypeDeclObjClass(param.type) ?: continue + val cls = resolveTypeDeclObjClass(param.localType) ?: continue val slot = paramSlotPlan.slots[param.name]?.index ?: continue paramTypeMap[slot] = cls } for (param in argsDeclaration.params) { val slot = paramSlotPlan.slots[param.name]?.index ?: continue - paramTypeDeclMap[slot] = param.type + paramTypeDeclMap[slot] = param.localType } // Parse function body while tracking declared locals to compute precise capacity hints @@ -10160,7 +10211,7 @@ class Compiler( val memberTypeDecl = TypeDecl.Function( receiver = receiverTypeDecl, contextReceivers = contextReceiverTypeDecls, - params = argsDeclaration.params.map { it.type }, + params = argsDeclaration.params.map { it.signatureType }, returnType = inferredReturnDecl ?: TypeDecl.TypeAny, nullable = false ) @@ -10194,7 +10245,7 @@ class Compiler( if (!compileBytecode) return@let stmt val paramKnownClasses = mutableMapOf() for (param in argsDeclaration.params) { - val cls = resolveTypeDeclObjClass(param.type) ?: continue + val cls = resolveTypeDeclObjClass(param.localType) ?: continue paramKnownClasses[param.name] = cls } wrapFunctionBytecode( @@ -10357,7 +10408,7 @@ class Compiler( typeDecl = if (isDelegated) null else TypeDecl.Function( receiver = receiverTypeDecl, contextReceivers = contextReceiverTypeDecls, - params = argsDeclaration.params.map { it.type }, + params = argsDeclaration.params.map { it.signatureType }, returnType = inferredReturnDecl ?: TypeDecl.TypeAny, nullable = false ), @@ -10366,7 +10417,32 @@ class Compiler( captureSlots = captureSlots, slotIndex = declSlotIndex, scopeId = declScopeId, - startPos = start + startPos = start, + resolvedMetadata = ResolvedFunctionMetadata( + name = name, + namePos = nameStartPos, + declarationPos = start, + visibility = visibility, + annotations = declarationAnnotationSpecs.map { it.name }, + typeParams = mergedTypeParamDecls, + parameters = argsDeclaration.params.map { parameter -> + ResolvedFunctionParameter( + name = parameter.name, + type = parameter.type, + isEllipsis = parameter.isEllipsis, + hasDefault = parameter.defaultValue != null, + defaultSource = parameter.defaultSource, + pos = parameter.pos, + ) + }, + functionType = TypeDecl.Function( + receiver = receiverTypeDecl, + contextReceivers = contextReceiverTypeDecls, + params = argsDeclaration.params.map { it.signatureType }, + returnType = inferredReturnDecl ?: TypeDecl.TypeAny, + nullable = false, + ), + ), ) val declaredFn = FunctionDeclStatement(spec) if (isStatic && parentIsClassBody) { @@ -11725,6 +11801,16 @@ class Compiler( return script } + /** + * Compile [source] and return semantic top-level function metadata without evaluating it. + */ + suspend fun resolveFunctionMetadata( + source: Source, + importManager: ImportProvider, + ): List = + compileWithResolution(source, importManager, compileBytecode = false) + .resolvedFunctionMetadata() + suspend fun dryRun(source: Source, importManager: ImportProvider): ResolutionReport { return CompileTimeResolver.dryRun(source, importManager) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt index 111f4ea..b0f00c9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt @@ -50,6 +50,7 @@ data class FunctionDeclSpec( val slotIndex: Int?, val scopeId: Int?, val startPos: Pos, + val resolvedMetadata: ResolvedFunctionMetadata, ) internal suspend fun executeFunctionDecl( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PropertyAccessorStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PropertyAccessorStatement.kt index 26be1b7..f12542f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PropertyAccessorStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PropertyAccessorStatement.kt @@ -18,6 +18,7 @@ package net.sergeych.lyng import net.sergeych.lyng.bytecode.CmdFrame import net.sergeych.lyng.bytecode.CmdVm +import net.sergeych.lyng.bytecode.seedFrameLocalsFromScope import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjNull @@ -34,6 +35,7 @@ class PropertyAccessorStatement( val bytecodeStmt = requireBytecodeBody(scope, body, "property accessor") val fn = bytecodeStmt.bytecodeFunction() val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, arguments -> + seedFrameLocalsFromScope(frame, scope) val slotPlan = fn.localSlotPlanByName() val slotIndex = slotPlan[argName] ?: scope.raiseIllegalState("property accessor argument $argName missing from slot plan") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ResolvedFunctionMetadata.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ResolvedFunctionMetadata.kt new file mode 100644 index 0000000..1f377d9 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ResolvedFunctionMetadata.kt @@ -0,0 +1,53 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * 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 + +/** + * Compiler-resolved metadata for a function parameter. + * + * [defaultSource] preserves the source expression for tools that generate declarations. It is + * null for parameters without defaults and may also be null for declarations assembled by a host + * rather than parsed from source; use [hasDefault] to distinguish those cases. + */ +data class ResolvedFunctionParameter( + val name: String, + val type: TypeDecl, + val isEllipsis: Boolean, + val hasDefault: Boolean, + val defaultSource: String?, + val pos: Pos, +) + +/** + * Semantic function declaration information produced by the compiler without evaluating source. + * + * Unlike the editor Mini-AST, this descriptor contains compiler-resolved parameter and return + * types, including an inferred return type when inference succeeds. Annotation names describe the + * declaration syntax; annotation functions are not executed while this metadata is collected. + */ +data class ResolvedFunctionMetadata( + val name: String, + val namePos: Pos, + val declarationPos: Pos, + val visibility: Visibility, + val annotations: List, + val typeParams: List, + val parameters: List, + val functionType: TypeDecl.Function, +) { + val returnType: TypeDecl get() = functionType.returnType +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 80f2b27..4cf43b6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -55,6 +55,10 @@ class Script( ) : Statement() { fun statements(): List = statements + /** Compiler-resolved metadata for top-level function declarations in source order. */ + fun resolvedFunctionMetadata(): List = + statements.filterIsInstance().map { it.spec.resolvedMetadata } + /** * Explicitly apply this script's import/module bindings to [scope] without executing the script. * This is intended for embedding scenarios where the host owns scope lifecycle and wants 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 1514a64..0f631e4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -3261,12 +3261,14 @@ class CmdDeclExtProperty(internal val constId: Int, internal val slot: Int) : Cm override suspend fun perform(frame: CmdFrame) { val decl = frame.fn.constants[constId] as? BytecodeConst.ExtensionPropertyDecl ?: error("DECL_EXT_PROPERTY expects ExtensionPropertyDecl at $constId") - val type = frame.ensureScope().resolveExtensionReceiverClass(decl.extTypeName) - frame.ensureScope().addExtension( + val declarationScope = frame.ensureScope() + val property = decl.property.withDeclarationScope(declarationScope) + val type = declarationScope.resolveExtensionReceiverClass(decl.extTypeName) + declarationScope.addExtension( type, - decl.property.name, + property.name, ObjRecord( - decl.property, + property, isMutable = false, visibility = decl.visibility, writeVisibility = decl.setterVisibility, @@ -3274,9 +3276,9 @@ class CmdDeclExtProperty(internal val constId: Int, internal val slot: Int) : Cm type = ObjRecord.Type.Property ) ) - val getterName = extensionPropertyGetterName(decl.extTypeName, decl.property.name) - val getterWrapper = ObjExtensionPropertyGetterCallable(decl.property.name, decl.property) - frame.ensureScope().addItem( + val getterName = extensionPropertyGetterName(decl.extTypeName, property.name) + val getterWrapper = ObjExtensionPropertyGetterCallable(property.name, property) + declarationScope.addItem( getterName, false, getterWrapper, @@ -3288,10 +3290,10 @@ class CmdDeclExtProperty(internal val constId: Int, internal val slot: Int) : Cm if (getterLocal != null) { frame.setObjUnchecked(frame.fn.scopeSlotCount + getterLocal, getterWrapper) } - if (decl.property.setter != null) { - val setterName = extensionPropertySetterName(decl.extTypeName, decl.property.name) - val setterWrapper = ObjExtensionPropertySetterCallable(decl.property.name, decl.property) - frame.ensureScope() + if (property.setter != null) { + val setterName = extensionPropertySetterName(decl.extTypeName, property.name) + val setterWrapper = ObjExtensionPropertySetterCallable(property.name, property) + declarationScope .addItem( setterName, false, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt index eba42a4..d95ac90 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt @@ -56,9 +56,9 @@ internal fun trySeedFrameLocalsFromScopeFast(frame: CmdFrame, scope: Scope): Boo record.type == ObjRecord.Type.Property -> return false record.value is ObjProperty -> return false else -> when (val direct = record.value) { - is FrameSlotRef -> direct.resolvedCaptureValueOrNull() ?: return false - is RecordSlotRef -> direct.resolvedCaptureValueOrNull() ?: return false - is ScopeSlotRef -> direct.resolvedCaptureValueOrNull() ?: return false + is FrameSlotRef -> if (record.isMutable) direct else direct.resolvedCaptureValueOrNull() ?: return false + is RecordSlotRef -> if (record.isMutable) direct else direct.resolvedCaptureValueOrNull() ?: return false + is ScopeSlotRef -> if (record.isMutable) direct else direct.resolvedCaptureValueOrNull() ?: return false else -> direct } } @@ -86,9 +86,9 @@ internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) { scope.resolve(record, name) } else { when (val direct = record.value) { - is net.sergeych.lyng.FrameSlotRef -> direct.resolvedCaptureValueOrNull() ?: direct - is net.sergeych.lyng.RecordSlotRef -> direct.resolvedCaptureValueOrNull() ?: direct - is net.sergeych.lyng.ScopeSlotRef -> direct.resolvedCaptureValueOrNull() ?: direct + is net.sergeych.lyng.FrameSlotRef -> if (record.isMutable) direct else direct.resolvedCaptureValueOrNull() ?: direct + is net.sergeych.lyng.RecordSlotRef -> if (record.isMutable) direct else direct.resolvedCaptureValueOrNull() ?: direct + is net.sergeych.lyng.ScopeSlotRef -> if (record.isMutable) direct else direct.resolvedCaptureValueOrNull() ?: direct else -> direct } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjProperty.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjProperty.kt index a653bab..8119d7b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjProperty.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjProperty.kt @@ -32,15 +32,21 @@ import net.sergeych.lyng.executeBytecodeWithSeed class ObjProperty( val name: String, val getter: Obj?, - val setter: Obj? + val setter: Obj?, + private val declarationScope: Scope? = null ) : Obj() { + fun withDeclarationScope(scope: Scope): ObjProperty = + ObjProperty(name, getter, setter, scope) + suspend fun callGetter(scope: Scope, instance: Obj, declaringClass: ObjClass? = null): Obj { val g = getter ?: scope.raiseError("property $name has no getter") // Execute getter in a child scope of the instance with 'this' properly set // Match extension function behavior (access to instance scope + call scope). val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope) - val execScope = scope.applyClosure(instanceScope).createChildScope(newThisObj = instance) + val receiverCallScope = scope.applyClosure(instanceScope) + val execBase = declarationScope?.let(receiverCallScope::applyClosure) ?: receiverCallScope + val execScope = execBase.createChildScope(newThisObj = instance) execScope.currentClassCtx = declaringClass (g as? BytecodeCallable)?.callOnFast(execScope)?.let { return it } return when (g) { @@ -60,7 +66,9 @@ class ObjProperty( // Execute setter in a child scope of the instance with 'this' properly set and the value as an argument // Match extension function behavior (access to instance scope + call scope). val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope) - val execScope = scope.applyClosure(instanceScope).createChildScope(args = Arguments(value), newThisObj = instance) + val receiverCallScope = scope.applyClosure(instanceScope) + val execBase = declarationScope?.let(receiverCallScope::applyClosure) ?: receiverCallScope + val execScope = execBase.createChildScope(args = Arguments(value), newThisObj = instance) execScope.currentClassCtx = declaringClass (s as? BytecodeCallable)?.callOnFast(execScope)?.let { return } when (s) { diff --git a/lynglib/src/commonTest/kotlin/ScriptImportPreparationTest.kt b/lynglib/src/commonTest/kotlin/ScriptImportPreparationTest.kt index bb0229d..4eb21b1 100644 --- a/lynglib/src/commonTest/kotlin/ScriptImportPreparationTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptImportPreparationTest.kt @@ -249,4 +249,39 @@ class ScriptImportPreparationTest { val result = script.execute(manager.newStdScope()) as ObjString assertEquals("

Imported

", result.value) } + + @Test + fun importedExtensionPropertyAccessorsUseDeclaringModuleScope() = runTest { + val manager = Script.defaultImportManager.copy().apply { + addTextPackages( + """ + package imported.extprop + + private var prefix = "module" + + private fun decorate(value: String): String = prefix + ":" + value + + var String.decorated: String + get() = decorate(this) + set(value) { prefix = value } + """.trimIndent() + ) + } + val script = Compiler.compile( + Source( + "", + """ + import imported.extprop + + val before = "ok".decorated + "ignored".decorated = "updated" + before + "|" + "ok".decorated + """.trimIndent() + ), + manager + ) + + val result = script.execute(manager.newStdScope()) as ObjString + assertEquals("module:ok|updated:ok", result.value) + } } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ImportedExternExtensionPropertyTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ImportedExternExtensionPropertyTest.kt new file mode 100644 index 0000000..703bef3 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ImportedExternExtensionPropertyTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * 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 + +import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.bridge.globalBinder +import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjString +import kotlin.test.Test +import kotlin.test.assertEquals + +class ImportedExternExtensionPropertyTest { + @Test + fun importedExtensionGetterCanCallExternFromItsImportedRuntimeModule() = runTest { + val imports = Script.defaultImportManager.copy() + imports.addPackage("test.contract.runtime") { module -> + module.eval( + """ + package test.contract.runtime + + class ContractContext(val label: String) + class ContractRegistryBase + class ContractRegistry : ContractRegistryBase + + extern val Contracts: ContractRegistry + extern fun contractHandle(nameOrId: String): ContractContext + """.trimIndent() + ) + + val contextClass = module.resolve( + requireNotNull(module["ContractContext"]), + "ContractContext" + ) as ObjClass + val registryClass = module.resolve( + requireNotNull(module["ContractRegistry"]), + "ContractRegistry" + ) as ObjClass + val context = contextClass.callOn( + module.createChildScope(args = Arguments(ObjString("bound"))) + ) + val registry = registryClass.callOn(module.createChildScope()) + module.globalBinder().bindGlobalVarRaw("Contracts", get = { registry }) + module.globalBinder().bindGlobalFunRaw("contractHandle") { _, args -> + assertEquals("Alpha", (args.firstAndOnly() as ObjString).value) + context + } + } + imports.addTextPackages( + """ + package test.contract.Alpha + import test.contract.runtime + + class AlphaContractAbi(val target: ContractContext) + + val ContractRegistryBase.AlphaHandle: ContractContext + get() = contractHandle("Alpha") + + val ContractRegistryBase.Alpha: AlphaContractAbi + get() = AlphaContractAbi(contractHandle("Alpha")) + """.trimIndent() + ) + + val scope = imports.newStdScope() + val handleResult = scope.eval( + """ + import test.contract.runtime + import test.contract.Alpha + + Contracts.AlphaHandle.label + """.trimIndent() + ) as ObjString + assertEquals("bound", handleResult.value) + + val result = scope.eval( + """ + import test.contract.runtime + import test.contract.Alpha + + Contracts.Alpha.target.label + """.trimIndent() + ) as ObjString + + assertEquals("bound", result.value) + } +} diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ResolvedFunctionMetadataTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ResolvedFunctionMetadataTest.kt new file mode 100644 index 0000000..5cae672 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ResolvedFunctionMetadataTest.kt @@ -0,0 +1,135 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * 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 + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class ResolvedFunctionMetadataTest { + + @Test + fun resolvesCompleteExportSignatureWithoutExecutingSource() = runTest { + val source = Source( + "metadata.lyng", + """ + @Export + fun exported( + head: T, + rest: T..., + limit: Int = 7, + suffix: String = "!", + ): List { [] } + + fun helper(value: String): String { value } + """.trimIndent() + ) + + val functions = Compiler.resolveFunctionMetadata(source, Script.defaultImportManager) + val exported = functions.single { it.name == "exported" } + val helper = functions.single { it.name == "helper" } + + assertEquals(listOf("Export"), exported.annotations) + assertTrue(helper.annotations.isEmpty()) + + assertEquals("metadata.lyng", exported.namePos.source.fileName) + assertSame(source, exported.namePos.source) + assertEquals("exported", exported.namePos.currentLine.substring(exported.namePos.column, exported.namePos.column + 8)) + assertSame(source, exported.declarationPos.source) + + val typeParameter = exported.typeParams.single() + assertEquals("T", typeParameter.name) + assertEquals(TypeDecl.Variance.Invariant, typeParameter.variance) + assertSimple(typeParameter.bound, "Object") + assertSimple(typeParameter.defaultType, "String") + + assertEquals(listOf("head", "rest", "limit", "suffix"), exported.parameters.map { it.name }) + assertTypeVariable(exported.parameters[0].type, "T") + assertFalse(exported.parameters[0].isEllipsis) + assertFalse(exported.parameters[0].hasDefault) + assertNull(exported.parameters[0].defaultSource) + + assertTypeVariable(exported.parameters[1].type, "T") + assertTrue(exported.parameters[1].isEllipsis) + assertFalse(exported.parameters[1].hasDefault) + assertNull(exported.parameters[1].defaultSource) + + assertSimple(exported.parameters[2].type, "Int") + assertFalse(exported.parameters[2].isEllipsis) + assertTrue(exported.parameters[2].hasDefault) + assertEquals("7", exported.parameters[2].defaultSource) + assertTrue(exported.parameters[3].hasDefault) + assertEquals("\"!\"", exported.parameters[3].defaultSource) + + val returnType = assertIs(exported.returnType) + assertEquals("List", returnType.name) + assertEquals(1, returnType.args.size) + assertTypeVariable(returnType.args.single(), "T") + + val functionType = assertIs(exported.functionType) + assertNull(functionType.receiver) + assertTrue(functionType.contextReceivers.isEmpty()) + assertEquals(4, functionType.params.size) + assertTypeVariable(functionType.params[0], "T") + val variadicType = assertIs(functionType.params[1]) + assertTypeVariable(variadicType.elementType, "T") + assertSimple(functionType.params[2], "Int") + assertGenericListOfTypeVariable(functionType.returnType, "T") + } + + @Test + fun reportsCompilerInferredReturnType() = runTest { + val source = Source( + "inferred.lyng", + """ + @Export + fun increment(value: Int) = value + 1 + """.trimIndent() + ) + + val function = Compiler.resolveFunctionMetadata(source, Script.defaultImportManager).single() + + assertEquals("increment", function.name) + assertEquals(listOf("Export"), function.annotations) + assertSimple(function.returnType, "Int") + assertSimple(assertIs(function.functionType).returnType, "Int") + } + + private fun assertSimple(type: TypeDecl?, name: String, nullable: Boolean = false) { + val simple = assertIs(type) + assertEquals(name, simple.name) + assertEquals(nullable, simple.isNullable) + } + + private fun assertTypeVariable(type: TypeDecl?, name: String, nullable: Boolean = false) { + val variable = assertIs(type) + assertEquals(name, variable.name) + assertEquals(nullable, variable.isNullable) + } + + private fun assertGenericListOfTypeVariable(type: TypeDecl?, variableName: String) { + val generic = assertIs(type) + assertEquals("List", generic.name) + assertEquals(1, generic.args.size) + assertTypeVariable(generic.args.single(), variableName) + } +} diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/VariadicParameterTypingTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/VariadicParameterTypingTest.kt new file mode 100644 index 0000000..71fd91d --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/VariadicParameterTypingTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * 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 + +import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.obj.ObjString +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class VariadicParameterTypingTest { + @Test + fun typedVariadicParameterIsAListInsideFunctionBody() = runTest { + val result = Script.newScope().eval( + """ + fun join(prefix: String, values: Int...): String = + prefix + values.joinToString(",") + + fun sum(values: Int...): Int { + var result = 0 + for (value in values) result += value + return result + } + + join("n=", 1, 2, 3) + "|" + sum(4, 5, 6) + """.trimIndent() + ) as ObjString + + assertEquals("n=1,2,3|15", result.value) + } + + @Test + fun variadicMetadataKeepsElementTypeInEllipsisSignature() = runTest { + val metadata = Compiler.resolveFunctionMetadata( + Source("variadic-metadata", "fun join(values: Int...): String = values.joinToString(\",\")"), + Script.defaultImportManager, + ).single() + + val parameter = metadata.parameters.single() + assertEquals("Int", (parameter.type as TypeDecl.Simple).name) + val signatureType = assertIs(metadata.functionType.params.single()) + assertEquals("Int", (signatureType.elementType as TypeDecl.Simple).name) + } +}