diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 226af69..1056564 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -302,8 +302,6 @@ android { } } dependencies { - implementation(libs.firebase.crashlytics.buildtools) - implementation(libs.compiler) } publishing { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 3313fbe..b934fb6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -188,6 +188,7 @@ class Compiler( private val callableReturnTypeByName: MutableMap = mutableMapOf() private val callableReturnTypeDeclByName: MutableMap = mutableMapOf() private val lambdaReturnTypeByRef: MutableMap = mutableMapOf() + private val exactLambdaRefByScopeId: MutableMap> = mutableMapOf() private val lambdaCaptureEntriesByRef: MutableMap> = mutableMapOf() private val classFieldTypesByName: MutableMap> = mutableMapOf() @@ -1894,6 +1895,7 @@ class Compiler( forcedLocalSlotInfo = forcedLocalInfo, forcedLocalScopeId = forcedLocalScopeId, slotTypeByScopeId = slotTypeByScopeId, + exactLambdaRefByScopeId = exactLambdaRefByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownClassMapForBytecode(), knownClassNames = knownClassNamesForBytecode(), @@ -2249,6 +2251,7 @@ class Compiler( scopeSlotNameSet = scopeSlotNameSet, moduleScopeId = moduleScopeId, slotTypeByScopeId = slotTypeByScopeId, + exactLambdaRefByScopeId = exactLambdaRefByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownClassMapForBytecode(), knownClassNames = knownClassNamesForBytecode(), @@ -2282,6 +2285,7 @@ class Compiler( scopeSlotNameSet = scopeSlotNameSet, moduleScopeId = moduleScopeId, slotTypeByScopeId = slotTypeByScopeId, + exactLambdaRefByScopeId = exactLambdaRefByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownClassMapForBytecode(), knownClassNames = knownClassNamesForBytecode(), @@ -2340,6 +2344,7 @@ class Compiler( globalSlotInfo = globalSlotInfo, globalSlotScopeId = globalSlotScopeId, slotTypeByScopeId = slotTypeByScopeId, + exactLambdaRefByScopeId = exactLambdaRefByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownNames, knownClassNames = knownClassNamesForBytecode(), @@ -3539,6 +3544,7 @@ class Compiler( body } val bytecodeFn = (fnStatements as? BytecodeStatement)?.bytecodeFunction() + val inlineBodyRef = argsDeclaration?.let { null } ?: extractInlineLambdaBodyRef(body) val ref = LambdaFnRef( valueFn = { closureScope -> val captureRecords = closureScope.captureRecords @@ -3621,6 +3627,7 @@ class Compiler( argsDeclaration = argsDeclaration, captureEntries = captureEntries, inferredReturnClass = returnClass, + inlineBodyRef = inlineBodyRef, preferredThisType = expectedReceiverType, wrapAsExtensionCallable = wrapAsExtensionCallable, returnLabels = returnLabels, @@ -3635,6 +3642,18 @@ class Compiler( return ref } + private fun extractInlineLambdaBodyRef(statement: Statement): ObjRef? { + val target = if (statement is BytecodeStatement) statement.original else statement + return when (target) { + is ExpressionStatement -> target.ref + is BlockStatement -> { + val statements = target.statements() + if (statements.size == 1) extractInlineLambdaBodyRef(statements[0]) else null + } + else -> null + } + } + private suspend fun parseArrayLiteral(): List { // it should be called after Token.Type.LBRACKET is consumed val entries = mutableListOf() @@ -4968,6 +4987,26 @@ class Compiler( ?: slotTypeDeclByScopeId[slotLoc.scopeId]?.get(slotLoc.slot)?.let { resolveTypeDeclObjClass(it) } } + private fun lookupExactLambdaRefByName(name: String): LambdaFnRef? { + val slotLoc = lookupSlotLocation(name, includeModule = true) ?: return null + return exactLambdaRefByScopeId[slotLoc.scopeId]?.get(slotLoc.slot) + } + + private fun resolveExactLambdaRef(ref: ObjRef?): LambdaFnRef? { + return when (ref) { + is LambdaFnRef -> ref + is LocalVarRef -> lookupExactLambdaRefByName(ref.name) + is FastLocalVarRef -> lookupExactLambdaRefByName(ref.name) + is LocalSlotRef -> { + val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId + val ownerSlot = ref.captureOwnerSlot ?: ref.slot + exactLambdaRefByScopeId[ownerScopeId]?.get(ownerSlot) + ?: ref.name.takeIf { it.isNotEmpty() }?.let(::lookupExactLambdaRefByName) + } + else -> null + } + } + private fun resolveReceiverTypeDecl(ref: ObjRef): TypeDecl? { return when (ref) { is LocalSlotRef -> { @@ -10415,6 +10454,15 @@ class Compiler( encodedPayloadTypeByName[name] = payloadClass } } + if (slotIndex != null && scopeId != null) { + val exactLambdaRef = if (!isMutable) resolveExactLambdaRef(directRef) else null + val scopeMap = exactLambdaRefByScopeId.getOrPut(scopeId) { mutableMapOf() } + if (exactLambdaRef != null) { + scopeMap[slotIndex] = exactLambdaRef + } else { + scopeMap.remove(slotIndex) + } + } if (initObjClass != null) { if (slotIndex != null && scopeId != null) { slotTypeByScopeId.getOrPut(scopeId) { mutableMapOf() }[slotIndex] = initObjClass diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 98ab1ee..fb956a6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -33,6 +33,7 @@ class BytecodeCompiler( private val globalSlotInfo: Map = emptyMap(), private val globalSlotScopeId: Int? = null, private val slotTypeByScopeId: Map> = emptyMap(), + private val exactLambdaRefByScopeId: Map> = emptyMap(), private val slotTypeDeclByScopeId: Map> = emptyMap(), private val knownNameObjClass: Map = emptyMap(), private val knownClassNames: Set = emptySet(), @@ -89,6 +90,9 @@ class BytecodeCompiler( private val slotInitClassByKey = mutableMapOf() private val intLoopVarNames = LinkedHashSet() private val valueFnRefs = LinkedHashSet() + private val exactLambdaRefBySlot = LinkedHashMap() + private val activeInlineLambdas = LinkedHashSet() + private val inlineThisBindings = ArrayDeque() private val loopVarKeys = LinkedHashSet() private val loopVarSlots = HashSet() private val loopStack = ArrayDeque() @@ -104,6 +108,8 @@ class BytecodeCompiler( val hasIterator: Boolean, ) + private data class InlineThisBinding(val slot: Int, val typeName: String?) + fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): CmdFunction? { prepareCompilation(stmt) setPos(stmt.pos) @@ -559,7 +565,10 @@ class BytecodeCompiler( val mapped = resolveSlot(ref) ?: return null var resolved = slotTypes[mapped] ?: SlotType.UNKNOWN if (resolved == SlotType.UNKNOWN) { - val key = ScopeSlotKey(refScopeId(ref), refSlot(ref)) + val key = ScopeSlotKey( + ref.captureOwnerScopeId ?: refScopeId(ref), + ref.captureOwnerSlot ?: refSlot(ref) + ) val inferred = slotTypeFromClass(slotInitClassByKey[key]) if (inferred != null) { updateSlotType(mapped, inferred) @@ -696,7 +705,7 @@ class BytecodeCompiler( compileThisVariantRef(typeName) ?: return null } ?: compileThisRef() val ownerClass = ref.preferredThisTypeName()?.let { resolveTypeNameClass(it) } - ?: implicitThisTypeName?.let { resolveTypeNameClass(it) } + ?: currentImplicitThisTypeName()?.let { resolveTypeNameClass(it) } val fieldId = ref.fieldId ?: -1 val methodId = ref.methodId ?: -1 if (fieldId < 0 && methodId < 0) { @@ -804,6 +813,9 @@ class BytecodeCompiler( } private fun compileThisRef(): CompiledValue { + inlineThisBindings.lastOrNull()?.let { binding -> + return CompiledValue(binding.slot, SlotType.OBJ) + } val slot = allocSlot() builder.emit(Opcode.LOAD_THIS, slot) updateSlotType(slot, SlotType.OBJ) @@ -811,6 +823,9 @@ class BytecodeCompiler( } private fun compileThisVariantRef(typeName: String): CompiledValue? { + inlineThisBindings.lastOrNull { it.typeName == typeName }?.let { binding -> + return CompiledValue(binding.slot, SlotType.OBJ) + } val typeId = builder.addConst(BytecodeConst.StringVal(typeName)) if (typeId > 0xFFFF) return null val slot = allocSlot() @@ -819,6 +834,10 @@ class BytecodeCompiler( return CompiledValue(slot, SlotType.OBJ) } + private fun currentImplicitThisTypeName(): String? { + return inlineThisBindings.lastOrNull()?.typeName ?: implicitThisTypeName + } + private fun compileConst(obj: Obj): CompiledValue? { val slot = allocSlot() when (obj) { @@ -2509,6 +2528,7 @@ class BytecodeCompiler( } updateSlotType(slot, value.type) propagateObjClass(value.type, value.slot, slot) + trackExactLambdaAtSlot(slot, null) updateNameObjClassFromSlot(localTarget.name, slot) return value } @@ -2552,6 +2572,7 @@ class BytecodeCompiler( } updateSlotType(slot, value.type) propagateObjClass(value.type, value.slot, slot) + trackExactLambdaAtSlot(slot, null) updateNameObjClassFromSlot(nameTarget, slot) return value } @@ -3587,7 +3608,7 @@ class BytecodeCompiler( private fun compileThisFieldSlotRef(ref: ThisFieldSlotRef): CompiledValue? { val receiver = compileThisRef() - val ownerClass = implicitThisTypeName?.let { resolveTypeNameClass(it) } + val ownerClass = currentImplicitThisTypeName()?.let { resolveTypeNameClass(it) } val fieldId = ref.fieldId() ?: -1 val methodId = ref.methodId() ?: -1 if (fieldId < 0 && methodId < 0) { @@ -4566,6 +4587,10 @@ class BytecodeCompiler( private fun compileCall(ref: CallRef): CompiledValue? { val callPos = callSitePos() + val lambdaTarget = resolveInlineCallableLambda(ref.target) + if (lambdaTarget != null) { + compileInlineDirectLambdaCall(ref, lambdaTarget)?.let { return it } + } val fieldTarget = ref.target as? FieldRef if (fieldTarget != null && isKnownClassReceiver(fieldTarget.target)) { val receiverClass = resolveReceiverClass(fieldTarget.target) @@ -4715,6 +4740,9 @@ class BytecodeCompiler( private fun compileMethodCall(ref: MethodCallRef): CompiledValue? { compileListFillIntCall(ref)?.let { return it } + compileInlineUnaryLambdaMethodCall(ref)?.let { return it } + compileInlineReceiverLambdaMethodCall(ref)?.let { return it } + compileInlineIterableLambdaMethodCall(ref)?.let { return it } val callPos = callSitePos() val receiverClass = resolveReceiverClass(ref.receiver) ?: ObjDynamic.type val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null @@ -4893,6 +4921,151 @@ class BytecodeCompiler( return CompiledValue(dst, SlotType.OBJ) } + private fun compileInlineUnaryLambdaMethodCall(ref: MethodCallRef): CompiledValue? { + val behavior = when (ref.name) { + "let" -> InlineUnaryLambdaMethodBehavior.RETURN_BLOCK_RESULT + "also" -> InlineUnaryLambdaMethodBehavior.RETURN_RECEIVER + else -> return null + } + if (ref.args.size != 1 || ref.args.any { it.isSplat || it.name != null }) return null + if (!ref.explicitTypeArgs.isNullOrEmpty()) return null + val lambdaRef = extractExactLambdaRef(ref.args.first().value) ?: return null + if (hasModuleCapture(lambdaRef)) return null + val inlineRef = lambdaRef.inlineBodyRef ?: return null + if (!isMethodInlineSafe(lambdaRef, inlineRef, allowReceiverRefs = false, allowCaptures = true)) return null + val paramName = lambdaRef.inlineParamNames()?.singleOrNull() ?: return null + val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null + return if (!ref.isOptional) { + val receiverSlot = materializeInlineBinding(receiver) + when (behavior) { + InlineUnaryLambdaMethodBehavior.RETURN_BLOCK_RESULT -> + compileInlineLambdaBody(lambdaRef, inlineRef, listOf(paramName to receiverSlot)) + InlineUnaryLambdaMethodBehavior.RETURN_RECEIVER -> { + compileInlineLambdaBody(lambdaRef, inlineRef, listOf(paramName to receiverSlot)) ?: return null + CompiledValue(receiverSlot, receiver.type) + } + } + } else { + val receiverObj = ensureObjSlot(receiver) + val dst = allocSlot() + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, receiverObj.slot, nullSlot, cmpSlot) + val nullLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel)) + ) + val receiverSlot = materializeInlineBinding(receiver) + when (behavior) { + InlineUnaryLambdaMethodBehavior.RETURN_BLOCK_RESULT -> { + val inlineResult = + compileInlineLambdaBody(lambdaRef, inlineRef, listOf(paramName to receiverSlot)) ?: return null + val inlineObj = ensureObjSlot(inlineResult) + builder.emit(Opcode.MOVE_OBJ, inlineObj.slot, dst) + } + InlineUnaryLambdaMethodBehavior.RETURN_RECEIVER -> { + compileInlineLambdaBody(lambdaRef, inlineRef, listOf(paramName to receiverSlot)) ?: return null + builder.emit(Opcode.MOVE_OBJ, receiverObj.slot, dst) + } + } + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, dst) + builder.mark(endLabel) + updateSlotType(dst, SlotType.OBJ) + CompiledValue(dst, SlotType.OBJ) + } + } + + private fun compileInlineReceiverLambdaMethodCall(ref: MethodCallRef): CompiledValue? { + val behavior = when (ref.name) { + "apply" -> InlineReceiverLambdaMethodBehavior.RETURN_RECEIVER + "run" -> InlineReceiverLambdaMethodBehavior.RETURN_BLOCK_RESULT + else -> return null + } + if (ref.args.size != 1 || ref.args.any { it.isSplat || it.name != null }) return null + if (!ref.explicitTypeArgs.isNullOrEmpty()) return null + val lambdaRef = extractExactLambdaRef(ref.args.first().value) ?: return null + if (hasModuleCapture(lambdaRef)) return null + val receiverInfo = receiverInlineInfo(lambdaRef) ?: return null + val inlineRef = lambdaRef.inlineBodyRef ?: return null + if (!isMethodInlineSafe(lambdaRef, inlineRef, allowReceiverRefs = true, allowCaptures = true)) return null + val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null + val receiverObj = ensureObjSlot(receiver) + return if (!ref.isOptional) { + compileInlineReceiverLambdaInvocation(receiverObj, lambdaRef, behavior, receiverInfo) + } else { + val dst = allocSlot() + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, receiverObj.slot, nullSlot, cmpSlot) + val nullLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel)) + ) + val nonNullResult = + compileInlineReceiverLambdaInvocation(receiverObj, lambdaRef, behavior, receiverInfo) ?: return null + val nonNullObj = ensureObjSlot(nonNullResult) + builder.emit(Opcode.MOVE_OBJ, nonNullObj.slot, dst) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, dst) + builder.mark(endLabel) + updateSlotType(dst, SlotType.OBJ) + CompiledValue(dst, SlotType.OBJ) + } + } + + private fun compileInlineIterableLambdaMethodCall(ref: MethodCallRef): CompiledValue? { + val behavior = when (ref.name) { + "forEach" -> InlineIterableLambdaMethodBehavior.FOR_EACH + "map" -> InlineIterableLambdaMethodBehavior.MAP + "filter" -> InlineIterableLambdaMethodBehavior.FILTER + else -> return null + } + if (ref.args.size != 1 || ref.args.any { it.isSplat || it.name != null }) return null + if (!ref.explicitTypeArgs.isNullOrEmpty()) return null + val lambdaRef = extractExactLambdaRef(ref.args.first().value) ?: return null + if (hasAnyCapture(lambdaRef)) return null + val inlineRef = lambdaRef.inlineBodyRef ?: return null + if (!isMethodInlineSafe(lambdaRef, inlineRef, allowReceiverRefs = false, allowCaptures = false)) return null + val paramNames = lambdaRef.inlineParamNames() ?: return null + if (paramNames.size != 1) return null + val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null + val receiverObj = ensureObjSlot(receiver) + return if (!ref.isOptional) { + compileInlineIterableLambdaLoop(receiverObj, ref, lambdaRef, inlineRef, paramNames[0], behavior) + } else { + val dst = allocSlot() + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, receiverObj.slot, nullSlot, cmpSlot) + val nullLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel)) + ) + val nonNullResult = + compileInlineIterableLambdaLoop(receiverObj, ref, lambdaRef, inlineRef, paramNames[0], behavior) ?: return null + val nonNullObj = ensureObjSlot(nonNullResult) + builder.emit(Opcode.MOVE_OBJ, nonNullObj.slot, dst) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + builder.mark(nullLabel) + builder.emit(Opcode.CONST_NULL, dst) + builder.mark(endLabel) + updateSlotType(dst, SlotType.OBJ) + CompiledValue(dst, SlotType.OBJ) + } + } + private fun compileListFillIntCall(ref: MethodCallRef): CompiledValue? { if (ref.name != "fill" || !isListTypeRef(ref.receiver)) return null if (ref.args.size != 2 || ref.args.any { it.isSplat || it.name != null }) return null @@ -4900,15 +5073,499 @@ class BytecodeCompiler( if (lambdaRef.inferredReturnClass != ObjInt.type) return null val size = compileArgValue(ref.args[0].value) ?: return null if (size.type != SlotType.INT) return null - val callable = ensureObjSlot(compileArgValue(ref.args[1].value) ?: return null) + lambdaRef.inlineBodyRef?.let { inlineRef -> + return compileInlineListFillInt(size, lambdaRef, inlineRef) + } + run { + val callable = ensureObjSlot(compileArgValue(ref.args[1].value) ?: return null) + val dst = allocSlot() + builder.emit(Opcode.LIST_FILL_INT, size.slot, callable.slot, dst) + updateSlotType(dst, SlotType.OBJ) + slotObjClass[dst] = ObjList.type + listElementClassBySlot[dst] = ObjInt.type + return CompiledValue(dst, SlotType.OBJ) + } + } + + private fun compileInlineDirectLambdaCall(ref: CallRef, lambdaRef: LambdaFnRef): CompiledValue? { + if (ref.isOptionalInvoke) return null + if (ref.tailBlock) return null + if (!ref.explicitTypeArgs.isNullOrEmpty()) return null + val inlineRef = lambdaRef.inlineBodyRef ?: return null + val bindings = prepareInlineLambdaBindings(lambdaRef, ref.args) ?: return null + return compileInlineLambdaBody(lambdaRef, inlineRef, bindings) + } + + private fun prepareInlineLambdaBindings( + lambdaRef: LambdaFnRef, + args: List + ): List>? { + if (args.any { it.isSplat || it.name != null }) return null + val paramNames = lambdaRef.inlineParamNames() ?: return null + if (args.size != paramNames.size) return null + if (args.isEmpty()) return emptyList() + val bindings = ArrayList>(args.size) + for ((index, arg) in args.withIndex()) { + val compiled = compileArgValue(arg.value) ?: return null + bindings += paramNames[index] to materializeInlineBinding(compiled) + } + return bindings + } + + private fun LambdaFnRef.inlineParamNames(): List? { + val declaration = argsDeclaration + if (declaration == null) { + return listOf("it") + } + if (declaration.params.any { it.isEllipsis || it.defaultValue != null }) return null + return declaration.params.map { it.name } + } + + private fun materializeInlineBinding(value: CompiledValue): Int { + val slot = allocSlot() + emitMove(value, slot) + updateSlotType(slot, value.type) + if (value.type == SlotType.OBJ) { + slotObjClass[value.slot]?.let { slotObjClass[slot] = it } + } + return slot + } + + private enum class InlineUnaryLambdaMethodBehavior { + RETURN_BLOCK_RESULT, + RETURN_RECEIVER + } + + private enum class InlineReceiverLambdaMethodBehavior { + RETURN_BLOCK_RESULT, + RETURN_RECEIVER + } + + private enum class InlineIterableLambdaMethodBehavior { + FOR_EACH, + MAP, + FILTER + } + + private data class InlineReceiverInfo( + val explicitBindings: List>, + val thisTypeName: String? + ) + + private fun hasModuleCapture(lambdaRef: LambdaFnRef): Boolean { + val captures = (lambdaCaptureEntriesByRef[lambdaRef] ?: lambdaRef.captureEntries).orEmpty() + return captures.any { it.ownerKind == CaptureOwnerFrameKind.MODULE } + } + + private fun hasAnyCapture(lambdaRef: LambdaFnRef): Boolean { + val captures = (lambdaCaptureEntriesByRef[lambdaRef] ?: lambdaRef.captureEntries).orEmpty() + return captures.isNotEmpty() + } + + private fun isMethodInlineSafe( + lambdaRef: LambdaFnRef, + inlineRef: ObjRef, + allowReceiverRefs: Boolean, + allowCaptures: Boolean + ): Boolean { + val allowedLocalNames = LinkedHashSet() + lambdaRef.inlineParamNames()?.let { allowedLocalNames.addAll(it) } + if (allowCaptures) { + val captures = (lambdaCaptureEntriesByRef[lambdaRef] ?: lambdaRef.captureEntries).orEmpty() + allowedLocalNames.addAll(captures.map { it.ownerName }) + } + + fun isAllowedName(name: String): Boolean { + return name == "this" || allowedLocalNames.contains(name) + } + + fun scan(ref: ObjRef): Boolean { + return when (ref) { + is ConstRef, + is TypeDeclRef, + is ClassOperatorRef -> true + is LambdaFnRef -> false + is LocalSlotRef -> isAllowedName(ref.name) + is LocalVarRef -> isAllowedName(ref.name) + is FastLocalVarRef -> isAllowedName(ref.name) + is BoundLocalVarRef -> false + is FieldRef -> scan(ref.target) + is MethodCallRef -> scan(ref.receiver) && ref.args.all { arg -> + val expr = arg.value as? ExpressionStatement ?: return@all false + scan(expr.ref) + } + is CallRef -> scan(ref.target) && ref.args.all { arg -> + val expr = arg.value as? ExpressionStatement ?: return@all false + scan(expr.ref) + } + is BinaryOpRef -> scan(ref.left) && scan(ref.right) + is UnaryOpRef -> scan(ref.a) + is LogicalAndRef -> scan(ref.left()) && scan(ref.right()) + is LogicalOrRef -> scan(ref.left()) && scan(ref.right()) + is ConditionalRef -> scan(ref.condition) && scan(ref.ifTrue) && scan(ref.ifFalse) + is ElvisRef -> scan(ref.left) && scan(ref.right) + is CastRef -> scan(ref.castValueRef()) && scan(ref.castTypeRef()) + is RangeRef -> listOfNotNull(ref.left, ref.right, ref.step).all(::scan) + is AssignRef -> scan(ref.target) && scan(ref.value) + is AssignOpRef -> scan(ref.target) && scan(ref.value) + is AssignIfNullRef -> scan(ref.target) && scan(ref.value) + is IncDecRef -> scan(ref.target) + is IndexRef -> scan(ref.targetRef) && scan(ref.indexRef) + is ListLiteralRef -> ref.entries().all { entry -> + when (entry) { + is ListEntry.Element -> scan(entry.ref) + is ListEntry.Spread -> scan(entry.ref) + } + } + is MapLiteralRef -> ref.entries().all { entry -> + when (entry) { + is MapLiteralEntry.Named -> scan(entry.value) + is MapLiteralEntry.Spread -> scan(entry.ref) + } + } + is StatementRef -> { + val expr = ref.statement as? ExpressionStatement ?: return false + scan(expr.ref) + } + is ImplicitThisMemberRef, + is ImplicitThisMethodCallRef, + is ThisFieldSlotRef, + is ThisMethodSlotCallRef, + is QualifiedThisFieldSlotRef, + is QualifiedThisMethodSlotCallRef, + is QualifiedThisRef -> allowReceiverRefs + else -> false + } + } + + return scan(inlineRef) + } + + private fun receiverInlineInfo(lambdaRef: LambdaFnRef): InlineReceiverInfo? { + val declaration = lambdaRef.argsDeclaration + return if (declaration == null) { + InlineReceiverInfo(listOf("it" to ensureVoidSlot()), lambdaRef.preferredThisType) + } else { + if (declaration.params.any { it.isEllipsis || it.defaultValue != null }) return null + if (declaration.params.isNotEmpty()) return null + InlineReceiverInfo(emptyList(), lambdaRef.preferredThisType) + } + } + + private fun compileInlineReceiverLambdaInvocation( + receiverObj: CompiledValue, + lambdaRef: LambdaFnRef, + behavior: InlineReceiverLambdaMethodBehavior, + receiverInfo: InlineReceiverInfo + ): CompiledValue? { + val inlineRef = lambdaRef.inlineBodyRef ?: return null + val receiverSlot = materializeInlineBinding(receiverObj) + val previousBinding = InlineThisBinding(receiverSlot, receiverInfo.thisTypeName) + inlineThisBindings.addLast(previousBinding) + return try { + when (behavior) { + InlineReceiverLambdaMethodBehavior.RETURN_BLOCK_RESULT -> + compileInlineLambdaBody(lambdaRef, inlineRef, receiverInfo.explicitBindings) + InlineReceiverLambdaMethodBehavior.RETURN_RECEIVER -> { + compileInlineLambdaBody(lambdaRef, inlineRef, receiverInfo.explicitBindings) ?: return null + CompiledValue(receiverSlot, SlotType.OBJ) + } + } + } finally { + inlineThisBindings.removeLast() + } + } + + private fun createEmptyMutableList(): CompiledValue? { + val calleeId = builder.addConst(BytecodeConst.ObjRef(ObjList.type)) + val calleeSlot = allocSlot() + builder.emit(Opcode.CONST_OBJ, calleeId, calleeSlot) + updateSlotType(calleeSlot, SlotType.OBJ) val dst = allocSlot() - builder.emit(Opcode.LIST_FILL_INT, size.slot, callable.slot, dst) + builder.emit(Opcode.CALL_SLOT, calleeSlot, 0, 0, dst) + updateSlotType(dst, SlotType.OBJ) + slotObjClass[dst] = ObjList.type + return CompiledValue(dst, SlotType.OBJ) + } + + private fun compileInlineIterableLambdaLoop( + receiverObj: CompiledValue, + ref: MethodCallRef, + lambdaRef: LambdaFnRef, + inlineRef: ObjRef, + paramName: String, + behavior: InlineIterableLambdaMethodBehavior + ): CompiledValue? { + val iterableMethods = ObjIterable.instanceMethodIdMap(includeAbstract = true) + val iteratorMethodId = iterableMethods["iterator"] + ?: throw BytecodeCompileException("Missing member id for Iterable.iterator", refPosOrCurrent(ref.receiver)) + val iteratorMethods = ObjIterator.instanceMethodIdMap(includeAbstract = true) + val hasNextMethodId = iteratorMethods["hasNext"] + ?: throw BytecodeCompileException("Missing member id for Iterator.hasNext", refPosOrCurrent(ref.receiver)) + val nextMethodId = iteratorMethods["next"] + ?: throw BytecodeCompileException("Missing member id for Iterator.next", refPosOrCurrent(ref.receiver)) + + val iterSlot = allocSlot() + builder.emit(Opcode.CALL_MEMBER_SLOT, receiverObj.slot, iteratorMethodId, 0, 0, iterSlot) + builder.emit(Opcode.ITER_PUSH, iterSlot) + + val result = when (behavior) { + InlineIterableLambdaMethodBehavior.FOR_EACH -> CompiledValue(ensureVoidSlot(), SlotType.OBJ) + InlineIterableLambdaMethodBehavior.MAP, + InlineIterableLambdaMethodBehavior.FILTER -> createEmptyMutableList() ?: return null + } + if (behavior == InlineIterableLambdaMethodBehavior.FILTER) { + listElementClassFromReceiverRef(ref.receiver)?.let { listElementClassBySlot[result.slot] = it } + } + if (behavior == InlineIterableLambdaMethodBehavior.MAP) { + lambdaRef.inferredReturnClass?.let { listElementClassBySlot[result.slot] = it } + } + + val loopLabel = builder.label() + val endLabel = builder.label() + builder.mark(loopLabel) + val hasNextSlot = allocSlot() + builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, hasNextMethodId, 0, 0, hasNextSlot) + val condSlot = allocSlot() + builder.emit(Opcode.OBJ_TO_BOOL, hasNextSlot, condSlot) + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(CmdBuilder.Operand.IntVal(condSlot), CmdBuilder.Operand.LabelRef(endLabel)) + ) + val nextSlot = allocSlot() + builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, nextMethodId, 0, 0, nextSlot) + val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN)) + when (behavior) { + InlineIterableLambdaMethodBehavior.FOR_EACH -> { + compileInlineLambdaBody(lambdaRef, inlineRef, listOf(paramName to nextObj.slot)) ?: return null + } + InlineIterableLambdaMethodBehavior.MAP -> { + val mapped = compileInlineLambdaBody(lambdaRef, inlineRef, listOf(paramName to nextObj.slot)) ?: return null + appendToList(result, mapped) ?: return null + } + InlineIterableLambdaMethodBehavior.FILTER -> { + val predicate = compileInlineLambdaBody(lambdaRef, inlineRef, listOf(paramName to nextObj.slot)) ?: return null + val predicateBool = compileValueAsBool(predicate) + val skipLabel = builder.label() + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(CmdBuilder.Operand.IntVal(predicateBool.slot), CmdBuilder.Operand.LabelRef(skipLabel)) + ) + appendToList(result, nextObj) ?: return null + builder.mark(skipLabel) + } + } + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel))) + builder.mark(endLabel) + builder.emit(Opcode.ITER_POP) + return result + } + + private fun appendToList(listValue: CompiledValue, itemValue: CompiledValue): CompiledValue? { + val addMethodId = ObjList.type.instanceMethodIdMap(includeAbstract = true)["add"] + ?: throw BytecodeCompileException("Missing member id for List.add", Pos.builtIn) + val listObj = ensureObjSlot(listValue) + val itemObj = ensureObjSlot(itemValue) + val argSlot = allocSlot() + builder.emit(Opcode.MOVE_OBJ, itemObj.slot, argSlot) + updateSlotType(argSlot, SlotType.OBJ) + val dst = allocSlot() + builder.emit(Opcode.CALL_MEMBER_SLOT, listObj.slot, addMethodId, argSlot, 1, dst) + noteListElementClassMutation(listObj.slot, itemObj) + updateSlotType(dst, SlotType.OBJ) + return CompiledValue(dst, SlotType.OBJ) + } + + private fun compileValueAsBool(value: CompiledValue): CompiledValue { + if (value.type == SlotType.BOOL) return value + val dst = allocSlot() + when (value.type) { + SlotType.INT -> builder.emit(Opcode.INT_TO_BOOL, value.slot, dst) + SlotType.OBJ, SlotType.UNKNOWN, SlotType.REAL -> { + val obj = ensureObjSlot(value) + builder.emit(Opcode.OBJ_TO_BOOL, obj.slot, dst) + } + } + updateSlotType(dst, SlotType.BOOL) + return CompiledValue(dst, SlotType.BOOL) + } + + private fun compileInlineLambdaBody( + lambdaRef: LambdaFnRef, + inlineRef: ObjRef, + explicitBindings: List> + ): CompiledValue? { + if (!activeInlineLambdas.add(lambdaRef)) return null + val previousOverrides = ArrayList>(lambdaRef.captureEntries.size + explicitBindings.size) + for (capture in lambdaRef.captureEntries) { + val slot = resolveInlineCaptureSlot(capture) ?: continue + previousOverrides += capture.ownerName to loopSlotOverrides.put(capture.ownerName, slot) + } + for ((name, slot) in explicitBindings) { + previousOverrides += name to loopSlotOverrides.put(name, slot) + } + return try { + compileRefWithFallback(inlineRef, null, refPosOrCurrent(inlineRef)) + } finally { + for ((name, previous) in previousOverrides.asReversed()) { + if (previous == null) loopSlotOverrides.remove(name) else loopSlotOverrides[name] = previous + } + activeInlineLambdas.remove(lambdaRef) + } + } + + private fun resolveInlineCallableLambda(target: ObjRef): LambdaFnRef? { + val lambdaRef = when (target) { + is LambdaFnRef -> target + is LocalSlotRef -> { + val ownerScopeId = target.captureOwnerScopeId ?: target.scopeId + val ownerSlot = target.captureOwnerSlot ?: target.slot + exactLambdaRefByScopeId[ownerScopeId]?.get(ownerSlot) + ?: resolveLocalSlotByRefOrName(target)?.let { exactLambdaRefBySlot[it] } + } + is LocalVarRef -> resolveDirectNameSlot(target.name)?.slot?.let { exactLambdaRefBySlot[it] } + is FastLocalVarRef -> resolveDirectNameSlot(target.name)?.slot?.let { exactLambdaRefBySlot[it] } + is BoundLocalVarRef -> exactLambdaRefBySlot[target.slotIndex()] + else -> null + } + return lambdaRef?.takeUnless { activeInlineLambdas.contains(it) } + } + + private fun trackExactLambdaAtSlot(slot: Int, lambdaRef: LambdaFnRef?) { + if (lambdaRef == null) { + exactLambdaRefBySlot.remove(slot) + } else { + exactLambdaRefBySlot[slot] = lambdaRef + } + } + + private fun preloadExactLambdaRefs() { + if (exactLambdaRefByScopeId.isEmpty()) return + for ((scopeId, slots) in exactLambdaRefByScopeId) { + for ((slotIndex, lambdaRef) in slots) { + val key = ScopeSlotKey(scopeId, slotIndex) + val localIndex = localSlotIndexByKey[key] + if (localIndex != null) { + trackExactLambdaAtSlot(scopeSlotCount + localIndex, lambdaRef) + continue + } + val scopeIndex = scopeSlotMap[key] + if (scopeIndex != null) { + trackExactLambdaAtSlot(scopeIndex, lambdaRef) + } + } + } + } + + private fun collectExactLambdaModuleCaptures() { + if (exactLambdaRefByScopeId.isEmpty()) return + val seen = LinkedHashSet() + for (slots in exactLambdaRefByScopeId.values) { + for (lambdaRef in slots.values) { + if (!seen.add(lambdaRef)) continue + val captures = (lambdaCaptureEntriesByRef[lambdaRef] ?: lambdaRef.captureEntries).orEmpty() + for (entry in captures) { + if (entry.ownerKind != CaptureOwnerFrameKind.MODULE) continue + val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId) + if (useScopeSlots) { + if (!scopeSlotMap.containsKey(key)) { + scopeSlotMap[key] = scopeSlotMap.size + } + if (!scopeSlotNameMap.containsKey(key)) { + scopeSlotNameMap[key] = entry.ownerName + } + if (!scopeSlotMutableMap.containsKey(key)) { + scopeSlotMutableMap[key] = entry.ownerIsMutable + } + } else if (globalSlotInfo.isNotEmpty() && globalSlotScopeId != null) { + if (!localSlotInfoMap.containsKey(key)) { + localSlotInfoMap[key] = LocalSlotInfo( + entry.ownerName, + entry.ownerIsMutable, + entry.ownerIsDelegated + ) + } + captureSlotKeys.add(key) + } + } + } + } + } + + private fun extractExactLambdaRef(value: Obj?): LambdaFnRef? { + val expr = value as? ExpressionStatement ?: return null + return when (val ref = expr.ref) { + is LambdaFnRef -> ref + is LocalSlotRef -> resolveLocalSlotByRefOrName(ref)?.let { exactLambdaRefBySlot[it] } + is LocalVarRef -> resolveDirectNameSlot(ref.name)?.slot?.let { exactLambdaRefBySlot[it] } + is FastLocalVarRef -> resolveDirectNameSlot(ref.name)?.slot?.let { exactLambdaRefBySlot[it] } + is BoundLocalVarRef -> exactLambdaRefBySlot[ref.slotIndex()] + else -> null + } + } + + private fun compileInlineListFillInt(size: CompiledValue, lambdaRef: LambdaFnRef, inlineRef: ObjRef): CompiledValue { + if (isImplicitItIdentityRef(inlineRef)) { + val dst = allocSlot() + builder.emit(Opcode.LIST_IOTA_INT, size.slot, dst) + updateSlotType(dst, SlotType.OBJ) + slotObjClass[dst] = ObjList.type + listElementClassBySlot[dst] = ObjInt.type + return CompiledValue(dst, SlotType.OBJ) + } + + val dst = allocSlot() + builder.emit(Opcode.LIST_NEW_INT, size.slot, dst) updateSlotType(dst, SlotType.OBJ) slotObjClass[dst] = ObjList.type listElementClassBySlot[dst] = ObjInt.type + + val iSlot = allocSlot() + val zeroId = builder.addConst(BytecodeConst.IntVal(0)) + builder.emit(Opcode.CONST_INT, zeroId, iSlot) + updateSlotType(iSlot, SlotType.INT) + + val loopLabel = builder.label() + val endLabel = builder.label() + builder.mark(loopLabel) + builder.emit( + Opcode.JMP_IF_GTE_INT, + listOf( + CmdBuilder.Operand.IntVal(iSlot), + CmdBuilder.Operand.IntVal(size.slot), + CmdBuilder.Operand.LabelRef(endLabel) + ) + ) + + val paramName = lambdaRef.inlineParamNames()?.singleOrNull() + ?: throw BytecodeCompileException("unsupported List.fill lambda parameters", refPosOrCurrent(inlineRef)) + val compiledValue = compileInlineLambdaBody(lambdaRef, inlineRef, listOf(paramName to iSlot)) + ?: throw BytecodeCompileException("failed to inline List.fill lambda", refPosOrCurrent(inlineRef)) + val intValue = coerceToLoopInt(compiledValue) + ?: throw BytecodeCompileException("inlined List.fill lambda must produce Int", refPosOrCurrent(inlineRef)) + builder.emit(Opcode.SET_INDEX_INT, dst, iSlot, intValue.slot) + builder.emit(Opcode.INC_INT, iSlot) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel))) + builder.mark(endLabel) return CompiledValue(dst, SlotType.OBJ) } + private fun resolveInlineCaptureSlot(entry: LambdaCaptureEntry): Int? { + if (entry.ownerKind != CaptureOwnerFrameKind.LOCAL) return null + val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId) + localSlotIndexByKey[key]?.let { return scopeSlotCount + it } + return null + } + + private fun isImplicitItIdentityRef(ref: ObjRef): Boolean { + return when (ref) { + is LocalVarRef -> ref.name == "it" + is FastLocalVarRef -> ref.name == "it" + is LocalSlotRef -> ref.name == "it" + else -> false + } + } + private fun compileThisMethodSlotCall(ref: ThisMethodSlotCallRef): CompiledValue? { val callPos = callSitePos() val receiver = compileThisRef() @@ -5991,6 +6648,7 @@ class BytecodeCompiler( ?: updateSlotObjClass(localSlot, stmt.initializer, stmt.initializerObjClass) updateListElementClassFromDecl(localSlot, scopeId, stmt.slotIndex) updateListElementClassFromInitializer(localSlot, stmt.initializer) + trackExactLambdaAtSlot(localSlot, if (!stmt.isMutable) extractExactLambdaRef(stmt.initializer) else null) updateNameObjClassFromSlot(stmt.name, localSlot) val shadowedScopeSlot = scopeSlotIndexByName.containsKey(stmt.name) val isModuleScope = moduleScopeId != null && scopeId == moduleScopeId @@ -6024,6 +6682,7 @@ class BytecodeCompiler( ?: updateSlotObjClass(scopeSlot, stmt.initializer, stmt.initializerObjClass) updateListElementClassFromDecl(scopeSlot, scopeId, stmt.slotIndex) updateListElementClassFromInitializer(scopeSlot, stmt.initializer) + trackExactLambdaAtSlot(scopeSlot, if (!stmt.isMutable) extractExactLambdaRef(stmt.initializer) else null) val declId = builder.addConst( BytecodeConst.LocalDecl( stmt.name, @@ -7859,7 +8518,9 @@ class BytecodeCompiler( scopeSlotRefPosByKey[scopeKey] = ref.pos() } } - return resolved + if (resolved != null) return resolved + resolveCapturedOwnerSlot(ref)?.let { return it } + return null } if (ref.isDelegated) { val localKey = ScopeSlotKey(refScopeId(ref), refSlot(ref)) @@ -7893,6 +8554,14 @@ class BytecodeCompiler( return scopeSlotMap[key] } + private fun resolveCapturedOwnerSlot(ref: LocalSlotRef): Int? { + val ownerScopeId = ref.captureOwnerScopeId ?: return null + val ownerSlot = ref.captureOwnerSlot ?: return null + val key = ScopeSlotKey(ownerScopeId, ownerSlot) + localSlotIndexByKey[key]?.let { return scopeSlotCount + it } + return scopeSlotMap[key] + } + private fun updateSlotType(slot: Int, type: SlotType) { if (forcedObjSlots.contains(slot) && type != SlotType.OBJ) return if (type == SlotType.UNKNOWN) { @@ -7980,7 +8649,7 @@ class BytecodeCompiler( } } is ThisFieldSlotRef -> { - val ownerClass = implicitThisTypeName?.let { resolveTypeNameClass(it) } ?: return null + val ownerClass = currentImplicitThisTypeName()?.let { resolveTypeNameClass(it) } ?: return null val fieldClass = inferFieldReturnClass(ownerClass, ref.name) ?: return null when (fieldClass.className) { "Buffer", "MutableBuffer", "BitBuffer" -> ObjInt.type @@ -8070,6 +8739,8 @@ class BytecodeCompiler( loopVarKeys.clear() loopVarSlots.clear() valueFnRefs.clear() + exactLambdaRefBySlot.clear() + activeInlineLambdas.clear() addrSlotByScopeSlot.clear() loopStack.clear() if (slotTypeByScopeId.isNotEmpty()) { @@ -8102,6 +8773,7 @@ class BytecodeCompiler( collectLoopVarNames(stmt) } collectScopeSlots(stmt) + collectExactLambdaModuleCaptures() if (allowLocalSlots) { collectLoopSlotPlans(stmt, 0) } @@ -8293,6 +8965,7 @@ class BytecodeCompiler( } } } + preloadExactLambdaRefs() if (allowLocalSlots && captureSlotKeys.isNotEmpty() && slotInitClassByKey.isNotEmpty()) { val scopeSlotsBase = scopeSlotMap.size for (key in captureSlotKeys) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index d4591d8..847d707 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -18,10 +18,7 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.* -import net.sergeych.lyng.obj.Obj -import net.sergeych.lyng.obj.ObjClass -import net.sergeych.lyng.obj.ObjRecord -import net.sergeych.lyng.obj.ValueFnRef +import net.sergeych.lyng.obj.* class BytecodeStatement private constructor( val original: Statement, @@ -84,6 +81,7 @@ class BytecodeStatement private constructor( globalSlotInfo: Map = emptyMap(), globalSlotScopeId: Int? = null, slotTypeByScopeId: Map> = emptyMap(), + exactLambdaRefByScopeId: Map> = emptyMap(), knownNameObjClass: Map = emptyMap(), knownClassNames: Set = emptySet(), knownObjectNames: Set = emptySet(), @@ -122,6 +120,7 @@ class BytecodeStatement private constructor( globalSlotInfo = globalSlotInfo, globalSlotScopeId = globalSlotScopeId, slotTypeByScopeId = slotTypeByScopeId, + exactLambdaRefByScopeId = exactLambdaRefByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownNameObjClass, knownClassNames = knownClassNames, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt index 013e0ed..e15fd1c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -231,8 +231,12 @@ class CmdBuilder { listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.SET_INDEX_INT -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.LIST_NEW_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT) Opcode.LIST_FILL_INT -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.LIST_IOTA_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT) Opcode.MAKE_RANGE -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.LIST_LITERAL -> @@ -835,7 +839,9 @@ class CmdBuilder { Opcode.SET_INDEX -> CmdSetIndex(operands[0], operands[1], operands[2]) Opcode.GET_INDEX_INT -> CmdGetIndexInt(operands[0], operands[1], operands[2]) Opcode.SET_INDEX_INT -> CmdSetIndexInt(operands[0], operands[1], operands[2]) + Opcode.LIST_NEW_INT -> CmdListNewInt(operands[0], operands[1]) Opcode.LIST_FILL_INT -> CmdListFillInt(operands[0], operands[1], operands[2]) + Opcode.LIST_IOTA_INT -> CmdListIotaInt(operands[0], operands[1]) Opcode.LIST_LITERAL -> CmdListLiteral(operands[0], operands[1], operands[2], operands[3]) Opcode.GET_MEMBER_SLOT -> CmdGetMemberSlot(operands[0], operands[1], operands[2], operands[3]) Opcode.SET_MEMBER_SLOT -> CmdSetMemberSlot(operands[0], operands[1], operands[2], operands[3]) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt index 7f6ea0d..14362a9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -495,7 +495,9 @@ object CmdDisassembler { is CmdSetIndex -> Opcode.SET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.valueSlot) is CmdGetIndexInt -> Opcode.GET_INDEX_INT to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.dst) is CmdSetIndexInt -> Opcode.SET_INDEX_INT to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.valueSlot) + is CmdListNewInt -> Opcode.LIST_NEW_INT to intArrayOf(cmd.sizeSlot, cmd.dst) is CmdListFillInt -> Opcode.LIST_FILL_INT to intArrayOf(cmd.sizeSlot, cmd.callableSlot, cmd.dst) + is CmdListIotaInt -> Opcode.LIST_IOTA_INT to intArrayOf(cmd.sizeSlot, cmd.dst) is CmdListLiteral -> Opcode.LIST_LITERAL to intArrayOf(cmd.planId, cmd.baseSlot, cmd.count, cmd.dst) is CmdGetMemberSlot -> Opcode.GET_MEMBER_SLOT to intArrayOf(cmd.recvSlot, cmd.fieldId, cmd.methodId, cmd.dst) is CmdSetMemberSlot -> Opcode.SET_MEMBER_SLOT to intArrayOf(cmd.recvSlot, cmd.fieldId, cmd.methodId, cmd.valueSlot) @@ -619,8 +621,12 @@ object CmdDisassembler { listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.SET_INDEX_INT -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.LIST_NEW_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT) Opcode.LIST_FILL_INT -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.LIST_IOTA_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT) Opcode.LIST_LITERAL -> listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.GET_MEMBER_SLOT -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index 5c986dd..4606c8f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -3730,6 +3730,34 @@ class CmdCallMemberSlot( } } +class CmdListIotaInt( + internal val sizeSlot: Int, + internal val dst: Int, +) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + val size = frame.getInt(sizeSlot).toInt() + if (size < 0) frame.ensureScope().raiseIllegalArgument("list size must be non-negative") + val values = LongArray(size) + for (i in 0 until size) { + values[i] = i.toLong() + } + frame.storeObjResult(dst, ObjList(values)) + return + } +} + +class CmdListNewInt( + internal val sizeSlot: Int, + internal val dst: Int, +) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + val size = frame.getInt(sizeSlot).toInt() + if (size < 0) frame.ensureScope().raiseIllegalArgument("list size must be non-negative") + frame.storeObjResult(dst, ObjList(LongArray(size))) + return + } +} + class CmdGetIndex( internal val targetSlot: Int, internal val indexSlot: Int, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt index 9772eda..9283326 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -175,6 +175,8 @@ enum class Opcode(val code: Int) { CALL_SLOT(0x93), CALL_BRIDGE_SLOT(0x94), + LIST_NEW_INT(0xA0), + LIST_IOTA_INT(0xA1), GET_INDEX(0xA2), SET_INDEX(0xA3), GET_INDEX_INT(0xA4), diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt index a9cf3ac..7ae1e83 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt @@ -30,6 +30,7 @@ class LambdaFnRef( val argsDeclaration: ArgsDeclaration?, val captureEntries: List, val inferredReturnClass: ObjClass?, + val inlineBodyRef: ObjRef?, val preferredThisType: String?, val wrapAsExtensionCallable: Boolean, val returnLabels: Set, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 8166f74..d7d99ee 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -959,7 +959,7 @@ open class ObjClass( } private fun initClassScope(): Scope { - if (classScope == null) classScope = Scope() + if (classScope == null) classScope = Scope(parent = null) return classScope!! } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt index 73d93f7..1fc58ce 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt @@ -30,19 +30,27 @@ import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonType open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { - private var boxedList: MutableList? = null - private var primitiveIntList: LongArray? = null + internal var boxedList: MutableList? = null + internal var primitiveIntList: LongArray? = null + // Logical size of primitiveIntList; capacity = primitiveIntList!!.size + internal var primitiveIntSize: Int = 0 init { - if (!adoptPrimitiveIntList(initialList)) { - boxedList = initialList + if (initialList.isNotEmpty()) { + if (!adoptPrimitiveIntList(initialList)) { + boxedList = initialList + } } + // Empty initialList: both null — lazy mode, avoids boxing on first append } val list: MutableList get() = ensureBoxedList() - internal fun sizeFast(): Int = primitiveIntList?.size ?: boxedList?.size ?: 0 + internal fun sizeFast(): Int = when { + primitiveIntList != null -> primitiveIntSize + else -> boxedList?.size ?: 0 + } internal fun getObjAtFast(index: Int): Obj = primitiveIntList?.let { ObjInt.of(it[index]) } ?: boxedList!![index] @@ -80,9 +88,24 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { internal fun appendFast(value: Obj) { val ints = primitiveIntList - if (ints != null && value is ObjInt) { - primitiveIntList = ints.copyOf(ints.size + 1).also { it[ints.size] = value.value } - return + if (value is ObjInt) { + if (ints != null) { + // Primitive mode: amortized growth (no copy when capacity allows) + if (primitiveIntSize < ints.size) { + ints[primitiveIntSize++] = value.value + } else { + val grown = ints.copyOf(maxOf(ints.size * 2, 16)) + grown[primitiveIntSize++] = value.value + primitiveIntList = grown + } + return + } + if (boxedList == null) { + // Lazy empty state: first int element starts primitive mode + primitiveIntList = LongArray(16).also { it[0] = value.value } + primitiveIntSize = 1 + return + } } ensureBoxedList().add(value) } @@ -91,10 +114,12 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { val ints = primitiveIntList val otherInts = other.primitiveIntList if (ints != null && otherInts != null) { - primitiveIntList = LongArray(ints.size + otherInts.size).also { - ints.copyInto(it, 0, 0, ints.size) - otherInts.copyInto(it, ints.size, 0, otherInts.size) - } + val otherSize = other.primitiveIntSize + val newSize = primitiveIntSize + otherSize + val dest = if (newSize <= ints.size) ints else ints.copyOf(newSize) + otherInts.copyInto(dest, primitiveIntSize, 0, otherSize) + primitiveIntList = dest + primitiveIntSize = newSize return } ensureBoxedList().addAll(other.list) @@ -108,6 +133,7 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { ints[i] = value.value } primitiveIntList = ints + primitiveIntSize = ints.size boxedList = null return true } @@ -120,12 +146,13 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { boxedList = empty return empty } - val materialized = ArrayList(ints.size) - for (value in ints) { - materialized.add(ObjInt.of(value)) + val materialized = ArrayList(primitiveIntSize) + for (i in 0.. = mutableListOf()) : Obj() { internal constructor(intValues: LongArray) : this(mutableListOf()) { primitiveIntList = intValues + primitiveIntSize = intValues.size boxedList = null } @@ -253,9 +281,11 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { val ints = primitiveIntList val otherInts = other.primitiveIntList if (ints != null && otherInts != null) { - ObjList(LongArray(ints.size + otherInts.size).also { - ints.copyInto(it, 0, 0, ints.size) - otherInts.copyInto(it, ints.size, 0, otherInts.size) + val mySize = primitiveIntSize + val otherSize = other.primitiveIntSize + ObjList(LongArray(mySize + otherSize).also { + ints.copyInto(it, 0, 0, mySize) + otherInts.copyInto(it, mySize, 0, otherSize) }) } else { ObjList((list + other.list).toMutableList()) @@ -276,7 +306,9 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { open override suspend fun plusAssign(scope: Scope, other: Obj): Obj { - if (other is ObjList) { + if (other is ObjInt || other is ObjString || other is ObjBool || other is ObjReal || other is ObjNull) { + appendFast(other) + } else if (other is ObjList) { appendAllFast(other) } else if (!shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable)) { val otherList = (other.invokeInstanceMethod(scope, "toList") as ObjList).list @@ -327,8 +359,8 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { override suspend fun contains(scope: Scope, other: Obj): Boolean { val ints = primitiveIntList if (ints != null && other is ObjInt) { - for (value in ints) { - if (value == other.value) return true + for (i in 0.. = mutableListOf()) : Obj() { override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) { val ints = primitiveIntList if (ints != null) { - for (value in ints) { - if (!callback(ObjInt.of(value))) break + for (i in 0.. = mutableListOf()) : Obj() { override suspend fun toKotlin(scope: Scope): Any { val ints = primitiveIntList - if (ints != null) return ints.map { it } + if (ints != null) return (0.. = mutableListOf()) : Obj() { } override fun hashCode(): Int { - return primitiveIntList?.contentHashCode() ?: list.hashCode() + val ints = primitiveIntList + return if (ints != null) { + var result = 1 + for (i in 0.. = mutableListOf()) : Obj() { override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { val ints = primitiveIntList if (ints != null) { - encoder.encodeAnyList(scope, ints.mapTo(ArrayList(ints.size)) { ObjInt.of(it) }) + val boxed = ArrayList(primitiveIntSize) + for (i in 0.. = mutableListOf()) : Obj() { override suspend fun toJson(scope: Scope): JsonElement { val ints = primitiveIntList if (ints != null) { - return JsonArray(ints.map { ObjInt.of(it).toJson(scope) }) + return JsonArray((0.. = mutableListOf()) : Obj() { var first = true val ints = primitiveIntList if (ints != null) { - for (v in ints) { + for (i in 0.. = mutableListOf()) : Obj() { addFnDoc( name= "ensureCapacity", doc = """ - ensure the list capacity allows storing specified amount if items without reallocation. + ensure the list capacity allows storing specified amount if items without reallocation. If current capacity is greater or equal to `count`, does nothing. Note that possible reallocation could be a costly operation, """.trimIndent(), @@ -539,9 +578,15 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { moduleName = "lyng.stdlib" ) { val self = thisAs() - val list = self.list as ArrayList val count = requireOnlyArg().value.toInt() - list.ensureCapacity(count) + if (count > 0) { + val ints = self.primitiveIntList + when { + ints != null -> if (ints.size < count) self.primitiveIntList = ints.copyOf(count) + self.boxedList == null -> { self.primitiveIntList = LongArray(count); self.primitiveIntSize = 0 } + else -> (self.boxedList as? ArrayList)?.ensureCapacity(count) + } + } self } diff --git a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt index faae3d0..89d17d8 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt @@ -17,8 +17,10 @@ import kotlinx.coroutines.test.runTest import net.sergeych.lyng.* +import net.sergeych.lyng.obj.ObjNull import net.sergeych.lyng.obj.toInt import kotlin.test.Test +import kotlin.test.assertFalse import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -161,7 +163,7 @@ class BytecodeRecentOpsTest { } @Test - fun listFillIntUsesPrimitiveFillBytecode() = runTest { + fun listFillConstantExpressionUsesInlineBytecode() = runTest { val scope = Script.newScope() scope.eval( """ @@ -172,26 +174,333 @@ class BytecodeRecentOpsTest { """.trimIndent() ) val disasm = scope.disassembleSymbol("calc") - assertTrue(disasm.contains("LIST_FILL_INT"), disasm) + assertTrue(disasm.contains("LIST_NEW_INT"), disasm) + assertFalse(disasm.contains("LIST_FILL_INT"), disasm) assertEquals(4, scope.eval("calc()").toInt()) } @Test - fun listFillIntWithIndexLambdaKeepsSemantics() = runTest { + fun listFillCapturedExpressionUsesInlineBytecode() = runTest { val scope = Script.newScope() scope.eval( """ fun calc() { - val xs = List.fill(5) { it * 3 } + val k = 3 + val xs = List.fill(5) { it * k } xs[0] + xs[4] } """.trimIndent() ) val disasm = scope.disassembleSymbol("calc") - assertTrue(disasm.contains("LIST_FILL_INT"), disasm) + assertTrue(disasm.contains("LIST_NEW_INT"), disasm) + assertFalse(disasm.contains("LIST_FILL_INT"), disasm) assertEquals(12, scope.eval("calc()").toInt()) } + @Test + fun listFillIdentityUsesIotaBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val xs = List.fill(5) { it } + xs[0] + xs[4] + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertTrue(disasm.contains("LIST_IOTA_INT"), disasm) + assertEquals(4, scope.eval("calc()").toInt()) + } + + @Test + fun directLambdaLiteralCallUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + { x -> x + 1 }(10) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("MAKE_LAMBDA_FN"), disasm) + assertFalse(disasm.contains("CALL_SLOT"), disasm) + assertEquals(11, scope.eval("calc()").toInt()) + } + + @Test + fun directLambdaLiteralCallWithCaptureUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val k = 3 + { x -> x * k }(4) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("MAKE_LAMBDA_FN"), disasm) + assertFalse(disasm.contains("CALL_SLOT"), disasm) + assertEquals(12, scope.eval("calc()").toInt()) + } + + @Test + fun localImmutableLambdaCallUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val f = { x -> x + 1 } + f(10) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_SLOT"), disasm) + assertEquals(11, scope.eval("calc()").toInt()) + } + + @Test + fun localImmutableCapturedLambdaCallUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val k = 3 + val f = { x -> x * k } + f(4) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_SLOT"), disasm) + assertEquals(12, scope.eval("calc()").toInt()) + } + + @Test + fun aliasedImmutableLambdaCallUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val f = { x -> x + 1 } + val g = f + g(10) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_SLOT"), disasm) + assertEquals(11, scope.eval("calc()").toInt()) + } + + @Test + fun topLevelImmutableLambdaCallUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + val f = { x -> x + 1 } + fun calc() { + f(10) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_SLOT"), disasm) + assertEquals(11, scope.eval("calc()").toInt()) + } + + @Test + fun topLevelAliasedImmutableLambdaCallUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + val f = { x -> x + 1 } + val g = f + fun calc() { + g(10) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_SLOT"), disasm) + assertEquals(11, scope.eval("calc()").toInt()) + } + + @Test + fun topLevelCapturedLambdaCallUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + val k = 3 + val f = { x -> x * k } + fun calc() { + f(4) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_SLOT"), disasm) + assertEquals(12, scope.eval("calc()").toInt()) + } + + @Test + fun letLiteralUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + 10.let { it + 1 } + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(11, scope.eval("calc()").toInt()) + } + + @Test + fun letAliasedLambdaUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val k = 3 + val f = { x -> x * k } + 4.let(f) + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(12, scope.eval("calc()").toInt()) + } + + @Test + fun alsoLiteralUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + var acc = 0 + val result = 10.also { x -> + acc = x + 1 + } + acc + result + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(21, scope.eval("calc()").toInt()) + } + + @Test + fun optionalLetLiteralUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc(flag: Bool) { + val x: Int? = if(flag) 10 else null + x?.let { it + 1 } + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(11, scope.eval("calc(true)").toInt()) + assertEquals(ObjNull, scope.eval("calc(false)")) + } + + @Test + fun applyReceiverLambdaUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + class Box(value: Int) + + fun calc() { + val box = Box(10).apply { value += 1 } + box.value + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(11, scope.eval("calc()").toInt()) + } + + @Test + fun runReceiverLambdaUsesInlineBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + class Box(value: Int) + + fun calc() { + Box(10).run { value + 1 } + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(11, scope.eval("calc()").toInt()) + } + + @Test + fun forEachUsesInlineLoopBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + [1, 2, 3, 4].forEach { it + 1 } + 5 + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertTrue(disasm.contains("ITER_PUSH"), disasm) + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(5, scope.eval("calc()").toInt()) + } + + @Test + fun mapUsesInlineLoopBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val f = { x -> x * 2 } + val xs = [1, 2, 3].map(f) + xs[0] + xs[2] + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertTrue(disasm.contains("ITER_PUSH"), disasm) + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(8, scope.eval("calc()").toInt()) + } + + @Test + fun filterUsesInlineLoopBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val xs = [1, 2, 3, 4].filter { it % 2 == 0 } + xs[0] + xs[1] + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertTrue(disasm.contains("ITER_PUSH"), disasm) + assertFalse(disasm.contains("CALL_DYNAMIC_MEMBER"), disasm) + assertEquals(6, scope.eval("calc()").toInt()) + } + @Test fun optionalIndexPreIncSkipsOnNullReceiver() = runTest { eval( diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OptTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OptTest.kt new file mode 100644 index 0000000..987d12e --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OptTest.kt @@ -0,0 +1,59 @@ +/* + * 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.toInt +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.TimeSource + +class OptTest { + + @Test + fun testAddToArray() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun buildArray(n: Int) { + val a: List = List.fill(n) { it * 10 + 1 } + a.size + } + """.trimIndent() + ) + + repeat(3) { pass -> + val size = scope.eval("buildArray(200000)").toInt() + assertEquals(200000, size, "warmup pass ${pass + 1} failed") + } + + val passes = 3 + var bestMs = Long.MAX_VALUE + var totalMs = 0L + repeat(passes) { pass -> + val start = TimeSource.Monotonic.markNow() + val size = scope.eval("buildArray(10000000)").toInt() + val elapsedMs = start.elapsedNow().inWholeMilliseconds + assertEquals(10000000, size, "measured pass ${pass + 1} failed") + bestMs = minOf(bestMs, elapsedMs) + totalMs += elapsedMs + println("add-to-array pass ${pass + 1}/$passes: ${elapsedMs}ms size=$size") + } + println("add-to-array best=${bestMs}ms avg=${totalMs / passes}ms after warmup") + } +} diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index d5afb71..455faa1 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -434,7 +434,7 @@ fun List.sort(): void { /* Build a new list of `size` elements by calling `block(index)` for each index. - `capacity` less size is ignored (size will be used as capacity). + `capacity` less than size is ignored (size will be used as capacity). */ static fun List.fill(size: Int, capacity = -1, block: (Int)->T): List { require(size >= 0, "size must not be negative")