Add DB decode annotations and preserved declaration metadata

This commit is contained in:
sergeych committed 2026-04-25 13:36:33 +03:00
1 parent 2abe7e2f96
commit 50e34e520e
23 files changed
+1690 -55

No files matched your search

@@ -88,7 +88,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
a.visibility ?: defaultVisibility,
recordType = recordType,
declaringClass = declaringClass,
isTransient = a.isTransient
isTransient = a.isTransient,
annotations = a.annotations
)
}
return
@@ -108,7 +109,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
a.visibility ?: defaultVisibility,
recordType = recordType,
declaringClass = declaringClass,
isTransient = a.isTransient
isTransient = a.isTransient,
annotations = a.annotations
)
}
@@ -505,5 +507,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
val accessType: AccessType? = null,
val visibility: Visibility? = null,
val isTransient: Boolean = false,
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
val annotations: List<DeclAnnotation> = emptyList(),
)
}
@@ -41,6 +41,17 @@ data class ClassDeclSpec(
val initScope: List<Statement>,
)
private suspend fun evaluateConstructorAnnotations(scope: Scope, args: ArgsDeclaration?): ArgsDeclaration? {
if (args == null) return null
if (args.params.none { it.annotationSpecs.isNotEmpty() }) return args
return args.copy(
params = args.params.map { item ->
if (item.annotationSpecs.isEmpty()) item
else item.copy(annotations = item.annotationSpecs.evaluateDeclAnnotations(scope))
}
)
}
internal suspend fun executeClassDecl(
scope: Scope,
spec: ClassDeclSpec,
@@ -61,7 +72,7 @@ internal suspend fun executeClassDecl(
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray())
newClass.isAnonymous = spec.isAnonymous
newClass.isSingletonObject = true
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
newClass.constructorMeta = evaluateConstructorAnnotations(scope, ArgsDeclaration(emptyList(), Token.Type.RPAREN))
for (i in parentClasses.indices) {
val argsList = spec.baseSpecs[i].args
if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList
@@ -86,6 +97,7 @@ internal suspend fun executeClassDecl(
}
if (spec.isExtern) {
val evaluatedConstructorArgs = evaluateConstructorAnnotations(scope, spec.constructorArgs)
val parentClasses = spec.baseSpecs.mapNotNull { baseSpec ->
val rec = scope[baseSpec.name]
val cls = rec?.value as? ObjClass
@@ -106,8 +118,8 @@ internal suspend fun executeClassDecl(
}
val stub = resolved ?: ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).apply {
this.isAbstract = true
constructorMeta = spec.constructorArgs
spec.constructorArgs?.params?.forEach { p ->
constructorMeta = evaluatedConstructorArgs
evaluatedConstructorArgs?.params?.forEach { p ->
if (p.accessType != null) {
createField(
p.name,
@@ -118,6 +130,7 @@ internal suspend fun executeClassDecl(
pos = Pos.builtIn,
isTransient = p.isTransient,
type = ObjRecord.Type.ConstructorField,
annotations = p.annotations,
fieldId = spec.constructorFieldIds?.get(p.name)
)
}
@@ -161,16 +174,17 @@ internal suspend fun executeClassDecl(
}
}
val evaluatedConstructorArgs = evaluateConstructorAnnotations(scope, spec.constructorArgs)
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).also {
it.isAbstract = spec.isAbstract
it.isClosed = spec.isClosed
it.instanceConstructor = constructorCode
it.constructorMeta = spec.constructorArgs
it.constructorMeta = evaluatedConstructorArgs
for (i in parentClasses.indices) {
val argsList = spec.baseSpecs[i].args
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
}
spec.constructorArgs?.params?.forEach { p ->
evaluatedConstructorArgs?.params?.forEach { p ->
if (p.accessType != null) {
it.createField(
p.name,
@@ -181,6 +195,7 @@ internal suspend fun executeClassDecl(
pos = Pos.builtIn,
isTransient = p.isTransient,
type = ObjRecord.Type.ConstructorField,
annotations = p.annotations,
fieldId = spec.constructorFieldIds?.get(p.name)
)
}
@@ -38,6 +38,7 @@ class ClassInstanceFieldDeclStatement(
val isClosed: Boolean,
val isOverride: Boolean,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
val fieldId: Int?,
val initStatement: Statement?,
override val pos: Pos,
@@ -56,6 +57,7 @@ class ClassInstancePropertyDeclStatement(
val isClosed: Boolean,
val isOverride: Boolean,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
val prop: ObjProperty,
val methodId: Int?,
val initStatement: Statement?,
@@ -75,6 +77,7 @@ class ClassInstanceDelegatedDeclStatement(
val isClosed: Boolean,
val isOverride: Boolean,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
val methodId: Int?,
val initStatement: Statement?,
override val pos: Pos,
@@ -32,6 +32,7 @@ class ClassStaticFieldInitStatement(
val initializer: Statement?,
val isDelegated: Boolean,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
private val startPos: Pos,
) : Statement() {
override val pos: Pos = startPos
@@ -39,6 +40,7 @@ class ClassStaticFieldInitStatement(
override suspend fun execute(scope: Scope): Obj {
val initValue = initializer?.let { execBytecodeOnly(scope, it, "class static field init") }?.byValueCopy()
?: ObjNull
val annotations = annotationSpecs.evaluateDeclAnnotations(scope)
val cls = scope.thisObj as? ObjClass
?: scope.raiseIllegalState("static field init requires class scope")
return if (isDelegated) {
@@ -61,7 +63,8 @@ class ClassStaticFieldInitStatement(
writeVisibility,
startPos,
isTransient = isTransient,
type = ObjRecord.Type.Delegated
type = ObjRecord.Type.Delegated,
annotations = annotations
).apply {
delegate = finalDelegate
}
@@ -72,7 +75,8 @@ class ClassStaticFieldInitStatement(
visibility,
writeVisibility,
recordType = ObjRecord.Type.Delegated,
isTransient = isTransient
isTransient = isTransient,
annotations = annotations
).apply {
delegate = finalDelegate
}
@@ -85,7 +89,8 @@ class ClassStaticFieldInitStatement(
visibility,
writeVisibility,
startPos,
isTransient = isTransient
isTransient = isTransient,
annotations = annotations
)
scope.addItem(
name,
@@ -94,7 +99,8 @@ class ClassStaticFieldInitStatement(
visibility,
writeVisibility,
recordType = ObjRecord.Type.Field,
isTransient = isTransient
isTransient = isTransient,
annotations = annotations
)
initValue
}
@@ -2014,6 +2014,7 @@ class Compiler(
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
private var isTransientFlag: Boolean = false
private val pendingDeclAnnotations: MutableList<ParsedDeclAnnotation> = mutableListOf()
private var lastLabel: String? = null
private val strictSlotRefs: Boolean = settings.strictSlotRefs
private val allowUnresolvedRefs: Boolean = settings.allowUnresolvedRefs
@@ -2689,6 +2690,7 @@ class Compiler(
lastAnnotation = null
lastLabel = null
isTransientFlag = false
pendingDeclAnnotations.clear()
while (true) {
val t = cc.next()
return when (t.type) {
@@ -2706,15 +2708,17 @@ class Compiler(
}
Token.Type.ATLABEL -> {
val label = t.value
if (label == "Transient") {
val parsedAnnotation = parseDeclAnnotation(t)
if (parsedAnnotation.name == "Transient") {
isTransientFlag = true
continue
}
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
lastLabel = label
lastLabel = parsedAnnotation.name
}
pendingDeclAnnotations += parsedAnnotation
if (parsedAnnotation.name != "Transient") {
lastAnnotation = parsedAnnotation.toStatementAnnotation()
}
lastAnnotation = parseAnnotation(t)
continue
}
@@ -4033,11 +4037,17 @@ class Compiler(
Token.Type.ID, Token.Type.ATLABEL -> {
var isTransient = false
if (t.type == Token.Type.ATLABEL) {
if (t.value == "Transient") {
val annotationSpecs = mutableListOf<ParsedDeclAnnotation>()
while (t.type == Token.Type.ATLABEL) {
val spec = parseDeclAnnotation(t)
annotationSpecs += spec
if (spec.name == "Transient") {
isTransient = true
t = cc.next()
} else throw ScriptError(t.pos, "Unexpected label in argument list")
}
t = cc.next()
}
if (annotationSpecs.isNotEmpty() && !isClassDeclaration) {
throw ScriptError(t.pos, "parameter annotations are currently supported only on class constructor parameters")
}
// visibility
@@ -4114,7 +4124,8 @@ class Compiler(
defaultValue,
effectiveAccess,
visibility,
isTransient
isTransient,
annotationSpecs = annotationSpecs
)
// important: valid argument list continues with ',' and ends with '->' or ')'
@@ -7120,19 +7131,25 @@ class Compiler(
return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number")
}
suspend fun parseAnnotation(t: Token): (suspend (Scope, ObjString, Statement) -> Statement) {
private suspend fun parseDeclAnnotation(t: Token): ParsedDeclAnnotation {
val extraArgs = parseArgsOrNull()
resolutionSink?.reference(t.value, t.pos)
// println("annotation ${t.value}: args: $extraArgs")
return { scope, name, body ->
val extras = extraArgs?.first?.toArguments(scope, extraArgs.second)?.list
val required = listOf(name, body)
val args = extras?.let { required + it } ?: required
val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}")
if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}")
(fn.execute(scope.createChildScope(Arguments(args))) as? Statement)
?: scope.raiseClassCastError("function annotation must return callable")
}
val compiledArgs = extraArgs?.first?.map { arg ->
val value = arg.value
arg.copy(
value = if (value is Statement) wrapBytecode(value) else value
)
} ?: emptyList()
return ParsedDeclAnnotation(
name = t.value,
args = compiledArgs,
tailBlockMode = extraArgs?.second ?: false,
pos = t.pos
)
}
suspend fun parseAnnotation(t: Token): (suspend (Scope, ObjString, Statement) -> Statement) {
return parseDeclAnnotation(t).toStatementAnnotation()
}
suspend fun parseArgsOrNull(): Pair<List<ParsedArgument>, Boolean>? =
@@ -9232,6 +9249,8 @@ class Compiler(
isTransient: Boolean = isTransientFlag
): Statement {
isTransientFlag = false
val declarationAnnotationSpecs = pendingDeclAnnotations.toList()
pendingDeclAnnotations.clear()
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
var start = cc.currentPos()
var extTypeName: String? = null
@@ -9700,6 +9719,7 @@ class Compiler(
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
annotations = emptyList(),
accessTypeLabel = "Callable",
initializer = initExpr,
pos = start
@@ -10251,6 +10271,8 @@ class Compiler(
isTransient: Boolean = isTransientFlag
): Statement {
isTransientFlag = false
val declarationAnnotationSpecs = pendingDeclAnnotations.toList()
pendingDeclAnnotations.clear()
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
val markStart = cc.savePos()
val nextToken = cc.next()
@@ -10643,6 +10665,9 @@ class Compiler(
!isStatic &&
!isProperty
) {
if (declarationAnnotationSpecs.isNotEmpty()) {
throw ScriptError(start, "declaration annotations are currently supported only on class members")
}
if (isDelegate) {
val initExpr = initialExpression ?: throw ScriptError(start, "Delegate must be initialized")
val slotPlan = slotPlanStack.lastOrNull()
@@ -10663,6 +10688,9 @@ class Compiler(
}
if (isStatic) {
if (extTypeName != null && declarationAnnotationSpecs.isNotEmpty()) {
throw ScriptError(start, "declaration annotations are not supported on extension properties")
}
if (declaringClassNameCaptured != null) {
val directRef = unwrapDirectRef(initialExpression)
val declClass = resolveTypeDeclObjClass(varTypeDecl)
@@ -10685,6 +10713,7 @@ class Compiler(
initializer = initialExpression,
isDelegated = isDelegate,
isTransient = isTransient,
annotationSpecs = declarationAnnotationSpecs,
startPos = start
)
return NopStatement
@@ -10847,6 +10876,9 @@ class Compiler(
}
if (extTypeName != null) {
if (declarationAnnotationSpecs.isNotEmpty()) {
throw ScriptError(start, "declaration annotations are not supported on extension properties")
}
declareLocalName(extensionPropertyGetterName(extTypeName, name), isMutable = false)
if (setter != null) {
declareLocalName(extensionPropertySetterName(extTypeName, name), isMutable = false)
@@ -10883,6 +10915,7 @@ class Compiler(
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
annotations = emptyList(),
accessTypeLabel = accessType,
initializer = initExpr,
pos = start
@@ -10898,6 +10931,7 @@ class Compiler(
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
annotationSpecs = declarationAnnotationSpecs,
methodId = memberMethodId,
initStatement = initStmt,
pos = start
@@ -10919,6 +10953,7 @@ class Compiler(
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
annotations = emptyList(),
prop = prop,
pos = start
)
@@ -10933,6 +10968,7 @@ class Compiler(
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
annotationSpecs = declarationAnnotationSpecs,
prop = prop,
methodId = memberMethodId,
initStatement = initStmt,
@@ -10950,6 +10986,7 @@ class Compiler(
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
annotations = emptyList(),
isLateInitVal = isLateInitVal,
initializer = initialExpression,
pos = start
@@ -10966,6 +11003,7 @@ class Compiler(
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
annotationSpecs = declarationAnnotationSpecs,
fieldId = memberFieldId,
initStatement = initStmt,
pos = start
@@ -0,0 +1,129 @@
/*
* 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 net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjIterable
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjMap
import net.sergeych.lyng.obj.ObjString
/**
* Preserved declaration annotation evaluated at declaration-creation time.
*/
data class DeclAnnotation(
val name: String,
val positional: List<Obj> = emptyList(),
val named: Map<String, Obj> = emptyMap(),
)
/**
* Parsed declaration annotation awaiting declaration-time evaluation.
*/
data class ParsedDeclAnnotation(
val name: String,
val args: List<ParsedArgument> = emptyList(),
val tailBlockMode: Boolean = false,
val pos: Pos = Pos.builtIn,
) {
suspend fun evaluate(scope: Scope): DeclAnnotation {
val resolved = evaluateDeclAnnotationArguments(scope, args, tailBlockMode)
return DeclAnnotation(name, resolved.list, resolved.named)
}
fun toStatementAnnotation(): suspend (Scope, ObjString, Statement) -> Statement = { scope, declName, body ->
val extras = args.toArguments(scope, tailBlockMode).list
val required = listOf(declName, body)
val callArgs = if (extras.isEmpty()) required else required + extras
val fn = scope.get(name)?.value ?: scope.raiseSymbolNotFound("annotation not found: $name")
if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}")
(fn.execute(scope.createChildScope(Arguments(callArgs))) as? Statement)
?: scope.raiseClassCastError("function annotation must return callable")
}
}
suspend fun Iterable<ParsedDeclAnnotation>.evaluateDeclAnnotations(scope: Scope): List<DeclAnnotation> {
val result = mutableListOf<DeclAnnotation>()
for (spec in this) {
result += spec.evaluate(scope)
}
return result
}
private suspend fun evaluateDeclAnnotationArguments(
scope: Scope,
args: List<ParsedArgument>,
tailBlockMode: Boolean,
): Arguments {
suspend fun eval(value: Obj): Obj = when (value) {
is BytecodeBodyProvider -> (value.bytecodeBody() ?: scope.raiseIllegalState("annotation argument requires bytecode body")).execute(scope)
is Statement -> BytecodeStatement.wrap(value, "@annotation", allowLocalSlots = true).execute(scope)
else -> value.callOn(scope)
}
val resolved = ArrayList<ParsedArgument>(args.size)
for (arg in args) {
resolved += arg.copy(value = eval(arg.value))
}
val positional: MutableList<Obj> = mutableListOf()
var named: MutableMap<String, Obj>? = null
var namedSeen = false
for ((idx, x) in resolved.withIndex()) {
if (x.name != null) {
if (named == null) named = linkedMapOf()
if (named.containsKey(x.name)) scope.raiseIllegalArgument("argument '${x.name}' is already set")
named[x.name] = x.value
namedSeen = true
continue
}
val value = x.value
if (x.isSplat) {
when {
value is ObjMap -> {
if (named == null) named = linkedMapOf()
for ((k, v) in value.map) {
if (k !is ObjString) scope.raiseIllegalArgument("named splat expects a Map with string keys")
val key = k.value
if (named.containsKey(key)) scope.raiseIllegalArgument("argument '$key' is already set")
named[key] = v
}
namedSeen = true
}
value is ObjList -> {
if (namedSeen) scope.raiseIllegalArgument("positional splat cannot follow named arguments")
positional.addAll(value.list)
}
value.isInstanceOf(ObjIterable) -> {
if (namedSeen) scope.raiseIllegalArgument("positional splat cannot follow named arguments")
val iterable = value.invokeInstanceMethod(scope, "toList") as ObjList
positional.addAll(iterable.list)
}
else -> scope.raiseClassCastError("expected list of objects for splat argument")
}
} else {
val isLast = idx == resolved.size - 1
if (namedSeen && !(isLast && tailBlockMode)) {
scope.raiseIllegalArgument("positional argument cannot follow named arguments")
}
positional.add(value)
}
}
return Arguments(positional, tailBlockMode, named ?: emptyMap())
}
@@ -33,6 +33,7 @@ class InstanceFieldInitStatement(
val isClosed: Boolean,
val isOverride: Boolean,
val isTransient: Boolean,
val annotations: List<DeclAnnotation> = emptyList(),
val isLateInitVal: Boolean,
val initializer: Statement?,
override val pos: Pos,
@@ -50,7 +51,8 @@ class InstanceFieldInitStatement(
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
isTransient = isTransient,
annotations = annotations
)
return ObjVoid
}
@@ -74,6 +76,7 @@ class InstancePropertyInitStatement(
val isClosed: Boolean,
val isOverride: Boolean,
val isTransient: Boolean,
val annotations: List<DeclAnnotation> = emptyList(),
val prop: ObjProperty,
override val pos: Pos,
) : Statement() {
@@ -88,7 +91,8 @@ class InstancePropertyInitStatement(
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
isTransient = isTransient,
annotations = annotations
)
return ObjVoid
}
@@ -104,6 +108,7 @@ class InstanceDelegatedInitStatement(
val isClosed: Boolean,
val isOverride: Boolean,
val isTransient: Boolean,
val annotations: List<DeclAnnotation> = emptyList(),
val accessTypeLabel: String,
val initializer: Statement,
override val pos: Pos,
@@ -130,7 +135,8 @@ class InstanceDelegatedInitStatement(
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
isTransient = isTransient,
annotations = annotations
).apply {
delegate = finalDelegate
}
@@ -718,6 +718,7 @@ open class Scope(
isTransient: Boolean = false,
callSignature: CallSignature? = null,
typeDecl: TypeDecl? = null,
annotations: List<DeclAnnotation> = emptyList(),
fieldId: Int? = null,
methodId: Int? = null
): ObjRecord {
@@ -731,6 +732,7 @@ open class Scope(
isTransient = isTransient,
callSignature = callSignature,
typeDecl = typeDecl,
annotations = annotations,
memberName = name,
fieldId = fieldId,
methodId = methodId
@@ -6360,7 +6360,8 @@ class BytecodeCompiler(
isMutable = stmt.isMutable,
visibility = stmt.visibility,
writeVisibility = stmt.writeVisibility,
isTransient = stmt.isTransient
isTransient = stmt.isTransient,
annotationSpecs = stmt.annotationSpecs
)
)
} else {
@@ -6370,7 +6371,8 @@ class BytecodeCompiler(
isMutable = stmt.isMutable,
visibility = stmt.visibility,
writeVisibility = stmt.writeVisibility,
isTransient = stmt.isTransient
isTransient = stmt.isTransient,
annotationSpecs = stmt.annotationSpecs
)
)
}
@@ -6397,6 +6399,7 @@ class BytecodeCompiler(
writeVisibility = stmt.writeVisibility,
typeDecl = stmt.typeDecl,
isTransient = stmt.isTransient,
annotationSpecs = stmt.annotationSpecs,
isAbstract = stmt.isAbstract,
isClosed = stmt.isClosed,
isOverride = stmt.isOverride,
@@ -6419,6 +6422,7 @@ class BytecodeCompiler(
visibility = stmt.visibility,
writeVisibility = stmt.writeVisibility,
isTransient = stmt.isTransient,
annotationSpecs = stmt.annotationSpecs,
isAbstract = stmt.isAbstract,
isClosed = stmt.isClosed,
isOverride = stmt.isOverride,
@@ -6442,6 +6446,7 @@ class BytecodeCompiler(
visibility = stmt.visibility,
writeVisibility = stmt.writeVisibility,
isTransient = stmt.isTransient,
annotationSpecs = stmt.annotationSpecs,
isAbstract = stmt.isAbstract,
isClosed = stmt.isClosed,
isOverride = stmt.isOverride,
@@ -6475,6 +6480,7 @@ class BytecodeCompiler(
visibility = stmt.visibility,
writeVisibility = stmt.writeVisibility,
isTransient = stmt.isTransient,
annotations = stmt.annotations,
isAbstract = stmt.isAbstract,
isClosed = stmt.isClosed,
isOverride = stmt.isOverride
@@ -6497,6 +6503,7 @@ class BytecodeCompiler(
visibility = stmt.visibility,
writeVisibility = stmt.writeVisibility,
isTransient = stmt.isTransient,
annotations = stmt.annotations,
isAbstract = stmt.isAbstract,
isClosed = stmt.isClosed,
isOverride = stmt.isOverride
@@ -6517,6 +6524,7 @@ class BytecodeCompiler(
visibility = stmt.visibility,
writeVisibility = stmt.writeVisibility,
isTransient = stmt.isTransient,
annotations = stmt.annotations,
isAbstract = stmt.isAbstract,
isClosed = stmt.isClosed,
isOverride = stmt.isOverride,
@@ -18,6 +18,7 @@
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.ArgsDeclaration
import net.sergeych.lyng.ParsedDeclAnnotation
import net.sergeych.lyng.Pos
import net.sergeych.lyng.TypeDecl
import net.sergeych.lyng.Visibility
@@ -85,6 +86,7 @@ sealed class BytecodeConst {
val visibility: Visibility,
val writeVisibility: Visibility?,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation>,
) : BytecodeConst()
data class ClassDelegatedDecl(
val name: String,
@@ -92,6 +94,7 @@ sealed class BytecodeConst {
val visibility: Visibility,
val writeVisibility: Visibility?,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation>,
) : BytecodeConst()
data class ClassInstanceInitDecl(
val initStatement: Obj,
@@ -103,6 +106,7 @@ sealed class BytecodeConst {
val writeVisibility: Visibility?,
val typeDecl: TypeDecl?,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation>,
val isAbstract: Boolean,
val isClosed: Boolean,
val isOverride: Boolean,
@@ -116,6 +120,7 @@ sealed class BytecodeConst {
val visibility: Visibility,
val writeVisibility: Visibility?,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation>,
val isAbstract: Boolean,
val isClosed: Boolean,
val isOverride: Boolean,
@@ -130,6 +135,7 @@ sealed class BytecodeConst {
val visibility: Visibility,
val writeVisibility: Visibility?,
val isTransient: Boolean,
val annotationSpecs: List<ParsedDeclAnnotation>,
val isAbstract: Boolean,
val isClosed: Boolean,
val isOverride: Boolean,
@@ -143,6 +149,7 @@ sealed class BytecodeConst {
val visibility: Visibility,
val writeVisibility: Visibility?,
val isTransient: Boolean,
val annotations: List<net.sergeych.lyng.DeclAnnotation>,
val isAbstract: Boolean,
val isClosed: Boolean,
val isOverride: Boolean,
@@ -153,6 +160,7 @@ sealed class BytecodeConst {
val visibility: Visibility,
val writeVisibility: Visibility?,
val isTransient: Boolean,
val annotations: List<net.sergeych.lyng.DeclAnnotation>,
val isAbstract: Boolean,
val isClosed: Boolean,
val isOverride: Boolean,
@@ -164,6 +172,7 @@ sealed class BytecodeConst {
val visibility: Visibility,
val writeVisibility: Visibility?,
val isTransient: Boolean,
val annotations: List<net.sergeych.lyng.DeclAnnotation>,
val isAbstract: Boolean,
val isClosed: Boolean,
val isOverride: Boolean,
@@ -365,6 +365,7 @@ class BytecodeStatement private constructor(
stmt.initializer?.let { unwrapDeep(it) },
stmt.isDelegated,
stmt.isTransient,
stmt.annotationSpecs,
stmt.pos
)
}
@@ -385,6 +386,7 @@ class BytecodeStatement private constructor(
stmt.isClosed,
stmt.isOverride,
stmt.isTransient,
stmt.annotationSpecs,
stmt.fieldId,
stmt.initStatement?.let { unwrapDeep(it) },
stmt.pos
@@ -400,6 +402,7 @@ class BytecodeStatement private constructor(
stmt.isClosed,
stmt.isOverride,
stmt.isTransient,
stmt.annotationSpecs,
stmt.prop,
stmt.methodId,
stmt.initStatement?.let { unwrapDeep(it) },
@@ -416,6 +419,7 @@ class BytecodeStatement private constructor(
stmt.isClosed,
stmt.isOverride,
stmt.isTransient,
stmt.annotationSpecs,
stmt.methodId,
stmt.initStatement?.let { unwrapDeep(it) },
stmt.pos
@@ -431,6 +435,7 @@ class BytecodeStatement private constructor(
stmt.isClosed,
stmt.isOverride,
stmt.isTransient,
stmt.annotations,
stmt.isLateInitVal,
stmt.initializer?.let { unwrapDeep(it) },
stmt.pos
@@ -446,6 +451,7 @@ class BytecodeStatement private constructor(
stmt.isClosed,
stmt.isOverride,
stmt.isTransient,
stmt.annotations,
stmt.prop,
stmt.pos
)
@@ -461,6 +467,7 @@ class BytecodeStatement private constructor(
stmt.isClosed,
stmt.isOverride,
stmt.isTransient,
stmt.annotations,
stmt.accessTypeLabel,
unwrapDeep(stmt.initializer),
stmt.pos
@@ -2805,6 +2805,7 @@ class CmdDeclClassField(internal val constId: Int, internal val slot: Int) : Cmd
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassFieldDecl
?: error("DECL_CLASS_FIELD expects ClassFieldDecl at $constId")
val scope = frame.ensureScope()
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
val cls = scope.thisObj as? ObjClass
?: scope.raiseIllegalState("class field init requires class scope")
val value = frame.slotToObj(slot).byValueCopy()
@@ -2815,7 +2816,8 @@ class CmdDeclClassField(internal val constId: Int, internal val slot: Int) : Cmd
decl.visibility,
decl.writeVisibility,
Pos.builtIn,
isTransient = decl.isTransient
isTransient = decl.isTransient,
annotations = annotations
)
scope.addItem(
decl.name,
@@ -2824,7 +2826,8 @@ class CmdDeclClassField(internal val constId: Int, internal val slot: Int) : Cmd
decl.visibility,
decl.writeVisibility,
recordType = ObjRecord.Type.Field,
isTransient = decl.isTransient
isTransient = decl.isTransient,
annotations = annotations
)
return
}
@@ -2835,6 +2838,7 @@ class CmdDeclClassDelegated(internal val constId: Int, internal val slot: Int) :
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassDelegatedDecl
?: error("DECL_CLASS_DELEGATED expects ClassDelegatedDecl at $constId")
val scope = frame.ensureScope()
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
val cls = scope.thisObj as? ObjClass
?: scope.raiseIllegalState("class delegated init requires class scope")
val initValue = frame.slotToObj(slot)
@@ -2857,7 +2861,8 @@ class CmdDeclClassDelegated(internal val constId: Int, internal val slot: Int) :
decl.writeVisibility,
Pos.builtIn,
isTransient = decl.isTransient,
type = ObjRecord.Type.Delegated
type = ObjRecord.Type.Delegated,
annotations = annotations
).apply {
delegate = finalDelegate
}
@@ -2868,7 +2873,8 @@ class CmdDeclClassDelegated(internal val constId: Int, internal val slot: Int) :
decl.visibility,
decl.writeVisibility,
recordType = ObjRecord.Type.Delegated,
isTransient = decl.isTransient
isTransient = decl.isTransient,
annotations = annotations
).apply {
delegate = finalDelegate
}
@@ -2895,6 +2901,7 @@ class CmdDeclClassInstanceField(internal val constId: Int, internal val slot: In
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassInstanceFieldDecl
?: error("DECL_CLASS_INSTANCE_FIELD expects ClassInstanceFieldDecl at $constId")
val scope = frame.ensureScope()
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
val cls = scope.thisObj as? ObjClass
?: scope.raiseIllegalState("class instance field requires class scope")
cls.createField(
@@ -2911,7 +2918,8 @@ class CmdDeclClassInstanceField(internal val constId: Int, internal val slot: In
isTransient = decl.isTransient,
typeDecl = decl.typeDecl,
type = ObjRecord.Type.Field,
fieldId = decl.fieldId
fieldId = decl.fieldId,
annotations = annotations
)
if (!decl.isAbstract) {
decl.initStatement?.let { cls.instanceInitializers += it }
@@ -2926,6 +2934,7 @@ class CmdDeclClassInstanceProperty(internal val constId: Int, internal val slot:
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassInstancePropertyDecl
?: error("DECL_CLASS_INSTANCE_PROPERTY expects ClassInstancePropertyDecl at $constId")
val scope = frame.ensureScope()
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
val cls = scope.thisObj as? ObjClass
?: scope.raiseIllegalState("class instance property requires class scope")
cls.addProperty(
@@ -2938,7 +2947,8 @@ class CmdDeclClassInstanceProperty(internal val constId: Int, internal val slot:
isOverride = decl.isOverride,
pos = decl.pos,
prop = decl.prop,
methodId = decl.methodId
methodId = decl.methodId,
annotations = annotations
)
if (!decl.isAbstract) {
decl.initStatement?.let { cls.instanceInitializers += it }
@@ -2953,6 +2963,7 @@ class CmdDeclClassInstanceDelegated(internal val constId: Int, internal val slot
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassInstanceDelegatedDecl
?: error("DECL_CLASS_INSTANCE_DELEGATED expects ClassInstanceDelegatedDecl at $constId")
val scope = frame.ensureScope()
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
val cls = scope.thisObj as? ObjClass
?: scope.raiseIllegalState("class instance delegated requires class scope")
cls.createField(
@@ -2968,7 +2979,8 @@ class CmdDeclClassInstanceDelegated(internal val constId: Int, internal val slot
isOverride = decl.isOverride,
isTransient = decl.isTransient,
type = ObjRecord.Type.Delegated,
methodId = decl.methodId
methodId = decl.methodId,
annotations = annotations
)
if (!decl.isAbstract) {
decl.initStatement?.let { cls.instanceInitializers += it }
@@ -2994,7 +3006,8 @@ class CmdDeclInstanceField(internal val constId: Int, internal val slot: Int) :
isAbstract = decl.isAbstract,
isClosed = decl.isClosed,
isOverride = decl.isOverride,
isTransient = decl.isTransient
isTransient = decl.isTransient,
annotations = decl.annotations
)
if (slot >= frame.fn.scopeSlotCount) {
val localIndex = slot - frame.fn.scopeSlotCount
@@ -3023,7 +3036,8 @@ class CmdDeclInstanceProperty(internal val constId: Int, internal val slot: Int)
isAbstract = decl.isAbstract,
isClosed = decl.isClosed,
isOverride = decl.isOverride,
isTransient = decl.isTransient
isTransient = decl.isTransient,
annotations = decl.annotations
)
if (slot >= frame.fn.scopeSlotCount) {
val localIndex = slot - frame.fn.scopeSlotCount
@@ -3076,7 +3090,8 @@ class CmdDeclInstanceDelegated(internal val constId: Int, internal val slot: Int
isAbstract = decl.isAbstract,
isClosed = decl.isClosed,
isOverride = decl.isOverride,
isTransient = decl.isTransient
isTransient = decl.isTransient,
annotations = decl.annotations
).apply {
delegate = finalDelegate
}
@@ -3705,9 +3720,26 @@ class CmdGetClassScope(
decl = declared
break
}
val resolved = rec ?: scope.raiseSymbolNotFound(name)
val declClass = decl ?: cls
val resolvedRec = cls.resolveRecord(scope, resolved, name, declClass)
val resolvedRec = if (rec != null) {
val declClass = decl ?: cls
cls.resolveRecord(scope, rec, name, declClass)
} else {
val metaRec = cls.objClass.getInstanceMemberOrNull(name)
if (metaRec == null || metaRec.isAbstract) {
scope.raiseSymbolNotFound(name)
}
val declClass = metaRec.declaringClass ?: cls.objClass
val resolved = cls.resolveRecord(scope, metaRec, name, declClass)
if (resolved.type == ObjRecord.Type.Fun) {
resolved.copy(
value = ObjExternCallable.fromBridge {
resolved.value.invoke(scope, cls, args, declClass)
}
)
} else {
resolved
}
}
val value = resolvedRec.value
frame.storeObjResult(dst, value)
return
@@ -28,6 +28,23 @@ import net.sergeych.lynon.LynonType
// Simple id generator for class identities (not thread-safe; fine for scripts)
private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
private fun DeclAnnotation.toObj(): Obj {
val namedArgs = linkedMapOf<Obj, Obj>()
for ((k, v) in named) {
namedArgs[ObjString(k)] = v
}
return ObjMap(
linkedMapOf(
ObjString("name") to ObjString(name),
ObjString("positional") to ObjImmutableList(positional),
ObjString("named") to ObjMap(namedArgs)
)
)
}
private fun annotationListObj(annotations: List<DeclAnnotation>): Obj =
ObjImmutableList(annotations.map { it.toObj() })
val ObjClassType by lazy {
object : ObjClass("Class") {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
@@ -98,6 +115,30 @@ val ObjClassType by lazy {
val rec = cls.getInstanceMemberOrNull(name)
rec?.value ?: ObjNull
}
addFnDoc(
name = "getConstructorAnnotations",
doc = "Return preserved annotations for a constructor parameter by name as descriptor maps with fields `name`, `positional`, and `named`.",
params = listOf(ParamDoc("name", type("lyng.String"))),
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Map"))),
moduleName = "lyng.stdlib"
) {
val cls = thisAs<ObjClass>()
val name = requiredArg<ObjString>(0).value
val param = cls.constructorMeta?.params?.firstOrNull { it.name == name }
annotationListObj(param?.annotations ?: emptyList())
}
addFnDoc(
name = "getMemberAnnotations",
doc = "Return preserved annotations for a member by name as descriptor maps with fields `name`, `positional`, and `named`.",
params = listOf(ParamDoc("name", type("lyng.String"))),
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Map"))),
moduleName = "lyng.stdlib"
) {
val cls = thisAs<ObjClass>()
val name = requiredArg<ObjString>(0).value
val rec = cls.getInstanceMemberOrNull(name) ?: cls.classScope?.objects?.get(name)
annotationListObj(rec?.annotations ?: emptyList())
}
}
}
@@ -851,6 +892,7 @@ open class ObjClass(
methodId: Int? = null,
typeDecl: net.sergeych.lyng.TypeDecl? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
annotations: List<DeclAnnotation> = emptyList(),
): ObjRecord {
// Validation of override rules: only for non-system declarations
var existing: ObjRecord? = null
@@ -953,6 +995,7 @@ open class ObjClass(
type = type,
callSignature = callSignature,
typeDecl = typeDecl,
annotations = annotations,
memberName = name,
fieldId = effectiveFieldId,
methodId = effectiveMethodId
@@ -979,7 +1022,8 @@ open class ObjClass(
type: ObjRecord.Type = ObjRecord.Type.Field,
fieldId: Int? = null,
methodId: Int? = null,
callSignature: net.sergeych.lyng.CallSignature? = null
callSignature: net.sergeych.lyng.CallSignature? = null,
annotations: List<DeclAnnotation> = emptyList()
): ObjRecord {
initClassScope()
val existing = classScope!!.objects[name]
@@ -1021,6 +1065,7 @@ open class ObjClass(
recordType = type,
isTransient = isTransient,
callSignature = callSignature,
annotations = annotations,
fieldId = effectiveFieldId,
methodId = effectiveMethodId
)
@@ -1067,7 +1112,8 @@ open class ObjClass(
isOverride: Boolean = false,
pos: Pos = Pos.builtIn,
prop: ObjProperty? = null,
methodId: Int? = null
methodId: Int? = null,
annotations: List<DeclAnnotation> = emptyList()
) {
val g = getter?.let { ObjExternCallable.fromBridge { it() } }
val s = setter?.let { ObjExternCallable.fromBridge { it(requiredArg(0)); ObjVoid } }
@@ -1076,7 +1122,8 @@ open class ObjClass(
name, finalProp, false, visibility, writeVisibility, pos, declaringClass,
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
type = ObjRecord.Type.Property,
methodId = methodId
methodId = methodId,
annotations = annotations
)
}
@@ -16,6 +16,7 @@
*/
package net.sergeych.lyng.obj
import net.sergeych.lyng.DeclAnnotation
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Visibility
@@ -40,6 +41,7 @@ data class ObjRecord(
var receiver: Obj? = null,
val callSignature: net.sergeych.lyng.CallSignature? = null,
val typeDecl: net.sergeych.lyng.TypeDecl? = null,
val annotations: List<DeclAnnotation> = emptyList(),
val memberName: String? = null,
val fieldId: Int? = null,
val methodId: Int? = null,
@@ -0,0 +1,68 @@
/*
* 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 net.sergeych.lyng.obj.ObjInt
import kotlin.test.Test
import kotlin.test.assertEquals
class DeclAnnotationIntrospectionTest {
@Test
fun classAnnotationQueriesExposeConstructorAndMemberAnnotations() = runTest {
val scope = Scope()
val result = scope.eval(
"""
val suffix = "!"
object Marker
class Sample(
@Transient @Tag(1, label: "ctor", extra: suffix) val x: Int
) {
@Transient @DbDecodeWith(Marker)
var y: Int = 10
}
val ctorAnnotations: ImmutableList<Map<String, Object>> = Sample.getConstructorAnnotations("x")
val ctorTag: Map<String, Object> = ctorAnnotations[1]
val ctorPositional: ImmutableList<Object> = ctorTag["positional"] as ImmutableList<Object>
val ctorNamed: Map<String, Object> = ctorTag["named"] as Map<String, Object>
assertEquals(2, ctorAnnotations.size)
assertEquals("Transient", ctorAnnotations[0]["name"])
assertEquals("Tag", ctorTag["name"])
assertEquals(1, ctorPositional[0])
assertEquals("ctor", ctorNamed["label"])
assertEquals("!", ctorNamed["extra"])
val memberAnnotations: ImmutableList<Map<String, Object>> = Sample.getMemberAnnotations("y")
val memberDecodeWith: Map<String, Object> = memberAnnotations[1]
val memberPositional: ImmutableList<Object> = memberDecodeWith["positional"] as ImmutableList<Object>
assertEquals(2, memberAnnotations.size)
assertEquals("Transient", memberAnnotations[0]["name"])
assertEquals("DbDecodeWith", memberDecodeWith["name"])
assertEquals(Marker, memberPositional[0])
memberAnnotations.size + ctorAnnotations.size
""".trimIndent()
) as ObjInt
assertEquals(4L, result.value)
}
}
+19 -1
View File
@@ -14,6 +14,24 @@ extern class NotImplementedException
/* Raised when an awaited asynchronous task was cancelled before producing a value. */
extern class CancellationException : Exception
/* Runtime metaobject describing a class. */
extern class Class {
/* Full name of this class including package if available. */
val className: String
/* Simple name of this class (without package). */
val name: String
/* Declared instance fields of this class and its ancestors (C3 order), without duplicates. */
val fields: List<String>
/* Declared instance methods of this class and its ancestors (C3 order), without duplicates. */
val methods: List<String>
/* Lookup a member by name in this class (including ancestors) and return it, or null if absent. */
fun get(name: String): Object?
/* Preserved annotations for a constructor parameter as descriptor maps with keys `name`, `positional`, and `named`. */
fun getConstructorAnnotations(name: String): ImmutableList<Map<String, Object>>
/* Preserved annotations for a member as descriptor maps with keys `name`, `positional`, and `named`. */
fun getMemberAnnotations(name: String): ImmutableList<Map<String, Object>>
}
/* A handle to a running asynchronous task. */
extern class Deferred {
/* Cancel the task if it is still active. Safe to call multiple times. */
@@ -648,4 +666,4 @@ class LaunchPool(maxWorkers, maxQueueSize = Channel.UNLIMITED) {
w.await()
}
}
}
}