Compare commits
2 Commits
1a22b60b7d
...
2f118a1fef
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f118a1fef | |||
| 898ebfa536 |
@ -1248,7 +1248,7 @@ scopeB()
|
||||
assertThrows { 42.description }
|
||||
```
|
||||
|
||||
This isolation ensures that libraries can use extensions internally without worrying about name collisions with other libraries or the user's code. When a module is imported using `use`, its top-level extensions become available in the importing scope.
|
||||
This isolation ensures that libraries can use extensions internally without worrying about name collisions with other libraries or the user's code. When a package is imported using `import`, its top-level extensions become available in the importing scope. An imported extension property's getter and setter retain the lexical scope of the package that declared them, so they can safely call that package's private helpers and bound `extern` functions and properties.
|
||||
|
||||
## dynamic symbols
|
||||
|
||||
|
||||
@ -65,6 +65,8 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
- Declaration-side variadic parameters use ellipsis suffix:
|
||||
- functions: `fun f(head, tail...) { ... }`
|
||||
- lambdas: `{ x, rest... -> ... }`
|
||||
- a typed parameter `values: T...` has signature type `T...`, but inside the callable `values`
|
||||
is a `List<T>` containing the collected arguments.
|
||||
- Call-side splats use `...expr` and are expanded by argument kind:
|
||||
- positional splat: `f(...[1,2,3])`
|
||||
- named splat: `f(...{ a: 1, b: 2 })` (map-style)
|
||||
|
||||
@ -41,6 +41,18 @@ Ellipsis can also appear in **function types** to denote a variadic position:
|
||||
|
||||
Ellipsis argument receives what is left from arguments after processing regular one that could be before or after.
|
||||
|
||||
For a typed declaration, the ellipsis type describes each argument while the name inside the
|
||||
function is a list of those arguments. Thus `values: Int...` is `Int...` in the function signature
|
||||
and `List<Int>` in the function body:
|
||||
|
||||
```lyng
|
||||
fun sum(values: Int...): Int {
|
||||
var result = 0
|
||||
for (value in values) result += value
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
Ellipsis could be a first argument:
|
||||
|
||||
fun testCountArgs(data...,size) {
|
||||
|
||||
@ -107,6 +107,79 @@ val run2 = script.execute(scope)
|
||||
`Scope.eval("...")` is the low-level shortcut that compiles and executes on the given scope.
|
||||
For most embedding use cases, prefer `session.eval("...")`.
|
||||
|
||||
### 2.2) Inspect function signatures without executing source
|
||||
|
||||
Declaration generators and other host tools can ask the compiler for semantic metadata about
|
||||
top-level functions without running the script:
|
||||
|
||||
```kotlin
|
||||
val source = Source(
|
||||
"service.lyng",
|
||||
"""
|
||||
@Export
|
||||
fun greet<T: Object = String>(
|
||||
first: T,
|
||||
rest: T...,
|
||||
punctuation: String = "!",
|
||||
): String = first.toString() + punctuation
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val functions = Compiler.resolveFunctionMetadata(
|
||||
source,
|
||||
Script.defaultImportManager,
|
||||
)
|
||||
val greet = functions.single()
|
||||
|
||||
println(greet.name) // greet
|
||||
println(greet.annotations) // [Export]
|
||||
println(greet.parameters[1].isEllipsis) // true
|
||||
println(greet.parameters[2].defaultSource) // "!"
|
||||
println(greet.returnType) // String
|
||||
```
|
||||
|
||||
`resolveFunctionMetadata(...)` parses the source and resolves its imports and types, but it does
|
||||
not execute the script, annotation functions, or default expressions. It returns
|
||||
`ResolvedFunctionMetadata` entries in source order. Each entry provides:
|
||||
|
||||
- the function name, visibility, annotations, and source positions;
|
||||
- generic parameters with their variance, bounds, and default types;
|
||||
- parameters, variadic markers, and the original text of default expressions when available;
|
||||
- the complete function type, including receivers, context receivers, and the declared or inferred
|
||||
return type.
|
||||
|
||||
For a variadic declaration such as `values: Int...`, `ResolvedFunctionParameter.type` is the
|
||||
element type `Int`, `isEllipsis` is `true`, and the parameter in `functionType` is `Int...`.
|
||||
Inside Lyng function code, the collected `values` variable has type `List<Int>`.
|
||||
|
||||
If you already compiled the source, obtain the same information without resolving it again:
|
||||
|
||||
```kotlin
|
||||
val script = Compiler.compile(source, Script.defaultImportManager)
|
||||
val functions = script.resolvedFunctionMetadata()
|
||||
```
|
||||
|
||||
Only top-level function declarations are returned. Use the Mini-AST APIs when tooling also needs
|
||||
nested declarations or a syntax-oriented representation.
|
||||
|
||||
### 2.3) Prepare imports separately from execution
|
||||
|
||||
Low-level hosts that own a scope can install a compiled script's imports without executing its
|
||||
statements:
|
||||
|
||||
```kotlin
|
||||
val script = Compiler.compile(source, importManager)
|
||||
val scope = importManager.newModule()
|
||||
|
||||
script.importInto(scope)
|
||||
val result = script.execute(scope)
|
||||
```
|
||||
|
||||
`importInto(...)` is idempotent for a scope and is useful when import preparation and execution
|
||||
belong to different host lifecycle phases. To create a fresh raw module with the script's imports
|
||||
already installed, use `script.instantiateModule(seedScope)`. When supplied, `seedScope` provides
|
||||
the import provider and any seed-bound imports used by the new module.
|
||||
|
||||
### 3) Preferred: bind extern globals from Kotlin
|
||||
|
||||
For module-level APIs, the default workflow is:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,28 @@ class Compiler(
|
||||
return script
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve semantic metadata for top-level functions in [source] without evaluating it.
|
||||
*
|
||||
* Functions are returned in source order. Their metadata includes annotations, visibility,
|
||||
* generic parameters, parameter declarations and defaults, receiver information, and the
|
||||
* declared or compiler-inferred return type. Annotation names are collected syntactically;
|
||||
* annotation functions and default expressions are not executed.
|
||||
*
|
||||
* This performs parsing, import resolution, and semantic compilation, but does not generate
|
||||
* executable bytecode. Use [compile] when the resulting script must also be executed.
|
||||
*
|
||||
* @param source named Lyng source whose top-level functions should be inspected
|
||||
* @param importManager provider used to resolve the source's imports and external types
|
||||
* @return metadata for each top-level function declaration in source order
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ data class FunctionDeclSpec(
|
||||
val slotIndex: Int?,
|
||||
val scopeId: Int?,
|
||||
val startPos: Pos,
|
||||
val resolvedMetadata: ResolvedFunctionMetadata,
|
||||
)
|
||||
|
||||
internal suspend fun executeFunctionDecl(
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* [type] is the declared element type for a variadic parameter, not its collected local type.
|
||||
* For example, `values: Int...` has a [type] of `Int`, [isEllipsis] is `true`, and the
|
||||
* corresponding entry in [ResolvedFunctionMetadata.functionType] is `Int...`.
|
||||
*
|
||||
* [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.
|
||||
*
|
||||
* @property name parameter name as written in the declaration
|
||||
* @property type compiler-resolved declared type, or the variadic element type when [isEllipsis]
|
||||
* is `true`
|
||||
* @property isEllipsis whether this parameter collects repeated arguments
|
||||
* @property hasDefault whether the declaration supplies a default expression
|
||||
* @property defaultSource exact source text of the default expression, when available
|
||||
* @property pos source position of the parameter declaration
|
||||
*/
|
||||
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.
|
||||
* The descriptor is intended for declaration generators and other embedding tools that need
|
||||
* semantic signatures without running the script.
|
||||
*
|
||||
* @property name declared function name
|
||||
* @property namePos source position of [name]
|
||||
* @property declarationPos source position at the start of the function declaration
|
||||
* @property visibility resolved declaration visibility
|
||||
* @property annotations annotation names in declaration order, without evaluating annotations
|
||||
* @property typeParams resolved generic type parameters, including bounds and defaults
|
||||
* @property parameters resolved parameters in declaration order
|
||||
* @property functionType complete callable type, including receiver, context receivers, variadic
|
||||
* markers, and the declared or inferred return type
|
||||
*/
|
||||
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,
|
||||
) {
|
||||
/** Declared or compiler-inferred return type of this function. */
|
||||
val returnType: TypeDecl get() = functionType.returnType
|
||||
}
|
||||
@ -19,17 +19,16 @@ package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.yield
|
||||
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
||||
import net.sergeych.lyng.bridge.bind
|
||||
import net.sergeych.lyng.bridge.bindObject
|
||||
import net.sergeych.lyng.bytecode.BytecodeLambdaCallable
|
||||
import net.sergeych.lyng.bytecode.CmdFunction
|
||||
import net.sergeych.lyng.bytecode.CmdVm
|
||||
import net.sergeych.lyng.bytecode.BytecodeLambdaCallable
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.serialization.ObjJsonClass
|
||||
import net.sergeych.lyng.serialization.bindSerializationFormat
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.stdlib_included.complexLyng
|
||||
import net.sergeych.lyng.stdlib_included.decimalLyng
|
||||
import net.sergeych.lyng.stdlib_included.legacyDigestLyng
|
||||
@ -55,6 +54,18 @@ class Script(
|
||||
) : Statement() {
|
||||
fun statements(): List<Statement> = statements
|
||||
|
||||
/**
|
||||
* Return compiler-resolved metadata for this script's top-level function declarations.
|
||||
*
|
||||
* Results retain source order and include declared or inferred signature types. This is a
|
||||
* read-only view of information produced while compiling the script; calling it does not
|
||||
* execute the script, its annotations, or parameter default expressions.
|
||||
*
|
||||
* @return metadata for every top-level function declaration 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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user