Support object extension indexers

This commit is contained in:
sergeych committed 2026-04-07 00:49:51 +03:00
1 parent 8386337c42
commit 583067780f
8 files changed
+252 -35

No files matched your search

@@ -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)
val implicitType = currentImplicitThisTypeName()
val ids = resolveMemberIds(next.value, next.pos, implicitType)
ThisMethodSlotCallRef(next.value, ids.methodId, args, tailBlock, isOptional)
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,26 +2831,32 @@ 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 ->
QualifiedThisMethodSlotCallRef(
left.typeName,
next.value,
resolveMemberIds(next.value, next.pos, left.typeName).methodId,
args,
tailBlock,
isOptional
).also {
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
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,
resolveMemberIds(next.value, next.pos, left.typeName).methodId,
args,
tailBlock,
isOptional
).also {
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
}
}
else -> {
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
@@ -2847,7 +2864,7 @@ class Compiler(
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