fixed execution on JS and native platforms

This commit is contained in:
Sergey Chernov 2025-11-12 10:32:11 +01:00
parent 28e8648794
commit 852383e3b1
6 changed files with 80 additions and 30 deletions

View File

@ -25,21 +25,41 @@ import net.sergeych.lyng.obj.ObjRecord
* from [closureScope] with proper precedence
*/
class ClosureScope(val callScope: Scope, val closureScope: Scope) :
Scope(callScope, callScope.args, thisObj = callScope.thisObj) {
// Important: use closureScope.thisObj so unqualified members (e.g., fields) resolve to the instance
// we captured, not to the caller's `this` (e.g., FlowBuilder).
Scope(callScope, callScope.args, thisObj = closureScope.thisObj) {
override fun get(name: String): ObjRecord? {
// we take arguments from the callerScope, the rest
// from the closure.
// Priority:
// 1) Arguments from the caller scope (if present in this frame)
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`), e.g., fields like `coll`, `factor`
// 3) Symbols from the captured closure scope (its locals and parents)
// 4) Instance members of the caller's `this` (e.g., FlowBuilder.emit)
// 5) Fallback to the standard chain (this frame -> parent (callScope) -> class members)
// note using super, not callScope, as arguments are assigned by the constructor
// and are not assigned yet to vars in callScope self:
super.objects[name]?.let {
// if( name == "predicate" ) {
// println("predicate: ${it.type.isArgument}: ${it.value}")
// }
if( it.type.isArgument ) return it
}
return closureScope.get(name)
// and are not yet exposed via callScope.get at this point:
super.objects[name]?.let { if (it.type.isArgument) return it }
// Prefer instance fields/methods declared on the captured receiver:
// First, resolve real instance fields stored in the instance scope (constructor vars like `coll`, `factor`)
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)
?.instanceScope
?.objects
?.get(name)
?.let { return it }
// Then, try class-declared members (methods/properties declared in the class body)
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
// Then delegate to the full closure scope chain (locals, parents, etc.)
closureScope.get(name)?.let { return it }
// Allow resolving instance members of the caller's `this` (e.g., FlowBuilder.emit)
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
// Fallback to the standard lookup chain: this frame -> parent (callScope) -> class members
return super.get(name)
}
}

View File

@ -1432,22 +1432,25 @@ class Compiler(
var wasBroken = false
var result: Obj = ObjVoid
lateinit var doScope: Scope
do {
while (true) {
doScope = it.createChildScope().apply { skipScopeCreation = true }
try {
result = body.execute(doScope)
} catch (e: LoopBreakContinueException) {
if (e.label == label || e.label == null) {
if (e.doContinue) continue
else {
if (!e.doContinue) {
result = e.result
wasBroken = true
break
}
// for continue: just fall through to condition check below
} else {
// Not our label, let outer loops handle it
throw e
}
throw e
}
} while (condition.execute(doScope).toBool())
if (!condition.execute(doScope).toBool()) break
}
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
result
}

View File

@ -218,8 +218,10 @@ class Script(
ObjVoid
}
// Delay in milliseconds (plain numeric). For time-aware variants use lyng.time.Duration API.
addVoidFn("delay") {
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
val ms = (this.args.firstAndOnly().toDouble()).roundToLong()
delay(ms)
}
addConst("Object", rootObjectType)

View File

@ -51,19 +51,24 @@ open class ObjException(
suspend fun getStackTrace(): ObjList {
return cachedStackTrace.get {
val result = ObjList()
val cls = scope.get("StackTraceEntry")!!.value as ObjClass
val maybeCls = scope.get("StackTraceEntry")?.value as? ObjClass
var s: Scope? = scope
var lastPos: Pos? = null
while (s != null) {
val pos = s.pos
if (pos != lastPos && !pos.currentLine.isEmpty()) {
result.list += cls.callWithArgs(
scope,
pos.source.objSourceName,
ObjInt(pos.line.toLong()),
ObjInt(pos.column.toLong()),
ObjString(pos.currentLine)
)
if (maybeCls != null) {
result.list += maybeCls.callWithArgs(
scope,
pos.source.objSourceName,
ObjInt(pos.line.toLong()),
ObjInt(pos.column.toLong()),
ObjString(pos.currentLine)
)
} else {
// Fallback textual entry if StackTraceEntry class is not available in this scope
result.list += ObjString("${'$'}{pos.source.objSourceName}:${'$'}{pos.line}:${'$'}{pos.column}: ${'$'}{pos.currentLine}")
}
}
s = s.parent
lastPos = pos

View File

@ -41,7 +41,19 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
return value.compareTo(other.doubleValue)
}
override fun toString(): String = value.toString()
override fun toString(): String {
// Normalize scientific notation to match tests across platforms.
// Kotlin/JVM prints 1e-6 as "1.0E-6" by default; tests accept "1E-6" (or a plain decimal).
val s = value.toString()
val ePos = s.indexOf('E').let { if (it >= 0) it else s.indexOf('e') }
if (ePos >= 0) {
val mantissa = s.substring(0, ePos)
val exponent = s.substring(ePos + 1) // skip the 'E'/'e'
val mantissaNorm = if (mantissa.endsWith(".0")) mantissa.dropLast(2) else mantissa
return mantissaNorm + "E" + exponent
}
return s
}
override fun hashCode(): Int {
return value.hashCode()

View File

@ -19,11 +19,13 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import net.sergeych.lyng.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
import net.sergeych.tools.bm
import kotlin.test.*
import kotlin.time.Duration.Companion.seconds
class ScriptTest {
@ -1765,7 +1767,11 @@ class ScriptTest {
@Test
fun testIntExponentRealForm() = runTest {
assertEquals("1.0E-6", eval("1e-6").toString())
when(val x = eval("1e-6").toString()) {
"0.000001", "1E-6", "1e-6" -> true
else -> fail("Excepted 1e-6 got $x")
}
// assertEquals("1.0E-6", eval("1e-6").toString())
}
@Test
@ -2126,8 +2132,9 @@ class ScriptTest {
@Test
fun doWhileValuesLabelTest() = runTest {
eval(
"""
withTimeout(5.seconds) {
eval(
"""
var count = 0
var count2 = 0
var count3 = 0
@ -2148,7 +2155,8 @@ class ScriptTest {
assertEquals("found 11/5", result)
assertEquals( 4, count3)
""".trimIndent()
)
)
}
}
@Test