Add resolved export metadata and fix imported accessors

This commit is contained in:
Sergey Chernov 2026-07-27 22:35:49 +04:00
parent 1a22b60b7d
commit 898ebfa536
14 changed files with 530 additions and 37 deletions

View File

@ -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

View File

@ -509,5 +509,15 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
val isTransient: Boolean = false,
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
val annotations: List<DeclAnnotation> = 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
}
}

View File

@ -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<String, ObjClass>()
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<String, Int>()
@ -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<String, ObjClass>()
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<ResolvedFunctionMetadata> =
compileWithResolution(source, importManager, compileBytecode = false)
.resolvedFunctionMetadata()
suspend fun dryRun(source: Source, importManager: ImportProvider): ResolutionReport {
return CompileTimeResolver.dryRun(source, importManager)
}

View File

@ -50,6 +50,7 @@ data class FunctionDeclSpec(
val slotIndex: Int?,
val scopeId: Int?,
val startPos: Pos,
val resolvedMetadata: ResolvedFunctionMetadata,
)
internal suspend fun executeFunctionDecl(

View File

@ -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")

View File

@ -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<String>,
val typeParams: List<TypeDecl.TypeParam>,
val parameters: List<ResolvedFunctionParameter>,
val functionType: TypeDecl.Function,
) {
val returnType: TypeDecl get() = functionType.returnType
}

View File

@ -55,6 +55,10 @@ class Script(
) : Statement() {
fun statements(): List<Statement> = statements
/** Compiler-resolved metadata for top-level function declarations in source order. */
fun resolvedFunctionMetadata(): List<ResolvedFunctionMetadata> =
statements.filterIsInstance<FunctionDeclStatement>().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

View File

@ -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,

View File

@ -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
}
}

View File

@ -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) {

View File

@ -249,4 +249,39 @@ class ScriptImportPreparationTest {
val result = script.execute(manager.newStdScope()) as ObjString
assertEquals("<html><h3>Imported</h3></html>", 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(
"<extension-property-import>",
"""
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)
}
}

View File

@ -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)
}
}

View File

@ -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<T: Object = String>(
head: T,
rest: T...,
limit: Int = 7,
suffix: String = "!",
): List<T> { [] }
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<TypeDecl.Generic>(exported.returnType)
assertEquals("List", returnType.name)
assertEquals(1, returnType.args.size)
assertTypeVariable(returnType.args.single(), "T")
val functionType = assertIs<TypeDecl.Function>(exported.functionType)
assertNull(functionType.receiver)
assertTrue(functionType.contextReceivers.isEmpty())
assertEquals(4, functionType.params.size)
assertTypeVariable(functionType.params[0], "T")
val variadicType = assertIs<TypeDecl.Ellipsis>(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<TypeDecl.Function>(function.functionType).returnType, "Int")
}
private fun assertSimple(type: TypeDecl?, name: String, nullable: Boolean = false) {
val simple = assertIs<TypeDecl.Simple>(type)
assertEquals(name, simple.name)
assertEquals(nullable, simple.isNullable)
}
private fun assertTypeVariable(type: TypeDecl?, name: String, nullable: Boolean = false) {
val variable = assertIs<TypeDecl.TypeVar>(type)
assertEquals(name, variable.name)
assertEquals(nullable, variable.isNullable)
}
private fun assertGenericListOfTypeVariable(type: TypeDecl?, variableName: String) {
val generic = assertIs<TypeDecl.Generic>(type)
assertEquals("List", generic.name)
assertEquals(1, generic.args.size)
assertTypeVariable(generic.args.single(), variableName)
}
}

View File

@ -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<TypeDecl.Ellipsis>(metadata.functionType.params.single())
assertEquals("Int", (signatureType.elementType as TypeDecl.Simple).name)
}
}