Compare commits

..

20 Commits

Author SHA1 Message Date
0973a6afeb Fix JVM import caching and class/object bytecode dispatch 2026-04-21 23:25:07 +03:00
953f237ca3 Optimize primitive list fill capacity and append 2026-04-21 21:37:04 +03:00
fbb5688696 Drive higher-order lambda inlining from call metadata 2026-04-21 20:51:16 +03:00
f4ab2ebab4 Extend lambda inlining to getOrPut and implicit it calls 2026-04-21 19:20:25 +03:00
0c3242cbd8 Generalize higher-order lambda inlining 2026-04-21 19:12:16 +03:00
1d5caaa836 Broaden lambda method inlining with captures 2026-04-21 19:07:05 +03:00
3be2892025 Use fast compiled callbacks in dynamic and flow helpers 2026-04-21 18:06:31 +03:00
c80900c503 Prevent cross-script module slot aliasing 2026-04-21 17:34:00 +03:00
ee634c8dff Extend generic fast callable entry points 2026-04-21 17:26:53 +03:00
6c91b77a85 Use fast lambda calls in scope facade 2026-04-21 16:43:02 +03:00
3721ee8332 Fast-path compiled lambda calls with args 2026-04-21 16:04:04 +03:00
97990f00ce Cover when-based exact callable fast paths 2026-04-21 14:37:27 +03:00
80693e7690 Inline exact lambdas through wrapper expressions 2026-04-21 14:36:18 +03:00
029fe874fa Propagate exact callable refs across expressions 2026-04-21 14:33:23 +03:00
ffa64d691b Use direct calls for constant constructors 2026-04-21 13:30:09 +03:00
30c9a5a565 Propagate exact lambda refs across bytecode slots 2026-04-21 13:20:27 +03:00
db3a780645 Generalize bytecode fast-call dispatch 2026-04-21 13:09:50 +03:00
fc7d26ee4b Fast-path bytecode statement execution 2026-04-21 11:39:47 +03:00
a61b5a31be Add non-suspending fast calls for bytecode callables 2026-04-21 11:35:20 +03:00
33d170f525 Optimize fast-path bytecode lambda invocation 2026-04-21 11:29:14 +03:00
38 changed files with 2475 additions and 523 deletions

15
examples/fillspeed.lyng Normal file
View File

@ -0,0 +1,15 @@
import lyng.time
val n = 700_000
fun tm<T>(block: ()->T): T {
val t = Instant()
block().also {
println("tm: ${Instant() - t}")
}
}
val x = tm { List.fill(n) { it * 10 + 1 } }
val y = tm { List.fill(n, n + 10) { it * 10 + 1 } }
tm { x.add(-1) }
tm { y.add(-2) }

View File

@ -434,6 +434,57 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
}
}
fun supportsFastFrameBinding(arguments: Arguments): Boolean {
if (arguments.named.isNotEmpty() || arguments.tailBlockMode) return false
return params.none { it.isEllipsis || it.defaultValue != null }
}
fun assignToFrameFast(
scope: Scope,
arguments: Arguments = scope.args,
paramSlotPlan: Map<String, Int>,
frame: FrameAccess,
slotOffset: Int = 0
) {
if (!supportsFastFrameBinding(arguments)) {
scope.raiseIllegalState("fast frame binding is not supported for this call shape")
}
fun slotFor(name: String): Int {
val full = paramSlotPlan[name] ?: scope.raiseIllegalState("parameter slot for '$name' is missing")
val slot = full - slotOffset
if (slot < 0) scope.raiseIllegalState("parameter slot for '$name' is out of range")
return slot
}
fun assign(slot: Int, value: Obj) {
when (value) {
is net.sergeych.lyng.obj.ObjInt -> frame.setInt(slot, value.value)
is net.sergeych.lyng.obj.ObjReal -> frame.setReal(slot, value.value)
is net.sergeych.lyng.obj.ObjBool -> frame.setBool(slot, value.value)
else -> frame.setObj(slot, value)
}
}
if (arguments.list.size > params.size) {
scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}")
}
if (arguments.list.size < params.size) {
for (i in arguments.list.size until params.size) {
val a = params[i]
if (!a.type.isNullable) {
scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}")
}
}
}
for (i in params.indices) {
val slot = slotFor(params[i].name)
val value = if (i < arguments.list.size) arguments.list[i] else ObjNull
assign(slot, value.byValueCopy())
}
}
/**
* Single argument declaration descriptor.
*

View File

@ -16,4 +16,12 @@
package net.sergeych.lyng
interface BytecodeCallable
import net.sergeych.lyng.obj.Obj
interface BytecodeCallable {
fun callOnFast(scope: Scope): Obj? = null
}
interface BytecodeArgCallable {
fun callWithArgsFast(scope: Scope, args: Arguments): Obj? = null
}

View File

@ -28,6 +28,7 @@ internal suspend fun executeBytecodeWithSeed(scope: Scope, stmt: Statement, labe
else -> null
} ?: scope.raiseIllegalState("$label requires bytecode statement")
scope.pos = bytecode.pos
bytecode.callOnFast(scope)?.let { return it }
return CmdVm().execute(bytecode.bytecodeFunction(), scope, scope.args) { frame, _ ->
seedFrameLocalsFromScope(frame, scope)
}

View File

@ -20,5 +20,30 @@ package net.sergeych.lyng
* Compile-time call metadata for known functions. Used to select lambda receiver semantics.
*/
data class CallSignature(
val tailBlockReceiverType: String? = null
)
val tailBlockReceiverType: String? = null,
val inlineHigherOrder: HigherOrderInline? = null
) {
data class HigherOrderInline(
val kind: Kind,
val result: ResultMode,
val argCount: Int = 1,
val lambdaArgIndex: Int = 0
)
enum class Kind {
UNARY_ARGUMENT,
RECEIVER,
ITERABLE,
MAP_GET_OR_PUT
}
enum class ResultMode {
BLOCK_RESULT,
RETURN_RECEIVER,
FOR_EACH,
MAP,
FILTER,
MAP_NOT_NULL,
ASSOCIATE_BY
}
}

View File

