Add DB decode annotations and preserved declaration metadata
This commit is contained in:
parent
2abe7e2f96
commit
50e34e520e
@ -13,6 +13,7 @@
|
|||||||
- Prefer defining Lyng entities (enums/classes/type shapes) in `.lyng` files; only define them in Kotlin when there is Kotlin/platform-specific implementation detail that cannot be expressed in Lyng.
|
- Prefer defining Lyng entities (enums/classes/type shapes) in `.lyng` files; only define them in Kotlin when there is Kotlin/platform-specific implementation detail that cannot be expressed in Lyng.
|
||||||
- Avoid hardcoding Lyng API documentation in Kotlin registrars when it can be declared in `.lyng`; Kotlin-side docs should be fallback/bridge only.
|
- Avoid hardcoding Lyng API documentation in Kotlin registrars when it can be declared in `.lyng`; Kotlin-side docs should be fallback/bridge only.
|
||||||
- For mixed pluggable modules (Lyng + Kotlin), embed module `.lyng` sources as generated Kotlin string literals, evaluate them into module scope during registration, then attach Kotlin implementations/bindings.
|
- For mixed pluggable modules (Lyng + Kotlin), embed module `.lyng` sources as generated Kotlin string literals, evaluate them into module scope during registration, then attach Kotlin implementations/bindings.
|
||||||
|
- When a change adds or changes Lyng-visible runtime/module behavior, update the corresponding `.lyng` declaration in the same change, including declaration-level docs/comments for new API surface.
|
||||||
|
|
||||||
## Kotlin/Wasm generation guardrails
|
## Kotlin/Wasm generation guardrails
|
||||||
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
|
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
|
||||||
|
|||||||
@ -207,14 +207,27 @@ assertThrows(RollbackException) {
|
|||||||
- `isEmpty()` — fast emptiness check where possible.
|
- `isEmpty()` — fast emptiness check where possible.
|
||||||
- `iterator()` — normal row iteration while the transaction is active.
|
- `iterator()` — normal row iteration while the transaction is active.
|
||||||
- `toList()` — materialize detached `SqlRow` snapshots that may be used after the transaction ends.
|
- `toList()` — materialize detached `SqlRow` snapshots that may be used after the transaction ends.
|
||||||
|
- `decodeAs<T>()` — transaction-scoped iterable view that decodes each row into `T`.
|
||||||
|
|
||||||
##### `SqlRow`
|
##### `SqlRow`
|
||||||
|
|
||||||
- `row[index]` — zero-based positional access.
|
- `row[index]` — zero-based positional access.
|
||||||
- `row["columnName"]` — case-insensitive lookup by output column label.
|
- `row["columnName"]` — case-insensitive lookup by output column label.
|
||||||
|
- `row.decodeAs<T>()` — decode one row into a typed Lyng value.
|
||||||
|
|
||||||
Name-based access fails with `SqlUsageException` if the name is missing or ambiguous.
|
Name-based access fails with `SqlUsageException` if the name is missing or ambiguous.
|
||||||
|
|
||||||
|
##### `DbFieldAdapter`
|
||||||
|
|
||||||
|
Custom DB field projection hook used by `@DbDecodeWith(...)`.
|
||||||
|
|
||||||
|
- `decode(rawValue, column, row, targetType)` — adapt one raw DB field value to a Lyng value for the requested target type.
|
||||||
|
- `encode(value, targetType)` — future symmetric hook for SQL parameter encoding.
|
||||||
|
|
||||||
|
Use `@DbDecodeWith(adapter)` on class constructor parameters and class-body fields/properties that participate in `decodeAs<T>()`.
|
||||||
|
|
||||||
|
Annotation arguments are evaluated once when the declaration is created, and the resulting adapter instance is retained in declaration metadata.
|
||||||
|
|
||||||
##### `ExecutionResult`
|
##### `ExecutionResult`
|
||||||
|
|
||||||
- `affectedRowsCount`
|
- `affectedRowsCount`
|
||||||
@ -249,6 +262,22 @@ Portable result metadata categories:
|
|||||||
- `DateTime`
|
- `DateTime`
|
||||||
- `Instant`
|
- `Instant`
|
||||||
|
|
||||||
|
Typed row decode rules:
|
||||||
|
|
||||||
|
- object/class targets map constructor parameters by column label, case-insensitively
|
||||||
|
- remaining matching serializable mutable fields are assigned after constructor call
|
||||||
|
- `@DbDecodeWith(adapter)` on a constructor parameter or class-body field/property takes precedence over built-in JSON/Lynon decoding
|
||||||
|
- `@DbDecodeWith(adapter)` must receive exactly one adapter instance implementing `DbFieldAdapter`
|
||||||
|
- adapter output must match the target member type or decoding fails with `SqlUsageException`
|
||||||
|
- missing required non-null constructor fields fail
|
||||||
|
- defaulted or nullable constructor fields may be omitted from the result
|
||||||
|
- extra result columns currently fail in strict mode
|
||||||
|
- if a row has exactly one column, that value may be decoded directly as the requested target type
|
||||||
|
- JSON-like native column types (`json`, `jsonb`) are decoded through typed canonical `Json` when the target type is not `String`
|
||||||
|
- binary columns are decoded through `Lynon` when the target type is not `Buffer`
|
||||||
|
- `Buffer` targets keep the raw binary payload without Lynon decoding
|
||||||
|
- plain text columns are not implicitly treated as JSON
|
||||||
|
|
||||||
For temporal types, see [time functions](time.md).
|
For temporal types, see [time functions](time.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -387,6 +416,8 @@ This means:
|
|||||||
|
|
||||||
- do not keep `ResultSet` objects after the transaction block returns
|
- do not keep `ResultSet` objects after the transaction block returns
|
||||||
- materialize rows with `toList()` inside the transaction when they must outlive it
|
- materialize rows with `toList()` inside the transaction when they must outlive it
|
||||||
|
- the iterable returned by `decodeAs<T>()` is also transaction-scoped
|
||||||
|
- decoded objects produced while iterating `decodeAs<T>()` are detached ordinary Lyng values
|
||||||
|
|
||||||
The same rule applies to generated keys from `ExecutionResult.getGeneratedKeys()`: the `ResultSet` is transaction-scoped, but rows returned by `toList()` are detached.
|
The same rule applies to generated keys from `ExecutionResult.getGeneratedKeys()`: the `ResultSet` is transaction-scoped, but rows returned by `toList()` are detached.
|
||||||
|
|
||||||
|
|||||||
@ -18,21 +18,33 @@
|
|||||||
package net.sergeych.lyng.io.db
|
package net.sergeych.lyng.io.db
|
||||||
|
|
||||||
import net.sergeych.lyng.Arguments
|
import net.sergeych.lyng.Arguments
|
||||||
|
import net.sergeych.lyng.DeclAnnotation
|
||||||
import net.sergeych.lyng.ModuleScope
|
import net.sergeych.lyng.ModuleScope
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.ScopeFacade
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.TypeDecl
|
||||||
import net.sergeych.lyng.obj.Obj
|
import net.sergeych.lyng.obj.Obj
|
||||||
import net.sergeych.lyng.obj.ObjBool
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjBuffer
|
||||||
|
import net.sergeych.lyng.obj.ObjBitBuffer
|
||||||
import net.sergeych.lyng.obj.ObjClass
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
import net.sergeych.lyng.obj.ObjEnumClass
|
import net.sergeych.lyng.obj.ObjEnumClass
|
||||||
import net.sergeych.lyng.obj.ObjEnumEntry
|
import net.sergeych.lyng.obj.ObjEnumEntry
|
||||||
import net.sergeych.lyng.obj.ObjException
|
import net.sergeych.lyng.obj.ObjException
|
||||||
import net.sergeych.lyng.obj.ObjImmutableList
|
import net.sergeych.lyng.obj.ObjImmutableList
|
||||||
|
import net.sergeych.lyng.obj.ObjInstance
|
||||||
import net.sergeych.lyng.obj.ObjInt
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
import net.sergeych.lyng.obj.ObjNull
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
|
import net.sergeych.lyng.obj.ObjRecord
|
||||||
import net.sergeych.lyng.obj.ObjString
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.ObjTypeExpr
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
import net.sergeych.lyng.obj.thisAs
|
import net.sergeych.lyng.obj.thisAs
|
||||||
import net.sergeych.lyng.requireScope
|
import net.sergeych.lyng.requireScope
|
||||||
|
import net.sergeych.lyng.serialization.ObjJsonClass
|
||||||
|
import net.sergeych.lynon.BitArray
|
||||||
|
import net.sergeych.lynon.ObjLynonClass
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.collections.List
|
import kotlin.collections.List
|
||||||
import kotlin.collections.Map
|
import kotlin.collections.Map
|
||||||
import kotlin.collections.MutableList
|
import kotlin.collections.MutableList
|
||||||
@ -43,11 +55,14 @@ import kotlin.collections.forEachIndexed
|
|||||||
import kotlin.collections.getOrNull
|
import kotlin.collections.getOrNull
|
||||||
import kotlin.collections.getOrPut
|
import kotlin.collections.getOrPut
|
||||||
import kotlin.collections.indices
|
import kotlin.collections.indices
|
||||||
|
import kotlin.collections.LinkedHashMap
|
||||||
import kotlin.collections.linkedMapOf
|
import kotlin.collections.linkedMapOf
|
||||||
import kotlin.collections.listOf
|
import kotlin.collections.listOf
|
||||||
import kotlin.collections.map
|
import kotlin.collections.map
|
||||||
import kotlin.collections.mutableListOf
|
import kotlin.collections.mutableListOf
|
||||||
|
import kotlin.collections.set
|
||||||
import kotlin.text.lowercase
|
import kotlin.text.lowercase
|
||||||
|
import kotlin.text.substringAfterLast
|
||||||
|
|
||||||
internal data class SqlColumnMeta(
|
internal data class SqlColumnMeta(
|
||||||
val name: String,
|
val name: String,
|
||||||
@ -90,6 +105,9 @@ internal class SqlCoreModule private constructor(
|
|||||||
val sqlConstraintException: ObjException.Companion.ExceptionClass,
|
val sqlConstraintException: ObjException.Companion.ExceptionClass,
|
||||||
val sqlUsageException: ObjException.Companion.ExceptionClass,
|
val sqlUsageException: ObjException.Companion.ExceptionClass,
|
||||||
val rollbackException: ObjException.Companion.ExceptionClass,
|
val rollbackException: ObjException.Companion.ExceptionClass,
|
||||||
|
val iterableClass: ObjClass,
|
||||||
|
val iteratorClass: ObjClass,
|
||||||
|
val dbFieldAdapterClass: ObjClass,
|
||||||
val sqlTypes: SqlTypeEntries,
|
val sqlTypes: SqlTypeEntries,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
@ -106,6 +124,11 @@ internal class SqlCoreModule private constructor(
|
|||||||
sqlConstraintException = module.requireClass("SqlConstraintException") as ObjException.Companion.ExceptionClass,
|
sqlConstraintException = module.requireClass("SqlConstraintException") as ObjException.Companion.ExceptionClass,
|
||||||
sqlUsageException = module.requireClass("SqlUsageException") as ObjException.Companion.ExceptionClass,
|
sqlUsageException = module.requireClass("SqlUsageException") as ObjException.Companion.ExceptionClass,
|
||||||
rollbackException = module.requireClass("RollbackException") as ObjException.Companion.ExceptionClass,
|
rollbackException = module.requireClass("RollbackException") as ObjException.Companion.ExceptionClass,
|
||||||
|
iterableClass = module.importProvider.rootScope.get("Iterable")?.value as? ObjClass
|
||||||
|
?: error("lyng.stdlib.Iterable declaration is missing"),
|
||||||
|
iteratorClass = module.importProvider.rootScope.get("Iterator")?.value as? ObjClass
|
||||||
|
?: error("lyng.stdlib.Iterator declaration is missing"),
|
||||||
|
dbFieldAdapterClass = module.requireClass("DbFieldAdapter"),
|
||||||
sqlTypes = SqlTypeEntries.resolve(module),
|
sqlTypes = SqlTypeEntries.resolve(module),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -148,6 +171,8 @@ internal class SqlRuntimeTypes private constructor(
|
|||||||
val rowClass: ObjClass,
|
val rowClass: ObjClass,
|
||||||
val columnClass: ObjClass,
|
val columnClass: ObjClass,
|
||||||
val executionResultClass: ObjClass,
|
val executionResultClass: ObjClass,
|
||||||
|
val decodedIterableClass: ObjClass,
|
||||||
|
val decodedIteratorClass: ObjClass,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun create(prefix: String, core: SqlCoreModule): SqlRuntimeTypes {
|
fun create(prefix: String, core: SqlCoreModule): SqlRuntimeTypes {
|
||||||
@ -157,6 +182,8 @@ internal class SqlRuntimeTypes private constructor(
|
|||||||
val rowClass = object : ObjClass("${prefix}Row", core.rowClass) {}
|
val rowClass = object : ObjClass("${prefix}Row", core.rowClass) {}
|
||||||
val columnClass = object : ObjClass("${prefix}Column", core.columnClass) {}
|
val columnClass = object : ObjClass("${prefix}Column", core.columnClass) {}
|
||||||
val executionResultClass = object : ObjClass("${prefix}ExecutionResult", core.executionResultClass) {}
|
val executionResultClass = object : ObjClass("${prefix}ExecutionResult", core.executionResultClass) {}
|
||||||
|
val decodedIterableClass = object : ObjClass("${prefix}DecodedIterable", core.iterableClass) {}
|
||||||
|
val decodedIteratorClass = object : ObjClass("${prefix}DecodedIterator", core.iteratorClass) {}
|
||||||
val runtime = SqlRuntimeTypes(
|
val runtime = SqlRuntimeTypes(
|
||||||
core = core,
|
core = core,
|
||||||
databaseClass = databaseClass,
|
databaseClass = databaseClass,
|
||||||
@ -165,6 +192,8 @@ internal class SqlRuntimeTypes private constructor(
|
|||||||
rowClass = rowClass,
|
rowClass = rowClass,
|
||||||
columnClass = columnClass,
|
columnClass = columnClass,
|
||||||
executionResultClass = executionResultClass,
|
executionResultClass = executionResultClass,
|
||||||
|
decodedIterableClass = decodedIterableClass,
|
||||||
|
decodedIteratorClass = decodedIteratorClass,
|
||||||
)
|
)
|
||||||
runtime.bind()
|
runtime.bind()
|
||||||
return runtime
|
return runtime
|
||||||
@ -251,6 +280,19 @@ internal class SqlRuntimeTypes private constructor(
|
|||||||
self.lifetime.ensureActive(this)
|
self.lifetime.ensureActive(this)
|
||||||
ObjImmutableList(self.rows)
|
ObjImmutableList(self.rows)
|
||||||
}
|
}
|
||||||
|
resultSetClass.addFn(
|
||||||
|
"decodeAs",
|
||||||
|
callSignature = core.resultSetClass.getInstanceMemberOrNull("decodeAs")?.callSignature
|
||||||
|
) {
|
||||||
|
val self = thisAs<SqlResultSetObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
SqlDecodedIterableObj(
|
||||||
|
self.types,
|
||||||
|
self.lifetime,
|
||||||
|
self.rows.map { it as SqlRowObj },
|
||||||
|
resolveDecodeTargetType(requireScope())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
rowClass.addProperty("size", getter = {
|
rowClass.addProperty("size", getter = {
|
||||||
val self = thisAs<SqlRowObj>()
|
val self = thisAs<SqlRowObj>()
|
||||||
@ -260,6 +302,12 @@ internal class SqlRuntimeTypes private constructor(
|
|||||||
val self = thisAs<SqlRowObj>()
|
val self = thisAs<SqlRowObj>()
|
||||||
ObjImmutableList(self.values)
|
ObjImmutableList(self.values)
|
||||||
})
|
})
|
||||||
|
rowClass.addFn(
|
||||||
|
"decodeAs",
|
||||||
|
callSignature = core.rowClass.getInstanceMemberOrNull("decodeAs")?.callSignature
|
||||||
|
) {
|
||||||
|
decodeSqlRow(requireScope(), thisAs(), resolveDecodeTargetType(requireScope()))
|
||||||
|
}
|
||||||
|
|
||||||
columnClass.addProperty("name", getter = { ObjString(thisAs<SqlColumnObj>().meta.name) })
|
columnClass.addProperty("name", getter = { ObjString(thisAs<SqlColumnObj>().meta.name) })
|
||||||
columnClass.addProperty("sqlType", getter = { thisAs<SqlColumnObj>().meta.sqlType })
|
columnClass.addProperty("sqlType", getter = { thisAs<SqlColumnObj>().meta.sqlType })
|
||||||
@ -276,6 +324,27 @@ internal class SqlRuntimeTypes private constructor(
|
|||||||
self.lifetime.ensureActive(this)
|
self.lifetime.ensureActive(this)
|
||||||
SqlResultSetObj(self.types, self.lifetime, self.result.generatedKeys)
|
SqlResultSetObj(self.types, self.lifetime, self.result.generatedKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodedIterableClass.addFn("iterator") {
|
||||||
|
val self = thisAs<SqlDecodedIterableObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
SqlDecodedIteratorObj(self.types, self.lifetime, self.rows.iterator(), self.targetType)
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedIteratorClass.addFn("hasNext") {
|
||||||
|
val self = thisAs<SqlDecodedIteratorObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
ObjBool(self.rows.hasNext())
|
||||||
|
}
|
||||||
|
decodedIteratorClass.addFn("next") {
|
||||||
|
val self = thisAs<SqlDecodedIteratorObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
decodeSqlRow(requireScope(), self.rows.next(), self.targetType)
|
||||||
|
}
|
||||||
|
decodedIteratorClass.addFn("cancelIteration") {
|
||||||
|
thisAs<SqlDecodedIteratorObj>().lifetime.ensureActive(this)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +388,7 @@ internal class SqlResultSetObj(
|
|||||||
val lifetime: SqlTransactionLifetime,
|
val lifetime: SqlTransactionLifetime,
|
||||||
data: SqlResultSetData,
|
data: SqlResultSetData,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
|
val columnMeta: List<SqlColumnMeta> = data.columns
|
||||||
val columns: List<Obj> = data.columns.map { SqlColumnObj(types, it) }
|
val columns: List<Obj> = data.columns.map { SqlColumnObj(types, it) }
|
||||||
val rows: List<Obj> = buildRows(types, data)
|
val rows: List<Obj> = buildRows(types, data)
|
||||||
|
|
||||||
@ -334,13 +404,14 @@ internal class SqlResultSetObj(
|
|||||||
indexByName.getOrPut(column.name.lowercase()) { mutableListOf() }.add(index)
|
indexByName.getOrPut(column.name.lowercase()) { mutableListOf() }.add(index)
|
||||||
}
|
}
|
||||||
return data.rows.map { rowValues ->
|
return data.rows.map { rowValues ->
|
||||||
SqlRowObj(types, rowValues, indexByName)
|
SqlRowObj(types, data.columns, rowValues, indexByName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SqlRowObj(
|
internal class SqlRowObj(
|
||||||
val types: SqlRuntimeTypes,
|
val types: SqlRuntimeTypes,
|
||||||
|
val columns: List<SqlColumnMeta>,
|
||||||
val values: List<Obj>,
|
val values: List<Obj>,
|
||||||
private val indexByName: Map<String, List<Int>>,
|
private val indexByName: Map<String, List<Int>>,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
@ -381,6 +452,26 @@ internal class SqlRowObj(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class SqlDecodedIterableObj(
|
||||||
|
val types: SqlRuntimeTypes,
|
||||||
|
val lifetime: SqlTransactionLifetime,
|
||||||
|
val rows: List<SqlRowObj>,
|
||||||
|
val targetType: TypeDecl,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = types.decodedIterableClass
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlDecodedIteratorObj(
|
||||||
|
val types: SqlRuntimeTypes,
|
||||||
|
val lifetime: SqlTransactionLifetime,
|
||||||
|
val rows: Iterator<SqlRowObj>,
|
||||||
|
val targetType: TypeDecl,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = types.decodedIteratorClass
|
||||||
|
}
|
||||||
|
|
||||||
internal class SqlColumnObj(
|
internal class SqlColumnObj(
|
||||||
val types: SqlRuntimeTypes,
|
val types: SqlRuntimeTypes,
|
||||||
val meta: SqlColumnMeta,
|
val meta: SqlColumnMeta,
|
||||||
@ -397,3 +488,339 @@ internal class SqlExecutionResultObj(
|
|||||||
override val objClass: ObjClass
|
override val objClass: ObjClass
|
||||||
get() = types.executionResultClass
|
get() = types.executionResultClass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveDecodeTargetType(scope: Scope): TypeDecl {
|
||||||
|
val explicit = scope.args.explicitTypeArgs.singleOrNull()
|
||||||
|
if (explicit != null) return explicit
|
||||||
|
val bound = scope["T"]?.value
|
||||||
|
return when (bound) {
|
||||||
|
is ObjTypeExpr -> bound.typeDecl
|
||||||
|
is ObjClass -> TypeDecl.Simple(bound.className, false)
|
||||||
|
else -> scope.raiseIllegalArgument("decodeAs requires exactly one type argument")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun decodeSqlRow(scope: Scope, row: SqlRowObj, targetType: TypeDecl): Obj {
|
||||||
|
val targetClass = resolveTypeDeclClass(scope, targetType)
|
||||||
|
if (targetClass != null && shouldUseStructuredRowDecoding(row, targetClass)) {
|
||||||
|
return decodeStructuredRow(scope, row, targetType, targetClass)
|
||||||
|
}
|
||||||
|
if (row.values.size == 1) {
|
||||||
|
return decodeSqlValue(scope, row.types, row, row.columns[0], row.values[0], targetType)
|
||||||
|
}
|
||||||
|
scope.raiseError(
|
||||||
|
ObjException(
|
||||||
|
row.types.core.sqlUsageException,
|
||||||
|
scope,
|
||||||
|
ObjString("Can't decode SQL row with ${row.values.size} columns as ${renderTypeName(targetType)}")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldUseStructuredRowDecoding(row: SqlRowObj, targetClass: ObjClass): Boolean {
|
||||||
|
if (row.values.size > 1) return true
|
||||||
|
val memberNames = linkedMapOf<String, Int>()
|
||||||
|
targetClass.constructorMeta?.params?.forEach { memberNames[it.name.lowercase()] = 1 }
|
||||||
|
row.columns.forEach { column ->
|
||||||
|
if (column.name.lowercase() in memberNames) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun decodeStructuredRow(
|
||||||
|
scope: Scope,
|
||||||
|
row: SqlRowObj,
|
||||||
|
targetType: TypeDecl,
|
||||||
|
targetClass: ObjClass,
|
||||||
|
): Obj {
|
||||||
|
val meta = targetClass.constructorMeta
|
||||||
|
?: raiseSqlUsage(scope, row.types, "Can't decode SQL row as ${targetClass.className}: target class has no constructor metadata")
|
||||||
|
|
||||||
|
val normalizedColumns = buildColumnLookup(scope, row)
|
||||||
|
val consumed = mutableSetOf<String>()
|
||||||
|
val namedArgs = LinkedHashMap<String, Obj>()
|
||||||
|
|
||||||
|
for (param in meta.params) {
|
||||||
|
if (param.isTransient) continue
|
||||||
|
val lowered = param.name.lowercase()
|
||||||
|
val column = normalizedColumns[lowered]
|
||||||
|
if (column == null) {
|
||||||
|
if (param.defaultValue == null && !param.type.isNullable) {
|
||||||
|
raiseSqlUsage(scope, row.types, "Missing SQL column '${param.name}' for ${targetClass.className}")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
namedArgs[param.name] = decodeSqlValue(
|
||||||
|
scope,
|
||||||
|
row.types,
|
||||||
|
row,
|
||||||
|
column.first,
|
||||||
|
row.values[column.second],
|
||||||
|
param.type,
|
||||||
|
param.annotations
|
||||||
|
)
|
||||||
|
consumed += lowered
|
||||||
|
}
|
||||||
|
|
||||||
|
val callScope = scope.createChildScope(args = Arguments(list = emptyList(), named = namedArgs))
|
||||||
|
val instance = targetClass.callOn(callScope)
|
||||||
|
if (instance !is ObjInstance) {
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
val knownFields = collectSerializableFieldTargets(scope, row, targetClass, instance)
|
||||||
|
|
||||||
|
for ((lowered, target) in knownFields) {
|
||||||
|
if (consumed.contains(lowered)) continue
|
||||||
|
val column = normalizedColumns[lowered] ?: continue
|
||||||
|
val targetTypeDecl = target.record.typeDecl ?: TypeDecl.TypeAny
|
||||||
|
target.record.value = decodeSqlValue(
|
||||||
|
scope,
|
||||||
|
row.types,
|
||||||
|
row,
|
||||||
|
column.first,
|
||||||
|
row.values[column.second],
|
||||||
|
targetTypeDecl,
|
||||||
|
target.annotations
|
||||||
|
)
|
||||||
|
consumed += lowered
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowedNames = meta.params.filter { !it.isTransient }.map { it.name.lowercase() }.toMutableSet()
|
||||||
|
allowedNames += knownFields.keys
|
||||||
|
for (column in row.columns) {
|
||||||
|
val lowered = column.name.lowercase()
|
||||||
|
if (lowered !in allowedNames) {
|
||||||
|
raiseSqlUsage(scope, row.types, "Unknown SQL result column '${column.name}' while decoding ${renderTypeName(targetType)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.invokeInstanceMethod(scope, "onDeserialized", Arguments.EMPTY) { ObjVoid }
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class FieldDecodeTarget(
|
||||||
|
val record: ObjRecord,
|
||||||
|
val annotations: List<DeclAnnotation>,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun collectSerializableFieldTargets(
|
||||||
|
scope: Scope,
|
||||||
|
row: SqlRowObj,
|
||||||
|
targetClass: ObjClass,
|
||||||
|
instance: ObjInstance,
|
||||||
|
): Map<String, FieldDecodeTarget> {
|
||||||
|
val result = linkedMapOf<String, FieldDecodeTarget>()
|
||||||
|
for ((name, record) in instance.serializingVars) {
|
||||||
|
val simpleName = name.substringAfterLast("::")
|
||||||
|
val lowered = simpleName.lowercase()
|
||||||
|
val classAnnotations = targetClass.getInstanceMemberOrNull(simpleName)?.annotations ?: emptyList()
|
||||||
|
val previous = result.put(lowered, FieldDecodeTarget(record, classAnnotations))
|
||||||
|
if (previous != null) {
|
||||||
|
raiseSqlUsage(scope, row.types, "Ambiguous serializable target field '$lowered' in ${targetClass.className}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildColumnLookup(scope: Scope, row: SqlRowObj): Map<String, Pair<SqlColumnMeta, Int>> {
|
||||||
|
val result = linkedMapOf<String, Pair<SqlColumnMeta, Int>>()
|
||||||
|
row.columns.forEachIndexed { index, column ->
|
||||||
|
val lowered = column.name.lowercase()
|
||||||
|
if (result.containsKey(lowered)) {
|
||||||
|
raiseSqlUsage(scope, row.types, "Ambiguous SQL result column: ${column.name}")
|
||||||
|
}
|
||||||
|
result[lowered] = column to index
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun decodeSqlValue(
|
||||||
|
scope: Scope,
|
||||||
|
types: SqlRuntimeTypes,
|
||||||
|
row: SqlRowObj,
|
||||||
|
column: SqlColumnMeta,
|
||||||
|
value: Obj,
|
||||||
|
targetType: TypeDecl,
|
||||||
|
annotations: List<DeclAnnotation> = emptyList(),
|
||||||
|
): Obj {
|
||||||
|
val adapterAnnotation = findDbDecodeWithAnnotation(scope, types, annotations)
|
||||||
|
if (adapterAnnotation != null) {
|
||||||
|
val adapted = applyDbFieldAdapter(scope, types, row, column, value, targetType, adapterAnnotation)
|
||||||
|
if (adapted === ObjNull) {
|
||||||
|
if (targetType.isNullable || targetType == TypeDecl.TypeNullableAny) return ObjNull
|
||||||
|
raiseSqlUsage(scope, types, "SQL column '${column.name}' is null but target type ${renderTypeName(targetType)} is non-null")
|
||||||
|
}
|
||||||
|
if (!matchesTypeDeclCompat(scope, adapted, targetType)) {
|
||||||
|
raiseSqlUsage(
|
||||||
|
scope,
|
||||||
|
types,
|
||||||
|
"DB adapter result for column '${column.name}' does not match target type ${renderTypeName(targetType)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return adapted
|
||||||
|
}
|
||||||
|
if (value === ObjNull) {
|
||||||
|
if (targetType.isNullable || targetType == TypeDecl.TypeNullableAny) return ObjNull
|
||||||
|
raiseSqlUsage(scope, types, "SQL column '${column.name}' is null but target type ${renderTypeName(targetType)} is non-null")
|
||||||
|
}
|
||||||
|
if (matchesTypeDeclCompat(scope, value, targetType)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (value is ObjBuffer) {
|
||||||
|
return try {
|
||||||
|
val decoded = ObjLynonClass.decodeAny(scope, ObjBitBuffer(BitArray(value.byteArray, 8)))
|
||||||
|
if (!matchesTypeDeclCompat(scope, decoded, targetType)) {
|
||||||
|
raiseSqlUsage(
|
||||||
|
scope,
|
||||||
|
types,
|
||||||
|
"Lynon-decoded SQL column '${column.name}' does not match target type ${renderTypeName(targetType)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
decoded
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
raiseSqlUsage(
|
||||||
|
scope,
|
||||||
|
types,
|
||||||
|
"Failed to decode Lynon column '${column.name}' as ${renderTypeName(targetType)}: ${e.message ?: e::class.simpleName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isJsonLikeNativeType(column.nativeType) && value is ObjString) {
|
||||||
|
return try {
|
||||||
|
ObjJsonClass.decodeFromJsonElement(scope, Json.parseToJsonElement(value.value), targetType)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
raiseSqlUsage(
|
||||||
|
scope,
|
||||||
|
types,
|
||||||
|
"Failed to decode JSON column '${column.name}' as ${renderTypeName(targetType)}: ${e.message ?: e::class.simpleName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raiseSqlUsage(
|
||||||
|
scope,
|
||||||
|
types,
|
||||||
|
"SQL column '${column.name}' of native type ${column.nativeType} can't be decoded as ${renderTypeName(targetType)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findDbDecodeWithAnnotation(
|
||||||
|
scope: Scope,
|
||||||
|
types: SqlRuntimeTypes,
|
||||||
|
annotations: List<DeclAnnotation>,
|
||||||
|
): DeclAnnotation? {
|
||||||
|
val matches = annotations.filter { it.name == "DbDecodeWith" }
|
||||||
|
if (matches.size > 1) {
|
||||||
|
raiseSqlUsage(scope, types, "Only one @DbDecodeWith(...) annotation is allowed per declaration")
|
||||||
|
}
|
||||||
|
return matches.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun applyDbFieldAdapter(
|
||||||
|
scope: Scope,
|
||||||
|
types: SqlRuntimeTypes,
|
||||||
|
row: SqlRowObj,
|
||||||
|
column: SqlColumnMeta,
|
||||||
|
value: Obj,
|
||||||
|
targetType: TypeDecl,
|
||||||
|
annotation: DeclAnnotation,
|
||||||
|
): Obj {
|
||||||
|
if (annotation.named.isNotEmpty() || annotation.positional.size != 1) {
|
||||||
|
raiseSqlUsage(scope, types, "@DbDecodeWith(...) expects exactly one adapter instance argument")
|
||||||
|
}
|
||||||
|
val adapter = annotation.positional.first()
|
||||||
|
if (!adapter.isInstanceOf(types.core.dbFieldAdapterClass)) {
|
||||||
|
raiseSqlUsage(scope, types, "@DbDecodeWith(...) argument must implement DbFieldAdapter")
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
adapter.invokeInstanceMethod(
|
||||||
|
scope,
|
||||||
|
"decode",
|
||||||
|
Arguments(
|
||||||
|
value,
|
||||||
|
SqlColumnObj(types, column),
|
||||||
|
row,
|
||||||
|
runtimeTargetTypeObject(scope, targetType)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
raiseSqlUsage(
|
||||||
|
scope,
|
||||||
|
types,
|
||||||
|
"Failed to decode SQL column '${column.name}' with @DbDecodeWith(...): ${e.message ?: e::class.simpleName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runtimeTargetTypeObject(scope: Scope, targetType: TypeDecl): Obj {
|
||||||
|
return resolveTypeDeclClass(scope, targetType) ?: ObjTypeExpr(targetType)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isJsonLikeNativeType(nativeType: String): Boolean {
|
||||||
|
val normalized = nativeType.trim().substringBefore('(').lowercase()
|
||||||
|
return normalized == "json" || normalized == "jsonb"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveTypeDeclClass(scope: Scope, type: TypeDecl): ObjClass? = when (type) {
|
||||||
|
is TypeDecl.Simple -> {
|
||||||
|
val direct = scope[type.name]?.value as? ObjClass
|
||||||
|
direct ?: scope[type.name.substringAfterLast('.')]?.value as? ObjClass
|
||||||
|
}
|
||||||
|
is TypeDecl.Generic -> {
|
||||||
|
val direct = scope[type.name]?.value as? ObjClass
|
||||||
|
direct ?: scope[type.name.substringAfterLast('.')]?.value as? ObjClass
|
||||||
|
}
|
||||||
|
is TypeDecl.Ellipsis -> resolveTypeDeclClass(scope, type.elementType)
|
||||||
|
is TypeDecl.TypeVar -> when (val bound = scope[type.name]?.value) {
|
||||||
|
is ObjClass -> bound
|
||||||
|
is ObjTypeExpr -> resolveTypeDeclClass(scope, bound.typeDecl)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchesTypeDeclCompat(scope: Scope, value: Obj, typeDecl: TypeDecl): Boolean {
|
||||||
|
if (value === ObjNull) return typeDecl.isNullable || typeDecl == TypeDecl.TypeNullableAny
|
||||||
|
fun resolve(typeName: String): ObjClass? {
|
||||||
|
val direct = scope[typeName]?.value as? ObjClass
|
||||||
|
return direct ?: scope[typeName.substringAfterLast('.')]?.value as? ObjClass
|
||||||
|
}
|
||||||
|
return when (typeDecl) {
|
||||||
|
TypeDecl.TypeAny, TypeDecl.TypeNullableAny -> true
|
||||||
|
is TypeDecl.TypeVar -> {
|
||||||
|
val cls = resolve(typeDecl.name)
|
||||||
|
if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name)
|
||||||
|
}
|
||||||
|
is TypeDecl.Simple -> {
|
||||||
|
val cls = resolve(typeDecl.name)
|
||||||
|
if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name.substringAfterLast('.'))
|
||||||
|
}
|
||||||
|
is TypeDecl.Generic -> {
|
||||||
|
val cls = resolve(typeDecl.name)
|
||||||
|
if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name.substringAfterLast('.'))
|
||||||
|
}
|
||||||
|
is TypeDecl.Function -> value.isInstanceOf("Callable")
|
||||||
|
is TypeDecl.Ellipsis -> matchesTypeDeclCompat(scope, value, typeDecl.elementType)
|
||||||
|
is TypeDecl.Union -> typeDecl.options.any { matchesTypeDeclCompat(scope, value, it) }
|
||||||
|
is TypeDecl.Intersection -> typeDecl.options.all { matchesTypeDeclCompat(scope, value, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderTypeName(type: TypeDecl): String = when (type) {
|
||||||
|
TypeDecl.TypeAny -> "Object"
|
||||||
|
TypeDecl.TypeNullableAny -> "Object?"
|
||||||
|
is TypeDecl.Simple -> type.name + if (type.isNullable) "?" else ""
|
||||||
|
is TypeDecl.Generic -> type.name + "<" + type.args.joinToString(",") { renderTypeName(it) } + ">" + if (type.isNullable) "?" else ""
|
||||||
|
is TypeDecl.Function -> "Callable"
|
||||||
|
is TypeDecl.Ellipsis -> renderTypeName(type.elementType) + "..."
|
||||||
|
is TypeDecl.TypeVar -> type.name + if (type.isNullable) "?" else ""
|
||||||
|
is TypeDecl.Union -> type.options.joinToString(" | ") { renderTypeName(it) } + if (type.isNullable) "?" else ""
|
||||||
|
is TypeDecl.Intersection -> type.options.joinToString(" & ") { renderTypeName(it) } + if (type.isNullable) "?" else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun raiseSqlUsage(scope: Scope, types: SqlRuntimeTypes?, message: String): Nothing {
|
||||||
|
val exClass = types?.core?.sqlUsageException
|
||||||
|
if (exClass != null) {
|
||||||
|
scope.raiseError(ObjException(exClass, scope, ObjString(message)))
|
||||||
|
}
|
||||||
|
scope.raiseIllegalArgument(message)
|
||||||
|
}
|
||||||
|
|||||||
@ -137,6 +137,230 @@ class LyngSqliteModuleTest {
|
|||||||
assertEquals(2L, result.value)
|
assertEquals(2L, result.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsProjectsJsonColumnIntoObjectField() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
class Row(id: Int, payload: Point)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(id integer not null, payload json not null)")
|
||||||
|
tx.execute("insert into data(id, payload) values(?, ?)", 7, "{\"x\":4,\"y\":5}")
|
||||||
|
val row = tx.select("select id, payload from data").decodeAs<Row>().first
|
||||||
|
assertEquals(7, row.id)
|
||||||
|
assertEquals(4, row.payload.x)
|
||||||
|
assertEquals(5, row.payload.y)
|
||||||
|
row.payload.y
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-decode-json-field>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertEquals(5L, result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsSupportsSingleJsonColumnProjection() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload json not null)")
|
||||||
|
tx.execute("insert into data(payload) values(?)", "{\"x\":9,\"y\":11}")
|
||||||
|
val point = tx.select("select payload from data").decodeAs<Point>().first
|
||||||
|
assertEquals(9, point.x)
|
||||||
|
assertEquals(11, point.y)
|
||||||
|
point.x + point.y
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-decode-json-single>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertEquals(20L, result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsDoesNotAutoDecodePlainTextAsJson() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload text not null)")
|
||||||
|
tx.execute("insert into data(payload) values(?)", "{\"x\":1,\"y\":2}")
|
||||||
|
tx.select("select payload from data").decodeAs<Point>().first
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val error = assertFailsWith<ExecutionError> {
|
||||||
|
Compiler.compile(Source("<sqlite-decode-json-text-guard>", code), scope.importManager).execute(scope)
|
||||||
|
}
|
||||||
|
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsSupportsSingleLynonBinaryProjection() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
import lyng.serialization
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload blob not null)")
|
||||||
|
tx.execute("insert into data(payload) values(?)", Lynon.encode(Point(6, 8)).toBuffer())
|
||||||
|
val point = tx.select("select payload from data").decodeAs<Point>().first
|
||||||
|
assertEquals(6, point.x)
|
||||||
|
assertEquals(8, point.y)
|
||||||
|
point.x + point.y
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-decode-lynon-single>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertEquals(14L, result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsSupportsDbDecodeWithOnConstructorParamsAndFields() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
object TrimmedStringAdapter: DbFieldAdapter {
|
||||||
|
override fun decode(rawValue, column, row, targetType) =
|
||||||
|
when(rawValue) {
|
||||||
|
null -> null
|
||||||
|
else -> rawValue.toString().trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class User(
|
||||||
|
id: Int,
|
||||||
|
@DbDecodeWith(TrimmedStringAdapter) name: String
|
||||||
|
) {
|
||||||
|
@DbDecodeWith(TrimmedStringAdapter)
|
||||||
|
var note: String = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(id integer not null, name text not null, note text not null)")
|
||||||
|
tx.execute("insert into data(id, name, note) values(?, ?, ?)", 10, " Alice ", " hello ")
|
||||||
|
val user = tx.select("select id, name, note from data").decodeAs<User>().first
|
||||||
|
assertEquals(10, user.id)
|
||||||
|
assertEquals("Alice", user.name)
|
||||||
|
assertEquals("hello", user.note)
|
||||||
|
user.note.size
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-decode-dbdecodewith>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertEquals(5L, result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsFailsWhenDbDecodeWithReturnsWrongType() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
object BadAdapter: DbFieldAdapter {
|
||||||
|
override fun decode(rawValue, column, row, targetType) = 42
|
||||||
|
}
|
||||||
|
|
||||||
|
class User(@DbDecodeWith(BadAdapter) name: String)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(name text not null)")
|
||||||
|
tx.execute("insert into data(name) values(?)", "Alice")
|
||||||
|
tx.select("select name from data").decodeAs<User>().first
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val error = assertFailsWith<ExecutionError> {
|
||||||
|
Compiler.compile(Source("<sqlite-decode-dbdecodewith-bad-type>", code), scope.importManager).execute(scope)
|
||||||
|
}
|
||||||
|
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsKeepsRawBufferForBufferTarget() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
import lyng.buffer
|
||||||
|
import lyng.serialization
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload blob not null)")
|
||||||
|
val encoded = Lynon.encode(Point(1, 2)).toBuffer()
|
||||||
|
tx.execute("insert into data(payload) values(?)", encoded)
|
||||||
|
val payload = tx.select("select payload from data").decodeAs<Buffer>().first
|
||||||
|
assertEquals(encoded.size, payload.size)
|
||||||
|
payload.size
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-decode-buffer-raw>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertTrue(result.value > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsFailsForNonLynonBinaryTypedProjection() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
import lyng.buffer
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload blob not null)")
|
||||||
|
tx.execute("insert into data(payload) values(?)", "hello".encodeUtf8())
|
||||||
|
tx.select("select payload from data").decodeAs<Point>().first
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val error = assertFailsWith<ExecutionError> {
|
||||||
|
Compiler.compile(Source("<sqlite-decode-lynon-binary-guard>", code), scope.importManager).execute(scope)
|
||||||
|
}
|
||||||
|
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNestedTransactionRollbackUsesSavepoint() = runTest {
|
fun testNestedTransactionRollbackUsesSavepoint() = runTest {
|
||||||
val scope = Script.newScope()
|
val scope = Script.newScope()
|
||||||
|
|||||||
@ -19,11 +19,13 @@ package net.sergeych.lyng.io.db.sqlite
|
|||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
|
import net.sergeych.lyng.Compiler
|
||||||
import net.sergeych.lyng.ExecutionError
|
import net.sergeych.lyng.ExecutionError
|
||||||
import net.sergeych.lyng.ModuleScope
|
import net.sergeych.lyng.ModuleScope
|
||||||
import net.sergeych.lyng.Pos
|
import net.sergeych.lyng.Pos
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
import net.sergeych.lyng.obj.Obj
|
import net.sergeych.lyng.obj.Obj
|
||||||
import net.sergeych.lyng.obj.ObjBool
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
import net.sergeych.lyng.obj.ObjBuffer
|
import net.sergeych.lyng.obj.ObjBuffer
|
||||||
@ -231,6 +233,230 @@ class LyngSqliteModuleNativeTest {
|
|||||||
assertEquals("beta", stringValue(scope, rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name"))))
|
assertEquals("beta", stringValue(scope, rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsProjectsJsonColumnIntoObjectField() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
class Row(id: Int, payload: Point)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(id integer not null, payload json not null)")
|
||||||
|
tx.execute("insert into data(id, payload) values(?, ?)", 7, "{\"x\":4,\"y\":5}")
|
||||||
|
val row = tx.select("select id, payload from data").decodeAs<Row>().first
|
||||||
|
assertEquals(7, row.id)
|
||||||
|
assertEquals(4, row.payload.x)
|
||||||
|
assertEquals(5, row.payload.y)
|
||||||
|
row.payload.y
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-native-decode-json-field>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertEquals(5L, result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsSupportsSingleJsonColumnProjection() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload json not null)")
|
||||||
|
tx.execute("insert into data(payload) values(?)", "{\"x\":9,\"y\":11}")
|
||||||
|
val point = tx.select("select payload from data").decodeAs<Point>().first
|
||||||
|
assertEquals(9, point.x)
|
||||||
|
assertEquals(11, point.y)
|
||||||
|
point.x + point.y
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-native-decode-json-single>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertEquals(20L, result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsDoesNotAutoDecodePlainTextAsJson() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload text not null)")
|
||||||
|
tx.execute("insert into data(payload) values(?)", "{\"x\":1,\"y\":2}")
|
||||||
|
tx.select("select payload from data").decodeAs<Point>().first
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val error = assertFailsWith<ExecutionError> {
|
||||||
|
Compiler.compile(Source("<sqlite-native-decode-json-text-guard>", code), scope.importManager).execute(scope)
|
||||||
|
}
|
||||||
|
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsSupportsSingleLynonBinaryProjection() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
import lyng.serialization
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload blob not null)")
|
||||||
|
tx.execute("insert into data(payload) values(?)", Lynon.encode(Point(6, 8)).toBuffer())
|
||||||
|
val point = tx.select("select payload from data").decodeAs<Point>().first
|
||||||
|
assertEquals(6, point.x)
|
||||||
|
assertEquals(8, point.y)
|
||||||
|
point.x + point.y
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-native-decode-lynon-single>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertEquals(14L, result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsSupportsDbDecodeWithOnConstructorParamsAndFields() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
object TrimmedStringAdapter: DbFieldAdapter {
|
||||||
|
override fun decode(rawValue, column, row, targetType) =
|
||||||
|
when(rawValue) {
|
||||||
|
null -> null
|
||||||
|
else -> rawValue.toString().trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class User(
|
||||||
|
id: Int,
|
||||||
|
@DbDecodeWith(TrimmedStringAdapter) name: String
|
||||||
|
) {
|
||||||
|
@DbDecodeWith(TrimmedStringAdapter)
|
||||||
|
var note: String = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(id integer not null, name text not null, note text not null)")
|
||||||
|
tx.execute("insert into data(id, name, note) values(?, ?, ?)", 10, " Alice ", " hello ")
|
||||||
|
val user = tx.select("select id, name, note from data").decodeAs<User>().first
|
||||||
|
assertEquals(10, user.id)
|
||||||
|
assertEquals("Alice", user.name)
|
||||||
|
assertEquals("hello", user.note)
|
||||||
|
user.note.size
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-native-decode-dbdecodewith>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertEquals(5L, result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsFailsWhenDbDecodeWithReturnsWrongType() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
object BadAdapter: DbFieldAdapter {
|
||||||
|
override fun decode(rawValue, column, row, targetType) = 42
|
||||||
|
}
|
||||||
|
|
||||||
|
class User(@DbDecodeWith(BadAdapter) name: String)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(name text not null)")
|
||||||
|
tx.execute("insert into data(name) values(?)", "Alice")
|
||||||
|
tx.select("select name from data").decodeAs<User>().first
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val error = assertFailsWith<ExecutionError> {
|
||||||
|
Compiler.compile(Source("<sqlite-native-decode-dbdecodewith-bad-type>", code), scope.importManager).execute(scope)
|
||||||
|
}
|
||||||
|
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsKeepsRawBufferForBufferTarget() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
import lyng.buffer
|
||||||
|
import lyng.serialization
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload blob not null)")
|
||||||
|
val encoded = Lynon.encode(Point(1, 2)).toBuffer()
|
||||||
|
tx.execute("insert into data(payload) values(?)", encoded)
|
||||||
|
val payload = tx.select("select payload from data").decodeAs<Buffer>().first
|
||||||
|
assertEquals(encoded.size, payload.size)
|
||||||
|
payload.size
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val result = Compiler.compile(Source("<sqlite-native-decode-buffer-raw>", code), scope.importManager).execute(scope) as ObjInt
|
||||||
|
assertTrue(result.value > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecodeAsFailsForNonLynonBinaryTypedProjection() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
import lyng.buffer
|
||||||
|
|
||||||
|
class Point(x: Int, y: Int)
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table data(payload blob not null)")
|
||||||
|
tx.execute("insert into data(payload) values(?)", "hello".encodeUtf8())
|
||||||
|
tx.select("select payload from data").decodeAs<Point>().first
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val error = assertFailsWith<ExecutionError> {
|
||||||
|
Compiler.compile(Source("<sqlite-native-decode-lynon-binary-guard>", code), scope.importManager).execute(scope)
|
||||||
|
}
|
||||||
|
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testExecuteRejectsReturningButSelectSupportsIt() = runTest {
|
fun testExecuteRejectsReturningButSelectSupportsIt() = runTest {
|
||||||
val scope = Script.newScope()
|
val scope = Script.newScope()
|
||||||
|
|||||||
@ -20,6 +20,32 @@ extern class SqlColumn {
|
|||||||
val nativeType: String
|
val nativeType: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Adapter interface for custom DB field projection.
|
||||||
|
|
||||||
|
Use it with `@DbDecodeWith(adapter)` on class constructor parameters or
|
||||||
|
class-body fields/properties participating in `decodeAs<T>()` projection.
|
||||||
|
|
||||||
|
`targetType` is the requested Lyng target type represented as a runtime
|
||||||
|
type object, such as a class or a type expression.
|
||||||
|
|
||||||
|
Default methods throw `NotImplementedException`.
|
||||||
|
*/
|
||||||
|
interface DbFieldAdapter {
|
||||||
|
/*
|
||||||
|
Decode one raw database field value into a Lyng value suitable for the
|
||||||
|
requested target type.
|
||||||
|
*/
|
||||||
|
fun decode(rawValue: Object?, column: SqlColumn, row: SqlRow, targetType: Object): Object? =
|
||||||
|
throw NotImplementedException("DB field adapter decode is not implemented")
|
||||||
|
|
||||||
|
/*
|
||||||
|
Encode one Lyng value into a database field representation.
|
||||||
|
*/
|
||||||
|
fun encode(value: Object?, targetType: Object): Object? =
|
||||||
|
throw NotImplementedException("DB field adapter encode is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
extern class SqlRow {
|
extern class SqlRow {
|
||||||
/* Number of columns in the row */
|
/* Number of columns in the row */
|
||||||
val size: Int
|
val size: Int
|
||||||
@ -34,6 +60,22 @@ extern class SqlRow {
|
|||||||
names and invalid indexes should also fail.
|
names and invalid indexes should also fail.
|
||||||
*/
|
*/
|
||||||
override fun getAt(indexOrName: String | Int): Object?
|
override fun getAt(indexOrName: String | Int): Object?
|
||||||
|
|
||||||
|
/*
|
||||||
|
Decode this row into a typed Lyng value.
|
||||||
|
|
||||||
|
For object/class targets, constructor parameters are matched by column
|
||||||
|
label first and then remaining matching serializable mutable fields are
|
||||||
|
assigned.
|
||||||
|
|
||||||
|
If a constructor parameter or class-body field/property has
|
||||||
|
`@DbDecodeWith(adapter)`, the adapter is applied first and its result
|
||||||
|
must match the target member type.
|
||||||
|
|
||||||
|
For single-column rows, the column value may also be decoded directly to
|
||||||
|
the requested target type.
|
||||||
|
*/
|
||||||
|
fun decodeAs<T>(): T
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -69,6 +111,16 @@ extern class ResultSet : Iterable<SqlRow> {
|
|||||||
internally, but this must not change visible later iteration behavior.
|
internally, but this must not change visible later iteration behavior.
|
||||||
*/
|
*/
|
||||||
override fun isEmpty(): Bool
|
override fun isEmpty(): Bool
|
||||||
|
|
||||||
|
/*
|
||||||
|
Return a transaction-scoped iterable view that decodes each row with
|
||||||
|
`SqlRow.decodeAs<T>()`.
|
||||||
|
|
||||||
|
The returned iterable itself must not be used after the owning
|
||||||
|
transaction ends. Materialized decoded objects may outlive the
|
||||||
|
transaction.
|
||||||
|
*/
|
||||||
|
fun decodeAs<T>(): Iterable<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
extern class ExecutionResult {
|
extern class ExecutionResult {
|
||||||
|
|||||||
@ -88,7 +88,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
a.visibility ?: defaultVisibility,
|
a.visibility ?: defaultVisibility,
|
||||||
recordType = recordType,
|
recordType = recordType,
|
||||||
declaringClass = declaringClass,
|
declaringClass = declaringClass,
|
||||||
isTransient = a.isTransient
|
isTransient = a.isTransient,
|
||||||
|
annotations = a.annotations
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -108,7 +109,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
a.visibility ?: defaultVisibility,
|
a.visibility ?: defaultVisibility,
|
||||||
recordType = recordType,
|
recordType = recordType,
|
||||||
declaringClass = declaringClass,
|
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 accessType: AccessType? = null,
|
||||||
val visibility: Visibility? = null,
|
val visibility: Visibility? = null,
|
||||||
val isTransient: Boolean = false,
|
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>,
|
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(
|
internal suspend fun executeClassDecl(
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
spec: ClassDeclSpec,
|
spec: ClassDeclSpec,
|
||||||
@ -61,7 +72,7 @@ internal suspend fun executeClassDecl(
|
|||||||
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray())
|
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray())
|
||||||
newClass.isAnonymous = spec.isAnonymous
|
newClass.isAnonymous = spec.isAnonymous
|
||||||
newClass.isSingletonObject = true
|
newClass.isSingletonObject = true
|
||||||
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
|
newClass.constructorMeta = evaluateConstructorAnnotations(scope, ArgsDeclaration(emptyList(), Token.Type.RPAREN))
|
||||||
for (i in parentClasses.indices) {
|
for (i in parentClasses.indices) {
|
||||||
val argsList = spec.baseSpecs[i].args
|
val argsList = spec.baseSpecs[i].args
|
||||||
if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList
|
if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList
|
||||||
@ -86,6 +97,7 @@ internal suspend fun executeClassDecl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (spec.isExtern) {
|
if (spec.isExtern) {
|
||||||
|
val evaluatedConstructorArgs = evaluateConstructorAnnotations(scope, spec.constructorArgs)
|
||||||
val parentClasses = spec.baseSpecs.mapNotNull { baseSpec ->
|
val parentClasses = spec.baseSpecs.mapNotNull { baseSpec ->
|
||||||
val rec = scope[baseSpec.name]
|
val rec = scope[baseSpec.name]
|
||||||
val cls = rec?.value as? ObjClass
|
val cls = rec?.value as? ObjClass
|
||||||
@ -106,8 +118,8 @@ internal suspend fun executeClassDecl(
|
|||||||
}
|
}
|
||||||
val stub = resolved ?: ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).apply {
|
val stub = resolved ?: ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).apply {
|
||||||
this.isAbstract = true
|
this.isAbstract = true
|
||||||
constructorMeta = spec.constructorArgs
|
constructorMeta = evaluatedConstructorArgs
|
||||||
spec.constructorArgs?.params?.forEach { p ->
|
evaluatedConstructorArgs?.params?.forEach { p ->
|
||||||
if (p.accessType != null) {
|
if (p.accessType != null) {
|
||||||
createField(
|
createField(
|
||||||
p.name,
|
p.name,
|
||||||
@ -118,6 +130,7 @@ internal suspend fun executeClassDecl(
|
|||||||
pos = Pos.builtIn,
|
pos = Pos.builtIn,
|
||||||
isTransient = p.isTransient,
|
isTransient = p.isTransient,
|
||||||
type = ObjRecord.Type.ConstructorField,
|
type = ObjRecord.Type.ConstructorField,
|
||||||
|
annotations = p.annotations,
|
||||||
fieldId = spec.constructorFieldIds?.get(p.name)
|
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 {
|
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).also {
|
||||||
it.isAbstract = spec.isAbstract
|
it.isAbstract = spec.isAbstract
|
||||||
it.isClosed = spec.isClosed
|
it.isClosed = spec.isClosed
|
||||||
it.instanceConstructor = constructorCode
|
it.instanceConstructor = constructorCode
|
||||||
it.constructorMeta = spec.constructorArgs
|
it.constructorMeta = evaluatedConstructorArgs
|
||||||
for (i in parentClasses.indices) {
|
for (i in parentClasses.indices) {
|
||||||
val argsList = spec.baseSpecs[i].args
|
val argsList = spec.baseSpecs[i].args
|
||||||
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
|
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
|
||||||
}
|
}
|
||||||
spec.constructorArgs?.params?.forEach { p ->
|
evaluatedConstructorArgs?.params?.forEach { p ->
|
||||||
if (p.accessType != null) {
|
if (p.accessType != null) {
|
||||||
it.createField(
|
it.createField(
|
||||||
p.name,
|
p.name,
|
||||||
@ -181,6 +195,7 @@ internal suspend fun executeClassDecl(
|
|||||||
pos = Pos.builtIn,
|
pos = Pos.builtIn,
|
||||||
isTransient = p.isTransient,
|
isTransient = p.isTransient,
|
||||||
type = ObjRecord.Type.ConstructorField,
|
type = ObjRecord.Type.ConstructorField,
|
||||||
|
annotations = p.annotations,
|
||||||
fieldId = spec.constructorFieldIds?.get(p.name)
|
fieldId = spec.constructorFieldIds?.get(p.name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class ClassInstanceFieldDeclStatement(
|
|||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
|
||||||
val fieldId: Int?,
|
val fieldId: Int?,
|
||||||
val initStatement: Statement?,
|
val initStatement: Statement?,
|
||||||
override val pos: Pos,
|
override val pos: Pos,
|
||||||
@ -56,6 +57,7 @@ class ClassInstancePropertyDeclStatement(
|
|||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
|
||||||
val prop: ObjProperty,
|
val prop: ObjProperty,
|
||||||
val methodId: Int?,
|
val methodId: Int?,
|
||||||
val initStatement: Statement?,
|
val initStatement: Statement?,
|
||||||
@ -75,6 +77,7 @@ class ClassInstanceDelegatedDeclStatement(
|
|||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
|
||||||
val methodId: Int?,
|
val methodId: Int?,
|
||||||
val initStatement: Statement?,
|
val initStatement: Statement?,
|
||||||
override val pos: Pos,
|
override val pos: Pos,
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class ClassStaticFieldInitStatement(
|
|||||||
val initializer: Statement?,
|
val initializer: Statement?,
|
||||||
val isDelegated: Boolean,
|
val isDelegated: Boolean,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation> = emptyList(),
|
||||||
private val startPos: Pos,
|
private val startPos: Pos,
|
||||||
) : Statement() {
|
) : Statement() {
|
||||||
override val pos: Pos = startPos
|
override val pos: Pos = startPos
|
||||||
@ -39,6 +40,7 @@ class ClassStaticFieldInitStatement(
|
|||||||
override suspend fun execute(scope: Scope): Obj {
|
override suspend fun execute(scope: Scope): Obj {
|
||||||
val initValue = initializer?.let { execBytecodeOnly(scope, it, "class static field init") }?.byValueCopy()
|
val initValue = initializer?.let { execBytecodeOnly(scope, it, "class static field init") }?.byValueCopy()
|
||||||
?: ObjNull
|
?: ObjNull
|
||||||
|
val annotations = annotationSpecs.evaluateDeclAnnotations(scope)
|
||||||
val cls = scope.thisObj as? ObjClass
|
val cls = scope.thisObj as? ObjClass
|
||||||
?: scope.raiseIllegalState("static field init requires class scope")
|
?: scope.raiseIllegalState("static field init requires class scope")
|
||||||
return if (isDelegated) {
|
return if (isDelegated) {
|
||||||
@ -61,7 +63,8 @@ class ClassStaticFieldInitStatement(
|
|||||||
writeVisibility,
|
writeVisibility,
|
||||||
startPos,
|
startPos,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
type = ObjRecord.Type.Delegated
|
type = ObjRecord.Type.Delegated,
|
||||||
|
annotations = annotations
|
||||||
).apply {
|
).apply {
|
||||||
delegate = finalDelegate
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
@ -72,7 +75,8 @@ class ClassStaticFieldInitStatement(
|
|||||||
visibility,
|
visibility,
|
||||||
writeVisibility,
|
writeVisibility,
|
||||||
recordType = ObjRecord.Type.Delegated,
|
recordType = ObjRecord.Type.Delegated,
|
||||||
isTransient = isTransient
|
isTransient = isTransient,
|
||||||
|
annotations = annotations
|
||||||
).apply {
|
).apply {
|
||||||
delegate = finalDelegate
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
@ -85,7 +89,8 @@ class ClassStaticFieldInitStatement(
|
|||||||
visibility,
|
visibility,
|
||||||
writeVisibility,
|
writeVisibility,
|
||||||
startPos,
|
startPos,
|
||||||
isTransient = isTransient
|
isTransient = isTransient,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
scope.addItem(
|
scope.addItem(
|
||||||
name,
|
name,
|
||||||
@ -94,7 +99,8 @@ class ClassStaticFieldInitStatement(
|
|||||||
visibility,
|
visibility,
|
||||||
writeVisibility,
|
writeVisibility,
|
||||||
recordType = ObjRecord.Type.Field,
|
recordType = ObjRecord.Type.Field,
|
||||||
isTransient = isTransient
|
isTransient = isTransient,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
initValue
|
initValue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2014,6 +2014,7 @@ class Compiler(
|
|||||||
|
|
||||||
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
|
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
|
||||||
private var isTransientFlag: Boolean = false
|
private var isTransientFlag: Boolean = false
|
||||||
|
private val pendingDeclAnnotations: MutableList<ParsedDeclAnnotation> = mutableListOf()
|
||||||
private var lastLabel: String? = null
|
private var lastLabel: String? = null
|
||||||
private val strictSlotRefs: Boolean = settings.strictSlotRefs
|
private val strictSlotRefs: Boolean = settings.strictSlotRefs
|
||||||
private val allowUnresolvedRefs: Boolean = settings.allowUnresolvedRefs
|
private val allowUnresolvedRefs: Boolean = settings.allowUnresolvedRefs
|
||||||
@ -2689,6 +2690,7 @@ class Compiler(
|
|||||||
lastAnnotation = null
|
lastAnnotation = null
|
||||||
lastLabel = null
|
lastLabel = null
|
||||||
isTransientFlag = false
|
isTransientFlag = false
|
||||||
|
pendingDeclAnnotations.clear()
|
||||||
while (true) {
|
while (true) {
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
return when (t.type) {
|
return when (t.type) {
|
||||||
@ -2706,15 +2708,17 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.ATLABEL -> {
|
Token.Type.ATLABEL -> {
|
||||||
val label = t.value
|
val parsedAnnotation = parseDeclAnnotation(t)
|
||||||
if (label == "Transient") {
|
if (parsedAnnotation.name == "Transient") {
|
||||||
isTransientFlag = true
|
isTransientFlag = true
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4033,11 +4037,17 @@ class Compiler(
|
|||||||
|
|
||||||
Token.Type.ID, Token.Type.ATLABEL -> {
|
Token.Type.ID, Token.Type.ATLABEL -> {
|
||||||
var isTransient = false
|
var isTransient = false
|
||||||
if (t.type == Token.Type.ATLABEL) {
|
val annotationSpecs = mutableListOf<ParsedDeclAnnotation>()
|
||||||
if (t.value == "Transient") {
|
while (t.type == Token.Type.ATLABEL) {
|
||||||
|
val spec = parseDeclAnnotation(t)
|
||||||
|
annotationSpecs += spec
|
||||||
|
if (spec.name == "Transient") {
|
||||||
isTransient = true
|
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
|
// visibility
|
||||||
@ -4114,7 +4124,8 @@ class Compiler(
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
effectiveAccess,
|
effectiveAccess,
|
||||||
visibility,
|
visibility,
|
||||||
isTransient
|
isTransient,
|
||||||
|
annotationSpecs = annotationSpecs
|
||||||
)
|
)
|
||||||
|
|
||||||
// important: valid argument list continues with ',' and ends with '->' or ')'
|
// 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")
|
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()
|
val extraArgs = parseArgsOrNull()
|
||||||
resolutionSink?.reference(t.value, t.pos)
|
resolutionSink?.reference(t.value, t.pos)
|
||||||
// println("annotation ${t.value}: args: $extraArgs")
|
val compiledArgs = extraArgs?.first?.map { arg ->
|
||||||
return { scope, name, body ->
|
val value = arg.value
|
||||||
val extras = extraArgs?.first?.toArguments(scope, extraArgs.second)?.list
|
arg.copy(
|
||||||
val required = listOf(name, body)
|
value = if (value is Statement) wrapBytecode(value) else value
|
||||||
val args = extras?.let { required + it } ?: required
|
)
|
||||||
val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}")
|
} ?: emptyList()
|
||||||
if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}")
|
return ParsedDeclAnnotation(
|
||||||
(fn.execute(scope.createChildScope(Arguments(args))) as? Statement)
|
name = t.value,
|
||||||
?: scope.raiseClassCastError("function annotation must return callable")
|
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>? =
|
suspend fun parseArgsOrNull(): Pair<List<ParsedArgument>, Boolean>? =
|
||||||
@ -9232,6 +9249,8 @@ class Compiler(
|
|||||||
isTransient: Boolean = isTransientFlag
|
isTransient: Boolean = isTransientFlag
|
||||||
): Statement {
|
): Statement {
|
||||||
isTransientFlag = false
|
isTransientFlag = false
|
||||||
|
val declarationAnnotationSpecs = pendingDeclAnnotations.toList()
|
||||||
|
pendingDeclAnnotations.clear()
|
||||||
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
||||||
var start = cc.currentPos()
|
var start = cc.currentPos()
|
||||||
var extTypeName: String? = null
|
var extTypeName: String? = null
|
||||||
@ -9700,6 +9719,7 @@ class Compiler(
|
|||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
|
annotations = emptyList(),
|
||||||
accessTypeLabel = "Callable",
|
accessTypeLabel = "Callable",
|
||||||
initializer = initExpr,
|
initializer = initExpr,
|
||||||
pos = start
|
pos = start
|
||||||
@ -10251,6 +10271,8 @@ class Compiler(
|
|||||||
isTransient: Boolean = isTransientFlag
|
isTransient: Boolean = isTransientFlag
|
||||||
): Statement {
|
): Statement {
|
||||||
isTransientFlag = false
|
isTransientFlag = false
|
||||||
|
val declarationAnnotationSpecs = pendingDeclAnnotations.toList()
|
||||||
|
pendingDeclAnnotations.clear()
|
||||||
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
||||||
val markStart = cc.savePos()
|
val markStart = cc.savePos()
|
||||||
val nextToken = cc.next()
|
val nextToken = cc.next()
|
||||||
@ -10643,6 +10665,9 @@ class Compiler(
|
|||||||
!isStatic &&
|
!isStatic &&
|
||||||
!isProperty
|
!isProperty
|
||||||
) {
|
) {
|
||||||
|
if (declarationAnnotationSpecs.isNotEmpty()) {
|
||||||
|
throw ScriptError(start, "declaration annotations are currently supported only on class members")
|
||||||
|
}
|
||||||
if (isDelegate) {
|
if (isDelegate) {
|
||||||
val initExpr = initialExpression ?: throw ScriptError(start, "Delegate must be initialized")
|
val initExpr = initialExpression ?: throw ScriptError(start, "Delegate must be initialized")
|
||||||
val slotPlan = slotPlanStack.lastOrNull()
|
val slotPlan = slotPlanStack.lastOrNull()
|
||||||
@ -10663,6 +10688,9 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isStatic) {
|
if (isStatic) {
|
||||||
|
if (extTypeName != null && declarationAnnotationSpecs.isNotEmpty()) {
|
||||||
|
throw ScriptError(start, "declaration annotations are not supported on extension properties")
|
||||||
|
}
|
||||||
if (declaringClassNameCaptured != null) {
|
if (declaringClassNameCaptured != null) {
|
||||||
val directRef = unwrapDirectRef(initialExpression)
|
val directRef = unwrapDirectRef(initialExpression)
|
||||||
val declClass = resolveTypeDeclObjClass(varTypeDecl)
|
val declClass = resolveTypeDeclObjClass(varTypeDecl)
|
||||||
@ -10685,6 +10713,7 @@ class Compiler(
|
|||||||
initializer = initialExpression,
|
initializer = initialExpression,
|
||||||
isDelegated = isDelegate,
|
isDelegated = isDelegate,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
|
annotationSpecs = declarationAnnotationSpecs,
|
||||||
startPos = start
|
startPos = start
|
||||||
)
|
)
|
||||||
return NopStatement
|
return NopStatement
|
||||||
@ -10847,6 +10876,9 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (extTypeName != null) {
|
if (extTypeName != null) {
|
||||||
|
if (declarationAnnotationSpecs.isNotEmpty()) {
|
||||||
|
throw ScriptError(start, "declaration annotations are not supported on extension properties")
|
||||||
|
}
|
||||||
declareLocalName(extensionPropertyGetterName(extTypeName, name), isMutable = false)
|
declareLocalName(extensionPropertyGetterName(extTypeName, name), isMutable = false)
|
||||||
if (setter != null) {
|
if (setter != null) {
|
||||||
declareLocalName(extensionPropertySetterName(extTypeName, name), isMutable = false)
|
declareLocalName(extensionPropertySetterName(extTypeName, name), isMutable = false)
|
||||||
@ -10883,6 +10915,7 @@ class Compiler(
|
|||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
|
annotations = emptyList(),
|
||||||
accessTypeLabel = accessType,
|
accessTypeLabel = accessType,
|
||||||
initializer = initExpr,
|
initializer = initExpr,
|
||||||
pos = start
|
pos = start
|
||||||
@ -10898,6 +10931,7 @@ class Compiler(
|
|||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
|
annotationSpecs = declarationAnnotationSpecs,
|
||||||
methodId = memberMethodId,
|
methodId = memberMethodId,
|
||||||
initStatement = initStmt,
|
initStatement = initStmt,
|
||||||
pos = start
|
pos = start
|
||||||
@ -10919,6 +10953,7 @@ class Compiler(
|
|||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
|
annotations = emptyList(),
|
||||||
prop = prop,
|
prop = prop,
|
||||||
pos = start
|
pos = start
|
||||||
)
|
)
|
||||||
@ -10933,6 +10968,7 @@ class Compiler(
|
|||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
|
annotationSpecs = declarationAnnotationSpecs,
|
||||||
prop = prop,
|
prop = prop,
|
||||||
methodId = memberMethodId,
|
methodId = memberMethodId,
|
||||||
initStatement = initStmt,
|
initStatement = initStmt,
|
||||||
@ -10950,6 +10986,7 @@ class Compiler(
|
|||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
|
annotations = emptyList(),
|
||||||
isLateInitVal = isLateInitVal,
|
isLateInitVal = isLateInitVal,
|
||||||
initializer = initialExpression,
|
initializer = initialExpression,
|
||||||
pos = start
|
pos = start
|
||||||
@ -10966,6 +11003,7 @@ class Compiler(
|
|||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
|
annotationSpecs = declarationAnnotationSpecs,
|
||||||
fieldId = memberFieldId,
|
fieldId = memberFieldId,
|
||||||
initStatement = initStmt,
|
initStatement = initStmt,
|
||||||
pos = start
|
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 isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotations: List<DeclAnnotation> = emptyList(),
|
||||||
val isLateInitVal: Boolean,
|
val isLateInitVal: Boolean,
|
||||||
val initializer: Statement?,
|
val initializer: Statement?,
|
||||||
override val pos: Pos,
|
override val pos: Pos,
|
||||||
@ -50,7 +51,8 @@ class InstanceFieldInitStatement(
|
|||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient
|
isTransient = isTransient,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
return ObjVoid
|
return ObjVoid
|
||||||
}
|
}
|
||||||
@ -74,6 +76,7 @@ class InstancePropertyInitStatement(
|
|||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotations: List<DeclAnnotation> = emptyList(),
|
||||||
val prop: ObjProperty,
|
val prop: ObjProperty,
|
||||||
override val pos: Pos,
|
override val pos: Pos,
|
||||||
) : Statement() {
|
) : Statement() {
|
||||||
@ -88,7 +91,8 @@ class InstancePropertyInitStatement(
|
|||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient
|
isTransient = isTransient,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
return ObjVoid
|
return ObjVoid
|
||||||
}
|
}
|
||||||
@ -104,6 +108,7 @@ class InstanceDelegatedInitStatement(
|
|||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotations: List<DeclAnnotation> = emptyList(),
|
||||||
val accessTypeLabel: String,
|
val accessTypeLabel: String,
|
||||||
val initializer: Statement,
|
val initializer: Statement,
|
||||||
override val pos: Pos,
|
override val pos: Pos,
|
||||||
@ -130,7 +135,8 @@ class InstanceDelegatedInitStatement(
|
|||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
isTransient = isTransient
|
isTransient = isTransient,
|
||||||
|
annotations = annotations
|
||||||
).apply {
|
).apply {
|
||||||
delegate = finalDelegate
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
|
|||||||
@ -718,6 +718,7 @@ open class Scope(
|
|||||||
isTransient: Boolean = false,
|
isTransient: Boolean = false,
|
||||||
callSignature: CallSignature? = null,
|
callSignature: CallSignature? = null,
|
||||||
typeDecl: TypeDecl? = null,
|
typeDecl: TypeDecl? = null,
|
||||||
|
annotations: List<DeclAnnotation> = emptyList(),
|
||||||
fieldId: Int? = null,
|
fieldId: Int? = null,
|
||||||
methodId: Int? = null
|
methodId: Int? = null
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
@ -731,6 +732,7 @@ open class Scope(
|
|||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
callSignature = callSignature,
|
callSignature = callSignature,
|
||||||
typeDecl = typeDecl,
|
typeDecl = typeDecl,
|
||||||
|
annotations = annotations,
|
||||||
memberName = name,
|
memberName = name,
|
||||||
fieldId = fieldId,
|
fieldId = fieldId,
|
||||||
methodId = methodId
|
methodId = methodId
|
||||||
|
|||||||
@ -6360,7 +6360,8 @@ class BytecodeCompiler(
|
|||||||
isMutable = stmt.isMutable,
|
isMutable = stmt.isMutable,
|
||||||
visibility = stmt.visibility,
|
visibility = stmt.visibility,
|
||||||
writeVisibility = stmt.writeVisibility,
|
writeVisibility = stmt.writeVisibility,
|
||||||
isTransient = stmt.isTransient
|
isTransient = stmt.isTransient,
|
||||||
|
annotationSpecs = stmt.annotationSpecs
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -6370,7 +6371,8 @@ class BytecodeCompiler(
|
|||||||
isMutable = stmt.isMutable,
|
isMutable = stmt.isMutable,
|
||||||
visibility = stmt.visibility,
|
visibility = stmt.visibility,
|
||||||
writeVisibility = stmt.writeVisibility,
|
writeVisibility = stmt.writeVisibility,
|
||||||
isTransient = stmt.isTransient
|
isTransient = stmt.isTransient,
|
||||||
|
annotationSpecs = stmt.annotationSpecs
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -6397,6 +6399,7 @@ class BytecodeCompiler(
|
|||||||
writeVisibility = stmt.writeVisibility,
|
writeVisibility = stmt.writeVisibility,
|
||||||
typeDecl = stmt.typeDecl,
|
typeDecl = stmt.typeDecl,
|
||||||
isTransient = stmt.isTransient,
|
isTransient = stmt.isTransient,
|
||||||
|
annotationSpecs = stmt.annotationSpecs,
|
||||||
isAbstract = stmt.isAbstract,
|
isAbstract = stmt.isAbstract,
|
||||||
isClosed = stmt.isClosed,
|
isClosed = stmt.isClosed,
|
||||||
isOverride = stmt.isOverride,
|
isOverride = stmt.isOverride,
|
||||||
@ -6419,6 +6422,7 @@ class BytecodeCompiler(
|
|||||||
visibility = stmt.visibility,
|
visibility = stmt.visibility,
|
||||||
writeVisibility = stmt.writeVisibility,
|
writeVisibility = stmt.writeVisibility,
|
||||||
isTransient = stmt.isTransient,
|
isTransient = stmt.isTransient,
|
||||||
|
annotationSpecs = stmt.annotationSpecs,
|
||||||
isAbstract = stmt.isAbstract,
|
isAbstract = stmt.isAbstract,
|
||||||
isClosed = stmt.isClosed,
|
isClosed = stmt.isClosed,
|
||||||
isOverride = stmt.isOverride,
|
isOverride = stmt.isOverride,
|
||||||
@ -6442,6 +6446,7 @@ class BytecodeCompiler(
|
|||||||
visibility = stmt.visibility,
|
visibility = stmt.visibility,
|
||||||
writeVisibility = stmt.writeVisibility,
|
writeVisibility = stmt.writeVisibility,
|
||||||
isTransient = stmt.isTransient,
|
isTransient = stmt.isTransient,
|
||||||
|
annotationSpecs = stmt.annotationSpecs,
|
||||||
isAbstract = stmt.isAbstract,
|
isAbstract = stmt.isAbstract,
|
||||||
isClosed = stmt.isClosed,
|
isClosed = stmt.isClosed,
|
||||||
isOverride = stmt.isOverride,
|
isOverride = stmt.isOverride,
|
||||||
@ -6475,6 +6480,7 @@ class BytecodeCompiler(
|
|||||||
visibility = stmt.visibility,
|
visibility = stmt.visibility,
|
||||||
writeVisibility = stmt.writeVisibility,
|
writeVisibility = stmt.writeVisibility,
|
||||||
isTransient = stmt.isTransient,
|
isTransient = stmt.isTransient,
|
||||||
|
annotations = stmt.annotations,
|
||||||
isAbstract = stmt.isAbstract,
|
isAbstract = stmt.isAbstract,
|
||||||
isClosed = stmt.isClosed,
|
isClosed = stmt.isClosed,
|
||||||
isOverride = stmt.isOverride
|
isOverride = stmt.isOverride
|
||||||
@ -6497,6 +6503,7 @@ class BytecodeCompiler(
|
|||||||
visibility = stmt.visibility,
|
visibility = stmt.visibility,
|
||||||
writeVisibility = stmt.writeVisibility,
|
writeVisibility = stmt.writeVisibility,
|
||||||
isTransient = stmt.isTransient,
|
isTransient = stmt.isTransient,
|
||||||
|
annotations = stmt.annotations,
|
||||||
isAbstract = stmt.isAbstract,
|
isAbstract = stmt.isAbstract,
|
||||||
isClosed = stmt.isClosed,
|
isClosed = stmt.isClosed,
|
||||||
isOverride = stmt.isOverride
|
isOverride = stmt.isOverride
|
||||||
@ -6517,6 +6524,7 @@ class BytecodeCompiler(
|
|||||||
visibility = stmt.visibility,
|
visibility = stmt.visibility,
|
||||||
writeVisibility = stmt.writeVisibility,
|
writeVisibility = stmt.writeVisibility,
|
||||||
isTransient = stmt.isTransient,
|
isTransient = stmt.isTransient,
|
||||||
|
annotations = stmt.annotations,
|
||||||
isAbstract = stmt.isAbstract,
|
isAbstract = stmt.isAbstract,
|
||||||
isClosed = stmt.isClosed,
|
isClosed = stmt.isClosed,
|
||||||
isOverride = stmt.isOverride,
|
isOverride = stmt.isOverride,
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
package net.sergeych.lyng.bytecode
|
package net.sergeych.lyng.bytecode
|
||||||
|
|
||||||
import net.sergeych.lyng.ArgsDeclaration
|
import net.sergeych.lyng.ArgsDeclaration
|
||||||
|
import net.sergeych.lyng.ParsedDeclAnnotation
|
||||||
import net.sergeych.lyng.Pos
|
import net.sergeych.lyng.Pos
|
||||||
import net.sergeych.lyng.TypeDecl
|
import net.sergeych.lyng.TypeDecl
|
||||||
import net.sergeych.lyng.Visibility
|
import net.sergeych.lyng.Visibility
|
||||||
@ -85,6 +86,7 @@ sealed class BytecodeConst {
|
|||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
val writeVisibility: Visibility?,
|
val writeVisibility: Visibility?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation>,
|
||||||
) : BytecodeConst()
|
) : BytecodeConst()
|
||||||
data class ClassDelegatedDecl(
|
data class ClassDelegatedDecl(
|
||||||
val name: String,
|
val name: String,
|
||||||
@ -92,6 +94,7 @@ sealed class BytecodeConst {
|
|||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
val writeVisibility: Visibility?,
|
val writeVisibility: Visibility?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation>,
|
||||||
) : BytecodeConst()
|
) : BytecodeConst()
|
||||||
data class ClassInstanceInitDecl(
|
data class ClassInstanceInitDecl(
|
||||||
val initStatement: Obj,
|
val initStatement: Obj,
|
||||||
@ -103,6 +106,7 @@ sealed class BytecodeConst {
|
|||||||
val writeVisibility: Visibility?,
|
val writeVisibility: Visibility?,
|
||||||
val typeDecl: TypeDecl?,
|
val typeDecl: TypeDecl?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation>,
|
||||||
val isAbstract: Boolean,
|
val isAbstract: Boolean,
|
||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
@ -116,6 +120,7 @@ sealed class BytecodeConst {
|
|||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
val writeVisibility: Visibility?,
|
val writeVisibility: Visibility?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation>,
|
||||||
val isAbstract: Boolean,
|
val isAbstract: Boolean,
|
||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
@ -130,6 +135,7 @@ sealed class BytecodeConst {
|
|||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
val writeVisibility: Visibility?,
|
val writeVisibility: Visibility?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotationSpecs: List<ParsedDeclAnnotation>,
|
||||||
val isAbstract: Boolean,
|
val isAbstract: Boolean,
|
||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
@ -143,6 +149,7 @@ sealed class BytecodeConst {
|
|||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
val writeVisibility: Visibility?,
|
val writeVisibility: Visibility?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotations: List<net.sergeych.lyng.DeclAnnotation>,
|
||||||
val isAbstract: Boolean,
|
val isAbstract: Boolean,
|
||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
@ -153,6 +160,7 @@ sealed class BytecodeConst {
|
|||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
val writeVisibility: Visibility?,
|
val writeVisibility: Visibility?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotations: List<net.sergeych.lyng.DeclAnnotation>,
|
||||||
val isAbstract: Boolean,
|
val isAbstract: Boolean,
|
||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
@ -164,6 +172,7 @@ sealed class BytecodeConst {
|
|||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
val writeVisibility: Visibility?,
|
val writeVisibility: Visibility?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
|
val annotations: List<net.sergeych.lyng.DeclAnnotation>,
|
||||||
val isAbstract: Boolean,
|
val isAbstract: Boolean,
|
||||||
val isClosed: Boolean,
|
val isClosed: Boolean,
|
||||||
val isOverride: Boolean,
|
val isOverride: Boolean,
|
||||||
|
|||||||
@ -365,6 +365,7 @@ class BytecodeStatement private constructor(
|
|||||||
stmt.initializer?.let { unwrapDeep(it) },
|
stmt.initializer?.let { unwrapDeep(it) },
|
||||||
stmt.isDelegated,
|
stmt.isDelegated,
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
|
stmt.annotationSpecs,
|
||||||
stmt.pos
|
stmt.pos
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -385,6 +386,7 @@ class BytecodeStatement private constructor(
|
|||||||
stmt.isClosed,
|
stmt.isClosed,
|
||||||
stmt.isOverride,
|
stmt.isOverride,
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
|
stmt.annotationSpecs,
|
||||||
stmt.fieldId,
|
stmt.fieldId,
|
||||||
stmt.initStatement?.let { unwrapDeep(it) },
|
stmt.initStatement?.let { unwrapDeep(it) },
|
||||||
stmt.pos
|
stmt.pos
|
||||||
@ -400,6 +402,7 @@ class BytecodeStatement private constructor(
|
|||||||
stmt.isClosed,
|
stmt.isClosed,
|
||||||
stmt.isOverride,
|
stmt.isOverride,
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
|
stmt.annotationSpecs,
|
||||||
stmt.prop,
|
stmt.prop,
|
||||||
stmt.methodId,
|
stmt.methodId,
|
||||||
stmt.initStatement?.let { unwrapDeep(it) },
|
stmt.initStatement?.let { unwrapDeep(it) },
|
||||||
@ -416,6 +419,7 @@ class BytecodeStatement private constructor(
|
|||||||
stmt.isClosed,
|
stmt.isClosed,
|
||||||
stmt.isOverride,
|
stmt.isOverride,
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
|
stmt.annotationSpecs,
|
||||||
stmt.methodId,
|
stmt.methodId,
|
||||||
stmt.initStatement?.let { unwrapDeep(it) },
|
stmt.initStatement?.let { unwrapDeep(it) },
|
||||||
stmt.pos
|
stmt.pos
|
||||||
@ -431,6 +435,7 @@ class BytecodeStatement private constructor(
|
|||||||
stmt.isClosed,
|
stmt.isClosed,
|
||||||
stmt.isOverride,
|
stmt.isOverride,
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
|
stmt.annotations,
|
||||||
stmt.isLateInitVal,
|
stmt.isLateInitVal,
|
||||||
stmt.initializer?.let { unwrapDeep(it) },
|
stmt.initializer?.let { unwrapDeep(it) },
|
||||||
stmt.pos
|
stmt.pos
|
||||||
@ -446,6 +451,7 @@ class BytecodeStatement private constructor(
|
|||||||
stmt.isClosed,
|
stmt.isClosed,
|
||||||
stmt.isOverride,
|
stmt.isOverride,
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
|
stmt.annotations,
|
||||||
stmt.prop,
|
stmt.prop,
|
||||||
stmt.pos
|
stmt.pos
|
||||||
)
|
)
|
||||||
@ -461,6 +467,7 @@ class BytecodeStatement private constructor(
|
|||||||
stmt.isClosed,
|
stmt.isClosed,
|
||||||
stmt.isOverride,
|
stmt.isOverride,
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
|
stmt.annotations,
|
||||||
stmt.accessTypeLabel,
|
stmt.accessTypeLabel,
|
||||||
unwrapDeep(stmt.initializer),
|
unwrapDeep(stmt.initializer),
|
||||||
stmt.pos
|
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
|
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassFieldDecl
|
||||||
?: error("DECL_CLASS_FIELD expects ClassFieldDecl at $constId")
|
?: error("DECL_CLASS_FIELD expects ClassFieldDecl at $constId")
|
||||||
val scope = frame.ensureScope()
|
val scope = frame.ensureScope()
|
||||||
|
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
|
||||||
val cls = scope.thisObj as? ObjClass
|
val cls = scope.thisObj as? ObjClass
|
||||||
?: scope.raiseIllegalState("class field init requires class scope")
|
?: scope.raiseIllegalState("class field init requires class scope")
|
||||||
val value = frame.slotToObj(slot).byValueCopy()
|
val value = frame.slotToObj(slot).byValueCopy()
|
||||||
@ -2815,7 +2816,8 @@ class CmdDeclClassField(internal val constId: Int, internal val slot: Int) : Cmd
|
|||||||
decl.visibility,
|
decl.visibility,
|
||||||
decl.writeVisibility,
|
decl.writeVisibility,
|
||||||
Pos.builtIn,
|
Pos.builtIn,
|
||||||
isTransient = decl.isTransient
|
isTransient = decl.isTransient,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
scope.addItem(
|
scope.addItem(
|
||||||
decl.name,
|
decl.name,
|
||||||
@ -2824,7 +2826,8 @@ class CmdDeclClassField(internal val constId: Int, internal val slot: Int) : Cmd
|
|||||||
decl.visibility,
|
decl.visibility,
|
||||||
decl.writeVisibility,
|
decl.writeVisibility,
|
||||||
recordType = ObjRecord.Type.Field,
|
recordType = ObjRecord.Type.Field,
|
||||||
isTransient = decl.isTransient
|
isTransient = decl.isTransient,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2835,6 +2838,7 @@ class CmdDeclClassDelegated(internal val constId: Int, internal val slot: Int) :
|
|||||||
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassDelegatedDecl
|
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassDelegatedDecl
|
||||||
?: error("DECL_CLASS_DELEGATED expects ClassDelegatedDecl at $constId")
|
?: error("DECL_CLASS_DELEGATED expects ClassDelegatedDecl at $constId")
|
||||||
val scope = frame.ensureScope()
|
val scope = frame.ensureScope()
|
||||||
|
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
|
||||||
val cls = scope.thisObj as? ObjClass
|
val cls = scope.thisObj as? ObjClass
|
||||||
?: scope.raiseIllegalState("class delegated init requires class scope")
|
?: scope.raiseIllegalState("class delegated init requires class scope")
|
||||||
val initValue = frame.slotToObj(slot)
|
val initValue = frame.slotToObj(slot)
|
||||||
@ -2857,7 +2861,8 @@ class CmdDeclClassDelegated(internal val constId: Int, internal val slot: Int) :
|
|||||||
decl.writeVisibility,
|
decl.writeVisibility,
|
||||||
Pos.builtIn,
|
Pos.builtIn,
|
||||||
isTransient = decl.isTransient,
|
isTransient = decl.isTransient,
|
||||||
type = ObjRecord.Type.Delegated
|
type = ObjRecord.Type.Delegated,
|
||||||
|
annotations = annotations
|
||||||
).apply {
|
).apply {
|
||||||
delegate = finalDelegate
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
@ -2868,7 +2873,8 @@ class CmdDeclClassDelegated(internal val constId: Int, internal val slot: Int) :
|
|||||||
decl.visibility,
|
decl.visibility,
|
||||||
decl.writeVisibility,
|
decl.writeVisibility,
|
||||||
recordType = ObjRecord.Type.Delegated,
|
recordType = ObjRecord.Type.Delegated,
|
||||||
isTransient = decl.isTransient
|
isTransient = decl.isTransient,
|
||||||
|
annotations = annotations
|
||||||
).apply {
|
).apply {
|
||||||
delegate = finalDelegate
|
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
|
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassInstanceFieldDecl
|
||||||
?: error("DECL_CLASS_INSTANCE_FIELD expects ClassInstanceFieldDecl at $constId")
|
?: error("DECL_CLASS_INSTANCE_FIELD expects ClassInstanceFieldDecl at $constId")
|
||||||
val scope = frame.ensureScope()
|
val scope = frame.ensureScope()
|
||||||
|
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
|
||||||
val cls = scope.thisObj as? ObjClass
|
val cls = scope.thisObj as? ObjClass
|
||||||
?: scope.raiseIllegalState("class instance field requires class scope")
|
?: scope.raiseIllegalState("class instance field requires class scope")
|
||||||
cls.createField(
|
cls.createField(
|
||||||
@ -2911,7 +2918,8 @@ class CmdDeclClassInstanceField(internal val constId: Int, internal val slot: In
|
|||||||
isTransient = decl.isTransient,
|
isTransient = decl.isTransient,
|
||||||
typeDecl = decl.typeDecl,
|
typeDecl = decl.typeDecl,
|
||||||
type = ObjRecord.Type.Field,
|
type = ObjRecord.Type.Field,
|
||||||
fieldId = decl.fieldId
|
fieldId = decl.fieldId,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
if (!decl.isAbstract) {
|
if (!decl.isAbstract) {
|
||||||
decl.initStatement?.let { cls.instanceInitializers += it }
|
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
|
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassInstancePropertyDecl
|
||||||
?: error("DECL_CLASS_INSTANCE_PROPERTY expects ClassInstancePropertyDecl at $constId")
|
?: error("DECL_CLASS_INSTANCE_PROPERTY expects ClassInstancePropertyDecl at $constId")
|
||||||
val scope = frame.ensureScope()
|
val scope = frame.ensureScope()
|
||||||
|
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
|
||||||
val cls = scope.thisObj as? ObjClass
|
val cls = scope.thisObj as? ObjClass
|
||||||
?: scope.raiseIllegalState("class instance property requires class scope")
|
?: scope.raiseIllegalState("class instance property requires class scope")
|
||||||
cls.addProperty(
|
cls.addProperty(
|
||||||
@ -2938,7 +2947,8 @@ class CmdDeclClassInstanceProperty(internal val constId: Int, internal val slot:
|
|||||||
isOverride = decl.isOverride,
|
isOverride = decl.isOverride,
|
||||||
pos = decl.pos,
|
pos = decl.pos,
|
||||||
prop = decl.prop,
|
prop = decl.prop,
|
||||||
methodId = decl.methodId
|
methodId = decl.methodId,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
if (!decl.isAbstract) {
|
if (!decl.isAbstract) {
|
||||||
decl.initStatement?.let { cls.instanceInitializers += it }
|
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
|
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassInstanceDelegatedDecl
|
||||||
?: error("DECL_CLASS_INSTANCE_DELEGATED expects ClassInstanceDelegatedDecl at $constId")
|
?: error("DECL_CLASS_INSTANCE_DELEGATED expects ClassInstanceDelegatedDecl at $constId")
|
||||||
val scope = frame.ensureScope()
|
val scope = frame.ensureScope()
|
||||||
|
val annotations = decl.annotationSpecs.evaluateDeclAnnotations(scope)
|
||||||
val cls = scope.thisObj as? ObjClass
|
val cls = scope.thisObj as? ObjClass
|
||||||
?: scope.raiseIllegalState("class instance delegated requires class scope")
|
?: scope.raiseIllegalState("class instance delegated requires class scope")
|
||||||
cls.createField(
|
cls.createField(
|
||||||
@ -2968,7 +2979,8 @@ class CmdDeclClassInstanceDelegated(internal val constId: Int, internal val slot
|
|||||||
isOverride = decl.isOverride,
|
isOverride = decl.isOverride,
|
||||||
isTransient = decl.isTransient,
|
isTransient = decl.isTransient,
|
||||||
type = ObjRecord.Type.Delegated,
|
type = ObjRecord.Type.Delegated,
|
||||||
methodId = decl.methodId
|
methodId = decl.methodId,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
if (!decl.isAbstract) {
|
if (!decl.isAbstract) {
|
||||||
decl.initStatement?.let { cls.instanceInitializers += it }
|
decl.initStatement?.let { cls.instanceInitializers += it }
|
||||||
@ -2994,7 +3006,8 @@ class CmdDeclInstanceField(internal val constId: Int, internal val slot: Int) :
|
|||||||
isAbstract = decl.isAbstract,
|
isAbstract = decl.isAbstract,
|
||||||
isClosed = decl.isClosed,
|
isClosed = decl.isClosed,
|
||||||
isOverride = decl.isOverride,
|
isOverride = decl.isOverride,
|
||||||
isTransient = decl.isTransient
|
isTransient = decl.isTransient,
|
||||||
|
annotations = decl.annotations
|
||||||
)
|
)
|
||||||
if (slot >= frame.fn.scopeSlotCount) {
|
if (slot >= frame.fn.scopeSlotCount) {
|
||||||
val localIndex = 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,
|
isAbstract = decl.isAbstract,
|
||||||
isClosed = decl.isClosed,
|
isClosed = decl.isClosed,
|
||||||
isOverride = decl.isOverride,
|
isOverride = decl.isOverride,
|
||||||
isTransient = decl.isTransient
|
isTransient = decl.isTransient,
|
||||||
|
annotations = decl.annotations
|
||||||
)
|
)
|
||||||
if (slot >= frame.fn.scopeSlotCount) {
|
if (slot >= frame.fn.scopeSlotCount) {
|
||||||
val localIndex = 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,
|
isAbstract = decl.isAbstract,
|
||||||
isClosed = decl.isClosed,
|
isClosed = decl.isClosed,
|
||||||
isOverride = decl.isOverride,
|
isOverride = decl.isOverride,
|
||||||
isTransient = decl.isTransient
|
isTransient = decl.isTransient,
|
||||||
|
annotations = decl.annotations
|
||||||
).apply {
|
).apply {
|
||||||
delegate = finalDelegate
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
@ -3705,9 +3720,26 @@ class CmdGetClassScope(
|
|||||||
decl = declared
|
decl = declared
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
val resolved = rec ?: scope.raiseSymbolNotFound(name)
|
val resolvedRec = if (rec != null) {
|
||||||
val declClass = decl ?: cls
|
val declClass = decl ?: cls
|
||||||
val resolvedRec = cls.resolveRecord(scope, resolved, name, declClass)
|
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
|
val value = resolvedRec.value
|
||||||
frame.storeObjResult(dst, value)
|
frame.storeObjResult(dst, value)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -28,6 +28,23 @@ import net.sergeych.lynon.LynonType
|
|||||||
// Simple id generator for class identities (not thread-safe; fine for scripts)
|
// Simple id generator for class identities (not thread-safe; fine for scripts)
|
||||||
private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
|
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 {
|
val ObjClassType by lazy {
|
||||||
object : ObjClass("Class") {
|
object : ObjClass("Class") {
|
||||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||||
@ -98,6 +115,30 @@ val ObjClassType by lazy {
|
|||||||
val rec = cls.getInstanceMemberOrNull(name)
|
val rec = cls.getInstanceMemberOrNull(name)
|
||||||
rec?.value ?: ObjNull
|
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,
|
methodId: Int? = null,
|
||||||
typeDecl: net.sergeych.lyng.TypeDecl? = null,
|
typeDecl: net.sergeych.lyng.TypeDecl? = null,
|
||||||
callSignature: net.sergeych.lyng.CallSignature? = null,
|
callSignature: net.sergeych.lyng.CallSignature? = null,
|
||||||
|
annotations: List<DeclAnnotation> = emptyList(),
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
// Validation of override rules: only for non-system declarations
|
// Validation of override rules: only for non-system declarations
|
||||||
var existing: ObjRecord? = null
|
var existing: ObjRecord? = null
|
||||||
@ -953,6 +995,7 @@ open class ObjClass(
|
|||||||
type = type,
|
type = type,
|
||||||
callSignature = callSignature,
|
callSignature = callSignature,
|
||||||
typeDecl = typeDecl,
|
typeDecl = typeDecl,
|
||||||
|
annotations = annotations,
|
||||||
memberName = name,
|
memberName = name,
|
||||||
fieldId = effectiveFieldId,
|
fieldId = effectiveFieldId,
|
||||||
methodId = effectiveMethodId
|
methodId = effectiveMethodId
|
||||||
@ -979,7 +1022,8 @@ open class ObjClass(
|
|||||||
type: ObjRecord.Type = ObjRecord.Type.Field,
|
type: ObjRecord.Type = ObjRecord.Type.Field,
|
||||||
fieldId: Int? = null,
|
fieldId: Int? = null,
|
||||||
methodId: Int? = null,
|
methodId: Int? = null,
|
||||||
callSignature: net.sergeych.lyng.CallSignature? = null
|
callSignature: net.sergeych.lyng.CallSignature? = null,
|
||||||
|
annotations: List<DeclAnnotation> = emptyList()
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
initClassScope()
|
initClassScope()
|
||||||
val existing = classScope!!.objects[name]
|
val existing = classScope!!.objects[name]
|
||||||
@ -1021,6 +1065,7 @@ open class ObjClass(
|
|||||||
recordType = type,
|
recordType = type,
|
||||||
isTransient = isTransient,
|
isTransient = isTransient,
|
||||||
callSignature = callSignature,
|
callSignature = callSignature,
|
||||||
|
annotations = annotations,
|
||||||
fieldId = effectiveFieldId,
|
fieldId = effectiveFieldId,
|
||||||
methodId = effectiveMethodId
|
methodId = effectiveMethodId
|
||||||
)
|
)
|
||||||
@ -1067,7 +1112,8 @@ open class ObjClass(
|
|||||||
isOverride: Boolean = false,
|
isOverride: Boolean = false,
|
||||||
pos: Pos = Pos.builtIn,
|
pos: Pos = Pos.builtIn,
|
||||||
prop: ObjProperty? = null,
|
prop: ObjProperty? = null,
|
||||||
methodId: Int? = null
|
methodId: Int? = null,
|
||||||
|
annotations: List<DeclAnnotation> = emptyList()
|
||||||
) {
|
) {
|
||||||
val g = getter?.let { ObjExternCallable.fromBridge { it() } }
|
val g = getter?.let { ObjExternCallable.fromBridge { it() } }
|
||||||
val s = setter?.let { ObjExternCallable.fromBridge { it(requiredArg(0)); ObjVoid } }
|
val s = setter?.let { ObjExternCallable.fromBridge { it(requiredArg(0)); ObjVoid } }
|
||||||
@ -1076,7 +1122,8 @@ open class ObjClass(
|
|||||||
name, finalProp, false, visibility, writeVisibility, pos, declaringClass,
|
name, finalProp, false, visibility, writeVisibility, pos, declaringClass,
|
||||||
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
|
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
|
||||||
type = ObjRecord.Type.Property,
|
type = ObjRecord.Type.Property,
|
||||||
methodId = methodId
|
methodId = methodId,
|
||||||
|
annotations = annotations
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
import net.sergeych.lyng.DeclAnnotation
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.Visibility
|
import net.sergeych.lyng.Visibility
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ data class ObjRecord(
|
|||||||
var receiver: Obj? = null,
|
var receiver: Obj? = null,
|
||||||
val callSignature: net.sergeych.lyng.CallSignature? = null,
|
val callSignature: net.sergeych.lyng.CallSignature? = null,
|
||||||
val typeDecl: net.sergeych.lyng.TypeDecl? = null,
|
val typeDecl: net.sergeych.lyng.TypeDecl? = null,
|
||||||
|
val annotations: List<DeclAnnotation> = emptyList(),
|
||||||
val memberName: String? = null,
|
val memberName: String? = null,
|
||||||
val fieldId: Int? = null,
|
val fieldId: Int? = null,
|
||||||
val methodId: 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,24 @@ extern class NotImplementedException
|
|||||||
/* Raised when an awaited asynchronous task was cancelled before producing a value. */
|
/* Raised when an awaited asynchronous task was cancelled before producing a value. */
|
||||||
extern class CancellationException : Exception
|
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. */
|
/* A handle to a running asynchronous task. */
|
||||||
extern class Deferred {
|
extern class Deferred {
|
||||||
/* Cancel the task if it is still active. Safe to call multiple times. */
|
/* Cancel the task if it is still active. Safe to call multiple times. */
|
||||||
|
|||||||
280
notes/db/resultset_decode_api.md
Normal file
280
notes/db/resultset_decode_api.md
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
# ResultSet typed decode API
|
||||||
|
|
||||||
|
Status: draft design note
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Extend `lyng.io.db` with row deserialization into ordinary Lyng objects using the new typed serialization-style API naming.
|
||||||
|
|
||||||
|
Primary use case:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Point(x: Real, y: Real)
|
||||||
|
|
||||||
|
val point = db.transaction { tx ->
|
||||||
|
tx.select(
|
||||||
|
"select row as x, col as y from data where not is_deleted"
|
||||||
|
).decodeAs<Point>().first
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agreed API
|
||||||
|
|
||||||
|
Use `decodeAs<T>()` as the only public API form in v1.
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
|
||||||
|
- matches the new typed serialization naming (`Json.decodeAs(...)`)
|
||||||
|
- communicates decoding/materialization, not casting
|
||||||
|
- keeps the common case strongly typed and chain-friendly
|
||||||
|
- avoids adding a second runtime-type overload before it is needed
|
||||||
|
|
||||||
|
Planned Lyng-facing declarations:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
extern class SqlRow {
|
||||||
|
fun decodeAs<T>(): T
|
||||||
|
}
|
||||||
|
|
||||||
|
extern class ResultSet : Iterable<SqlRow> {
|
||||||
|
fun decodeAs<T>(): Iterable<T>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifetime semantics
|
||||||
|
|
||||||
|
`ResultSet.decodeAs<T>()` returns a transaction-scoped iterable view over the underlying result set.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- the returned iterable must not be used after the owning transaction ends
|
||||||
|
- decoded objects created during iteration are detached ordinary Lyng objects
|
||||||
|
- to keep decoded values after the transaction, materialize them inside the transaction
|
||||||
|
- normal materialization forms are `toList()`, `first`, `findFirst`, or manual iteration
|
||||||
|
|
||||||
|
Valid:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
val points = db.transaction { tx ->
|
||||||
|
tx.select("select x, y from point")
|
||||||
|
.decodeAs<Point>()
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Invalid:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
val decoded = db.transaction { tx ->
|
||||||
|
tx.select("select x, y from point").decodeAs<Point>()
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded.first
|
||||||
|
```
|
||||||
|
|
||||||
|
## ResultSet shape
|
||||||
|
|
||||||
|
`ResultSet.decodeAs<T>()` should preserve the current `ResultSet` paradigm:
|
||||||
|
|
||||||
|
- `ResultSet` stays the row-producing source
|
||||||
|
- `decodeAs<T>()` is a projection from `Iterable<SqlRow>` to `Iterable<T>`
|
||||||
|
- no new DB-specific collection type is introduced in v1
|
||||||
|
|
||||||
|
Implementation-wise, `ResultSet.decodeAs<T>()` can be defined as a lazy iterable that decodes each row via `SqlRow.decodeAs<T>()`.
|
||||||
|
|
||||||
|
## Mapping discussion to finalize
|
||||||
|
|
||||||
|
The following mapping behavior still needs explicit design decisions:
|
||||||
|
|
||||||
|
- how constructor parameters are matched from columns
|
||||||
|
- whether matching is case-insensitive
|
||||||
|
- whether mutable serializable fields are populated after constructor call
|
||||||
|
- treatment of default constructor values
|
||||||
|
- treatment of nullable vs non-nullable targets
|
||||||
|
- behavior for missing columns
|
||||||
|
- behavior for extra columns
|
||||||
|
- behavior for duplicate/ambiguous column labels
|
||||||
|
- whether `onDeserialized()` is called after row decode
|
||||||
|
- whether v1 supports only flat object decode or also nested shapes
|
||||||
|
|
||||||
|
## Current direction for mapping
|
||||||
|
|
||||||
|
Current likely direction, not finalized yet:
|
||||||
|
|
||||||
|
- constructor parameters map by column label
|
||||||
|
- matching is case-insensitive, consistent with `SqlRow["name"]`
|
||||||
|
- after constructor call, remaining matching serializable mutable fields may be assigned
|
||||||
|
- missing required non-null constructor values fail
|
||||||
|
- missing nullable constructor parameters become `null`
|
||||||
|
- defaulted constructor parameters use their defaults when the column is absent
|
||||||
|
- ambiguous duplicate column labels fail
|
||||||
|
- extra columns likely fail in strict mode for v1
|
||||||
|
- `onDeserialized()` likely should run after the object is fully populated
|
||||||
|
- v1 should likely stay flat and avoid nested/prefix-based mapping
|
||||||
|
|
||||||
|
## Projection/conversion rules
|
||||||
|
|
||||||
|
### General principle
|
||||||
|
|
||||||
|
Row decoding should be strict and predictable.
|
||||||
|
|
||||||
|
It should not globally treat every SQL string column as serialized JSON or every binary column as Lynon.
|
||||||
|
|
||||||
|
That would be too implicit:
|
||||||
|
|
||||||
|
- ordinary text columns are common and must stay ordinary text by default
|
||||||
|
- ordinary binary/blob columns are common and must stay raw binary by default
|
||||||
|
- automatic format decoding should happen only when there is a clear signal
|
||||||
|
|
||||||
|
### Proposed conversion precedence
|
||||||
|
|
||||||
|
For each constructor parameter or serializable mutable field:
|
||||||
|
|
||||||
|
1. resolve the source column by name
|
||||||
|
2. if the source value already matches the target type, use it directly
|
||||||
|
3. if an explicit DB decoding attribute is present on the target member, apply that decoding rule
|
||||||
|
4. otherwise, if the column metadata clearly indicates a special encoded DB type and the target is not the raw DB carrier type, apply the built-in format rule
|
||||||
|
5. otherwise fail with a decode/type mismatch error
|
||||||
|
|
||||||
|
### Direct match
|
||||||
|
|
||||||
|
Direct match means the row value is already assignable to the target type after the normal SQL backend conversion.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- SQL numeric column already surfaced as `Int`/`Real`/`Decimal`
|
||||||
|
- SQL bool column surfaced as `Bool`
|
||||||
|
- SQL date/time column surfaced as `Date`, `DateTime`, `Instant`
|
||||||
|
- SQL text column surfaced as `String`
|
||||||
|
- SQL binary column surfaced as `Buffer`
|
||||||
|
|
||||||
|
These should not trigger any extra JSON/Lynon decoding.
|
||||||
|
|
||||||
|
### Built-in encoded-column rules
|
||||||
|
|
||||||
|
Current likely direction:
|
||||||
|
|
||||||
|
- JSON/JSONB-like columns should decode through typed canonical `Json` when the target is not `String`
|
||||||
|
- binary columns should decode through `Lynon` when the target is not `Buffer`
|
||||||
|
|
||||||
|
This implies the current default:
|
||||||
|
|
||||||
|
- string -> non-string is eligible for automatic typed `Json` decode only when the column metadata says the DB column is JSON-like
|
||||||
|
- binary -> non-binary is decoded through `Lynon`
|
||||||
|
- binary -> `Buffer` stays raw `Buffer`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- PostgreSQL `json` / `jsonb` column into `Point` -> use typed `Json` decode
|
||||||
|
- PostgreSQL `jsonb` column into `Map<String, Object?>` -> use typed `Json` decode
|
||||||
|
- plain `text` / `varchar` column into `Point` -> fail unless explicitly annotated
|
||||||
|
- `bytea` / `blob` column into `Buffer` -> direct match, no Lynon decode
|
||||||
|
- `bytea` / `blob` column into `Point` -> decode with `Lynon`
|
||||||
|
|
||||||
|
### Attribute-based explicit decoding
|
||||||
|
|
||||||
|
Common explicit attributes look useful:
|
||||||
|
|
||||||
|
- `@DbJson`
|
||||||
|
- `@DbLynon`
|
||||||
|
|
||||||
|
Applied to constructor parameters and serializable mutable fields.
|
||||||
|
|
||||||
|
Meaning:
|
||||||
|
|
||||||
|
- `@DbJson` means decode the column value as typed canonical JSON into the target member type
|
||||||
|
- `@DbLynon` means decode the column value as Lynon into the target member type
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Record(
|
||||||
|
id: Int,
|
||||||
|
@DbJson payload: Payload,
|
||||||
|
@DbLynon cachedState: CacheEntry
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps the common DB formats easy to use without making plain `String` or `Buffer` columns magical.
|
||||||
|
|
||||||
|
Implementation note:
|
||||||
|
|
||||||
|
- declaration metadata now preserves evaluated constructor-parameter and class-member annotation arguments
|
||||||
|
- annotation arguments are evaluated once at declaration creation time and retained for the lifetime of the declaration
|
||||||
|
- `@DbDecodeWith(...)` now uses that preserved metadata path
|
||||||
|
|
||||||
|
### Generic custom decoder hook
|
||||||
|
|
||||||
|
A generic hook is useful too, but it should be adapter-based, not lambda-based.
|
||||||
|
|
||||||
|
Planned shape:
|
||||||
|
|
||||||
|
- `@DbDecodeWith(adapter)`
|
||||||
|
- `adapter` should be an instance of a dedicated interface such as `DbFieldAdapter`
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
- a named adapter interface is easier to document and evolve than arbitrary callables
|
||||||
|
- it gives us room for richer decoding context without baking ad-hoc callable signatures into annotations
|
||||||
|
- it keeps the DB mapping API explicit and self-describing
|
||||||
|
|
||||||
|
Current design direction:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
interface DbFieldAdapter {
|
||||||
|
fun decode(rawValue: Object?, column: SqlColumn, row: SqlRow, targetType: Object): Object? =
|
||||||
|
throw NotImplementedException("DB field adapter decode is not implemented")
|
||||||
|
|
||||||
|
fun encode(value: Object?, targetType: Object): Object? =
|
||||||
|
throw NotImplementedException("DB field adapter encode is not implemented")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Decided:
|
||||||
|
|
||||||
|
- `decode(...)` should receive the target type
|
||||||
|
- adapters may be any ordinary instance, not only singleton objects
|
||||||
|
- the same abstraction should later support symmetric `encode(...)`
|
||||||
|
- adapter result must be checked against the target member type after decoding
|
||||||
|
|
||||||
|
Still open before full implementation:
|
||||||
|
|
||||||
|
- exact annotation shape for `@DbDecodeWith(...)`
|
||||||
|
- whether target member name should also be passed
|
||||||
|
- whether `targetType` should later get a more specific declaration type than plain `Object`
|
||||||
|
|
||||||
|
Implemented in the current design:
|
||||||
|
|
||||||
|
- `@DbDecodeWith(adapter)` on constructor parameters
|
||||||
|
- `@DbDecodeWith(adapter)` on class-body fields/properties participating in `decodeAs<T>()`
|
||||||
|
|
||||||
|
Future improvement:
|
||||||
|
|
||||||
|
- compiler warning when preserved annotation metadata captures runtime state/closures
|
||||||
|
- extend preserved annotation metadata beyond constructor parameters and class members to functions and top-level declarations
|
||||||
|
|
||||||
|
### Arrays and maps
|
||||||
|
|
||||||
|
Arrays and maps should not get DB-specific bespoke mapping in v1 unless they are coming through a recognized encoded format.
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
- portable SQL array/map support is backend-specific and inconsistent
|
||||||
|
- JSON columns already give us a portable representation for `List` and `Map`
|
||||||
|
- adding DB-native array semantics now would complicate the contract too early
|
||||||
|
|
||||||
|
So in v1:
|
||||||
|
|
||||||
|
- if the backend already surfaces a value that directly matches the target type, use it
|
||||||
|
- otherwise `List` / `Map` reconstruction should happen via `@DbJson` or recognized JSON-like column metadata
|
||||||
|
|
||||||
|
### Recommended v1 policy
|
||||||
|
|
||||||
|
Current recommended projection policy:
|
||||||
|
|
||||||
|
- direct type match first
|
||||||
|
- then explicit member attribute (`@DbJson`, `@DbLynon`)
|
||||||
|
- then metadata-driven JSON decode for recognized JSON-like DB columns
|
||||||
|
- then Lynon decode for binary columns when the target is not `Buffer`
|
||||||
|
- no implicit JSON decode for arbitrary text columns
|
||||||
|
- fail on anything else
|
||||||
Loading…
x
Reference in New Issue
Block a user