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