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 * from [closureScope] with proper precedence
*/ */
class ClosureScope(val callScope: Scope, val closureScope: Scope) : 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? { override fun get(name: String): ObjRecord? {
// we take arguments from the callerScope, the rest // Priority:
// from the closure. // 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 // note using super, not callScope, as arguments are assigned by the constructor
// and are not assigned yet to vars in callScope self: // and are not yet exposed via callScope.get at this point:
super.objects[name]?.let { super.objects[name]?.let { if (it.type.isArgument) return it }
// if( name == "predicate" ) {
// println("predicate: ${it.type.isArgument}: ${it.value}") // Prefer instance fields/methods declared on the captured receiver:
// } // First, resolve real instance fields stored in the instance scope (constructor vars like `coll`, `factor`)
if( it.type.isArgument ) return it (closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)
} ?.instanceScope
return closureScope.get(name) ?.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 wasBroken = false
var result: Obj = ObjVoid var result: Obj = ObjVoid
lateinit var doScope: Scope lateinit var doScope: Scope
do { while (true) {
doScope = it.createChildScope().apply { skipScopeCreation = true } doScope = it.createChildScope().apply { skipScopeCreation = true }
try { try {
result = body.execute(doScope) result = body.execute(doScope)
} catch (e: LoopBreakContinueException) { } catch (e: LoopBreakContinueException) {
if (e.label == label || e.label == null) { if (e.label == label || e.label == null) {
if (e.doContinue) continue if (!e.doContinue) {
else {
result = e.result result = e.result
wasBroken = true wasBroken = true
break 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) } if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
result result
} }

View File

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

View File

@ -51,19 +51,24 @@ open class ObjException(
suspend fun getStackTrace(): ObjList { suspend fun getStackTrace(): ObjList {
return cachedStackTrace.get { return cachedStackTrace.get {
val result = ObjList() val result = ObjList()
val cls = scope.get("StackTraceEntry")!!.value as ObjClass val maybeCls = scope.get("StackTraceEntry")?.value as? ObjClass
var s: Scope? = scope var s: Scope? = scope
var lastPos: Pos? = null var lastPos: Pos? = null
while (s != null) { while (s != null) {
val pos = s.pos val pos = s.pos
if (pos != lastPos && !pos.currentLine.isEmpty()) { if (pos != lastPos && !pos.currentLine.isEmpty()) {
result.list += cls.callWithArgs( if (maybeCls != null) {
scope, result.list += maybeCls.callWithArgs(
pos.source.objSourceName, scope,
ObjInt(pos.line.toLong()), pos.source.objSourceName,
ObjInt(pos.column.toLong()), ObjInt(pos.line.toLong()),
ObjString(pos.currentLine) 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 s = s.parent
lastPos = pos lastPos = pos

View File

@ -41,7 +41,19 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
return value.compareTo(other.doubleValue) 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 { override fun hashCode(): Int {
return value.hashCode() return value.hashCode()

View File

@ -19,11 +19,13 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.InlineSourcesImportProvider import net.sergeych.lyng.pacman.InlineSourcesImportProvider
import net.sergeych.tools.bm import net.sergeych.tools.bm
import kotlin.test.* import kotlin.test.*
import kotlin.time.Duration.Companion.seconds
class ScriptTest { class ScriptTest {
@ -1765,7 +1767,11 @@ class ScriptTest {
@Test @Test
fun testIntExponentRealForm() = runTest { 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 @Test
@ -2126,8 +2132,9 @@ class ScriptTest {
@Test @Test
fun doWhileValuesLabelTest() = runTest { fun doWhileValuesLabelTest() = runTest {
eval( withTimeout(5.seconds) {
""" eval(
"""
var count = 0 var count = 0
var count2 = 0 var count2 = 0
var count3 = 0 var count3 = 0
@ -2148,7 +2155,8 @@ class ScriptTest {
assertEquals("found 11/5", result) assertEquals("found 11/5", result)
assertEquals( 4, count3) assertEquals( 4, count3)
""".trimIndent() """.trimIndent()
) )
}
} }
@Test @Test