From d8454a11fc462c8140e8067cd8c5340a4b7afe98 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 4 Apr 2026 04:01:43 +0300 Subject: [PATCH] optimize arithmetics --- docs/Range.md | 2 +- docs/pi_spigot_perf_baseline.md | 62 +++++ docs/tutorial.md | 10 +- examples/pi-bench.lyng | 42 ++-- examples/pi-bench.py | 83 +++++++ examples/pi-test.lyng | 49 ---- .../net/sergeych/CliDispatcherJvmTest.kt | 32 +++ .../io/net/LyngNetTcpServerExampleTest.kt | 14 +- .../lyng/io/net/LyngNetModuleJsNodeTest.kt | 17 ++ .../net/sergeych/lyngio/NetJsNodeTest.kt | 17 ++ .../kotlin/net/sergeych/lyng/Compiler.kt | 144 ++++++++++- .../lyng/bytecode/BytecodeCompiler.kt | 151 +++++++++--- .../net/sergeych/lyng/bytecode/CmdBuilder.kt | 9 + .../sergeych/lyng/bytecode/CmdDisassembler.kt | 9 + .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 102 +++++++- .../net/sergeych/lyng/bytecode/Opcode.kt | 3 + .../net/sergeych/lyng/obj/LambdaFnRef.kt | 2 + .../kotlin/net/sergeych/lyng/obj/ObjList.kt | 224 +++++++++++++++--- .../kotlin/BytecodeRecentOpsTest.kt | 85 ++++++- .../src/commonTest/kotlin/CoroutinesTest.kt | 19 +- .../jvmTest/kotlin/PiSpigotBenchmarkTest.kt | 45 ++-- 21 files changed, 913 insertions(+), 208 deletions(-) create mode 100644 docs/pi_spigot_perf_baseline.md create mode 100644 examples/pi-bench.py delete mode 100644 examples/pi-test.lyng rename lyngio/src/{jsTest => jsNodeTest}/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt (87%) rename lyngio/src/{jsTest => jsNodeTest}/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt (79%) diff --git a/docs/Range.md b/docs/Range.md index 9cb5ac3..783a45a 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -140,7 +140,7 @@ Open-ended ranges require an explicit step to iterate: You can use Char as both ends of the closed range: - val r = 'a' .. 'c' + val r = 'a'..'c' assert( 'b' in r) assert( 'e' !in r) for( ch in r ) diff --git a/docs/pi_spigot_perf_baseline.md b/docs/pi_spigot_perf_baseline.md new file mode 100644 index 0000000..ecf5ffb --- /dev/null +++ b/docs/pi_spigot_perf_baseline.md @@ -0,0 +1,62 @@ +## Pi Spigot JVM Baseline + +Saved on April 4, 2026 before the `List` indexed-access follow-up fix. + +Benchmark target: +- [examples/pi-bench.py](/home/sergeych/dev/lyng/examples/pi-bench.py) +- [examples/pi-bench.lyng](/home/sergeych/dev/lyng/examples/pi-bench.lyng) + +Execution path: +- Python: `python3 examples/pi-bench.py` +- Lyng JVM: `./gradlew :lyng:runJvm --args='/home/sergeych/dev/lyng/examples/pi-bench.lyng'` +- Constraint: do not use Kotlin/Native `lyng` CLI for perf comparisons + +Baseline measurements: +- Python full script: `167 ms` +- Lyng JVM full script: `1.287097604 s` +- Python warm function average over 5 runs: `126.126 ms` +- Lyng JVM warm function average over 5 runs: about `1071.6 ms` + +Baseline ratio: +- Full script: about `7.7x` slower on Lyng JVM +- Warm function only: about `8.5x` slower on Lyng JVM + +Primary finding at baseline: +- The hot `reminders[j]` accesses in `piSpigot` were still lowered through boxed object index ops and boxed arithmetic. +- Newly added `GET_INDEX_INT` and `SET_INDEX_INT` only reached `pi`, not `reminders`. +- Root cause: initializer element inference handled list literals, but not `List.fill(boxes) { 2 }`, so `reminders` did not become known `List` at compile time. + +## After Optimizations 1-4 + +Follow-up change: +- propagate inferred lambda return class into bytecode compilation +- infer `List.fill(...)` element type from the fill lambda +- lower `reminders[j]` reads and writes to `GET_INDEX_INT` and `SET_INDEX_INT` +- add primitive-backed `ObjList` storage for all-int lists +- lower `List.fill(Int) { Int }` to `LIST_FILL_INT` +- stop boxing the integer index inside `GET_INDEX_INT` / `SET_INDEX_INT` + +Verification: +- `piSpigot` disassembly now contains typed ops for `reminders`, for example: + - `GET_INDEX_INT s5(reminders), s10(j), ...` + - `SET_INDEX_INT s5(reminders), s10(j), ...` + +Post-change measurements using `jlyng`: +- Full script: `655.819559 ms` +- Warm 5-run total: `1.430945810 s` +- Warm average per run: about `286.2 ms` + +Observed improvement vs baseline: +- Full script: about `1.96x` faster (`1.287 s -> 0.656 s`) +- Warm function: about `3.74x` faster (`1071.6 ms -> 286.2 ms`) + +Residual gap vs Python baseline: +- Full script: Lyng JVM is still about `3.9x` slower than Python (`655.8 ms` vs `167 ms`) +- Warm function: Lyng JVM is still about `2.3x` slower than Python (`286.2 ms` vs `126.126 ms`) + +Current benchmark-test snapshot (`n=200`, JVM test harness): +- `optimized-int-division-rval-off`: `135 ms` +- `optimized-int-division-rval-on`: `125 ms` +- `piSpigot` bytecode now contains: + - `LIST_FILL_INT` for both `pi` and `reminders` + - `GET_INDEX_INT` / `SET_INDEX_INT` for the hot indexed loop diff --git a/docs/tutorial.md b/docs/tutorial.md index e0e7dfd..31a08e8 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1531,11 +1531,9 @@ It could be open and closed: Descending ranges are explicit too: - (5 downTo 1).toList() - >>> [5,4,3,2,1] - - (5 downUntil 1).toList() - >>> [5,4,3,2] + assertEquals([5,4,3,2,1], (5 downTo 1).toList()) + assertEquals([5,4,3,2], (5 downUntil 1).toList()) + >>> void Ranges could be inside other ranges: @@ -1549,7 +1547,7 @@ There are character ranges too: and you can use ranges in for-loops: - for( x in 'a' ..< 'c' ) println(x) + for( x in 'a'..<'c' ) println(x) >>> a >>> b >>> void diff --git a/examples/pi-bench.lyng b/examples/pi-bench.lyng index 9c9d243..dd83175 100644 --- a/examples/pi-bench.lyng +++ b/examples/pi-bench.lyng @@ -1,18 +1,18 @@ import lyng.time -val WORK_SIZE = 200 -val TASK_COUNT = 10 +val WORK_SIZE = 500 +val THREADS = 1 fn piSpigot(iThread: Int, n: Int) { - var pi = [] + var piIter = 0 + var pi = List.fill(n) { 0 } val boxes = n * 10 / 3 var reminders = List.fill(boxes) { 2 } var heldDigits = 0 - for (i in 0..n) { + for (i in 0.. - val counterState = counter - val t = launch { - piSpigot(counterState, WORK_SIZE) + println("piBench (lyng): THREADS = " + THREADS + ", WORK_SIZE = " + WORK_SIZE) + for (i in 0..= 0: + reminders[j] *= 10 + sum = reminders[j] + carriedOver + quotient = sum // (j * 2 + 1) + reminders[j] = sum % (j * 2 + 1) + carriedOver = quotient * j + j -= 1 + reminders[0] = sum % 10 + q = sum // 10 + if q == 9: + heldDigits += 1 + elif q == 10: + q = 0 + k = 1 + while k <= heldDigits: + replaced = pi[i - k] + if replaced == 9: + replaced = 0 + else: + replaced += 1 + pi[i - k] = replaced + k += 1 + heldDigits = 1 + else: + heldDigits = 1 + pi[piIter] = q + piIter += 1 + i += 1 + res = "" + for i in range(len(pi)-8, len(pi), 1): + res += str(pi[i]) + print(str(iThread) + ": " + res) + +def createProcesses(): + THREADS = 1 + WORK_SIZE = 500 + print("piBench (python3): THREADS = " + str(THREADS) + ", WORK_SIZE = " + str(WORK_SIZE)) + pa = [] + for i in range(THREADS): + p = Process(target=piSpigot, args=(i, WORK_SIZE)) + p.start() + pa.append(p) + for p in pa: + p.join() + +if __name__ == "__main__": + t1 = time.time() + createProcesses() + dt = time.time() - t1 + print("total time: %i ms" % (dt*1000)) diff --git a/examples/pi-test.lyng b/examples/pi-test.lyng deleted file mode 100644 index 7a2844d..0000000 --- a/examples/pi-test.lyng +++ /dev/null @@ -1,49 +0,0 @@ -fn piSpigot(n) { - var pi = [] - val boxes = n * 10 / 3 - var reminders = [] - for (i in 0..", + """ + var sum = 0 + var counter = 0 + + (1..3).map { n -> + val counterState = counter + val task = launch { counterState + n } + ++counter + task + }.forEach { sum += it.await() } + + sum + """.trimIndent() + ) + ) + + assertEquals(9, (result as ObjInt).value) + } finally { + session.cancelAndJoin() + } + } } diff --git a/lyngio/src/commonTest/kotlin/net/sergeych/lyng/io/net/LyngNetTcpServerExampleTest.kt b/lyngio/src/commonTest/kotlin/net/sergeych/lyng/io/net/LyngNetTcpServerExampleTest.kt index a2fd20e..dfb7bff 100644 --- a/lyngio/src/commonTest/kotlin/net/sergeych/lyng/io/net/LyngNetTcpServerExampleTest.kt +++ b/lyngio/src/commonTest/kotlin/net/sergeych/lyng/io/net/LyngNetTcpServerExampleTest.kt @@ -17,7 +17,9 @@ package net.sergeych.lyng.io.net -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import net.sergeych.lyng.Compiler import net.sergeych.lyng.Script @@ -29,9 +31,9 @@ import kotlin.test.assertEquals class LyngNetTcpServerExampleTest { @Test - fun tcpServerExampleRoundTripsOverLoopback() = runBlocking { + fun tcpServerExampleRoundTripsOverLoopback() = runTest { val engine = getSystemNetEngine() - if (!engine.isSupported || !engine.isTcpAvailable || !engine.isTcpServerAvailable) return@runBlocking + if (!engine.isSupported || !engine.isTcpAvailable || !engine.isTcpServerAvailable) return@runTest val scope = Script.newScope() createNetModule(PermitAllNetAccessPolicy, scope) @@ -60,8 +62,10 @@ class LyngNetTcpServerExampleTest { "${'$'}{accepted.await()}: ${'$'}reply" """.trimIndent() - val result = withTimeout(5_000) { - Compiler.compile(code).execute(scope).inspect(scope) + val result = withContext(Dispatchers.Default) { + withTimeout(5_000) { + Compiler.compile(code).execute(scope).inspect(scope) + } } assertEquals("\"ping: echo:ping\"", result) diff --git a/lyngio/src/jsTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt b/lyngio/src/jsNodeTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt similarity index 87% rename from lyngio/src/jsTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt rename to lyngio/src/jsNodeTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt index 1618bb2..543ebe4 100644 --- a/lyngio/src/jsTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt +++ b/lyngio/src/jsNodeTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt @@ -1,3 +1,20 @@ +/* + * 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.io.net import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/lyngio/src/jsTest/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt b/lyngio/src/jsNodeTest/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt similarity index 79% rename from lyngio/src/jsTest/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt rename to lyngio/src/jsNodeTest/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt index 1dfc94c..d2a3fef 100644 --- a/lyngio/src/jsTest/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt +++ b/lyngio/src/jsNodeTest/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt @@ -1,3 +1,20 @@ +/* + * 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.lyngio import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 8d93fd0..faf0634 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -170,6 +170,20 @@ class Compiler( private val typeAliases: MutableMap = mutableMapOf() private val methodReturnTypeDeclByRef: MutableMap = mutableMapOf() private val callReturnTypeDeclByRef: MutableMap = mutableMapOf() + private val iterableLikeTypeNames = setOf( + "Iterable", + "Collection", + "Array", + "List", + "ImmutableList", + "Set", + "ImmutableSet", + "Flow", + "ObservableList", + "RingBuffer", + "Range", + "IntRange" + ) private val callableReturnTypeByScopeId: MutableMap> = mutableMapOf() private val callableReturnTypeByName: MutableMap = mutableMapOf() private val callableReturnTypeDeclByName: MutableMap = mutableMapOf() @@ -2721,7 +2735,7 @@ class Compiler( Token.Type.LPAREN -> { cc.next() if (shouldTreatAsClassScopeCall(left, next.value)) { - val parsed = parseArgs(null) + val parsed = parseArgs(null, implicitItTypeNameForMemberLambda(left, next.value)) val args = parsed.first val tailBlock = parsed.second isCall = true @@ -2738,7 +2752,7 @@ class Compiler( val receiverType = if (next.value == "apply" || next.value == "run") { inferReceiverTypeFromRef(left) } else null - val parsed = parseArgs(receiverType) + val parsed = parseArgs(receiverType, implicitItTypeNameForMemberLambda(left, next.value)) val args = parsed.first val tailBlock = parsed.second if (left is LocalVarRef && left.name == "scope") { @@ -2807,9 +2821,7 @@ class Compiler( val receiverType = if (next.value == "apply" || next.value == "run") { inferReceiverTypeFromRef(left) } else null - val itType = if (next.value == "let" || next.value == "also") { - inferReceiverTypeFromRef(left) - } else null + val itType = implicitItTypeNameForMemberLambda(left, next.value) val lambda = parseLambdaExpression(receiverType, implicitItType = itType) val argPos = next.pos val args = listOf(ParsedArgument(ExpressionStatement(lambda, argPos), next.pos)) @@ -3239,6 +3251,8 @@ class Compiler( if (cls != null && itSlot != null) { val paramTypeMap = slotTypeByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() } paramTypeMap[itSlot] = cls + val paramTypeDeclMap = slotTypeDeclByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() } + paramTypeDeclMap[itSlot] = TypeDecl.Simple(implicitItType, false) } } @@ -3384,7 +3398,12 @@ class Compiler( } val itSlot = slotPlan["it"] if (itSlot != null) { - frame.frame.setObj(itSlot, itValue) + when (itValue) { + is ObjInt -> frame.frame.setInt(itSlot, itValue.value) + is ObjReal -> frame.frame.setReal(itSlot, itValue.value) + is ObjBool -> frame.frame.setBool(itSlot, itValue.value) + else -> frame.frame.setObj(itSlot, itValue) + } } } else { argsDeclaration.assignToFrame( @@ -3414,6 +3433,7 @@ class Compiler( paramSlotPlan = paramSlotPlanSnapshot, argsDeclaration = argsDeclaration, captureEntries = captureEntries, + inferredReturnClass = returnClass, preferredThisType = expectedReceiverType, wrapAsExtensionCallable = wrapAsExtensionCallable, returnLabels = returnLabels, @@ -4771,7 +4791,7 @@ class Compiler( val targetClass = resolveReceiverClassForMember(ref.targetRef) classMethodReturnTypeDecl(targetClass, "getAt") } - is MethodCallRef -> methodReturnTypeDeclByRef[ref] + is MethodCallRef -> methodReturnTypeDeclByRef[ref] ?: inferMethodCallReturnTypeDecl(ref) is CallRef -> callReturnTypeDeclByRef[ref] ?: inferCallReturnTypeDecl(ref) is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref) is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) } @@ -5012,8 +5032,7 @@ class Compiler( } private fun inferMethodCallReturnClass(ref: MethodCallRef): ObjClass? { - val receiverDecl = resolveReceiverTypeDecl(ref.receiver) - val genericReturnDecl = inferMethodCallReturnTypeDecl(ref.name, receiverDecl) + val genericReturnDecl = inferMethodCallReturnTypeDecl(ref) if (genericReturnDecl != null) { methodReturnTypeDeclByRef[ref] = genericReturnDecl resolveTypeDeclObjClass(genericReturnDecl)?.let { return it } @@ -5034,7 +5053,24 @@ class Compiler( return inferMethodCallReturnClass(ref.name) } + private fun inferMethodCallReturnTypeDecl(ref: MethodCallRef): TypeDecl? { + methodReturnTypeDeclByRef[ref]?.let { return it } + val inferred = inferMethodCallReturnTypeDecl(ref.name, resolveReceiverTypeDecl(ref.receiver), ref.args) + if (inferred != null) { + methodReturnTypeDeclByRef[ref] = inferred + } + return inferred + } + private fun inferMethodCallReturnTypeDecl(name: String, receiver: TypeDecl?): TypeDecl? { + return inferMethodCallReturnTypeDecl(name, receiver, emptyList()) + } + + private fun inferMethodCallReturnTypeDecl( + name: String, + receiver: TypeDecl?, + args: List + ): TypeDecl? { val base = when (receiver) { is TypeDecl.Generic -> receiver.name.substringAfterLast('.') is TypeDecl.Simple -> receiver.name.substringAfterLast('.') @@ -5048,6 +5084,10 @@ class Compiler( name == "next" && receiver is TypeDecl.Generic && base == "Iterator" -> { receiver.args.firstOrNull() } + name == "map" && base in iterableLikeTypeNames -> { + val mappedType = args.firstOrNull()?.let { inferCallableReturnTypeDeclFromArgument(it) } ?: TypeDecl.TypeAny + TypeDecl.Generic("List", listOf(mappedType), false) + } name == "toImmutableList" && receiver is TypeDecl.Generic && (base == "Iterable" || base == "Collection" || base == "Array" || base == "List" || base == "ImmutableList") -> { val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny TypeDecl.Generic("ImmutableList", listOf(arg), false) @@ -5120,6 +5160,79 @@ class Compiler( } } + private fun inferCallableReturnTypeDeclFromArgument(arg: ParsedArgument): TypeDecl? { + val stmt = arg.value as? ExpressionStatement ?: return null + val ref = stmt.ref + val fnType = inferTypeDeclFromRef(ref) as? TypeDecl.Function + if (fnType != null) { + return fnType.returnType + } + return when (ref) { + is ValueFnRef -> lambdaReturnTypeByRef[ref]?.let { TypeDecl.Simple(it.className, false) } + is ClassOperatorRef -> lambdaReturnTypeByRef[ref]?.let { TypeDecl.Simple(it.className, false) } + else -> null + } + } + + private fun inferIterableElementTypeDecl(receiver: TypeDecl?): TypeDecl? { + return when (receiver) { + is TypeDecl.Generic -> { + val base = receiver.name.substringAfterLast('.') + when { + base in iterableLikeTypeNames -> receiver.args.firstOrNull() ?: TypeDecl.TypeAny + else -> null + } + } + is TypeDecl.Simple -> when (receiver.name.substringAfterLast('.')) { + "Range", "IntRange" -> TypeDecl.Simple("Int", false) + "String" -> TypeDecl.Simple("Char", false) + else -> null + } + else -> null + } + } + + private fun inferIterableElementTypeDecl(receiver: ObjRef): TypeDecl? { + inferIterableElementTypeDecl(inferTypeDeclFromRef(receiver) ?: resolveReceiverTypeDecl(receiver))?.let { + return it + } + return when (receiver) { + is MethodCallRef -> if (receiver.name == "map") { + receiver.args.firstOrNull()?.let { inferCallableReturnTypeDeclFromArgument(it) } + } else { + null + } + else -> null + } + } + + private fun implicitItTypeNameForMemberLambda(receiver: ObjRef, memberName: String): String? { + if (memberName == "fill" && isListTypeRef(receiver)) { + return "Int" + } + if (memberName == "let" || memberName == "also") { + return inferReceiverTypeFromRef(receiver) + } + val typeDecl = when (memberName) { + "forEach", "map" -> inferIterableElementTypeDecl(receiver) + else -> null + } ?: return null + return when (typeDecl) { + is TypeDecl.Simple -> typeDecl.name.substringAfterLast('.') + is TypeDecl.Generic -> typeDecl.name.substringAfterLast('.') + else -> resolveTypeDeclObjClass(typeDecl)?.className + } + } + + private fun isListTypeRef(ref: ObjRef): Boolean { + return when (ref) { + is LocalVarRef -> ref.name == "List" + is LocalSlotRef -> ref.name == "List" + is FastLocalVarRef -> ref.name == "List" + else -> false + } + } + private fun inferEncodedPayloadClass(args: List): ObjClass? { val stmt = args.firstOrNull()?.value as? ExpressionStatement ?: return null val ref = stmt.ref @@ -6112,7 +6225,10 @@ class Compiler( * Parse arguments list during the call and detect last block argument * _following the parenthesis_ call: `(1,2) { ... }` */ - private suspend fun parseArgs(expectedTailBlockReceiver: String? = null): Pair, Boolean> { + private suspend fun parseArgs( + expectedTailBlockReceiver: String? = null, + implicitItType: String? = null + ): Pair, Boolean> { val args = mutableListOf() suspend fun tryParseNamedArg(): ParsedArgument? { @@ -6169,7 +6285,7 @@ class Compiler( var lastBlockArgument = false if (end.type == Token.Type.LBRACE) { // last argument - callable - val callableAccessor = parseLambdaExpression(expectedTailBlockReceiver) + val callableAccessor = parseLambdaExpression(expectedTailBlockReceiver, implicitItType = implicitItType) args += ParsedArgument( ExpressionStatement(callableAccessor, end.pos), end.pos @@ -8891,6 +9007,12 @@ class Compiler( is ListLiteralRef -> ObjList.type is MapLiteralRef -> ObjMap.type is RangeRef -> ObjRange.type + is LocalVarRef -> resolveReceiverTypeDecl(directRef)?.let { resolveTypeDeclObjClass(it) } + ?: inferObjClassFromRef(directRef) + is FastLocalVarRef -> resolveReceiverTypeDecl(directRef)?.let { resolveTypeDeclObjClass(it) } + ?: inferObjClassFromRef(directRef) + is LocalSlotRef -> resolveReceiverTypeDecl(directRef)?.let { resolveTypeDeclObjClass(it) } + ?: inferObjClassFromRef(directRef) is StatementRef -> { val decl = directRef.statement as? ClassDeclStatement decl?.let { resolveClassByName(it.typeName) } 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 6415819..f24b1ba 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -2713,9 +2713,14 @@ class BytecodeCompiler( } if (target is IndexRef) { val receiver = compileRefWithFallback(target.targetRef, null, Pos.builtIn) ?: return null + val elementSlotType = indexElementSlotType(receiver.slot, target.targetRef) if (!target.optionalRef) { val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null - builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot) + if (elementSlotType == SlotType.INT && index.type == SlotType.INT && value.type == SlotType.INT) { + builder.emit(Opcode.SET_INDEX_INT, receiver.slot, index.slot, value.slot) + } else { + builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot) + } noteListElementClassMutation(receiver.slot, value) } else { val nullSlot = allocSlot() @@ -2728,7 +2733,11 @@ class BytecodeCompiler( listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(endLabel)) ) val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null - builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot) + if (elementSlotType == SlotType.INT && index.type == SlotType.INT && value.type == SlotType.INT) { + builder.emit(Opcode.SET_INDEX_INT, receiver.slot, index.slot, value.slot) + } else { + builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot) + } noteListElementClassMutation(receiver.slot, value) builder.mark(endLabel) } @@ -3047,13 +3056,12 @@ class BytecodeCompiler( val current = allocSlot() val result = allocSlot() var rhs = compileRef(ref.value) ?: return compileEvalRef(ref) - val elementClass = listElementClassBySlot[receiver.slot] ?: listElementClassFromReceiverRef(indexTarget.targetRef) + val elementClass = indexElementClass(receiver.slot, indexTarget.targetRef) if (!indexTarget.optionalRef) { val index = compileRefWithFallback(indexTarget.indexRef, null, Pos.builtIn) ?: return null - if (elementClass == ObjInt.type) { - builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current) + if (elementClass == ObjInt.type && index.type == SlotType.INT) { val currentInt = allocSlot() - builder.emit(Opcode.UNBOX_INT_OBJ, current, currentInt) + builder.emit(Opcode.GET_INDEX_INT, receiver.slot, index.slot, currentInt) updateSlotType(currentInt, SlotType.INT) if (rhs.type != SlotType.INT) { coerceToArithmeticInt(ref.value, rhs)?.let { rhs = it } @@ -3067,9 +3075,9 @@ class BytecodeCompiler( else -> null } if (typed != null && typed.type == SlotType.INT) { - builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, currentInt) + builder.emit(Opcode.SET_INDEX_INT, receiver.slot, index.slot, typed.slot) noteListElementClassMutation(receiver.slot, typed) - return CompiledValue(currentInt, SlotType.INT) + return typed } } builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current) @@ -3321,9 +3329,14 @@ class BytecodeCompiler( } is IndexRef -> { val receiver = compileRefWithFallback(target.targetRef, null, Pos.builtIn) ?: return null + val elementSlotType = indexElementSlotType(receiver.slot, target.targetRef) if (!target.optionalRef) { val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null - builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, newValue.slot) + if (elementSlotType == SlotType.INT && index.type == SlotType.INT && newValue.type == SlotType.INT) { + builder.emit(Opcode.SET_INDEX_INT, receiver.slot, index.slot, newValue.slot) + } else { + builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, newValue.slot) + } } else { val recvNull = allocSlot() builder.emit(Opcode.CONST_NULL, recvNull) @@ -3335,7 +3348,11 @@ class BytecodeCompiler( listOf(CmdBuilder.Operand.IntVal(recvCmp), CmdBuilder.Operand.LabelRef(skipLabel)) ) val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null - builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, newValue.slot) + if (elementSlotType == SlotType.INT && index.type == SlotType.INT && newValue.type == SlotType.INT) { + builder.emit(Opcode.SET_INDEX_INT, receiver.slot, index.slot, newValue.slot) + } else { + builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, newValue.slot) + } builder.mark(skipLabel) } } @@ -3603,9 +3620,15 @@ class BytecodeCompiler( private fun compileIndexRef(ref: IndexRef): CompiledValue? { val receiver = compileRefWithFallback(ref.targetRef, null, Pos.builtIn) ?: return null + val elementSlotType = indexElementSlotType(receiver.slot, ref.targetRef) val dst = allocSlot() if (!ref.optionalRef) { val index = compileRefWithFallback(ref.indexRef, null, Pos.builtIn) ?: return null + if (elementSlotType == SlotType.INT && index.type == SlotType.INT) { + builder.emit(Opcode.GET_INDEX_INT, receiver.slot, index.slot, dst) + updateSlotType(dst, SlotType.INT) + return CompiledValue(dst, SlotType.INT) + } builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, dst) } else { val nullSlot = allocSlot() @@ -4234,6 +4257,7 @@ class BytecodeCompiler( val indexTarget = ref.target as? IndexRef ?: return null val receiver = compileRefWithFallback(indexTarget.targetRef, null, Pos.builtIn) ?: return null + val elementSlotType = indexElementSlotType(receiver.slot, indexTarget.targetRef) if (indexTarget.optionalRef) { val resultSlot = allocSlot() val nullSlot = allocSlot() @@ -4273,6 +4297,24 @@ class BytecodeCompiler( return CompiledValue(resultSlot, SlotType.OBJ) } val index = compileRefWithFallback(indexTarget.indexRef, null, Pos.builtIn) ?: return null + if (elementSlotType == SlotType.INT && index.type == SlotType.INT) { + val current = allocSlot() + builder.emit(Opcode.GET_INDEX_INT, receiver.slot, index.slot, current) + updateSlotType(current, SlotType.INT) + val oneSlot = allocSlot() + val oneId = builder.addConst(BytecodeConst.IntVal(1)) + builder.emit(Opcode.CONST_INT, oneId, oneSlot) + updateSlotType(oneSlot, SlotType.INT) + val result = allocSlot() + val op = if (ref.isIncrement) Opcode.ADD_INT else Opcode.SUB_INT + builder.emit(op, current, oneSlot, result) + updateSlotType(result, SlotType.INT) + builder.emit(Opcode.SET_INDEX_INT, receiver.slot, index.slot, result) + if (wantResult && ref.isPost) { + return CompiledValue(current, SlotType.INT) + } + return CompiledValue(result, SlotType.INT) + } val current = allocSlot() builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current) updateSlotType(current, SlotType.OBJ) @@ -4636,6 +4678,7 @@ class BytecodeCompiler( } private fun compileMethodCall(ref: MethodCallRef): CompiledValue? { + compileListFillIntCall(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 @@ -4814,6 +4857,22 @@ class BytecodeCompiler( return 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 + val lambdaRef = ((ref.args[1].value as? ExpressionStatement)?.ref as? LambdaFnRef) ?: return null + 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) + 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 compileThisMethodSlotCall(ref: ThisMethodSlotCallRef): CompiledValue? { val callPos = callSitePos() val receiver = compileThisRef() @@ -6119,6 +6178,42 @@ class BytecodeCompiler( } return when (directRef) { is ListLiteralRef -> listElementClassFromListLiteralRef(directRef) + is MethodCallRef -> listElementClassFromMethodCallRef(directRef) + else -> null + } + } + + private fun listElementClassFromMethodCallRef(ref: MethodCallRef): ObjClass? { + if (ref.name != "fill" || !isListTypeRef(ref.receiver)) return null + val block = ref.args.lastOrNull() ?: return null + return supportedListElementClass(listElementClassFromFillValue(block.value)) + } + + private fun listElementClassFromFillValue(value: Obj): ObjClass? { + val expr = value as? ExpressionStatement ?: return null + val ref = when (val directRef = expr.ref) { + is StatementRef -> (directRef.statement as? ExpressionStatement)?.ref + else -> directRef + } ?: return null + return when (ref) { + is ConstRef -> elementClassFromConst(ref.constValue) + is LambdaFnRef -> ref.inferredReturnClass + else -> null + } + } + + private fun isListTypeRef(ref: ObjRef): Boolean { + return when (ref) { + is LocalVarRef -> ref.name == "List" + is LocalSlotRef -> ref.name == "List" + is FastLocalVarRef -> ref.name == "List" + else -> false + } + } + + private fun supportedListElementClass(cls: ObjClass?): ObjClass? { + return when (cls) { + ObjInt.type, ObjReal.type, ObjString.type, ObjBool.type -> cls else -> null } } @@ -7774,6 +7869,12 @@ class BytecodeCompiler( } } + private fun indexElementClass(receiverSlot: Int, targetRef: ObjRef): ObjClass? = + listElementClassBySlot[receiverSlot] ?: listElementClassFromReceiverRef(targetRef) + + private fun indexElementSlotType(receiverSlot: Int, targetRef: ObjRef): SlotType? = + slotTypeFromClass(indexElementClass(receiverSlot, targetRef)) + private fun prepareCompilation(stmt: Statement) { builder = CmdBuilder() nextSlot = 0 @@ -8860,31 +8961,14 @@ class BytecodeCompiler( SlotType.OBJ -> { val isExactInt = isExactNonNullSlotClassOrTemp(value.slot, ObjInt.type) val isStableIntObj = slotObjClass[value.slot] == ObjInt.type && isStablePrimitiveSourceSlot(value.slot) - if (!isExactInt && !isStableIntObj && !isStablePrimitiveSourceSlot(value.slot)) return null - val objSlot = if (isExactInt || isStableIntObj) { - value.slot - } else { - val boxed = allocSlot() - builder.emit(Opcode.BOX_OBJ, value.slot, boxed) - updateSlotType(boxed, SlotType.OBJ) - emitAssertObjSlotIsInt(boxed) - } + if (!isExactInt && !isStableIntObj) return null + val objSlot = value.slot val intSlot = allocSlot() builder.emit(Opcode.UNBOX_INT_OBJ, objSlot, intSlot) updateSlotType(intSlot, SlotType.INT) CompiledValue(intSlot, SlotType.INT) } - SlotType.UNKNOWN -> { - if (!isStablePrimitiveSourceSlot(value.slot)) return null - val boxed = allocSlot() - builder.emit(Opcode.BOX_OBJ, value.slot, boxed) - updateSlotType(boxed, SlotType.OBJ) - val checked = emitAssertObjSlotIsInt(boxed) - val intSlot = allocSlot() - builder.emit(Opcode.UNBOX_INT_OBJ, checked, intSlot) - updateSlotType(intSlot, SlotType.INT) - CompiledValue(intSlot, SlotType.INT) - } + SlotType.UNKNOWN -> null else -> null } } @@ -8980,12 +9064,7 @@ class BytecodeCompiler( } private fun extractTypedRangeLocal(source: Statement): LocalSlotRef? { - if (rangeLocalNames.isEmpty()) return null - val target = if (source is BytecodeStatement) source.original else source - val expr = target as? ExpressionStatement ?: return null - val localRef = expr.ref as? LocalSlotRef ?: return null - if (localRef.isDelegated) return null - return if (rangeLocalNames.contains(localRef.name)) localRef else null + return null } private data class ScopeSlotKey(val scopeId: Int, val slot: Int) 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 fcffd93..013e0ed 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -227,6 +227,12 @@ class CmdBuilder { listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.SET_INDEX -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.GET_INDEX_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.SET_INDEX_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.LIST_FILL_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.MAKE_RANGE -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.LIST_LITERAL -> @@ -827,6 +833,9 @@ class CmdBuilder { Opcode.CALL_DYNAMIC_MEMBER -> CmdCallDynamicMember(operands[0], operands[1], operands[2], operands[3], operands[4]) Opcode.GET_INDEX -> CmdGetIndex(operands[0], operands[1], operands[2]) 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_FILL_INT -> CmdListFillInt(operands[0], operands[1], operands[2]) 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 b01aa70..7f6ea0d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -493,6 +493,9 @@ object CmdDisassembler { is CmdCallDynamicMember -> Opcode.CALL_DYNAMIC_MEMBER to intArrayOf(cmd.recvSlot, cmd.nameId, cmd.argBase, cmd.argCount, cmd.dst) is CmdGetIndex -> Opcode.GET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.dst) 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 CmdListFillInt -> Opcode.LIST_FILL_INT to intArrayOf(cmd.sizeSlot, cmd.callableSlot, 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) @@ -612,6 +615,12 @@ object CmdDisassembler { listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.SET_INDEX -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.GET_INDEX_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.SET_INDEX_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.LIST_FILL_INT -> + listOf(OperandKind.SLOT, 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 22c81e7..18cb3bb 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -3352,6 +3352,31 @@ class CmdListLiteral( } } +class CmdListFillInt( + internal val sizeSlot: Int, + internal val callableSlot: 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 callable = frame.storedSlotObj(callableSlot) + val scope = frame.ensureScope() + val result = ObjList(LongArray(size)) + for (i in 0 until size) { + val value = if (callable is BytecodeLambdaCallable && callable.supportsImplicitIntFillFastPath()) { + callable.invokeImplicitIntArg(scope, i.toLong()) + } else { + callable.callOn(scope.createChildScope(scope.pos, args = Arguments(ObjInt.of(i.toLong())))) + } + val intValue = (value as? ObjInt)?.value ?: scope.raiseClassCastError("expected Int fill result") + result.setIntAtFast(i, intValue) + } + frame.storeObjResult(dst, result) + return + } +} + private fun decodeMemberId(id: Int): Pair { return if (id <= -2) { Pair(-id - 2, true) @@ -3709,7 +3734,7 @@ class CmdGetIndex( val target = frame.storedSlotObj(targetSlot) val index = frame.storedSlotObj(indexSlot) if (target is ObjList && target::class == ObjList::class && index is ObjInt) { - frame.storeObjResult(dst, target.list[index.toInt()]) + frame.storeObjResult(dst, target.getObjAtFast(index.toInt())) return } val result = target.getAt(frame.ensureScope(), index) @@ -3728,7 +3753,7 @@ class CmdSetIndex( val index = frame.storedSlotObj(indexSlot) val value = frame.slotToObj(valueSlot) if (target is ObjList && target::class == ObjList::class && index is ObjInt) { - target.list[index.toInt()] = value + target.setObjAtFast(index.toInt(), value) return } target.putAt(frame.ensureScope(), index, value) @@ -3736,6 +3761,47 @@ class CmdSetIndex( } } +class CmdGetIndexInt( + internal val targetSlot: Int, + internal val indexSlot: Int, + internal val dst: Int, +) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + val target = frame.storedSlotObj(targetSlot) + val index = frame.getInt(indexSlot).toInt() + if (target is ObjList && target::class == ObjList::class) { + target.getIntAtFast(index)?.let { + frame.setInt(dst, it) + return + } + } + val result = target.getAt(frame.ensureScope(), ObjInt.of(index.toLong())) + if (result is ObjInt) { + frame.setInt(dst, result.value) + return + } + frame.ensureScope().raiseClassCastError("expected Int list element") + } +} + +class CmdSetIndexInt( + internal val targetSlot: Int, + internal val indexSlot: Int, + internal val valueSlot: Int, +) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + val target = frame.storedSlotObj(targetSlot) + val index = frame.getInt(indexSlot).toInt() + if (target is ObjList && target::class == ObjList::class) { + target.setIntAtFast(index, frame.getInt(valueSlot)) + return + } + val value = ObjInt.of(frame.getInt(valueSlot)) + target.putAt(frame.ensureScope(), ObjInt.of(index.toLong()), value) + return + } +} + class CmdMakeLambda(internal val id: Int, internal val dst: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { val lambdaConst = frame.fn.constants.getOrNull(id) as? BytecodeConst.LambdaFn @@ -3788,6 +3854,31 @@ class BytecodeLambdaCallable( ) } + fun supportsImplicitIntFillFastPath(): Boolean = argsDeclaration == null + + suspend fun invokeImplicitIntArg(scope: Scope, arg: Long): Obj { + val context = scope.applyClosureForBytecode(closureScope, preferredThisType).also { + it.args = Arguments.EMPTY + } + if (captureRecords != null) { + context.captureRecords = captureRecords + context.captureNames = captureNames + } else if (captureNames.isNotEmpty()) { + closureScope.raiseIllegalState("bytecode lambda capture records missing") + } + val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, _ -> + paramSlotPlan["it"]?.let { itSlot -> + frame.frame.setInt(itSlot, arg) + } + } + return try { + CmdVm().execute(fn, context, Arguments.EMPTY, binder) + } catch (e: ReturnException) { + if (e.label == null || returnLabels.contains(e.label)) e.result + else throw e + } + } + override suspend fun execute(scope: Scope): Obj { val context = scope.applyClosureForBytecode(closureScope, preferredThisType).also { it.args = scope.args @@ -3818,7 +3909,12 @@ class BytecodeLambdaCallable( } val itSlot = slotPlan["it"] if (itSlot != null) { - frame.frame.setObj(itSlot, itValue) + when (itValue) { + is ObjInt -> frame.frame.setInt(itSlot, itValue.value) + is ObjReal -> frame.frame.setReal(itSlot, itValue.value) + is ObjBool -> frame.frame.setBool(itSlot, itValue.value) + else -> frame.frame.setObj(itSlot, itValue) + } } } else { argsDeclaration.assignToFrame( 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 9a844cd..9772eda 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -177,7 +177,10 @@ enum class Opcode(val code: Int) { GET_INDEX(0xA2), SET_INDEX(0xA3), + GET_INDEX_INT(0xA4), LIST_LITERAL(0xA5), + SET_INDEX_INT(0xA6), + LIST_FILL_INT(0xA7), GET_MEMBER_SLOT(0xA8), SET_MEMBER_SLOT(0xA9), GET_CLASS_SCOPE(0xAA), 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 195559a..a9cf3ac 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt @@ -12,6 +12,7 @@ * 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.obj @@ -28,6 +29,7 @@ class LambdaFnRef( val paramSlotPlan: Map, val argsDeclaration: ArgsDeclaration?, val captureEntries: List, + val inferredReturnClass: ObjClass?, val preferredThisType: String?, val wrapAsExtensionCallable: Boolean, val returnLabels: Set, 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 8243f8a..7937d04 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt @@ -19,8 +19,8 @@ package net.sergeych.lyng.obj import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement -import net.sergeych.lyng.Scope import net.sergeych.lyng.Arguments +import net.sergeych.lyng.Scope import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.addFnDoc import net.sergeych.lyng.miniast.addPropertyDoc @@ -29,7 +29,120 @@ import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonType -open class ObjList(val list: MutableList = mutableListOf()) : Obj() { +open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { + private var boxedList: MutableList? = null + private var primitiveIntList: LongArray? = null + + init { + if (!adoptPrimitiveIntList(initialList)) { + boxedList = initialList + } + } + + val list: MutableList + get() = ensureBoxedList() + + internal fun sizeFast(): Int = primitiveIntList?.size ?: boxedList?.size ?: 0 + + internal fun getObjAtFast(index: Int): Obj = + primitiveIntList?.let { ObjInt.of(it[index]) } ?: boxedList!![index] + + internal fun getIntAtFast(index: Int): Long? = + primitiveIntList?.get(index) ?: (boxedList?.get(index) as? ObjInt)?.value + + internal fun setObjAtFast(index: Int, value: Obj) { + val ints = primitiveIntList + if (ints != null) { + if (value is ObjInt) { + ints[index] = value.value + return + } + ensureBoxedList()[index] = value + return + } + boxedList!![index] = value + } + + internal fun setIntAtFast(index: Int, value: Long) { + val ints = primitiveIntList + if (ints != null) { + ints[index] = value + return + } + boxedList?.let { + if (it[index] is ObjInt) { + it[index] = ObjInt.of(value) + return + } + } + ensureBoxedList()[index] = ObjInt.of(value) + } + + 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 + } + ensureBoxedList().add(value) + } + + internal fun appendAllFast(other: ObjList) { + 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) + } + return + } + ensureBoxedList().addAll(other.list) + } + + private fun adoptPrimitiveIntList(items: List): Boolean { + if (items.isEmpty()) return false + val ints = LongArray(items.size) + for (i in items.indices) { + val value = items[i] as? ObjInt ?: return false + ints[i] = value.value + } + primitiveIntList = ints + boxedList = null + return true + } + + private fun ensureBoxedList(): MutableList { + boxedList?.let { return it } + val ints = primitiveIntList + if (ints == null) { + val empty = mutableListOf() + boxedList = empty + return empty + } + val materialized = ArrayList(ints.size) + for (value in ints) { + materialized.add(ObjInt.of(value)) + } + boxedList = materialized + primitiveIntList = null + return materialized + } + + private fun sliceRange(start: Int, endExclusive: Int): Obj { + val ints = primitiveIntList + return if (ints != null) { + ObjList(ints.copyOfRange(start, endExclusive)) + } else { + ObjList(list.subList(start, endExclusive).toMutableList()) + } + } + + internal constructor(intValues: LongArray) : this(mutableListOf()) { + primitiveIntList = intValues + boxedList = null + } + protected open fun shouldTreatAsSingleElement(scope: Scope, other: Obj): Boolean { if (!other.isInstanceOf(ObjIterable)) return true val declaredElementType = scope.declaredListElementTypeForValue(this) @@ -48,9 +161,9 @@ open class ObjList(val list: MutableList = mutableListOf()) : Obj() { } return false } - if (list.size != other.list.size) return false - for (i in 0.. = mutableListOf()) : Obj() { override suspend fun getAt(scope: Scope, index: Obj): Obj { return when (index) { is ObjInt -> { - list[index.toInt()] + getObjAtFast(index.toInt()) } is ObjRange -> { when { index.start is ObjInt && index.end is ObjInt -> { if (index.isEndInclusive) - ObjList(list.subList(index.start.toInt(), index.end.toInt() + 1).toMutableList()) + sliceRange(index.start.toInt(), index.end.toInt() + 1) else - ObjList(list.subList(index.start.toInt(), index.end.toInt()).toMutableList()) + sliceRange(index.start.toInt(), index.end.toInt()) } index.isOpenStart && !index.isOpenEnd -> { if (index.isEndInclusive) - ObjList(list.subList(0, index.end!!.toInt() + 1).toMutableList()) + sliceRange(0, index.end!!.toInt() + 1) else - ObjList(list.subList(0, index.end!!.toInt()).toMutableList()) + sliceRange(0, index.end!!.toInt()) } index.isOpenEnd && !index.isOpenStart -> { - ObjList(list.subList(index.start!!.toInt(), list.size).toMutableList()) + sliceRange(index.start!!.toInt(), sizeFast()) } index.isOpenStart && index.isOpenEnd -> { - ObjList(list.toMutableList()) + sliceRange(0, sizeFast()) } else -> { @@ -96,16 +209,16 @@ open class ObjList(val list: MutableList = mutableListOf()) : Obj() { } open override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) { - list[index.toInt()] = newValue + setObjAtFast(index.toInt(), newValue) } override suspend fun compareTo(scope: Scope, other: Obj): Int { if (other is ObjList) { - val mySize = list.size - val otherSize = other.list.size + val mySize = sizeFast() + val otherSize = other.sizeFast() val commonSize = minOf(mySize, otherSize) for (i in 0.. = mutableListOf()) : Obj() { return res } if (other.isInstanceOf(ObjIterable)) { - val it1 = this.list.iterator() val it2 = other.invokeInstanceMethod(scope, "iterator") val hasNext2 = it2.getInstanceMethod(scope, "hasNext") val next2 = it2.getInstanceMethod(scope, "next") - - while (it1.hasNext()) { + + for (i in 0.. = mutableListOf()) : Obj() { override suspend fun plus(scope: Scope, other: Obj): Obj = when { - other is ObjList -> - ObjList((list + other.list).toMutableList()) + other is ObjList -> { + 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) + }) + } else { + ObjList((list + other.list).toMutableList()) + } + } !shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable) -> { val l = other.callMethod(scope, "toList") @@ -151,12 +273,12 @@ open class ObjList(val list: MutableList = mutableListOf()) : Obj() { open override suspend fun plusAssign(scope: Scope, other: Obj): Obj { if (other is ObjList) { - list.addAll(other.list) + appendAllFast(other) } else if (!shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable)) { val otherList = (other.invokeInstanceMethod(scope, "toList") as ObjList).list list.addAll(otherList) } else { - list.add(other) + appendFast(other) } return this } @@ -199,6 +321,13 @@ open class ObjList(val list: 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 + } + return false + } if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { // Fast path: int membership in a list of ints (common case in benches) if (other is ObjInt) { @@ -216,6 +345,13 @@ open class ObjList(val list: MutableList = 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 + } + return + } for (item in list) { if (!callback(item)) break } @@ -225,6 +361,8 @@ open class ObjList(val list: MutableList = mutableListOf()) : Obj() { get() = type override suspend fun toKotlin(scope: Scope): Any { + val ints = primitiveIntList + if (ints != null) return ints.map { it } return list.map { it.toKotlin(scope) } } @@ -256,8 +394,7 @@ open class ObjList(val list: MutableList = mutableListOf()) : Obj() { } override fun hashCode(): Int { - // check? - return list.hashCode() + return primitiveIntList?.contentHashCode() ?: list.hashCode() } override fun equals(other: Any?): Boolean { @@ -266,16 +403,31 @@ open class ObjList(val list: MutableList = mutableListOf()) : Obj() { other as ObjList - return list == other.list + val ints = primitiveIntList + val otherInts = other.primitiveIntList + return if (ints != null && otherInts != null) { + ints.contentEquals(otherInts) + } else { + list == other.list + } } override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { - encoder.encodeAnyList(scope,list) + val ints = primitiveIntList + if (ints != null) { + encoder.encodeAnyList(scope, ints.mapTo(ArrayList(ints.size)) { ObjInt.of(it) }) + return + } + encoder.encodeAnyList(scope, list) } override suspend fun lynonType(): LynonType = LynonType.List override suspend fun toJson(scope: Scope): JsonElement { + val ints = primitiveIntList + if (ints != null) { + return JsonArray(ints.map { ObjInt.of(it).toJson(scope) }) + } return JsonArray(list.map { it.toJson(scope) }) } @@ -283,9 +435,17 @@ open class ObjList(val list: MutableList = mutableListOf()) : Obj() { return ObjString(buildString { append("[") var first = true - for (v in list) { - if (first) first = false else append(",") - append(v.toString(scope).value) + val ints = primitiveIntList + if (ints != null) { + for (v in ints) { + if (first) first = false else append(",") + append(v) + } + } else { + for (v in list) { + if (first) first = false else append(",") + append(v.toString(scope).value) + } } append("]") }) @@ -307,7 +467,7 @@ open class ObjList(val list: MutableList = mutableListOf()) : Obj() { type = type("lyng.Int"), moduleName = "lyng.stdlib", getter = { - val s = (this.thisObj as ObjList).list.size + val s = (this.thisObj as ObjList).sizeFast() s.toObj() } ) diff --git a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt index ab62759..faae3d0 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt @@ -16,21 +16,11 @@ */ import kotlinx.coroutines.test.runTest -import net.sergeych.lyng.Compiler -import net.sergeych.lyng.ExecutionError -import net.sergeych.lyng.Pos -import net.sergeych.lyng.Script -import net.sergeych.lyng.ScriptError -import net.sergeych.lyng.Source -import net.sergeych.lyng.eval -import net.sergeych.lyng.toSource -import net.sergeych.lyng.bytecode.CmdDisassembler -import net.sergeych.lyng.bytecode.CmdFunction +import net.sergeych.lyng.* import net.sergeych.lyng.obj.toInt import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull import kotlin.test.assertTrue class BytecodeRecentOpsTest { @@ -129,6 +119,79 @@ class BytecodeRecentOpsTest { ) } + @Test + fun intListIndexOpsUsePrimitiveBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + var a: List = [1, 2, 3] + val s = a[1] + a[1] += 3 + a[1]++ + a[1] = s + a[1] + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertTrue(disasm.contains("GET_INDEX_INT"), disasm) + assertTrue(disasm.contains("SET_INDEX_INT"), disasm) + assertEquals(2, scope.eval("calc()").toInt()) + } + + @Test + fun listFillIntIndexOpsUsePrimitiveBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + var a = List.fill(4) { 2 } + val s = a[1] + a[1] += 3 + a[1] = s + a[1] + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertTrue(disasm.contains("GET_INDEX_INT"), disasm) + assertTrue(disasm.contains("SET_INDEX_INT"), disasm) + assertEquals(2, scope.eval("calc()").toInt()) + } + + @Test + fun listFillIntUsesPrimitiveFillBytecode() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val xs = List.fill(5) { 2 } + xs[0] + xs[4] + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertTrue(disasm.contains("LIST_FILL_INT"), disasm) + assertEquals(4, scope.eval("calc()").toInt()) + } + + @Test + fun listFillIntWithIndexLambdaKeepsSemantics() = runTest { + val scope = Script.newScope() + scope.eval( + """ + fun calc() { + val xs = List.fill(5) { it * 3 } + xs[0] + xs[4] + } + """.trimIndent() + ) + val disasm = scope.disassembleSymbol("calc") + assertTrue(disasm.contains("LIST_FILL_INT"), disasm) + assertEquals(12, scope.eval("calc()").toInt()) + } + @Test fun optionalIndexPreIncSkipsOnNullReceiver() = runTest { eval( diff --git a/lynglib/src/commonTest/kotlin/CoroutinesTest.kt b/lynglib/src/commonTest/kotlin/CoroutinesTest.kt index d7d485c..09345d3 100644 --- a/lynglib/src/commonTest/kotlin/CoroutinesTest.kt +++ b/lynglib/src/commonTest/kotlin/CoroutinesTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * 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. @@ -97,7 +97,7 @@ class TestCoroutines { counter = c + 1 // } } - }.forEach { (it as Deferred).await() } + }.forEach { it.await() } println(counter) assert( counter < 10 ) @@ -105,6 +105,21 @@ class TestCoroutines { ) } + @Test + fun testMapForEachDeferredInference() = runTest { + eval( + """ + var sum = 0 + + (1..3).map { n -> + launch { n } + }.forEach { sum += it.await() } + + assertEquals(6, sum) + """.trimIndent() + ) + } + @Test fun testFlows() = runTest { eval(""" diff --git a/lynglib/src/jvmTest/kotlin/PiSpigotBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/PiSpigotBenchmarkTest.kt index 294372f..5688c2e 100644 --- a/lynglib/src/jvmTest/kotlin/PiSpigotBenchmarkTest.kt +++ b/lynglib/src/jvmTest/kotlin/PiSpigotBenchmarkTest.kt @@ -16,25 +16,13 @@ */ import kotlinx.coroutines.test.runTest -import net.sergeych.lyng.Benchmarks -import net.sergeych.lyng.BytecodeBodyProvider -import net.sergeych.lyng.PerfFlags -import net.sergeych.lyng.PerfProfiles -import net.sergeych.lyng.Script -import net.sergeych.lyng.Statement -import net.sergeych.lyng.bytecode.BytecodeStatement -import net.sergeych.lyng.bytecode.CmdCallMemberSlot -import net.sergeych.lyng.bytecode.CmdFunction -import net.sergeych.lyng.bytecode.CmdGetIndex -import net.sergeych.lyng.bytecode.CmdIterPush -import net.sergeych.lyng.bytecode.CmdMakeRange -import net.sergeych.lyng.bytecode.CmdSetIndex +import net.sergeych.lyng.* +import net.sergeych.lyng.bytecode.* import net.sergeych.lyng.obj.ObjString import java.nio.file.Files import java.nio.file.Path import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue import kotlin.time.TimeSource class PiSpigotBenchmarkTest { @@ -42,17 +30,11 @@ class PiSpigotBenchmarkTest { fun benchmarkPiSpigot() = runTest { if (!Benchmarks.enabled) return@runTest - val source = Files.readString(resolveExample("pi-test.lyng")) - val legacySource = source.replace( - "val quotient = sum / denom", - "var quotient = floor((sum / (denom * 1.0))).toInt()" - ) - assertTrue(legacySource != source, "failed to build legacy piSpigot benchmark case") + val source = loadPiSpigotSource() val digits = 200 val expectedSuffix = "49303819" - val legacyElapsed = runCase("legacy-real-division", legacySource, digits, expectedSuffix, dumpBytecode = true) val saved = PerfProfiles.snapshot() PerfFlags.RVAL_FASTPATH = false val optimizedRvalOffElapsed = runCase( @@ -64,15 +46,11 @@ class PiSpigotBenchmarkTest { ) PerfProfiles.restore(saved) val optimizedElapsed = runCase("optimized-int-division-rval-on", source, digits, expectedSuffix, dumpBytecode = true) - val sourceSpeedup = legacyElapsed.toDouble() / optimizedRvalOffElapsed.toDouble() val runtimeSpeedup = optimizedRvalOffElapsed.toDouble() / optimizedElapsed.toDouble() - val totalSpeedup = legacyElapsed.toDouble() / optimizedElapsed.toDouble() println( - "[DEBUG_LOG] [BENCH] pi-spigot compare n=$digits legacy=${legacyElapsed} ms " + - "intDiv=${optimizedRvalOffElapsed} ms rvalOn=${optimizedElapsed} ms " + - "intDivSpeedup=${"%.2f".format(sourceSpeedup)}x " + - "rvalSpeedup=${"%.2f".format(runtimeSpeedup)}x " + - "total=${"%.2f".format(totalSpeedup)}x" + "[DEBUG_LOG] [BENCH] pi-spigot compare n=$digits " + + "rvalOff=${optimizedRvalOffElapsed} ms rvalOn=${optimizedElapsed} ms " + + "rvalSpeedup=${"%.2f".format(runtimeSpeedup)}x" ) } @@ -91,18 +69,18 @@ class PiSpigotBenchmarkTest { dumpHotOps(scope, "piSpigot") } - val first = scope.eval("piSpigot($digits)") as ObjString + val first = scope.eval("piSpigot(0, $digits)") as ObjString assertEquals(expectedSuffix, first.value) repeat(2) { - val warm = scope.eval("piSpigot($digits)") as ObjString + val warm = scope.eval("piSpigot(0, $digits)") as ObjString assertEquals(expectedSuffix, warm.value) } val iterations = 3 val start = TimeSource.Monotonic.markNow() repeat(iterations) { - val result = scope.eval("piSpigot($digits)") as ObjString + val result = scope.eval("piSpigot(0, $digits)") as ObjString assertEquals(expectedSuffix, result.value) } val elapsedMs = start.elapsedNow().inWholeMilliseconds @@ -135,6 +113,11 @@ class PiSpigotBenchmarkTest { ?: (stmt as? BytecodeBodyProvider)?.bytecodeBody()?.bytecodeFunction() } + private fun loadPiSpigotSource(): String { + val source = Files.readString(resolveExample("pi-bench.lyng")) + return source.substringBefore("\nval t0 = Instant()") + } + private fun resolveExample(name: String): Path { val direct = Path.of("examples", name) if (Files.exists(direct)) return direct