optimize arithmetics

This commit is contained in:
Sergey Chernov 2026-04-04 04:01:43 +03:00
parent 161f3f74e2
commit d8454a11fc
21 changed files with 913 additions and 208 deletions

View File

@ -0,0 +1,62 @@
## Pi Spigot JVM Baseline
Saved on April 4, 2026 before the `List<Int>` 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<Int>` 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

View File

@ -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:

View File

@ -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..<n) {
var carriedOver = 0
var sum = 0
for (k in 1..boxes) {
val j = boxes - k
for (j in (boxes - 1) downTo 0) {
val denom = j * 2 + 1
reminders[j] *= 10
sum = reminders[j] + carriedOver
@ -39,29 +39,29 @@ fn piSpigot(iThread: Int, n: Int) {
} else {
heldDigits = 1
}
pi.add(q)
pi[piIter] = q
++piIter
}
var s = ""
var res = ""
for (i in (n - 8)..<n) {
s += pi[i]
res += pi[i]
}
println(iThread, " - done: ", s)
println(iThread.toString() + ": " + res)
res
}
var counter = 0
for( r in 0..100 ) {
val t0 = Instant()
(1..TASK_COUNT).map { n ->
val counterState = counter
val t = launch {
piSpigot(counterState, WORK_SIZE)
println("piBench (lyng): THREADS = " + THREADS + ", WORK_SIZE = " + WORK_SIZE)
for (i in 0..<THREADS) {
piSpigot(i, WORK_SIZE)
}
++counter
t
}.forEach { (it as Deferred).await() }
val dt = Instant() - t0
println("all done, dt = ", dt)
delay(800)
}

83
examples/pi-bench.py Normal file
View File

@ -0,0 +1,83 @@
# 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.
#
import time
from multiprocessing import Process
def piSpigot(iThread, nx):
piIter = 0
pi = [None] * nx
boxes = nx * 10 // 3
reminders = [None]*boxes
i = 0
while i < boxes:
reminders[i] = 2
i += 1
heldDigits = 0
i = 0
while i < nx:
carriedOver = 0
sum = 0
j = boxes - 1
while j >= 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))

View File

@ -1,49 +0,0 @@
fn piSpigot(n) {
var pi = []
val boxes = n * 10 / 3
var reminders = []
for (i in 0..<boxes) {
reminders.add(2)
}
var heldDigits = 0
for (i in 0..n) {
var carriedOver = 0
var sum = 0
for (k in 1..boxes) {
val j = boxes - k
val denom = j * 2 + 1
reminders[j] *= 10
sum = reminders[j] + carriedOver
// Keep this integer-only. Real coercion here is much slower in the hot loop.
val quotient = sum / denom
reminders[j] = sum % denom
carriedOver = quotient * j
}
reminders[0] = sum % 10
var q = sum / 10
if (q == 9) {
++heldDigits
} else if (q == 10) {
q = 0
for (k in 1..heldDigits) {
var replaced = pi[i - k]
if (replaced == 9) {
replaced = 0
} else {
++replaced
}
pi[i - k] = replaced
}
heldDigits = 1
} else {
heldDigits = 1
}
pi.add(q)
}
var suffix = ""
for (i in (n - 8)..<n) {
suffix += pi[i]
}
suffix
}

View File

@ -20,9 +20,11 @@ import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.Script
import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjString
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
class CliDispatcherJvmTest {
@ -69,4 +71,34 @@ class CliDispatcherJvmTest {
session.cancelAndJoin()
}
}
@Test
fun cliEvalInfersDeferredItTypeFromMapLambdaLocal() = runBlocking {
val session = EvalSession(Script.newScope())
try {
val result = evalOnCliDispatcher(
session,
Source(
"<cli-repro>",
"""
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()
}
}
}

View File

@ -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,9 +62,11 @@ class LyngNetTcpServerExampleTest {
"${'$'}{accepted.await()}: ${'$'}reply"
""".trimIndent()
val result = withTimeout(5_000) {
val result = withContext(Dispatchers.Default) {
withTimeout(5_000) {
Compiler.compile(code).execute(scope).inspect(scope)
}
}
assertEquals("\"ping: echo:ping\"", result)
}

View File

@ -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

View File

@ -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

View File

@ -170,6 +170,20 @@ class Compiler(
private val typeAliases: MutableMap<String, TypeAliasDecl> = mutableMapOf()
private val methodReturnTypeDeclByRef: MutableMap<ObjRef, TypeDecl> = mutableMapOf()
private val callReturnTypeDeclByRef: MutableMap<CallRef, TypeDecl> = mutableMapOf()
private val iterableLikeTypeNames = setOf(
"Iterable",
"Collection",
"Array",
"List",
"ImmutableList",
"Set",
"ImmutableSet",
"Flow",
"ObservableList",
"RingBuffer",
"Range",
"IntRange"
)
private val callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
private val callableReturnTypeDeclByName: MutableMap<String, TypeDecl> = 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<ParsedArgument>
): 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<ParsedArgument>): 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<List<ParsedArgument>, Boolean> {
private suspend fun parseArgs(
expectedTailBlockReceiver: String? = null,
implicitItType: String? = null
): Pair<List<ParsedArgument>, Boolean> {
val args = mutableListOf<ParsedArgument>()
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) }

View File

@ -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
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
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
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
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)

View File

@ -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])

View File

@ -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 ->

View File

@ -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<Int, Boolean> {
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(

View File

@ -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),

View File

@ -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<String, Int>,
val argsDeclaration: ArgsDeclaration?,
val captureEntries: List<LambdaCaptureEntry>,
val inferredReturnClass: ObjClass?,
val preferredThisType: String?,
val wrapAsExtensionCallable: Boolean,
val returnLabels: Set<String>,

View File

@ -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<Obj> = mutableListOf()) : Obj() {
open class ObjList(initialList: MutableList<Obj> = mutableListOf()) : Obj() {
private var boxedList: MutableList<Obj>? = null
private var primitiveIntList: LongArray? = null
init {
if (!adoptPrimitiveIntList(initialList)) {
boxedList = initialList
}
}
val list: MutableList<Obj>
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<Obj>): 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<Obj> {
boxedList?.let { return it }
val ints = primitiveIntList
if (ints == null) {
val empty = mutableListOf<Obj>()
boxedList = empty
return empty
}
val materialized = ArrayList<Obj>(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<Obj> = mutableListOf()) : Obj() {
}
return false
}
if (list.size != other.list.size) return false
for (i in 0..<list.size) {
if (!list[i].equals(scope, other.list[i])) return false
if (sizeFast() != other.sizeFast()) return false
for (i in 0..<sizeFast()) {
if (!getObjAtFast(i).equals(scope, other.getObjAtFast(i))) return false
}
return true
}
@ -58,31 +171,31 @@ open class ObjList(val list: MutableList<Obj> = 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<Obj> = 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..<commonSize) {
val d = list[i].compareTo(scope, other.list[i])
val d = getObjAtFast(i).compareTo(scope, other.getObjAtFast(i))
if (d != 0) {
return d
}
@ -114,14 +227,13 @@ open class ObjList(val list: MutableList<Obj> = 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..<sizeFast()) {
if (!hasNext2.invoke(scope, it2).toBool()) return 1 // I'm longer
val v1 = it1.next()
val v1 = getObjAtFast(i)
val v2 = next2.invoke(scope, it2)
val d = v1.compareTo(scope, v2)
if (d != 0) return d
@ -133,8 +245,18 @@ open class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
override suspend fun plus(scope: Scope, other: Obj): Obj =
when {
other is ObjList ->
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<ObjList>(scope, "toList")
@ -151,12 +273,12 @@ open class ObjList(val list: MutableList<Obj> = 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<Obj> = 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<Obj> = 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<Obj> = 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<Obj> = 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<Obj> = 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?) {
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,10 +435,18 @@ open class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
return ObjString(buildString {
append("[")
var first = true
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<Obj> = 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()
}
)

View File

@ -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<Int> = [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(

View File

@ -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("""

View File

@ -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