Support object extension indexers
This commit is contained in:
parent
8386337c42
commit
583067780f
@ -10,6 +10,7 @@ For a programmer-focused migration summary across 1.5.x, see `docs/whats_new_1_5
|
||||
- The 1.5 line now brings together richer ranges and loops, interpolation, math modules, immutable and observable collections, richer `lyngio`, and much better CLI/IDE support.
|
||||
- `1.5.4` specifically fixes user-visible issues around decimal arithmetic, mixed numeric flows, list behavior, and observable list hooks.
|
||||
- `1.5.4` also fixes extension-member registration for named singleton `object` declarations, so `fun X.foo()` and `val X.bar` now work as expected.
|
||||
- `1.5.4` also lets named singleton `object` declarations use scoped indexer extensions with bracket syntax, so patterns like `Storage["name"]` can be implemented with `override fun Storage.getAt(...)` / `putAt(...)`.
|
||||
- The docs, homepage samples, and release metadata now point at the current stable version.
|
||||
|
||||
## User Highlights Across 1.5.x
|
||||
|
||||
@ -2771,7 +2771,14 @@ class Compiler(
|
||||
val next = cc.next()
|
||||
if (next.type == Token.Type.ID) {
|
||||
// could be () call or obj.method {} call
|
||||
val nt = cc.current()
|
||||
var nt = cc.current()
|
||||
var memberCallTypeArgs: List<TypeDecl>? = null
|
||||
if (nt.type == Token.Type.LT) {
|
||||
memberCallTypeArgs = tryParseCallTypeArgsAfterLt()
|
||||
if (memberCallTypeArgs != null) {
|
||||
nt = cc.current()
|
||||
}
|
||||
}
|
||||
when (nt.type) {
|
||||
Token.Type.LPAREN -> {
|
||||
cc.next()
|
||||
@ -2784,9 +2791,9 @@ class Compiler(
|
||||
val nestedClass = receiverClass?.let { resolveClassByName("${it.className}.${next.value}") }
|
||||
if (nestedClass != null) {
|
||||
val field = FieldRef(left, next.value, isOptional)
|
||||
operand = CallRef(field, args, tailBlock, isOptional)
|
||||
operand = CallRef(field, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||
} else {
|
||||
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||
}
|
||||
} else {
|
||||
// instance method call
|
||||
@ -2808,9 +2815,13 @@ class Compiler(
|
||||
operand = when (left) {
|
||||
is LocalVarRef -> if (left.name == "this") {
|
||||
resolutionSink?.referenceMember(next.value, next.pos)
|
||||
if (memberCallTypeArgs != null) {
|
||||
MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||
} else {
|
||||
val implicitType = currentImplicitThisTypeName()
|
||||
val ids = resolveMemberIds(next.value, next.pos, implicitType)
|
||||
ThisMethodSlotCallRef(next.value, ids.methodId, args, tailBlock, isOptional)
|
||||
}
|
||||
} else if (left.name == "scope") {
|
||||
if (next.value == "get" || next.value == "set") {
|
||||
val first = args.firstOrNull()?.value
|
||||
@ -2820,17 +2831,22 @@ class Compiler(
|
||||
resolutionSink?.referenceReflection(name.value, next.pos)
|
||||
}
|
||||
}
|
||||
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||
MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||
} else {
|
||||
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
|
||||
if (unionCall != null) {
|
||||
unionCall
|
||||
} else {
|
||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||
MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||
}
|
||||
}
|
||||
is QualifiedThisRef ->
|
||||
if (memberCallTypeArgs != null) {
|
||||
MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs).also {
|
||||
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
|
||||
}
|
||||
} else {
|
||||
QualifiedThisMethodSlotCallRef(
|
||||
left.typeName,
|
||||
next.value,
|
||||
@ -2841,13 +2857,14 @@ class Compiler(
|
||||
).also {
|
||||
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
|
||||
if (unionCall != null) {
|
||||
unionCall
|
||||
} else {
|
||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||
MethodCallRef(left, next.value, args, tailBlock, isOptional, memberCallTypeArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3253,12 +3270,13 @@ class Compiler(
|
||||
is LocalVarRef -> ref.name
|
||||
is FastLocalVarRef -> ref.name
|
||||
is LocalSlotRef -> ref.name
|
||||
is FieldRef -> ref.name
|
||||
else -> null
|
||||
}
|
||||
if (name != null) {
|
||||
if (lookupGenericFunctionDecl(name) != null) return true
|
||||
if (name.firstOrNull()?.isUpperCase() == true) return true
|
||||
return false
|
||||
return ref is FieldRef
|
||||
}
|
||||
return ref is ConstRef && ref.constValue is ObjClass
|
||||
}
|
||||
@ -6443,6 +6461,23 @@ class Compiler(
|
||||
}
|
||||
}
|
||||
val implicitThisTypeName = currentImplicitThisTypeName()
|
||||
if (left is FieldRef && !explicitTypeArgs.isNullOrEmpty()) {
|
||||
val nestedClass = if (shouldTreatAsClassScopeCall(left.target, left.name)) {
|
||||
resolveReceiverClassForMember(left.target)?.let { resolveClassByName("${it.className}.${left.name}") }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (nestedClass == null) {
|
||||
return MethodCallRef(
|
||||
left.target,
|
||||
left.name,
|
||||
args,
|
||||
detectedBlockArgument,
|
||||
left.isOptional || isOptional,
|
||||
explicitTypeArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
val result = when (left) {
|
||||
is ImplicitThisMemberRef ->
|
||||
if (left.methodId == null && left.fieldId != null) {
|
||||
|
||||
@ -4712,7 +4712,7 @@ class BytecodeCompiler(
|
||||
val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null
|
||||
val dst = allocSlot()
|
||||
fun emitDynamicCall(): CompiledValue? {
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
val nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
|
||||
if (!ref.isOptional) {
|
||||
@ -4759,7 +4759,7 @@ class BytecodeCompiler(
|
||||
if (methodId != null && resolvedMember?.declaringClass?.className != "Obj") {
|
||||
val encodedMethodId = encodeMemberId(receiverClass, methodId) ?: methodId
|
||||
if (!ref.isOptional) {
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
setPos(callPos)
|
||||
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, encodedMethodId, args.base, encodedCount, dst)
|
||||
@ -4778,7 +4778,7 @@ class BytecodeCompiler(
|
||||
Opcode.JMP_IF_TRUE,
|
||||
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel))
|
||||
)
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
setPos(callPos)
|
||||
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, encodedMethodId, args.base, encodedCount, dst)
|
||||
@ -4796,7 +4796,7 @@ class BytecodeCompiler(
|
||||
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, encodedFieldId, -1, calleeSlot)
|
||||
}
|
||||
if (!ref.isOptional) {
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
setPos(callPos)
|
||||
builder.emit(Opcode.CALL_SLOT, calleeSlot, args.base, encodedCount, dst)
|
||||
@ -4812,7 +4812,7 @@ class BytecodeCompiler(
|
||||
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel))
|
||||
)
|
||||
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, encodedFieldId, -1, calleeSlot)
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
setPos(callPos)
|
||||
builder.emit(Opcode.CALL_SLOT, calleeSlot, args.base, encodedCount, dst)
|
||||
@ -4845,7 +4845,7 @@ class BytecodeCompiler(
|
||||
builder.emit(Opcode.CONST_NULL, memberSlot)
|
||||
builder.mark(endLabel)
|
||||
}
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
||||
val args = compileCallArgs(ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
setPos(callPos)
|
||||
builder.emit(Opcode.CALL_SLOT, memberSlot, args.base, encodedCount, dst)
|
||||
@ -4858,7 +4858,7 @@ class BytecodeCompiler(
|
||||
)
|
||||
val callee = ensureObjSlot(extSlot)
|
||||
if (!ref.isOptional) {
|
||||
val args = compileCallArgsWithReceiver(receiver, ref.args, ref.tailBlock) ?: return null
|
||||
val args = compileCallArgsWithReceiver(receiver, ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
setPos(callPos)
|
||||
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
|
||||
@ -4874,7 +4874,7 @@ class BytecodeCompiler(
|
||||
Opcode.JMP_IF_TRUE,
|
||||
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel))
|
||||
)
|
||||
val args = compileCallArgsWithReceiver(receiver, ref.args, ref.tailBlock) ?: return null
|
||||
val args = compileCallArgsWithReceiver(receiver, ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
setPos(callPos)
|
||||
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
|
||||
@ -5067,13 +5067,14 @@ class BytecodeCompiler(
|
||||
private fun compileCallArgsWithReceiver(
|
||||
receiver: CompiledValue,
|
||||
args: List<ParsedArgument>,
|
||||
tailBlock: Boolean
|
||||
tailBlock: Boolean,
|
||||
explicitTypeArgs: List<TypeDecl>? = null
|
||||
): CallArgs? {
|
||||
val argSlots = IntArray(args.size + 1) { allocSlot() }
|
||||
val receiverObj = ensureObjSlot(receiver)
|
||||
builder.emit(Opcode.MOVE_OBJ, receiverObj.slot, argSlots[0])
|
||||
updateSlotType(argSlots[0], SlotType.OBJ)
|
||||
val needPlan = tailBlock || args.any { it.isSplat || it.name != null }
|
||||
val needPlan = tailBlock || args.any { it.isSplat || it.name != null } || !explicitTypeArgs.isNullOrEmpty()
|
||||
val specs = if (needPlan) ArrayList<BytecodeConst.CallArgSpec>(args.size + 1) else null
|
||||
specs?.add(BytecodeConst.CallArgSpec(null, false))
|
||||
for ((index, arg) in args.withIndex()) {
|
||||
@ -5086,7 +5087,13 @@ class BytecodeCompiler(
|
||||
specs?.add(BytecodeConst.CallArgSpec(arg.name, arg.isSplat))
|
||||
}
|
||||
val planId = if (needPlan) {
|
||||
builder.addConst(BytecodeConst.CallArgsPlan(tailBlock, specs ?: emptyList()))
|
||||
builder.addConst(
|
||||
BytecodeConst.CallArgsPlan(
|
||||
tailBlock = tailBlock,
|
||||
specs = specs ?: emptyList(),
|
||||
explicitTypeArgs = explicitTypeArgs ?: emptyList()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@ -3301,7 +3301,7 @@ private suspend fun resolveDynamicFieldValue(scope: Scope, receiver: Obj, name:
|
||||
}
|
||||
if (rec.type == ObjRecord.Type.Fun && !rec.isAbstract) {
|
||||
val recv = rec.receiver ?: receiver
|
||||
return rec.value.invoke(scope, recv, Arguments.EMPTY, rec.declaringClass)
|
||||
return invokeForFieldReadOrReturnCallable(scope, recv, rec, rec.declaringClass)
|
||||
}
|
||||
return rec.value
|
||||
}
|
||||
|
||||
@ -518,7 +518,7 @@ open class Obj {
|
||||
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
|
||||
val resolved = resolveRecord(scope, rec, name, caller)
|
||||
if (resolved.type == ObjRecord.Type.Fun)
|
||||
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, caller))
|
||||
return resolved.copy(value = invokeForFieldReadOrReturnCallable(scope, this, resolved, caller))
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
@ -532,7 +532,7 @@ open class Obj {
|
||||
val decl = rec.declaringClass ?: cls
|
||||
val resolved = resolveRecord(scope, rec, name, decl)
|
||||
if (resolved.type == ObjRecord.Type.Fun)
|
||||
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, decl))
|
||||
return resolved.copy(value = invokeForFieldReadOrReturnCallable(scope, this, resolved, decl))
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
@ -547,7 +547,7 @@ open class Obj {
|
||||
scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||
val resolved = resolveRecord(scope, rec, name, decl)
|
||||
if (resolved.type == ObjRecord.Type.Fun)
|
||||
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, decl))
|
||||
return resolved.copy(value = invokeForFieldReadOrReturnCallable(scope, this, resolved, decl))
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
@ -557,7 +557,7 @@ open class Obj {
|
||||
val prop = ext.value as ObjProperty
|
||||
ObjRecord(prop.callGetter(scope, this, ext.declaringClass), isMutable = false)
|
||||
} else {
|
||||
ext.copy(value = ext.value.invoke(scope, this, Arguments.EMPTY, ext.declaringClass))
|
||||
ext.copy(value = invokeForFieldReadOrReturnCallable(scope, this, ext, ext.declaringClass))
|
||||
}
|
||||
}
|
||||
|
||||
@ -660,6 +660,14 @@ open class Obj {
|
||||
if (hasNonRootIndexerMember("getAt")) {
|
||||
return invokeInstanceMethod(scope, "getAt", Arguments(index))
|
||||
}
|
||||
// Extension indexers are checked only after concrete class/indexer overrides.
|
||||
// If this path becomes hot in benchmarks, add a small receiver-shape/member cache here
|
||||
// rather than moving extension lookup ahead of the non-root member fast path.
|
||||
scope.findExtension(objClass, "getAt")?.let { ext ->
|
||||
if (ext.type != ObjRecord.Type.Delegated) {
|
||||
return ext.value.invoke(scope, this, Arguments(index), ext.declaringClass)
|
||||
}
|
||||
}
|
||||
if (index is ObjString) {
|
||||
return readField(scope, index.value).value
|
||||
}
|
||||
@ -679,6 +687,20 @@ open class Obj {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Same optimization note as in getAt(): extension indexers work here, but they are
|
||||
// intentionally behind concrete overrides to keep normal class-defined indexers fast.
|
||||
scope.findExtension(objClass, "putAt")?.let { ext ->
|
||||
if (ext.type != ObjRecord.Type.Delegated) {
|
||||
ext.value.invoke(scope, this, Arguments(index, newValue), ext.declaringClass)
|
||||
return
|
||||
}
|
||||
}
|
||||
scope.findExtension(objClass, "setAt")?.let { ext ->
|
||||
if (ext.type != ObjRecord.Type.Delegated) {
|
||||
ext.value.invoke(scope, this, Arguments(index, newValue), ext.declaringClass)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (index is ObjString) {
|
||||
writeField(scope, index.value, newValue)
|
||||
return
|
||||
@ -1130,3 +1152,24 @@ suspend fun <T>Obj.decodeSerializableWith(strategy: DeserializationStrategy<T>,
|
||||
*/
|
||||
suspend inline fun <reified T>Obj.decodeSerializable(scope: Scope= Scope()) =
|
||||
decodeSerializableWith<T>(serializer<T>(), scope)
|
||||
|
||||
internal suspend fun invokeForFieldReadOrReturnCallable(
|
||||
scope: Scope,
|
||||
receiver: Obj,
|
||||
record: ObjRecord,
|
||||
decl: ObjClass?
|
||||
): Obj {
|
||||
return try {
|
||||
record.value.invoke(scope, receiver, Arguments.EMPTY, decl)
|
||||
} catch (e: ExecutionError) {
|
||||
if (e.message.isMissingArgsForAutoFieldInvoke()) record.value else throw e
|
||||
} catch (e: ScriptError) {
|
||||
if (e.message.isMissingArgsForAutoFieldInvoke()) record.value else throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.isMissingArgsForAutoFieldInvoke(): Boolean {
|
||||
val lower = this?.lowercase() ?: return false
|
||||
if (!lower.contains("got 0")) return false
|
||||
return lower.contains("expected") || lower.contains("missing required argument")
|
||||
}
|
||||
|
||||
@ -483,6 +483,7 @@ class MethodCallRef(
|
||||
internal val args: List<ParsedArgument>,
|
||||
internal val tailBlock: Boolean,
|
||||
internal val isOptional: Boolean,
|
||||
internal val explicitTypeArgs: List<TypeDecl>? = null,
|
||||
) : ObjRef {
|
||||
// 4-entry PIC for method invocations (guarded by PerfFlags.METHOD_PIC)
|
||||
private var mKey1: Long = 0L; private var mVer1: Int = -1; private var mInvoker1: (suspend (Obj, Scope, Arguments) -> Obj)? = null
|
||||
|
||||
@ -16,10 +16,20 @@
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.bridge.bindObject
|
||||
import net.sergeych.lyng.bridge.data
|
||||
import net.sergeych.lyng.eval
|
||||
import net.sergeych.lyng.obj.ObjBool
|
||||
import net.sergeych.lyng.obj.ObjBuffer
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjInstance
|
||||
import net.sergeych.lyng.obj.ObjList
|
||||
import net.sergeych.lyng.obj.ObjNull
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
import net.sergeych.lyng.toSource
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -990,4 +1000,106 @@ class OOTest {
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtendingObjectWithExternals() = runTest {
|
||||
val s = EvalSession()
|
||||
s.eval("""
|
||||
extern object Storage {
|
||||
|
||||
extern val spaceUsed: Int
|
||||
extern val spaceAvailable: Int
|
||||
|
||||
/*
|
||||
Return packed binary data or null
|
||||
*/
|
||||
extern fun getPacked(key: String): Buffer?
|
||||
|
||||
/*
|
||||
Upsert packed binary data
|
||||
*/
|
||||
extern fun putPacked(key: String,value: Buffer)
|
||||
|
||||
/*
|
||||
Delete data.
|
||||
@return true if data were actually deleted, false means
|
||||
there were no data for the key.
|
||||
*/
|
||||
extern fun delete(key: String): Bool
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
val scope = s.getScope() as ModuleScope
|
||||
scope.bindObject("Storage") {
|
||||
init { _ ->
|
||||
data = mutableMapOf<String, ObjBuffer>()
|
||||
}
|
||||
addVal("spaceUsed") {
|
||||
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
||||
ObjInt(storage.values.sumOf { it.size }.toLong())
|
||||
}
|
||||
addVal("spaceAvailable") {
|
||||
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
||||
val capacity = 1_024
|
||||
ObjInt((capacity - storage.values.sumOf { it.size }).toLong())
|
||||
}
|
||||
addFun("getPacked") {
|
||||
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
||||
val key = (args.list[0] as ObjString).value
|
||||
storage[key] ?: ObjNull
|
||||
}
|
||||
addFun("putPacked") {
|
||||
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
||||
val key = (args.list[0] as ObjString).value
|
||||
val value = args.list[1] as ObjBuffer
|
||||
storage[key] = value
|
||||
ObjVoid
|
||||
}
|
||||
addFun("delete") {
|
||||
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
||||
val key = (args.list[0] as ObjString).value
|
||||
ObjBool(storage.remove(key) != null)
|
||||
}
|
||||
}
|
||||
s.eval("""
|
||||
import lyng.serialization
|
||||
|
||||
// Use names that do not collide with Obj built-ins so extension dispatch is exercised.
|
||||
override fun Storage.getAt(key: String): Object? {
|
||||
Storage.getPacked(key)?.let {
|
||||
Lynon.decode(it.toBitInput())
|
||||
}
|
||||
}
|
||||
|
||||
override fun Storage.putAt(key: String, value: Object) {
|
||||
Storage.putPacked(key, Lynon.encode(value).toBuffer())
|
||||
}
|
||||
|
||||
assertEquals(0, Storage.spaceUsed)
|
||||
assertEquals(1024, Storage.spaceAvailable)
|
||||
val missing: String? = Storage["missing"]
|
||||
assertEquals(null, missing)
|
||||
|
||||
Storage["name"] = "alice"
|
||||
Storage["count"] = 42
|
||||
|
||||
val name: String? = Storage["name"]
|
||||
val count: Int? = Storage["count"]
|
||||
assertEquals("alice", name)
|
||||
assertEquals(42, count)
|
||||
assert(Storage.spaceUsed > 0)
|
||||
assert(Storage.spaceAvailable < 1024)
|
||||
|
||||
val wrappedName: String? = __ext__Storage__getAt(Storage, "name")
|
||||
assertEquals("alice", wrappedName)
|
||||
__ext__Storage__putAt(Storage, "flag", true)
|
||||
val flag: Bool? = Storage["flag"]
|
||||
assertEquals(true, flag)
|
||||
|
||||
assert(Storage.delete("name"))
|
||||
val deletedName: String? = Storage["name"]
|
||||
assertEquals(null, deletedName)
|
||||
assert(!Storage.delete("name"))
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
18
notes/object_indexer_extension_optimization.md
Normal file
18
notes/object_indexer_extension_optimization.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Object indexer extension follow-up
|
||||
|
||||
Context
|
||||
- `Obj.getAt` / `Obj.putAt` now support scoped extension indexers for named singleton `object` declarations, so patterns like `Storage["name"]` can route through `override fun Storage.getAt(...)` and `override fun Storage.putAt(...)`.
|
||||
- Dispatch order is intentionally:
|
||||
1. concrete non-root class members
|
||||
2. scoped extensions
|
||||
3. built-in string-field fallback
|
||||
|
||||
Why this order
|
||||
- Normal class-defined indexers are the common fast path and should not pay extension lookup cost first.
|
||||
- Checking scoped extensions earlier caused avoidable overhead for regular overridden indexers.
|
||||
- Routing extension indexers through the generic root `Obj.getAt` member path caused recursion; direct extension dispatch avoids that.
|
||||
|
||||
Possible future optimization
|
||||
- If extension-based indexers show up in hot profiles, add a tiny receiver-shape/member cache at the extension-dispatch points in `Obj.getAt` / `Obj.putAt`.
|
||||
- The cache should stay behind the concrete non-root member fast path.
|
||||
- A good shape key is likely `(receiver class id/layout version, member name)` with the cached extension record/wrapper.
|
||||
Loading…
x
Reference in New Issue
Block a user