@ -187,6 +187,7 @@ class Compiler(
private val callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
private val callableReturnTypeDeclByName: MutableMap<String, TypeDecl> = mutableMapOf()
private val callSignatureByName: MutableMap<String, CallSignature> = 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>> =
@ -265,6 +266,11 @@ class Compiler(
if (plan.slots.containsKey(name)) continue
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
scopeSeedNames.add(name)
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(name)) {
callSignatureByName[name] = signature
}
}
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
if (nameObjClass[name] == null) {
@ -291,6 +297,11 @@ class Compiler(
)
scopeSeedNames.add(getterName)
}
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(getterName)) {
callSignatureByName[getterName] = signature
}
}
val prop = record.value as? ObjProperty
if (prop?.setter != null) {
val setterName = extensionPropertySetterName(cls.className, name)
@ -316,6 +327,11 @@ class Compiler(
)
scopeSeedNames.add(callableName)
}
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(callableName)) {
callSignatureByName[callableName] = signature
}
}
}
}
}
@ -347,6 +363,11 @@ class Compiler(
record.type == ObjRecord.Type.Delegated
)
scopeSeedNames.add(name)
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(name)) {
callSignatureByName[name] = signature
}
}
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
if (nameObjClass[name] == null) {
@ -762,6 +783,29 @@ class Compiler(
?: importManager.rootScope.getLocalRecordDirect(name)?.callSignature
}
private fun declaredCallSignature(name: String, extTypeName: String?, actualExtern: Boolean): CallSignature? {
val imported = if (actualExtern) {
importManager.rootScope.getLocalRecordDirect(name)?.callSignature
} else {
null
}
val inferred = when {
extTypeName == "Iterable" && name == "filter" -> CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.FILTER
)
)
else -> null
}
return when {
imported == null -> inferred
inferred == null -> imported
imported.inlineHigherOrder != null -> imported
else -> imported.copy(inlineHigherOrder = inferred.inlineHigherOrder)
}
}
internal data class MemberIds(val fieldId: Int?, val methodId: Int?)
private fun resolveMemberIds(name: String, pos: Pos, qualifier: String? = null): MemberIds {
@ -1429,6 +1473,11 @@ class Compiler(
}
private fun seedImportTypeMetadata(name: String, record: ObjRecord) {
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(name)) {
callSignatureByName[name] = signature
}
}
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
@ -1899,11 +1948,12 @@ class Compiler(
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
knownNameObjClass = knownClassMapForBytecode(),
knownClassNames = knownClassNamesForBytecode(),
knownObjectNames = objectDeclNames,
knownObjectNames = knownObjectNamesForBytecode(),
classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
scopeRefPosByName = moduleReferencePosByName,
@ -2227,6 +2277,23 @@ class Compiler(
return result
}
private fun knownObjectNamesForBytecode(): Set<String> {
val result = LinkedHashSet<String>()
fun addScope(scope: Scope?) {
if (scope == null) return
for ((name, rec) in scope.objects) {
if (rec.value is ObjInstance) result.add(name)
}
}
addScope(seedScope)
addScope(importManager.rootScope)
for (module in importedModules) {
addScope(module.scope)
}
result.addAll(objectDeclNames)
return result
}
private fun wrapBytecode(stmt: Statement): Statement {
if (codeContexts.lastOrNull() is CodeContext.Module) return stmt
if (codeContexts.lastOrNull() is CodeContext.ClassBody) return stmt
@ -2255,11 +2322,12 @@ class Compiler(
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
knownNameObjClass = knownClassMapForBytecode(),
knownClassNames = knownClassNamesForBytecode(),
knownObjectNames = objectDeclNames,
knownObjectNames = knownObjectNamesForBytecode(),
classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externCallableNames = externCallableNames,
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
@ -2289,11 +2357,12 @@ class Compiler(
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
knownNameObjClass = knownClassMapForBytecode(),
knownClassNames = knownClassNamesForBytecode(),
knownObjectNames = objectDeclNames,
knownObjectNames = knownObjectNamesForBytecode(),
classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externCallableNames = externCallableNames,
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
@ -2348,11 +2417,12 @@ class Compiler(
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
knownNameObjClass = knownNames,
knownClassNames = knownClassNamesForBytecode(),
knownObjectNames = objectDeclNames,
knownObjectNames = knownObjectNamesForBytecode(),
classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externCallableNames = externCallableNames,
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
@ -3545,14 +3615,90 @@ class Compiler(
}
val bytecodeFn = (fnStatements as? BytecodeStatement)?.bytecodeFunction()
val inlineBodyRef = argsDeclaration?.let { null } ?: extractInlineLambdaBodyRef(body)
val supportsDirectInvokeFastPath = bytecodeFn != null &&
bytecodeFn.scopeSlotCount == 0 &&
expectedReceiverType == null &&
!wrapAsExtensionCallable &&
!containsDelegatedRefs(body)
val ref = LambdaFnRef(
valueFn = { closureScope ->
val captureRecords = closureScope.captureRecords
val stmt = object : Statement(), BytecodeBodyProvider {
val stmt = object : Statement(), BytecodeBodyProvider, BytecodeCallable {
override val pos: Pos = fnStatements.pos
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement
override fun callOnFast(scope: Scope): Obj? {
val context = scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also {
it.args = scope.args
}
if (captureSlots.isNotEmpty()) {
if (captureRecords != null) {
context.captureRecords = captureRecords
context.captureNames = captureSlots.map { it.name }
} else {
val resolvedRecords = ArrayList<ObjRecord>(captureSlots.size)
val resolvedNames = ArrayList<String>(captureSlots.size)
for (capture in captureSlots) {
val rec = resolveStableCaptureRecord(
closureScope,
capture.name,
context.currentClassCtx
) ?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found")
resolvedRecords.add(freezeImmutableCaptureRecord(rec))
resolvedNames.add(capture.name)
}
context.captureRecords = resolvedRecords
context.captureNames = resolvedNames
}
}
val bytecodeBody = fnStatements as? BytecodeStatement ?: return null
val bytecodeFn = bytecodeBody.bytecodeFunction()
if (!supportsDirectInvokeFastPath || !bytecodeFn.fastOnly) return null
val fastPreboundNames = if (argsDeclaration == null) {
setOf("it")
} else {
argsDeclaration.params.mapTo(LinkedHashSet()) { it.name }
}
val declaredNames = bytecodeFn.constants
.mapNotNull { it as? BytecodeConst.LocalDecl }
.mapTo(mutableSetOf()) { it.name }
if (!canFastSeedUndeclaredLocals(bytecodeFn, declaredNames, fastPreboundNames)) return null
if (argsDeclaration != null && !argsDeclaration.supportsFastFrameBinding(scope.args)) return null
val slotPlan = bytecodeFn.localSlotPlanByName()
val binder: (net.sergeych.lyng.bytecode.CmdFrame, Arguments) -> Unit = { frame, arguments ->
if (argsDeclaration == null) {
val l = arguments.list
val itValue: Obj = when (l.size) {
0 -> ObjVoid
1 -> l[0]
else -> ObjList(l.toMutableList())
}
val itSlot = slotPlan["it"]
if (itSlot != null) {
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.assignToFrameFast(
context,
arguments,
slotPlan,
frame.frame
)
}
}
return try {
net.sergeych.lyng.bytecode.CmdVm().executeFastOnlyNoSuspend(bytecodeFn, context, scope.args, 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 = expectedReceiverType).also {
it.args = scope.args
@ -3628,6 +3774,7 @@ class Compiler(
captureEntries = captureEntries,
inferredReturnClass = returnClass,
inlineBodyRef = inlineBodyRef,
supportsDirectInvokeFastPath = supportsDirectInvokeFastPath,
preferredThisType = expectedReceiverType,
wrapAsExtensionCallable = wrapAsExtensionCallable,
returnLabels = returnLabels,
@ -9131,7 +9278,7 @@ class Compiler(
val extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) }
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
val externCallSignature = if (actualExtern) importManager.rootScope.getLocalRecordDirect(name)?.callSignature else null
val externCallSignature = declaredCallSignature(name, extTypeName, actualExtern)
val declKind = if (parentContext is CodeContext.ClassBody) SymbolKind.MEMBER else SymbolKind.FUNCTION
resolutionSink?.declareSymbol(name, declKind, isMutable = false, pos = nameStartPos, isOverride = isOverride)
@ -9153,7 +9300,9 @@ class Compiler(
}
if (extensionWrapperName != null) {
declareLocalName(extensionWrapperName, isMutable = false)
externCallSignature?.let { callSignatureByName[extensionWrapperName] = it }
}
externCallSignature?.let { callSignatureByName[name] = it }
if (actualExtern && declKind != SymbolKind.MEMBER) {
externCallableNames.add(name)
}
@ -9436,9 +9585,8 @@ class Compiler(
val fnBody = object : Statement(), BytecodeBodyProvider {
override val pos: Pos = start
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement
override suspend fun execute(scope: Scope): Obj {
scope.pos = start
override suspend fun execute(scope: Scope): Obj {
// restore closure where the function was defined, and making a copy of it
// for local space. If there is no closure, we are in, say, class context where
// the closure is in the class initialization and we needn't more:
@ -9447,6 +9595,7 @@ class Compiler(
it.args = scope.args
}
} ?: scope
context.pos = start
// Capacity hint: parameters + declared locals + small overhead
val capacityHint = paramNames.size + fnLocalDecls + 4

View File

@ -118,7 +118,14 @@ internal suspend fun executeFunctionDecl(
scope.addExtension(
type,
spec.name,
ObjRecord(ObjUnset, isMutable = false, visibility = spec.visibility, declaringClass = null, type = ObjRecord.Type.Delegated).apply {
ObjRecord(
ObjUnset,
isMutable = false,
visibility = spec.visibility,
declaringClass = null,
type = ObjRecord.Type.Delegated,
callSignature = spec.externCallSignature
).apply {
delegate = finalDelegate
}
)
@ -135,7 +142,8 @@ internal suspend fun executeFunctionDecl(
null,
spec.startPos,
isTransient = spec.isTransient,
type = ObjRecord.Type.Delegated
type = ObjRecord.Type.Delegated,
callSignature = spec.externCallSignature
).apply {
delegate = finalDelegate
}
@ -204,6 +212,7 @@ internal suspend fun executeFunctionDecl(
visibility = spec.visibility,
pos = spec.startPos,
type = ObjRecord.Type.Fun,
callSignature = spec.externCallSignature,
)
} else {
scope.addExtension(
@ -215,6 +224,7 @@ internal suspend fun executeFunctionDecl(
visibility = spec.visibility,
declaringClass = null,
type = ObjRecord.Type.Fun,
callSignature = spec.externCallSignature,
typeDecl = spec.typeDecl
)
)
@ -227,6 +237,7 @@ internal suspend fun executeFunctionDecl(
wrapper,
spec.visibility,
recordType = ObjRecord.Type.Fun,
callSignature = spec.externCallSignature,
typeDecl = spec.typeDecl
)
} ?: run {
@ -245,7 +256,8 @@ internal suspend fun executeFunctionDecl(
isOverride = spec.isOverride,
type = ObjRecord.Type.Fun,
methodId = spec.memberMethodId,
typeDecl = spec.typeDecl
typeDecl = spec.typeDecl,
callSignature = spec.externCallSignature
)
val memberValue = cls.members[spec.name]?.value ?: compiledFnBody
scope.addItem(

View File

@ -66,7 +66,11 @@ internal class ScopeBridge(internal val scope: Scope) : ScopeFacade {
override fun raiseIllegalState(message: String): Nothing = scope.raiseIllegalState(message)
override fun raiseNotImplemented(what: String): Nothing = scope.raiseNotImplemented(what)
override suspend fun call(callee: Obj, args: Arguments, newThisObj: Obj?): Obj {
return callee.callOn(scope.createChildScope(scope.pos, args = args, newThisObj = newThisObj))
if (newThisObj == null) {
(callee as? BytecodeArgCallable)?.callWithArgsFast(scope, args)?.let { return it }
}
val child = scope.createChildScope(scope.pos, args = args, newThisObj = newThisObj)
return (callee as? BytecodeCallable)?.callOnFast(child) ?: callee.callOn(child)
}
override suspend fun toStringOf(obj: Obj, forInspect: Boolean): ObjString = obj.toString(scope, forInspect)
override suspend fun inspect(obj: Obj): String = obj.inspect(scope)

View File

@ -108,11 +108,7 @@ class Script(
seedImportBindings(scope, seedScope)
}
if (moduleSlotPlan.isNotEmpty()) {
scope.applySlotPlan(moduleSlotPlan)
for (name in moduleSlotPlan.keys) {
val record = scope.objects[name] ?: scope.localBindings[name] ?: continue
scope.updateSlotFor(name, record)
}
installModuleSlotPlan(scope)
}
}
@ -120,13 +116,27 @@ class Script(
if (importBindings.isEmpty() && importedModules.isEmpty()) return
seedImportBindings(scope, seedScope)
if (moduleSlotPlan.isNotEmpty()) {
scope.applySlotPlan(moduleSlotPlan)
installModuleSlotPlan(scope)
}
}
private fun installModuleSlotPlan(scope: Scope) {
for ((name, index) in moduleSlotPlan) {
if (scope.getSlotIndexOf(name) != null) continue
if (scope.hasSlotPlanConflict(mapOf(name to index))) {
val record = scope.objects[name]
?: scope.localBindings[name]
?: ObjRecord(ObjUnset, isMutable = true)
scope.allocateSlotFor(name, record)
} else {
scope.applySlotPlan(mapOf(name to index))
}
}
for (name in moduleSlotPlan.keys) {
val record = scope.objects[name] ?: scope.localBindings[name] ?: continue
scope.updateSlotFor(name, record)
}
}
}
private suspend fun seedModuleLocals(
frame: net.sergeych.lyng.bytecode.CmdFrame,

View File

@ -40,6 +40,7 @@ sealed class BytecodeConst {
val captureNames: List<String>,
val paramSlotPlan: Map<String, Int>,
val argsDeclaration: ArgsDeclaration?,
val supportsDirectInvokeFastPath: Boolean,
val preferredThisType: String?,
val wrapAsExtensionCallable: Boolean,
val returnLabels: Set<String>,

View File

@ -23,19 +23,36 @@ import net.sergeych.lyng.obj.*
class BytecodeStatement private constructor(
val original: Statement,
private val function: CmdFunction,
) : Statement(original.isStaticConst, original.isConst, original.returnType) {
) : Statement(original.isStaticConst, original.isConst, original.returnType), BytecodeCallable {
override val pos: Pos = original.pos
private val declaredLocalNames: Set<String> by lazy(LazyThreadSafetyMode.NONE) {
function.constants
.mapNotNull { it as? BytecodeConst.LocalDecl }
.mapTo(mutableSetOf()) { it.name }
}
override fun callOnFast(scope: Scope): Obj? {
scope.pos = pos
if (!function.fastOnly) return null
if (!canFastSeedUndeclaredLocals(function, declaredLocalNames, emptySet())) return null
val binder: (CmdFrame, Arguments) -> Unit = { frame, _ ->
if (!trySeedFrameLocalsFromScopeFast(frame, scope)) {
scope.raiseIllegalState("fast local seeding is not available")
}
}
return CmdVm().executeFastOnlyNoSuspend(function, scope, scope.args, binder)
}
override suspend fun execute(scope: Scope): Obj {
scope.pos = pos
val declaredNames = function.constants
.mapNotNull { it as? BytecodeConst.LocalDecl }
.mapTo(mutableSetOf()) { it.name }
val fastResult = callOnFast(scope)
if (fastResult != null) return fastResult
val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, _ ->
val localNames = frame.fn.localSlotNames
for (i in localNames.indices) {
val name = localNames[i] ?: continue
if (declaredNames.contains(name)) continue
if (declaredLocalNames.contains(name)) continue
val slotType = frame.getLocalSlotTypeCode(i)
if (slotType != SlotType.UNKNOWN.code && slotType != SlotType.OBJ.code) {
continue
@ -89,6 +106,7 @@ class BytecodeStatement private constructor(
enumEntriesByName: Map<String, List<String>> = emptyMap(),
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
callSignatureByName: Map<String, CallSignature> = emptyMap(),
externCallableNames: Set<String> = emptySet(),
externBindingNames: Set<String> = emptySet(),
preparedModuleBindingNames: Set<String> = emptySet(),
@ -129,6 +147,7 @@ class BytecodeStatement private constructor(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externCallableNames = externCallableNames,
externBindingNames = externBindingNames,
preparedModuleBindingNames = preparedModuleBindingNames,

View File

@ -112,6 +112,7 @@ class CmdBuilder {
}
cmds.add(createCmd(ins.op, operands, scopeSlotCount, localSlotCaptures))
}
val cmdArray = cmds.toTypedArray()
return CmdFunction(
name = name,
localCount = localCount,
@ -128,8 +129,9 @@ class CmdBuilder {
localSlotDelegated = localSlotDelegated,
localSlotCaptures = localSlotCaptures,
constants = constPool.toList(),
cmds = cmds.toTypedArray(),
posByIp = posByInstr.toTypedArray()
cmds = cmdArray,
posByIp = posByInstr.toTypedArray(),
fastOnly = computeFastOnlyBytecode(scopeSlotCount, cmdArray)
)
}
@ -237,6 +239,10 @@ class CmdBuilder {
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_IOTA_INT ->
listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_NEW_INT_CAP ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_FILL_INT_CAP ->
listOf(OperandKind.SLOT, 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 ->
@ -842,6 +848,8 @@ class CmdBuilder {
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_NEW_INT_CAP -> CmdListNewIntCap(operands[0], operands[1], operands[2])
Opcode.LIST_FILL_INT_CAP -> CmdListFillIntCap(operands[0], operands[1], operands[2], operands[3])
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

@ -498,6 +498,8 @@ object CmdDisassembler {
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 CmdListNewIntCap -> Opcode.LIST_NEW_INT_CAP to intArrayOf(cmd.sizeSlot, cmd.capacitySlot, cmd.dst)
is CmdListFillIntCap -> Opcode.LIST_FILL_INT_CAP to intArrayOf(cmd.sizeSlot, cmd.capacitySlot, 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)
@ -627,6 +629,10 @@ object CmdDisassembler {
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_IOTA_INT ->
listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_NEW_INT_CAP ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_FILL_INT_CAP ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_LITERAL ->
listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_MEMBER_SLOT ->

View File

@ -35,6 +35,7 @@ data class CmdFunction(
val constants: List<BytecodeConst>,
val cmds: Array<Cmd>,
val posByIp: Array<net.sergeych.lyng.Pos?>,
val fastOnly: Boolean = false,
) {
init {
require(scopeSlotIndices.size == scopeSlotCount) { "scopeSlotIndices size mismatch" }
@ -71,3 +72,118 @@ data class CmdFunction(
}
}
internal fun computeFastOnlyBytecode(scopeSlotCount: Int, cmds: Array<Cmd>): Boolean {
if (scopeSlotCount != 0) return false
return cmds.all(::supportsFastOnlyExecution)
}
private fun supportsFastOnlyExecution(cmd: Cmd): Boolean {
return when (cmd) {
is CmdMoveIntLocal,
is CmdMoveRealLocal,
is CmdMoveBoolLocal,
is CmdConstObj,
is CmdConstInt,
is CmdConstIntLocal,
is CmdConstReal,
is CmdConstBool,
is CmdConstNull,
is CmdUnboxIntObjLocal,
is CmdUnboxRealObjLocal,
is CmdIntToRealLocal,
is CmdRealToIntLocal,
is CmdBoolToIntLocal,
is CmdIntToBoolLocal,
is CmdAddIntLocal,
is CmdSubIntLocal,
is CmdMulIntLocal,
is CmdDivIntLocal,
is CmdModIntLocal,
is CmdNegIntLocal,
is CmdIncIntLocal,
is CmdDecIntLocal,
is CmdAddRealLocal,
is CmdSubRealLocal,
is CmdMulRealLocal,
is CmdDivRealLocal,
is CmdNegRealLocal,
is CmdAndIntLocal,
is CmdOrIntLocal,
is CmdXorIntLocal,
is CmdShlIntLocal,
is CmdShrIntLocal,
is CmdUshrIntLocal,
is CmdInvIntLocal,
is CmdCmpEqIntLocal,
is CmdCmpNeqIntLocal,
is CmdCmpLtIntLocal,
is CmdCmpLteIntLocal,
is CmdCmpGtIntLocal,
is CmdCmpGteIntLocal,
is CmdCmpEqRealLocal,
is CmdCmpNeqRealLocal,
is CmdCmpLtRealLocal,
is CmdCmpLteRealLocal,
is CmdCmpGtRealLocal,
is CmdCmpGteRealLocal,
is CmdCmpEqBoolLocal,
is CmdCmpNeqBoolLocal,
is CmdCmpEqIntRealLocal,
is CmdCmpEqRealIntLocal,
is CmdCmpLtIntRealLocal,
is CmdCmpLtRealIntLocal,
is CmdCmpLteIntRealLocal,
is CmdCmpLteRealIntLocal,
is CmdCmpGtIntRealLocal,
is CmdCmpGtRealIntLocal,
is CmdCmpGteIntRealLocal,
is CmdCmpGteRealIntLocal,
is CmdCmpNeqIntRealLocal,
is CmdCmpNeqRealIntLocal,
is CmdCmpEqStrLocal,
is CmdCmpNeqStrLocal,
is CmdCmpLtStrLocal,
is CmdCmpLteStrLocal,
is CmdCmpGtStrLocal,
is CmdCmpGteStrLocal,
is CmdCmpEqIntObjLocal,
is CmdCmpNeqIntObjLocal,
is CmdCmpLtIntObjLocal,
is CmdCmpLteIntObjLocal,
is CmdCmpGtIntObjLocal,
is CmdCmpGteIntObjLocal,
is CmdCmpEqRealObjLocal,
is CmdCmpNeqRealObjLocal,
is CmdCmpLtRealObjLocal,
is CmdCmpLteRealObjLocal,
is CmdCmpGtRealObjLocal,
is CmdCmpGteRealObjLocal,
is CmdAddIntObjLocal,
is CmdSubIntObjLocal,
is CmdMulIntObjLocal,
is CmdDivIntObjLocal,
is CmdModIntObjLocal,
is CmdAddRealObjLocal,
is CmdSubRealObjLocal,
is CmdMulRealObjLocal,
is CmdDivRealObjLocal,
is CmdModRealObjLocal,
is CmdNotBoolLocal,
is CmdAndBoolLocal,
is CmdOrBoolLocal,
is CmdJmp,
is CmdJmpIfTrueLocal,
is CmdJmpIfFalseLocal,
is CmdJmpIfEqIntLocal,
is CmdJmpIfNeqIntLocal,
is CmdJmpIfLtIntLocal,
is CmdJmpIfLteIntLocal,
is CmdJmpIfGtIntLocal,
is CmdJmpIfGteIntLocal,
is CmdRet,
is CmdRetVoid -> true
else -> false
}
}

View File

@ -57,6 +57,64 @@ class CmdVm {
suspend fun execute(fn: CmdFunction, scope0: Scope, args: List<Obj>): Obj {
return execute(fn, scope0, Arguments.from(args))
}
fun executeFastOnlyNoSuspend(
fn: CmdFunction,
scope0: Scope,
args: Arguments,
binder: ((CmdFrame, Arguments) -> Unit)? = null
): Obj {
require(fn.fastOnly) { "fast-only execution requested for non-fast function ${fn.name}" }
result = null
val frame = CmdFrame(this, fn, scope0, args.list)
frame.applyCaptureRecords()
binder?.invoke(frame, args)
val cmds = fn.cmds
try {
while (result == null) {
val cmd = cmds[frame.ip++]
if (!cmd.performFast(frame)) {
error("fast-only command not supported: ${cmd::class.simpleName}")
}
}
} catch (e: Throwable) {
throw frame.normalizeThrowableFast(e)
}
return result ?: ObjVoid
}
suspend fun executeFastOnly(
fn: CmdFunction,
scope0: Scope,
args: Arguments,
binder: ((CmdFrame, Arguments) -> Unit)? = null
): Obj {
require(fn.fastOnly) { "fast-only execution requested for non-fast function ${fn.name}" }
result = null
val frame = CmdFrame(this, fn, scope0, args.list)
frame.applyCaptureRecords()
binder?.invoke(frame, args)
val cmds = fn.cmds
while (true) {
try {
while (result == null) {
val cmd = cmds[frame.ip++]
if (!cmd.performFast(frame)) {
error("fast-only command not supported: ${cmd::class.simpleName}")
}
}
break
} catch (e: Throwable) {
val throwable = frame.normalizeThrowable(e)
if (!frame.handleException(throwable)) {
frame.cancelIterators()
throw throwable
}
}
}
frame.cancelIterators()
return result ?: ObjVoid
}
}
sealed class Cmd {
@ -280,6 +338,11 @@ class CmdMakeRange(
}
class CmdConstNull(internal val dst: Int) : Cmd() {
override fun performFast(frame: CmdFrame): Boolean {
frame.setObj(dst, ObjNull)
return true
}
override suspend fun perform(frame: CmdFrame) {
frame.setObj(dst, ObjNull)
return
@ -2301,6 +2364,11 @@ class CmdJmpIfGteIntLocal(internal val a: Int, internal val b: Int, internal val
}
class CmdRet(internal val slot: Int) : Cmd() {
override fun performFast(frame: CmdFrame): Boolean {
frame.vm.result = frame.storedSlotObj(slot)
return true
}
override suspend fun perform(frame: CmdFrame) {
frame.vm.result = frame.slotToObj(slot)
return
@ -2322,6 +2390,11 @@ class CmdRetLabel(internal val labelId: Int, internal val slot: Int) : Cmd() {
}
class CmdRetVoid : Cmd() {
override fun performFast(frame: CmdFrame): Boolean {
frame.vm.result = ObjVoid
return true
}
override suspend fun perform(frame: CmdFrame) {
frame.vm.result = ObjVoid
return
@ -3217,10 +3290,21 @@ class CmdCallDirect(
frame.ensureScope().raiseIllegalState("bytecode runtime cannot call non-bytecode Statement")
}
}
val result = if (PerfFlags.SCOPE_POOL) {
frame.ensureScope().withChildFrame(args) { child -> callee.callOn(child) }
val directFastResult = (callee as? BytecodeArgCallable)?.callWithArgsFast(frame.ensureScope(), args)
val result = if (directFastResult != null) {
directFastResult
} else if (PerfFlags.SCOPE_POOL) {
frame.ensureScope().withChildFrame(args) { child ->
(callee as? BytecodeCallable)?.callOnFast(child) ?: callee.callOn(child)
}
} else {
callee.callOn(frame.ensureScope().createChildScope(frame.ensureScope().pos, args = args))
val scope = frame.ensureScope()
if (callee is BytecodeLambdaCallable && callee.supportsDirectInvokeFastPath()) {
callee.invokeWithArgsFast(scope, args) ?: callee.invokeWithArgs(scope, args)
} else {
val child = scope.createChildScope(scope.pos, args = args)
(callee as? BytecodeCallable)?.callOnFast(child) ?: callee.callOn(child)
}
}
frame.storeObjResult(dst, result)
return
@ -3247,18 +3331,28 @@ class CmdCallSlot(
frame.ensureScope().raiseUnset(message)
}
val args = frame.buildArguments(argBase, argCount)
val canPool = PerfFlags.SCOPE_POOL && callee !is Statement
val result = if (canPool) {
frame.ensureScope().withChildFrame(args) { child -> callee.callOn(child) }
} else {
val scope = frame.ensureScope()
val directFastResult = (callee as? BytecodeArgCallable)?.callWithArgsFast(scope, args)
val canPool = PerfFlags.SCOPE_POOL && callee !is Statement
val result = if (directFastResult != null) {
directFastResult
} else if (canPool) {
frame.ensureScope().withChildFrame(args) { child ->
(callee as? BytecodeCallable)?.callOnFast(child) ?: callee.callOn(child)
}
} else {
if (callee is Statement) {
val bytecodeBody = (callee as? BytecodeBodyProvider)?.bytecodeBody()
if (callee !is BytecodeStatement && callee !is BytecodeCallable && bytecodeBody == null) {
scope.raiseIllegalState("bytecode runtime cannot call non-bytecode Statement")
}
}
callee.callOn(scope.createChildScope(scope.pos, args = args))
if (callee is BytecodeLambdaCallable && callee.supportsDirectInvokeFastPath()) {
callee.invokeWithArgsFast(scope, args) ?: callee.invokeWithArgs(scope, args)
} else {
val child = scope.createChildScope(scope.pos, args = args)
(callee as? BytecodeCallable)?.callOnFast(child) ?: callee.callOn(child)
}
}
frame.storeObjResult(dst, result)
return
@ -3341,10 +3435,53 @@ class CmdListFillInt(
val scope = frame.ensureScope()
val result = ObjList(LongArray(size))
for (i in 0 until size) {
val args = Arguments(ObjInt.of(i.toLong()))
val value = if (callable is BytecodeLambdaCallable && callable.supportsImplicitIntFillFastPath()) {
callable.invokeImplicitIntArg(scope, i.toLong())
callable.invokeImplicitIntArgFast(scope, i.toLong()) ?: callable.invokeImplicitIntArg(scope, i.toLong())
} else if (callable is BytecodeArgCallable) {
callable.callWithArgsFast(scope, args) ?: run {
val child = scope.createChildScope(scope.pos, args = args)
(callable as? BytecodeCallable)?.callOnFast(child) ?: callable.callOn(child)
}
} else {
callable.callOn(scope.createChildScope(scope.pos, args = Arguments(ObjInt.of(i.toLong()))))
val child = scope.createChildScope(scope.pos, args = args)
(callable as? BytecodeCallable)?.callOnFast(child) ?: callable.callOn(child)
}
val intValue = (value as? ObjInt)?.value ?: scope.raiseClassCastError("expected Int fill result")
result.setIntAtFast(i, intValue)
}
frame.storeObjResult(dst, result)
return
}
}
class CmdListFillIntCap(
internal val sizeSlot: Int,
internal val capacitySlot: 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 capacity = frame.getInt(capacitySlot).toInt()
val actualCapacity = maxOf(size, capacity)
if (actualCapacity < 0) frame.ensureScope().raiseIllegalArgument("list capacity must be non-negative")
val callable = frame.storedSlotObj(callableSlot)
val scope = frame.ensureScope()
val result = ObjList(LongArray(actualCapacity), size)
for (i in 0 until size) {
val args = Arguments(ObjInt.of(i.toLong()))
val value = if (callable is BytecodeLambdaCallable && callable.supportsImplicitIntFillFastPath()) {
callable.invokeImplicitIntArgFast(scope, i.toLong()) ?: callable.invokeImplicitIntArg(scope, i.toLong())
} else if (callable is BytecodeArgCallable) {
callable.callWithArgsFast(scope, args) ?: run {
val child = scope.createChildScope(scope.pos, args = args)
(callable as? BytecodeCallable)?.callOnFast(child) ?: callable.callOn(child)
}
} else {
val child = scope.createChildScope(scope.pos, args = args)
(callable as? BytecodeCallable)?.callOnFast(child) ?: callable.callOn(child)
}
val intValue = (value as? ObjInt)?.value ?: scope.raiseClassCastError("expected Int fill result")
result.setIntAtFast(i, intValue)
@ -3758,6 +3895,22 @@ class CmdListNewInt(
}
}
class CmdListNewIntCap(
internal val sizeSlot: Int,
internal val capacitySlot: 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 capacity = frame.getInt(capacitySlot).toInt()
val actualCapacity = maxOf(size, capacity)
if (actualCapacity < 0) frame.ensureScope().raiseIllegalArgument("list capacity must be non-negative")
frame.storeObjResult(dst, ObjList(LongArray(actualCapacity), size))
return
}
}
class CmdGetIndex(
internal val targetSlot: Int,
internal val indexSlot: Int,
@ -3862,6 +4015,7 @@ class CmdMakeLambda(internal val id: Int, internal val dst: Int) : Cmd() {
captureNames = lambdaConst.captureNames,
paramSlotPlan = lambdaConst.paramSlotPlan,
argsDeclaration = lambdaConst.argsDeclaration,
supportsDirectInvokeFastPath = lambdaConst.supportsDirectInvokeFastPath,
preferredThisType = lambdaConst.preferredThisType,
returnLabels = lambdaConst.returnLabels,
pos = lambdaConst.pos
@ -3883,10 +4037,18 @@ class BytecodeLambdaCallable(
private val captureNames: List<String>,
private val paramSlotPlan: Map<String, Int>,
private val argsDeclaration: ArgsDeclaration?,
private val supportsDirectInvokeFastPath: Boolean,
private val preferredThisType: String?,
private val returnLabels: Set<String>,
override val pos: Pos,
) : Statement(), BytecodeCallable {
) : Statement(), BytecodeCallable, BytecodeArgCallable {
private val slotPlanByName: Map<String, Int> by lazy(LazyThreadSafetyMode.NONE) { fn.localSlotPlanByName() }
private val declaredLocalNames: Set<String> by lazy(LazyThreadSafetyMode.NONE) {
fn.constants
.mapNotNull { it as? BytecodeConst.LocalDecl }
.mapTo(mutableSetOf()) { it.name }
}
private fun freezeRecord(record: ObjRecord): ObjRecord {
if (record.isMutable) return record
val raw = record.value as Obj?
@ -3918,6 +4080,7 @@ class BytecodeLambdaCallable(
captureNames = captureNames,
paramSlotPlan = paramSlotPlan,
argsDeclaration = argsDeclaration,
supportsDirectInvokeFastPath = supportsDirectInvokeFastPath,
preferredThisType = preferredThisType,
returnLabels = returnLabels,
pos = pos
@ -3934,6 +4097,7 @@ class BytecodeLambdaCallable(
captureNames = captureNames,
paramSlotPlan = paramSlotPlan,
argsDeclaration = argsDeclaration,
supportsDirectInvokeFastPath = supportsDirectInvokeFastPath,
preferredThisType = preferredThisType,
returnLabels = returnLabels,
pos = pos
@ -3942,32 +4106,29 @@ 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
fun supportsDirectInvokeFastPath(): Boolean = supportsDirectInvokeFastPath
private val fastPreboundLocalNames: Set<String> by lazy(LazyThreadSafetyMode.NONE) {
if (argsDeclaration == null) {
setOf("it")
} else {
argsDeclaration.params.mapTo(LinkedHashSet()) { it.name }
}
}
override suspend fun execute(scope: Scope): Obj {
val context = scope.applyClosureForBytecode(closureScope, preferredThisType).also {
it.args = scope.args
private val supportsFastUndeclaredLocalInit: Boolean by lazy(LazyThreadSafetyMode.NONE) {
canFastSeedUndeclaredLocals(fn, declaredLocalNames, fastPreboundLocalNames)
}
private fun supportsFastOnlyVm(arguments: Arguments): Boolean {
if (!supportsDirectInvokeFastPath || !fn.fastOnly) return false
if (!supportsFastUndeclaredLocalInit) return false
return argsDeclaration == null || argsDeclaration.supportsFastFrameBinding(arguments)
}
private fun buildContext(callScope: Scope, args: Arguments): Scope {
val context = callScope.applyClosureForBytecode(closureScope, preferredThisType).also {
it.args = args
}
if (captureRecords != null) {
context.captureRecords = captureRecords
@ -3975,17 +4136,10 @@ class BytecodeLambdaCallable(
} else if (captureNames.isNotEmpty()) {
closureScope.raiseIllegalState("bytecode lambda capture records missing")
}
if (argsDeclaration == null) {
// Bound in the bytecode entry binder.
} else {
// args bound into frame slots in the bytecode entry binder
return context
}
return try {
val declaredNames = fn.constants
.mapNotNull { it as? BytecodeConst.LocalDecl }
.mapTo(mutableSetOf()) { it.name }
val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, arguments ->
val slotPlan = fn.localSlotPlanByName()
private fun bindArgumentsFast(frame: CmdFrame, context: Scope, arguments: Arguments) {
if (argsDeclaration == null) {
val l = arguments.list
val itValue: Obj = when (l.size) {
@ -3993,7 +4147,7 @@ class BytecodeLambdaCallable(
1 -> l[0]
else -> ObjList(l.toMutableList())
}
val itSlot = slotPlan["it"]
val itSlot = slotPlanByName["it"]
if (itSlot != null) {
when (itValue) {
is ObjInt -> frame.frame.setInt(itSlot, itValue.value)
@ -4003,17 +4157,33 @@ class BytecodeLambdaCallable(
}
}
} else {
argsDeclaration.assignToFrame(
argsDeclaration.assignToFrameFast(
context,
arguments,
slotPlan,
slotPlanByName,
frame.frame
)
}
}
private suspend fun bindArguments(frame: CmdFrame, context: Scope, arguments: Arguments) {
if (argsDeclaration == null) {
bindArgumentsFast(frame, context, arguments)
} else {
argsDeclaration.assignToFrame(
context,
arguments,
slotPlanByName,
frame.frame
)
}
}
private suspend fun seedUndeclaredLocals(frame: CmdFrame, context: Scope) {
val localNames = frame.fn.localSlotNames
for (i in localNames.indices) {
val name = localNames[i] ?: continue
if (declaredNames.contains(name)) continue
if (declaredLocalNames.contains(name)) continue
val slotType = frame.getLocalSlotTypeCode(i)
if (slotType != SlotType.UNKNOWN.code && slotType != SlotType.OBJ.code) {
continue
@ -4039,11 +4209,88 @@ class BytecodeLambdaCallable(
frame.frame.setObj(i, value)
}
}
CmdVm().execute(fn, context, scope.args, binder)
suspend fun invokeImplicitIntArg(scope: Scope, arg: Long): Obj {
val context = buildContext(scope, Arguments.EMPTY)
val fastBinder: (CmdFrame, Arguments) -> Unit = { frame, _ ->
slotPlanByName["it"]?.let { itSlot ->
frame.frame.setInt(itSlot, arg)
}
}
return try {
val vm = CmdVm()
if (supportsFastOnlyVm(Arguments.EMPTY)) {
vm.executeFastOnly(fn, context, Arguments.EMPTY, fastBinder)
} else {
val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, _ ->
slotPlanByName["it"]?.let { itSlot ->
frame.frame.setInt(itSlot, arg)
}
}
vm.execute(fn, context, Arguments.EMPTY, binder)
}
} catch (e: ReturnException) {
if (e.label == null || returnLabels.contains(e.label)) e.result
else throw e
}
}
fun invokeImplicitIntArgFast(scope: Scope, arg: Long): Obj? {
if (!supportsFastOnlyVm(Arguments.EMPTY)) return null
val context = buildContext(scope, Arguments.EMPTY)
val binder: (CmdFrame, Arguments) -> Unit = { frame, _ ->
slotPlanByName["it"]?.let { itSlot ->
frame.frame.setInt(itSlot, arg)
}
}
return try {
CmdVm().executeFastOnlyNoSuspend(fn, context, Arguments.EMPTY, binder)
} catch (e: ReturnException) {
if (e.label == null || returnLabels.contains(e.label)) e.result else throw e
}
}
suspend fun invokeWithArgs(scope: Scope, args: Arguments): Obj {
val context = buildContext(scope, args)
return try {
val vm = CmdVm()
if (supportsFastOnlyVm(args)) {
val binder: (CmdFrame, Arguments) -> Unit = { frame, arguments ->
bindArgumentsFast(frame, context, arguments)
}
vm.executeFastOnly(fn, context, args, binder)
} else {
val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, arguments ->
bindArguments(frame, context, arguments)
seedUndeclaredLocals(frame, context)
}
vm.execute(fn, context, args, binder)
}
} catch (e: ReturnException) {
if (e.label == null || returnLabels.contains(e.label)) e.result else throw e
}
}
fun invokeWithArgsFast(scope: Scope, args: Arguments): Obj? {
if (!supportsFastOnlyVm(args)) return null
val context = buildContext(scope, args)
val binder: (CmdFrame, Arguments) -> Unit = { frame, arguments ->
bindArgumentsFast(frame, context, arguments)
}
return try {
CmdVm().executeFastOnlyNoSuspend(fn, context, args, binder)
} catch (e: ReturnException) {
if (e.label == null || returnLabels.contains(e.label)) e.result else throw e
}
}
override fun callWithArgsFast(scope: Scope, args: Arguments): Obj? = invokeWithArgsFast(scope, args)
override fun callOnFast(scope: Scope): Obj? = invokeWithArgsFast(scope, scope.args)
override suspend fun execute(scope: Scope): Obj {
return invokeWithArgs(scope, scope.args)
}
}
class CmdIterPush(internal val iterSlot: Int) : Cmd() {
@ -4492,6 +4739,19 @@ class CmdFrame(
return ExecutionError(errorObject, pos, message, t)
}
fun normalizeThrowableFast(t: Throwable): Throwable {
if (t is ExecutionError || t is ReturnException || t is LoopBreakContinueException) return t
val parentScope = ensureScope()
val pos = (t as? ScriptError)?.pos ?: currentErrorPos() ?: parentScope.pos
val throwScope = parentScope.createChildScope(pos = pos)
val message = when (t) {
is ScriptError -> t.errorMessage
else -> t.message ?: t.toString()
}
val errorObject = ObjUnknownException(throwScope, message)
return ExecutionError(errorObject, pos, message, t)
}
suspend fun handleException(t: Throwable): Boolean {
val handler = tryStack.lastOrNull() ?: return false
vmIterDebug {
@ -5446,6 +5706,14 @@ class CmdFrame(
if (index < target.slotCount) return index
return index
}
if (target.hasSlotPlanConflict(mapOf(name to index))) {
val record = target.getLocalRecordDirect(name)
?: target.localBindings[name]
?: target.parent?.get(name)
?: target.get(name)
?: ObjRecord(ObjUnset, isMutable = true)
return target.allocateSlotFor(name, record)
}
target.applySlotPlan(mapOf(name to index))
val existing = target.getLocalRecordDirect(name) ?: target.localBindings[name]
if (existing != null) {

View File

@ -190,6 +190,8 @@ enum class Opcode(val code: Int) {
GET_DYNAMIC_MEMBER(0xAC),
SET_DYNAMIC_MEMBER(0xAD),
CALL_DYNAMIC_MEMBER(0xAE),
LIST_NEW_INT_CAP(0xAF),
LIST_FILL_INT_CAP(0xB0),
RESOLVE_SCOPE_SLOT(0xB1),
LOAD_OBJ_ADDR(0xB2),

View File

@ -17,7 +17,58 @@
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Scope
import net.sergeych.lyng.FrameSlotRef
import net.sergeych.lyng.RecordSlotRef
import net.sergeych.lyng.ScopeSlotRef
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.ObjProperty
internal fun canFastSeedUndeclaredLocals(
fn: CmdFunction,
declaredLocalNames: Set<String>,
preboundLocalNames: Set<String>
): Boolean {
if (fn.localSlotNames.isEmpty()) return true
for (i in fn.localSlotNames.indices) {
val name = fn.localSlotNames[i] ?: continue
if (declaredLocalNames.contains(name)) continue
if (fn.localSlotCaptures.getOrNull(i) == true) continue
if (preboundLocalNames.contains(name)) continue
return false
}
return true
}
internal fun trySeedFrameLocalsFromScopeFast(frame: CmdFrame, scope: Scope): Boolean {
val localNames = frame.fn.localSlotNames
if (localNames.isEmpty()) return true
val base = frame.fn.scopeSlotCount
for (i in localNames.indices) {
val name = localNames[i] ?: continue
val slotType = frame.getLocalSlotTypeCode(i)
if (slotType != SlotType.UNKNOWN.code && slotType != SlotType.OBJ.code) continue
if (slotType == SlotType.OBJ.code && frame.frame.getRawObj(i) != null) continue
val record = scope.getLocalRecordDirect(name)
?: scope.chainLookupIgnoreClosure(name, followClosure = true)
?: continue
val value = when {
record.type == ObjRecord.Type.Delegated -> return false
record.type == ObjRecord.Type.Property -> return false
record.value is ObjProperty -> return false
else -> when (val direct = record.value) {
is FrameSlotRef -> direct.resolvedCaptureValueOrNull() ?: return false
is RecordSlotRef -> direct.resolvedCaptureValueOrNull() ?: return false
is ScopeSlotRef -> direct.resolvedCaptureValueOrNull() ?: return false
else -> direct
}
}
if (value is FrameSlotRef && value.refersTo(frame.frame, i)) {
continue
}
frame.setObjUnchecked(base + i, value)
}
return true
}
internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) {
val localNames = frame.fn.localSlotNames

View File

@ -99,10 +99,11 @@ fun ObjClass.addFnDoc(
visibility: Visibility = Visibility.Public,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
code: suspend ScopeFacade.() -> Obj
) {
// Register runtime method
addFn(name, isOpen, visibility, code = code)
addFn(name, isOpen, visibility, callSignature = callSignature, code = code)
// Register docs for the member under this class
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addFnDoc.className, doc = "") {
@ -137,9 +138,10 @@ fun ObjClass.addClassFnDoc(
isOpen: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
code: suspend ScopeFacade.() -> Obj
) {
addClassFn(name, isOpen, code)
addClassFn(name, isOpen, callSignature, code)
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addClassFnDoc.className, doc = "") {
method(name = name, doc = doc, params = params, returns = returns, isStatic = true, tags = tags)

View File

@ -31,6 +31,7 @@ class LambdaFnRef(
val captureEntries: List<LambdaCaptureEntry>,
val inferredReturnClass: ObjClass?,
val inlineBodyRef: ObjRef?,
val supportsDirectInvokeFastPath: Boolean,
val preferredThisType: String?,
val wrapAsExtensionCallable: Boolean,
val returnLabels: Set<String>,

View File

@ -24,6 +24,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.serializer
import net.sergeych.lyng.BytecodeCallable
import net.sergeych.lyng.*
import net.sergeych.lyng.InteropOperator
import net.sergeych.lyng.OperatorInteropRegistry
@ -723,40 +724,39 @@ open class Obj {
scope.raiseNotImplemented()
}
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj {
val usePool = PerfFlags.SCOPE_POOL && this !is Statement
private suspend fun invokeWithBoundScope(
scope: Scope,
args: Arguments,
thisObj: Obj,
declaringClass: ObjClass? = null,
atPos: Pos = scope.pos
): Obj {
val usePool = PerfFlags.SCOPE_POOL && this !is Statement && atPos == scope.pos
return if (usePool) {
scope.withChildFrame(args, newThisObj = thisObj) { child ->
if (declaringClass != null) child.currentClassCtx = declaringClass
callOn(child)
(this as? BytecodeCallable)?.callOnFast(child) ?: callOn(child)
}
} else {
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj).also {
val child = scope.createChildScope(atPos, args = args, newThisObj = thisObj).also {
if (declaringClass != null) it.currentClassCtx = declaringClass
})
}
(this as? BytecodeCallable)?.callOnFast(child) ?: callOn(child)
}
}
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj {
return invokeWithBoundScope(scope, args, thisObj, declaringClass)
}
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
callOn(
scope.createChildScope(
scope.pos,
args = Arguments(args.toList()),
newThisObj = thisObj
)
)
invokeWithBoundScope(scope, Arguments(args.toList()), thisObj)
suspend fun invoke(scope: Scope, thisObj: Obj): Obj =
callOn(
scope.createChildScope(
scope.pos,
args = Arguments.EMPTY,
newThisObj = thisObj
)
)
invokeWithBoundScope(scope, Arguments.EMPTY, thisObj)
suspend fun invoke(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
callOn(scope.createChildScope(atPos, args = args, newThisObj = thisObj))
invokeWithBoundScope(scope, args, thisObj, atPos = atPos)
val asReadonly: ObjRecord by lazy { ObjRecord(this, false) }
@ -828,7 +828,13 @@ open class Obj {
name = "let",
doc = "Calls the specified function block with `this` value as its argument and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.UNARY_ARGUMENT,
result = CallSignature.ResultMode.BLOCK_RESULT
)
)
) {
call(args.firstAndOnly(), Arguments(thisObj))
}
@ -836,7 +842,13 @@ open class Obj {
name = "apply",
doc = "Calls the specified function block with `this` value as its receiver and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.RECEIVER,
result = CallSignature.ResultMode.RETURN_RECEIVER
)
)
) {
val body = args.firstAndOnly()
val scope = requireScope()
@ -852,7 +864,13 @@ open class Obj {
name = "also",
doc = "Calls the specified function block with `this` value as its argument and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.UNARY_ARGUMENT,
result = CallSignature.ResultMode.RETURN_RECEIVER
)
)
) {
call(args.firstAndOnly(), Arguments(thisObj))
thisObj
@ -861,7 +879,13 @@ open class Obj {
name = "run",
doc = "Calls the specified function block with `this` value as its receiver and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.RECEIVER,
result = CallSignature.ResultMode.BLOCK_RESULT
)
)
) {
call(args.firstAndOnly())
}

View File

@ -809,7 +809,8 @@ open class ObjClass(
if (initStmt is net.sergeych.lyng.Statement) {
executeBytecodeWithSeed(instance.instanceScope, initStmt, "instance init")
} else {
initStmt.callOn(instance.instanceScope)
(initStmt as? net.sergeych.lyng.BytecodeCallable)?.callOnFast(instance.instanceScope)
?: initStmt.callOn(instance.instanceScope)
}
}
} finally {
@ -821,13 +822,14 @@ open class ObjClass(
c.instanceConstructor?.let { ctor ->
val execScope =
instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
ctor.callOn(execScope)
(ctor as? net.sergeych.lyng.BytecodeCallable)?.callOnFast(execScope) ?: ctor.callOn(execScope)
}
}
}
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
return callOn(scope.createChildScope(Arguments(*plainArgs)))
val child = scope.createChildScope(Arguments(*plainArgs))
return (this as? net.sergeych.lyng.BytecodeCallable)?.callOnFast(child) ?: callOn(child)
}
@ -847,6 +849,7 @@ open class ObjClass(
fieldId: Int? = null,
methodId: Int? = null,
typeDecl: net.sergeych.lyng.TypeDecl? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
): ObjRecord {
// Validation of override rules: only for non-system declarations
var existing: ObjRecord? = null
@ -947,6 +950,7 @@ open class ObjClass(
isOverride = isOverride,
isTransient = isTransient,
type = type,
callSignature = callSignature,
typeDecl = typeDecl,
memberName = name,
fieldId = effectiveFieldId,
@ -973,7 +977,8 @@ open class ObjClass(
isTransient: Boolean = false,
type: ObjRecord.Type = ObjRecord.Type.Field,
fieldId: Int? = null,
methodId: Int? = null
methodId: Int? = null,
callSignature: net.sergeych.lyng.CallSignature? = null
): ObjRecord {
initClassScope()
val existing = classScope!!.objects[name]
@ -1014,6 +1019,7 @@ open class ObjClass(
writeVisibility,
recordType = type,
isTransient = isTransient,
callSignature = callSignature,
fieldId = effectiveFieldId,
methodId = effectiveMethodId
)
@ -1033,6 +1039,7 @@ open class ObjClass(
isOverride: Boolean = false,
pos: Pos = Pos.builtIn,
methodId: Int? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
code: (suspend net.sergeych.lyng.ScopeFacade.() -> Obj)? = null
) {
val stmt = code?.let { ObjExternCallable.fromBridge { it() } } ?: ObjNull
@ -1040,7 +1047,8 @@ open class ObjClass(
name, stmt, isMutable, visibility, writeVisibility, pos, declaringClass,
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
type = ObjRecord.Type.Fun,
methodId = methodId
methodId = methodId,
callSignature = callSignature
)
}
@ -1072,8 +1080,19 @@ open class ObjClass(
}
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend net.sergeych.lyng.ScopeFacade.() -> Obj) {
createClassField(name, ObjExternCallable.fromBridge { code() }, isOpen, type = ObjRecord.Type.Fun)
fun addClassFn(
name: String,
isOpen: Boolean = false,
callSignature: net.sergeych.lyng.CallSignature? = null,
code: suspend net.sergeych.lyng.ScopeFacade.() -> Obj
) {
createClassField(
name,
ObjExternCallable.fromBridge { code() },
isOpen,
type = ObjRecord.Type.Fun,
callSignature = callSignature
)
}

View File

@ -142,7 +142,7 @@ object ObjDecimalSupport {
}
val child = requireScope().createChildScope()
child.addConst(decimalContextVar, context)
block.callOn(child)
(block as? net.sergeych.lyng.BytecodeCallable)?.callOnFast(child) ?: block.callOn(child)
}
registerBuiltinConversions(decimalClass)
registerInterop(decimalClass)

View File

@ -64,13 +64,19 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
return (callback as? BytecodeLambdaCallable)?.rebindClosure(context) ?: callback
}
private suspend fun callCallback(callback: Obj, child: Scope): Obj {
return (callback as? net.sergeych.lyng.BytecodeCallable)?.callOnFast(child) ?: callback.callOn(child)
}
/**
* Use read callback to dynamically resolve the field name. Note that it does not work
* with method invocation which is implemented separately in [invokeInstanceMethod] below.
*/
override suspend fun readField(scope: Scope, name: String): ObjRecord {
val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
return readCallback?.callOn(execBase.createChildScope(Arguments(ObjString(name))))?.let {
return readCallback?.let { callback ->
callCallback(callback, execBase.createChildScope(Arguments(ObjString(name))))
}?.let {
if (writeCallback != null)
it.asMutable
else
@ -90,26 +96,34 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
onNotFoundResult: (suspend () -> Obj?)?
): Obj {
val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
val over = readCallback?.callOn(execBase.createChildScope(Arguments(ObjString(name))))
val over = readCallback?.let { callback ->
callCallback(callback, execBase.createChildScope(Arguments(ObjString(name))))
}
return over?.invoke(scope, scope.thisObj, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
writeCallback?.callOn(execBase.createChildScope(Arguments(ObjString(name), newValue)))
writeCallback?.let { callback ->
callCallback(callback, execBase.createChildScope(Arguments(ObjString(name), newValue)))
}
?: super.writeField(scope, name, newValue)
}
override suspend fun getAt(scope: Scope, index: Obj): Obj {
val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
return readCallback?.callOn(execBase.createChildScope(Arguments(index)))
return readCallback?.let { callback ->
callCallback(callback, execBase.createChildScope(Arguments(index)))
}
?: super.getAt(scope, index)
}
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
writeCallback?.callOn(execBase.createChildScope(Arguments(index, newValue)))
writeCallback?.let { callback ->
callCallback(callback, execBase.createChildScope(Arguments(index, newValue)))
}
?: super.putAt(scope, index, newValue)
}
@ -124,7 +138,7 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
// Snapshot the caller scope to capture locals/args even if the runtime pools/reuses frames.
// Module scope should stay late-bound to allow extern class rebinding and similar updates.
delegate.builderScope = if (scope is net.sergeych.lyng.ModuleScope) null else scope.snapshotForClosure()
builder.callOn(buildScope)
(builder as? net.sergeych.lyng.BytecodeCallable)?.callOnFast(buildScope) ?: builder.callOn(buildScope)
return delegate
}

View File

@ -395,8 +395,17 @@ suspend fun Obj.getLyngExceptionMessageWithStackTrace(scope: Scope? = null,showD
var at = "unknown"
val stack = if (!trace.list.isEmpty()) {
val first = trace.list[0]
at = (first.readField(s, "at").value as ObjString).value
"\n" + trace.list.map { " at " + it.toString(s).value }.joinToString("\n")
suspend fun formatTraceEntry(entry: Obj): String {
return when (entry) {
is ObjString -> entry.value.removePrefix("#")
else -> entry.toString(s).value
}
}
at = when (first) {
is ObjString -> formatTraceEntry(first)
else -> (first.readField(s, "at").value as ObjString).value
}
"\n" + trace.list.map { " at " + formatTraceEntry(it) }.joinToString("\n")
} else {
val pos = s.pos
if (pos.source.fileName.isNotEmpty() && pos.currentLine.isNotEmpty()) {

View File

@ -81,7 +81,7 @@ private suspend fun createLyngFlowInput(scope: Scope, producer: Obj, ownerSessio
val runProducer: suspend CoroutineScope.() -> Unit = {
var failure: Throwable? = null
try {
producer.callOn(builderScope)
(producer as? net.sergeych.lyng.BytecodeCallable)?.callOnFast(builderScope) ?: producer.callOn(builderScope)
} catch (x: ScriptFlowIsNoMoreCollected) {
// premature flow closing, OK
} catch (x: Throwable) {

View File

@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.CallSignature
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
@ -189,7 +190,13 @@ val ObjIterable by lazy {
doc = "Build a map from elements using the lambda result as key.",
params = listOf(ParamDoc("keySelector")),
returns = type("lyng.Map"),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.ASSOCIATE_BY
)
)
) {
val association = requireOnlyArg<Obj>()
val result = ObjMap()
@ -204,7 +211,13 @@ val ObjIterable by lazy {
doc = "Apply the lambda to each element in iteration order.",
params = listOf(ParamDoc("action")),
isOpen = true,
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.FOR_EACH
)
)
) {
val scope = requireScope()
val it = thisObj.invokeInstanceMethod(scope, "iterator")
@ -222,7 +235,13 @@ val ObjIterable by lazy {
params = listOf(ParamDoc("transform")),
returns = type("lyng.List"),
isOpen = true,
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.MAP
)
)
) {
val fn = requiredArg<Obj>(0)
val result = mutableListOf<Obj>()
@ -238,7 +257,13 @@ val ObjIterable by lazy {
params = listOf(ParamDoc("transform")),
returns = type("lyng.List"),
isOpen = true,
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.MAP_NOT_NULL
)
)
) {
val fn = requiredArg<Obj>(0)
val result = mutableListOf<Obj>()

View File

@ -165,9 +165,9 @@ open class ObjList(initialList: MutableList<Obj> = mutableListOf()) : Obj() {
}
}
internal constructor(intValues: LongArray) : this(mutableListOf()) {
internal constructor(intValues: LongArray, size: Int = intValues.size) : this(mutableListOf()) {
primitiveIntList = intValues
primitiveIntSize = intValues.size
primitiveIntSize = size
boxedList = null
}
@ -519,8 +519,8 @@ open class ObjList(initialList: MutableList<Obj> = mutableListOf()) : Obj() {
doc = "Append one or more elements to the end of this list.",
moduleName = "lyng.stdlib"
) {
val l = thisAs<ObjList>().list
for (a in args) l.add(a)
val l = thisAs<ObjList>()
for (a in args) l.appendFast(a)
ObjVoid
}
addFnDoc(

View File

@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.CallSignature
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.*
import net.sergeych.lynon.LynonDecoder
@ -262,7 +263,15 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
doc = "Get value by key or compute, store, and return the default from a lambda.",
params = listOf(ParamDoc("key"), ParamDoc("default")),
returns = type("lyng.Any"),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.MAP_GET_OR_PUT,
result = CallSignature.ResultMode.BLOCK_RESULT,
argCount = 2,
lambdaArgIndex = 1
)
)
) {
val key = requiredArg<Obj>(0)
thisAs<ObjMap>().map.getOrPut(key) {

View File

@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.BytecodeCallable
import net.sergeych.lyng.BytecodeBodyProvider
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
@ -41,14 +42,16 @@ class ObjProperty(
val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope)
val execScope = scope.applyClosure(instanceScope).createChildScope(newThisObj = instance)
execScope.currentClassCtx = declaringClass
(g as? BytecodeCallable)?.callOnFast(execScope)?.let { return it }
return when (g) {
is BytecodeStatement -> executeBytecodeWithSeed(execScope, g, "property getter")
is BytecodeBodyProvider -> {
val body = g.bytecodeBody()
if (body != null) executeBytecodeWithSeed(execScope, body, "property getter") else g.callOn(execScope)
if (body != null) executeBytecodeWithSeed(execScope, body, "property getter")
else (g as? BytecodeCallable)?.callOnFast(execScope) ?: g.callOn(execScope)
}
is Statement -> g.callOn(execScope)
else -> g.callOn(execScope)
is Statement -> (g as? BytecodeCallable)?.callOnFast(execScope) ?: g.callOn(execScope)
else -> (g as? BytecodeCallable)?.callOnFast(execScope) ?: g.callOn(execScope)
}
}
@ -59,14 +62,16 @@ class ObjProperty(
val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope)
val execScope = scope.applyClosure(instanceScope).createChildScope(args = Arguments(value), newThisObj = instance)
execScope.currentClassCtx = declaringClass
(s as? BytecodeCallable)?.callOnFast(execScope)?.let { return }
when (s) {
is BytecodeStatement -> executeBytecodeWithSeed(execScope, s, "property setter")
is BytecodeBodyProvider -> {
val body = s.bytecodeBody()
if (body != null) executeBytecodeWithSeed(execScope, body, "property setter") else s.callOn(execScope)
if (body != null) executeBytecodeWithSeed(execScope, body, "property setter")
else (s as? BytecodeCallable)?.callOnFast(execScope) ?: s.callOn(execScope)
}
is Statement -> s.callOn(execScope)
else -> s.callOn(execScope)
is Statement -> (s as? BytecodeCallable)?.callOnFast(execScope) ?: s.callOn(execScope)
else -> (s as? BytecodeCallable)?.callOnFast(execScope) ?: s.callOn(execScope)
}
}

View File

@ -38,17 +38,26 @@ class ImportManager(
val packageNames: List<String> get() = imports.keys.toList()
private class CacheCell(var scope: ModuleScope? = null)
private inner class Entry(
val packageName: String,
val builder: suspend (ModuleScope) -> Unit,
var cachedScope: ModuleScope? = null
val cacheCell: CacheCell = CacheCell()
) {
suspend fun getScope(pos: Pos): ModuleScope {
cachedScope?.let { return it }
return ModuleScope(inner, pos, packageName).apply {
cachedScope = this
builder(this)
cacheCell.scope?.let { return it }
val module = ModuleScope(inner, pos, packageName)
cacheCell.scope = module
return try {
builder(module)
module
} catch (e: Throwable) {
if (cacheCell.scope === module) {
cacheCell.scope = null
}
throw e
}
}
}
@ -152,14 +161,14 @@ class ImportManager(
op.withLock {
ImportManager(rootScope, securityManager).apply {
for ((name, entry) in this@ImportManager.imports) {
imports[name] = Entry(entry.packageName, entry.builder, entry.cachedScope)
imports[name] = Entry(entry.packageName, entry.builder, entry.cacheCell)
}
}
}
fun invalidatePackageCache(name: String) {
op.withLock {
imports[name]?.cachedScope = null
imports[name]?.cacheCell?.scope = null
}
}

View File

@ -58,7 +58,10 @@ abstract class Statement(
val type = ObjClass("Callable")
}
suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.createChildScope(args = Arguments(*args)))
suspend fun call(scope: Scope, vararg args: Obj): Obj {
val child = scope.createChildScope(args = Arguments(*args))
return (this as? BytecodeCallable)?.callOnFast(child) ?: execute(child)
}
protected fun bytecodeOnly(scope: Scope, label: String): Nothing {
return scope.raiseIllegalState("bytecode-only execution is required; $label needs compiled bytecode")

View File

@ -213,6 +213,24 @@ class BytecodeRecentOpsTest {
assertEquals(4, scope.eval("calc()").toInt())
}
@Test
fun listFillWithCapacityUsesPrimitiveCapacityBytecode() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
val xs = List.fill(5, 12) { it * 2 }
xs.add(99)
xs[0] + xs[4] + xs[5]
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("LIST_NEW_INT_CAP"), disasm)
assertFalse(disasm.contains("LIST_FILL_INT_CAP"), disasm)
assertEquals(107, scope.eval("calc()").toInt())
}
@Test
fun directLambdaLiteralCallUsesInlineBytecode() = runTest {
val scope = Script.newScope()
@ -229,6 +247,21 @@ class BytecodeRecentOpsTest {
assertEquals(11, scope.eval("calc()").toInt())
}
@Test
fun capturedLambdaCanCallListFillOnCapturedClassReceiver() = runTest {
val scope = Script.newScope()
val result = scope.eval(
"""
fun calc(n: Int) {
val xs = { List.fill(n) { it } }()
xs[4]
}
calc(5)
""".trimIndent()
)
assertEquals(4, result.toInt())
}
@Test
fun directLambdaLiteralCallWithCaptureUsesInlineBytecode() = runTest {
val scope = Script.newScope()
@ -346,6 +379,106 @@ class BytecodeRecentOpsTest {
assertEquals(12, scope.eval("calc()").toInt())
}
@Test
fun nestedInlineLambdaParamCallAvoidsCallSlot() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
{ g -> g(10) }({ x -> x + 1 })
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(11, scope.eval("calc()").toInt())
}
@Test
fun optionalExactLambdaCallUsesInlineBytecode() = runTest {
val scope = Script.newScope()
scope.eval(
"""
type IntFn = (Int)->Int
fun calc() {
val f: IntFn? = { 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 conditionalExactLambdaCallUsesInlineBytecode() = runTest {
val scope = Script.newScope()
scope.eval(
"""
val base = { x -> x + 1 }
fun calc(flag: Bool) {
(if(flag) base else base)(10)
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(11, scope.eval("calc(true)").toInt())
}
@Test
fun elvisExactLambdaCallUsesInlineBytecode() = runTest {
val scope = Script.newScope()
scope.eval(
"""
val base = { x -> x + 1 }
fun calc() {
(null ?: base)(10)
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(11, scope.eval("calc()").toInt())
}
@Test
fun castExactLambdaCallUsesInlineBytecode() = runTest {
val scope = Script.newScope()
scope.eval(
"""
type IntFn = (Int)->Int
val base: IntFn = { x -> x + 1 }
fun calc() {
(base as IntFn)(10)
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(11, scope.eval("calc()").toInt())
}
@Test
fun whenExactLambdaCallUsesInlineBytecode() = runTest {
val scope = Script.newScope()
scope.eval(
"""
val base = { x -> x + 1 }
fun calc(flag: Bool) {
(when(flag) {
true -> base
else -> base
})(10)
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(11, scope.eval("calc(true)").toInt())
}
@Test
fun letLiteralUsesInlineBytecode() = runTest {
val scope = Script.newScope()
@ -501,6 +634,166 @@ class BytecodeRecentOpsTest {
assertEquals(6, scope.eval("calc()").toInt())
}
@Test
fun mapLiteralUsesDirectConstructorCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
val m = { a: 1, b: 2 }
m.size
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(2, scope.eval("calc()").toInt())
}
@Test
fun mapEntryLiteralUsesDirectConstructorCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
val e = "a" => 2
e.value
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(2, scope.eval("calc()").toInt())
}
@Test
fun constructorNameUsesDirectCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
Map().size
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(0, scope.eval("calc()").toInt())
}
@Test
fun constructorAliasUsesDirectCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
val ctor = Map
val m = ctor() as Map
m.size
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(0, scope.eval("calc()").toInt())
}
@Test
fun ifExpressionConstructorAliasUsesDirectCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc(flag: Bool) {
val ctor = if(flag) Map else Map
val m = ctor() as Map
m.size
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(0, scope.eval("calc(true)").toInt())
}
@Test
fun elvisConstructorAliasUsesDirectCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
val ctor = null ?: Map
val m = ctor() as Map
m.size
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(0, scope.eval("calc()").toInt())
}
@Test
fun whenConstructorAliasUsesDirectCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc(flag: Bool) {
val ctor = when(flag) {
true -> Map
else -> Map
}
val m = ctor() as Map
m.size
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(0, scope.eval("calc(true)").toInt())
}
@Test
fun localNamedFunctionUsesDirectCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
fun twice(x: Int) { x * 2 }
twice(3)
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(6, scope.eval("calc()").toInt())
}
@Test
fun localNamedFunctionAliasUsesDirectCall() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun calc() {
fun twice(x: Int) { x * 2 }
val f = twice
f(3)
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("CALL_DIRECT"), disasm)
assertFalse(disasm.contains("CALL_SLOT"), disasm)
assertEquals(6, scope.eval("calc()").toInt())
}
@Test
fun optionalIndexPreIncSkipsOnNullReceiver() = runTest {
eval(

View File

@ -16,11 +16,17 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source
import net.sergeych.lyng.Statement
import net.sergeych.lyng.asFacade
import net.sergeych.lyng.obj.ObjDynamic
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.toInt
import net.sergeych.lyng.pacman.ImportManager
@ -68,6 +74,235 @@ class CompilerVmReviewRegressionTest {
assertContains(ex.errorMessage, "module binding 'answer'")
}
@Test
fun facadeCallUsesPreparedLambdaWithArgs() = runTest {
val lambda = Compiler.compile(
Source(
"<facade-call-lambda>",
"""
val base = 2
{ x -> x + base }
""".trimIndent()
),
Script.defaultImportManager
)
val scope = Script.newScope()
val callable = lambda.execute(scope)
assertEquals(42, scope.asFacade().call(callable, Arguments(ObjInt.of(40))).toInt())
}
@Test
fun genericInvokeHelpersUsePreparedLambdaEntryPoints() = runTest {
val unaryLambda = Compiler.compile(
Source(
"<generic-invoke-unary>",
"""
val delta = 2
{ x -> x + delta }
""".trimIndent()
),
Script.defaultImportManager
)
val nullaryLambda = Compiler.compile(
Source(
"<generic-invoke-nullary>",
"""
val base = 7
{ base }
""".trimIndent()
),
Script.defaultImportManager
)
val unaryScope = Script.newScope()
val nullaryScope = Script.newScope()
val unaryCallable = unaryLambda.execute(unaryScope)
val nullaryCallable = nullaryLambda.execute(nullaryScope)
assertEquals(42, unaryCallable.invoke(unaryScope, ObjString("receiver"), ObjInt.of(40)).toInt())
assertEquals(7, nullaryCallable.invoke(nullaryScope, ObjString("receiver")).toInt())
assertEquals(
42,
unaryCallable.invoke(
unaryScope,
Pos(Source("<generic-invoke-pos>", ""), 0, 0),
ObjString("receiver"),
Arguments(ObjInt.of(40))
).toInt()
)
}
@Test
fun statementCallUsesPreparedLambdaFastPath() = runTest {
val unaryLambda = Compiler.compile(
Source(
"<statement-call-unary>",
"""
val delta = 2
{ x -> x + delta }
""".trimIndent()
),
Script.defaultImportManager
)
val scope = Script.newScope()
val callable = unaryLambda.execute(scope) as Statement
assertEquals(42, callable.call(scope, ObjInt.of(40)).toInt())
}
@Test
fun preparedLambdaKeepsImmutableModuleCaptureAcrossOtherScriptsInSameScope() = runTest {
val unaryLambda = Compiler.compile(
Source(
"<cross-script-capture-unary>",
"""
val delta = 2
{ x -> x + delta }
""".trimIndent()
),
Script.defaultImportManager
)
val unrelatedScript = Compiler.compile(
Source(
"<cross-script-capture-unrelated>",
"""
val base = 7
base
""".trimIndent()
),
Script.defaultImportManager
)
val scope = Script.newScope()
val callable = unaryLambda.execute(scope) as Statement
unrelatedScript.execute(scope)
assertEquals(42, callable.call(scope, ObjInt.of(40)).toInt())
assertEquals(42, scope.asFacade().call(callable, Arguments(ObjInt.of(40))).toInt())
}
@Test
fun dynamicCallbacksUsePreparedLambdaFastPath() = runTest {
val script = Compiler.compile(
Source(
"<dynamic-fast-callbacks>",
"""
var seen = ""
dynamic {
get { name -> name + seen }
set { name, value -> seen = name + "=" + value }
}
""".trimIndent()
),
Script.defaultImportManager
)
val scope = Script.newScope()
val dynamic = script.execute(scope) as ObjDynamic
assertEquals("foo", (dynamic.readField(scope, "foo").value as ObjString).value)
dynamic.writeField(scope, "foo", ObjInt.of(7))
assertEquals("barfoo=7", (dynamic.getAt(scope, ObjString("bar")) as ObjString).value)
}
@Test
fun higherOrderMethodInliningSupportsCapturedValues() = runTest {
val script = Compiler.compile(
Source(
"<higher-order-inline-captures>",
"""
val suffix = "!"
val offset = 10
var sum = 0
val letResult = "a".let { it + suffix }
val applyResult = List<Int>().apply { add(offset); add(offset + 1) }
val mapped = [1, 2, 3].map { it + offset }
val filtered = [1, 2, 3].filter { it + offset >= 12 }
val notNull = [1, 2, 3].mapNotNull { if (it + offset >= 12) it + offset else null }
val associated = [1, 2, 3].associateBy { "k" + (it + offset) }
[1, 2, 3].forEach { sum += it + offset }
[letResult, applyResult, mapped, filtered, notNull, associated, sum]
""".trimIndent()
),
Script.defaultImportManager
)
val scope = Script.newScope()
val result = script.execute(scope) as ObjList
assertEquals("a!", (result.list[0] as ObjString).value)
val applied = result.list[1] as ObjList
assertEquals(listOf(10, 11), applied.list.map { it.toInt() })
val mapped = result.list[2] as ObjList
assertEquals(listOf(11, 12, 13), mapped.list.map { it.toInt() })
val filtered = result.list[3] as ObjList
assertEquals(listOf(2, 3), filtered.list.map { it.toInt() })
val notNull = result.list[4] as ObjList
assertEquals(listOf(12, 13), notNull.list.map { it.toInt() })
val associated = result.list[5].toString(scope).value
assertContains(associated, "\"k11\" => 1")
assertContains(associated, "\"k12\" => 2")
assertContains(associated, "\"k13\" => 3")
assertEquals(36, result.list[6].toInt())
}
@Test
fun directLambdaInliningMatchesImplicitItInvocationSemantics() = runTest {
val script = Compiler.compile(
Source(
"<direct-inline-it-semantics>",
"""
val zeroFn = { if (it == void) 1 else 0 }
val multiFn = { it }
val zero = zeroFn()
val multi = multiFn(1, 2, 3)
[zero, multi]
""".trimIndent()
),
Script.defaultImportManager
)
val scope = Script.newScope()
val result = script.execute(scope) as ObjList
assertEquals(1, result.list[0].toInt())
val multi = result.list[1] as ObjList
assertEquals(listOf(1, 2, 3), multi.list.map { it.toInt() })
}
@Test
fun mapGetOrPutUsesInlineDefaultLambda() = runTest {
val script = Compiler.compile(
Source(
"<map-get-or-put-inline>",
"""
val offset = 10
val m = Map()
val first = m.getOrPut("k") { offset + 1 }
val second = m.getOrPut("k") { offset + 2 }
[first, second, m["k"]]
""".trimIndent()
),
Script.defaultImportManager
)
val scope = Script.newScope()
val result = script.execute(scope) as ObjList
assertEquals(11, result.list[0].toInt())
assertEquals(11, result.list[1].toInt())
assertEquals(11, result.list[2].toInt())
}
@Test
fun subjectlessWhenReportsScriptError() = runTest {
val ex = assertFailsWith<ScriptError> {

View File

@ -17,10 +17,12 @@
package net.sergeych.lyng
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.obj.toInt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource
class OptTest {
@ -40,9 +42,11 @@ class OptTest {
repeat(3) { pass ->
val size = scope.eval("buildArray(200000)").toInt()
assertEquals(200000, size, "warmup pass ${pass + 1} failed")
delay(100)
}
val passes = 3
val passes = 4
var bestMs = Long.MAX_VALUE
var totalMs = 0L
repeat(passes) { pass ->
@ -56,4 +60,25 @@ class OptTest {
}
println("add-to-array best=${bestMs}ms avg=${totalMs / passes}ms after warmup")
}
@Test
fun testAddToArray2() = runTest {
eval(
$$"""
import lyng.time
val n = 700_000
fun tm<T>(block: ()->T): T {
val t = Instant()
block().also {
println("tm: ${Instant() - t}")
}
}
val x = tm { List.fill(n) { it * 10 + 1 } }
val y = tm { List.fill(n, n + 10) { it * 10 + 1 } }
tm { x.add(-1) }
tm { y.add(-2) }
""".trimIndent()
)
}
}

View File

@ -13,6 +13,11 @@ Current focus
Key recent changes
- Updated AI helper docs to reflect static typing, type expressions, and compile-time-only name resolution.
- Added stdlib random API: `Random` and deterministic `SeededRandom` with `nextInt`, `nextFloat`, and generic `next(range)`.
- Generalized primitive list optimization for compiler-generated `List.fill`:
- `List.fill(size) { intExpr }` and `List.fill(size, capacity) { intExpr }` now both have bytecode fast paths.
- Added `LIST_NEW_INT_CAP` / `LIST_FILL_INT_CAP` for the 3-arg capacity-preserving form.
- Fixed `ObjList.add(...)` to preserve primitive-int backing storage instead of forcing boxing through `.list`.
- `OptTest.testAddToArray2` no longer shows the old 10x anomaly for `List.fill(n, n + 10)` or append-to-extended-list.
Known failing tests
- None in :lynglib:jvmTest after Random/SeededRandom integration.

View File

@ -22,18 +22,25 @@ Candidates (not started)
6) Box/unbox audit (done)
- Unbox ObjInt/ObjReal in assign-op when target is INT/REAL to avoid boxing + obj ops.
- MixedCompareBenchmarkTest: 240 ms -> 234 ms.
7) Mixed compare coverage
7) Primitive list fill with capacity (done)
- Extended the compiler/runtime fast path from `List.fill(size) { intExpr }` to `List.fill(size, capacity) { intExpr }`.
- Added `LIST_NEW_INT_CAP` and `LIST_FILL_INT_CAP` so the 3-arg form keeps primitive-int storage instead of falling back to generic stdlib code.
- `OptTest.testAddToArray2`: `List.fill(n, n + 10) { ... }` dropped from the prior anomaly (~10x slower than 2-arg fill) to the same range as `List.fill(n) { ... }`, roughly `56-67 ms` vs `46-75 ms` after warmup.
8) Primitive list append preservation (done)
- Fixed `ObjList.add(...)` to append through the primitive-aware fast path instead of forcing `.list` and boxing the backing storage.
- `OptTest.testAddToArray2`: appending to the pre-extended list dropped from the prior anomaly (~10x slower) to sub-millisecond / low-millisecond timings (`~0.05-0.16 ms` for the extended list path, `~1.6-4.3 ms` for the baseline path, depending on warmup).
9) Mixed compare coverage
- Emit CMP_*_REAL when one operand is known ObjReal in more expression forms (not just assign-op).
- Verify with disassembly that fast cmp opcodes are emitted.
8) Range-loop invariant hoist
10) Range-loop invariant hoist
- Cache range end/step into temps once per loop; avoid repeated slot reads/boxing in body.
- Confirm no extra CONST_OBJ in hot path.
9) Boxing elision pass
11) Boxing elision pass
- Remove redundant BOX_OBJ when value feeds only primitive ops afterward (local liveness).
- Ensure no impact on closures/escaping values.
10) Closed-type fast paths expansion
12) Closed-type fast paths expansion
- Apply closed-type trust for ObjBool/ObjInt/ObjReal/ObjString in ternaries and conditional chains.
- Guard with exact non-null temp/slot checks only.
11) VM hot op micro-optimizations
13) VM hot op micro-optimizations
- Reduce frame reads/writes in ADD_INT, MUL_REAL, CMP_*_INT/REAL when operands are temps.
- Compare against baseline; revert if regression after 10-run median.

View File

@ -0,0 +1,146 @@
# Non-Suspending Call Optimization Plan
## Current state
Completed in the current phase:
- Higher-order lambda inlining is now metadata-driven through `CallSignature`.
- Built-in member methods (`let`, `also`, `apply`, `run`, `forEach`, `map`, `mapNotNull`, `associateBy`, `getOrPut`) publish inline metadata at declaration sites.
- Lyng extension wrappers now preserve and expose `callSignature`, so extension methods such as `Iterable.filter` use the same inlining path as built-in members.
- `BytecodeCompiler` no longer relies on a backend hardcoded name table for these higher-order inlining cases.
- JVM tests are green after the metadata move.
Primary motivation remains unchanged: suspend call overhead is still significant, and lambda inlining only removes part of it.
## Constraints
- Keep source positions, stack traces, and throw-site reporting correct.
- Do not reintroduce one-off special cases tied to specific stdlib method names.
- Prefer declaration metadata and reusable compiler/runtime mechanisms.
- Preserve Kotlin Multiplatform compatibility in `commonMain`.
- Avoid changing public language semantics just to optimize the runtime path.
## Why this is the next step
Lambda inlining helps when the callee body is directly available at the call site.
The next large remaining cost is calling compiled functions through suspend entry points even when the generated body never suspends.
That suggests a second optimization track:
1. detect bytecode callables that are safe to execute through a non-suspending fast path;
2. route direct calls to that path when the caller can prove it is safe;
3. keep the suspend path as the fallback for correctness.
## Proposed phases
### Phase 1: Define "non-suspending compiled callable"
Add explicit metadata on compiled functions / lambdas indicating whether their bytecode body may suspend.
Requirements:
- Computed once during bytecode generation.
- Conservative: false negatives are acceptable; false positives are not.
- Must account for:
- direct suspend-capable call opcodes;
- flow / coroutine constructs;
- delegated runtime helpers that may suspend;
- nested lambda creation if invocation may suspend.
Likely implementation direction:
- store `maySuspend` or `fastOnly`-adjacent metadata on `CmdFunction` or the callable wrapper;
- derive it from emitted bytecode opcodes and embedded lambda constants.
### Phase 2: Add a direct non-suspending invoke path
For bytecode callables proven non-suspending, add an execution entry point that avoids suspend machinery for ordinary direct calls.
Requirements:
- Reuse as much of the existing fast frame setup as possible.
- Keep exception translation and source mapping identical to the suspend path.
- Do not depend on JVM-only tricks.
Potential direction:
- extend `BytecodeCallable` with a capability query or richer fast-call API;
- let call sites choose among:
- inline body
- non-suspending compiled call
- existing suspend call
### Phase 3: Teach bytecode call sites to use it
Apply the new path only where the callee is known precisely.
Initial targets:
- direct lambda invocation where exact lambda ref is known but inlining is not possible;
- direct local function calls where the binding resolves to a compiled callable;
- extension wrapper calls where wrapper binding is known and non-suspending.
Do not start with dynamic dispatch or reflective calls.
### Phase 4: Validate behavioral fidelity
Must explicitly verify:
- thrown exceptions still report the same Lyng source positions;
- stack traces remain useful enough for debugging;
- optional calls / null propagation are unchanged;
- captures and implicit `this` still bind correctly.
### Phase 5: Measure before broadening
Benchmark after each widening step, especially:
- `OptTest.testAddToArray`
- iterable pipeline samples using `filter` / `map`
- direct lambda call microbenchmarks
- closure-heavy samples with captures
## Open technical questions
1. Where should non-suspending capability live?
- `CmdFunction`
- `BytecodeStatement`
- callable wrapper object
- `CallSignature`-adjacent metadata
2. Should the compiler emit a separate opcode for known non-suspending compiled calls, or should runtime dispatch pick the fast path from a normal call opcode?
3. Can we preserve the current error/stack behavior if we bypass suspend wrappers entirely, or do we need a thin compatibility layer?
4. Should capture-free and capture-heavy compiled lambdas share the same direct-call mechanism, or should captured callables stay on the safer path initially?
## Suggested order of execution
1. Add conservative `maySuspend` analysis for compiled bytecode functions.
2. Expose a non-suspending direct-call capability on compiled callables.
3. Use it for exact direct lambda calls first.
4. Extend to exact local function calls.
5. Re-measure.
6. Only then consider broader dispatch sites.
## Validation checklist
- `./gradlew :lynglib:compileKotlinJvm --console=plain`
- `./gradlew :lynglib:jvmTest --tests net.sergeych.lyng.OptTest.testAddToArray --console=plain`
- `./gradlew :lynglib:jvmTest --tests StdlibTest.testIterableFilter --tests CompilerVmReviewRegressionTest --console=plain`
- `./gradlew :lynglib:jvmTest --console=plain`
## Notes from the completed phase
Relevant current commits before this follow-up work:
- `3be2892` Use fast compiled callbacks in dynamic and flow helpers
- `1d5caaa` Broaden lambda method inlining with captures
- `0c3242c` Generalize higher-order lambda inlining
- `f4ab2eb` Extend lambda inlining to getOrPut and implicit it calls
Current working tree phase adds:
- metadata-driven higher-order inlining through member and extension signatures;
- extension wrapper signature propagation;
- removal of the compiler-side higher-order name table fallback.