From 2d4c4d345d0a70c9a4fe3e559dad0c073617703c Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 16 Jun 2025 15:44:22 +0400 Subject: [PATCH] fix #30: let, apply, also. Fix in context combining for lambda calls. --- docs/tutorial.md | 58 +++++++++++++++++++ lynglib/build.gradle.kts | 2 +- .../kotlin/net/sergeych/lyng/Arguments.kt | 2 + .../kotlin/net/sergeych/lyng/Compiler.kt | 9 +-- .../kotlin/net/sergeych/lyng/Context.kt | 20 ++++++- .../kotlin/net/sergeych/lyng/Obj.kt | 54 ++++++++++++----- .../kotlin/net/sergeych/lyng/ObjClass.kt | 9 +-- .../kotlin/net/sergeych/lyng/Script.kt | 4 +- lynglib/src/commonTest/kotlin/ScriptTest.kt | 45 +++++++++++++- 9 files changed, 176 insertions(+), 27 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index c417791..9f552ca 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -163,6 +163,64 @@ There is also "elvis operator", null-coalesce infix operator '?:' that returns r null ?: "nothing" >>> "nothing" +## Utility functions + +The following functions simplify nullable values processing and +allow to improve code look and readability. There are borrowed from Kotlin: + +### let + +`value.let {}` passes to the block value as the single parameter (by default it is assigned to `it`) and return block's returned value. It is useful dealing with null or to +get a snapshot of some externally varying value, or with `?.` to process nullable value in a safe manner: + + // this state is changed from parallel processes + class GlobalState(nullableParam) + + val state = GlobalState(null) + + fun sample() { + state.nullableParam?.let { "it's not null: "+it} ?: "it's null" + } + assertEquals(sample(), "it's null") + state.nullableParam = 5 + assertEquals(sample(), "it's not null: 5") + >>> void + +This is the same as: + + fun sample() { + val it = state.nullableParam + if( it != null ) "it's not null: "+it else "it's null" + } + +The important is that nullableParam got a local copy that can't be changed from any +parallel thread/coroutine. Remember: Lyng _is __not__ a single-threaded language_. + +## Also + +Much like let, but it does not alter returned value: + + assert( "test".also { println( it + "!") } == "test" ) + >>> test! + >>> void + +While it is not altering return value, the source object could be changed: + + class Point(x,y) + val p = Point(1,2).also { it.x++ } + assertEquals(p.x, 2) + >>> void + +## apply + +It works much like `also`, but is executed in the context of the source object: + + class Point(x,y) + // see the difference: apply changes this to newly created Point: + val p = Point(1,2).apply { x++; y++ } + assertEquals(p, Point(2,3)) + >>> void + ## Math It is rather simple, like everywhere else: diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index bc5c196..b5f6e5b 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "0.6.5-SNAPSHOT" +version = "0.6.7-SNAPSHOT" buildscript { repositories { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 18045c7..8367ce2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -28,6 +28,8 @@ suspend fun Collection.toArguments(context: Context,tailBlockMod data class Arguments(val list: List,val tailBlockMode: Boolean = false) : List by list { + constructor(vararg values: Obj) : this(values.toList()) + fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") return list.first() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index bf08fd8..9c212c5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -122,7 +122,7 @@ class Compiler( } Token.Type.DOT, Token.Type.NULL_COALESCE -> { - var isOptional = t.type == Token.Type.NULL_COALESCE + val isOptional = t.type == Token.Type.NULL_COALESCE operand?.let { left -> // dotcall: calling method on the operand, if next is ID, "(" var isCall = false @@ -154,7 +154,7 @@ class Compiler( Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { - isOptional = nt.type == Token.Type.NULL_COALESCE_BLOCKINVOKE +// isOptional = nt.type == Token.Type.NULL_COALESCE_BLOCKINVOKE // single lambda arg, like assertTrows { ... } cc.next() isCall = true @@ -393,7 +393,8 @@ class Compiler( var closure: Context? = null val callStatement = statement { - val context = closure!!.copy(pos, args) + // and the source closure of the lambda which might have other thisObj. + val context = closure!!.copy(pos, args).applyContext(this) if (argsDeclaration == null) { // no args: automatic var 'it' val l = args.list @@ -1667,7 +1668,7 @@ class Compiler( * The keywords that stop processing of expression term */ val stopKeywords = - setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class", "struct") + setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class") } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt index 2748223..5a0eeba 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt @@ -4,7 +4,7 @@ class Context( val parent: Context?, val args: Arguments = Arguments.EMPTY, var pos: Pos = Pos.builtIn, - val thisObj: Obj = ObjVoid, + var thisObj: Obj = ObjVoid, var skipContextCreation: Boolean = false, ) { constructor( @@ -13,6 +13,15 @@ class Context( ) : this(Script.defaultContext, args, pos) + /** + * Making this context priority one + */ + fun applyContext(other: Context): Context { + if (other.thisObj != ObjVoid) thisObj = other.thisObj + appliedContext = other + return this + } + fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented") @Suppress("unused") @@ -65,11 +74,16 @@ class Context( inline fun thisAs(): T = (thisObj as? T) ?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") + internal var appliedContext: Context? = null internal val objects = mutableMapOf() operator fun get(name: String): ObjRecord? = - objects[name] - ?: parent?.get(name) + if (name == "this") thisObj.asReadonly + else { + objects[name] + ?: parent?.get(name) + ?: appliedContext?.get(name) + } fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context = Context(this, args, pos, newThisObj ?: thisObj) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index 0c1cabe..8fa30d7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -64,7 +64,11 @@ open class Obj { */ open fun byValueCopy(): Obj = this - fun isInstanceOf(someClass: Obj) = someClass === objClass || objClass.allParentsSet.contains(someClass) + @Suppress("SuspiciousEqualsCombination") + fun isInstanceOf(someClass: Obj) = someClass === objClass || + objClass.allParentsSet.contains(someClass) || + someClass == rootObjectType + suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj = invokeInstanceMethod(context, name, Arguments(args.toList())) @@ -103,16 +107,7 @@ open class Obj { * Class of the object: definition of member functions (top-level), etc. * Note that using lazy allows to avoid endless recursion here */ - open val objClass: ObjClass by lazy { - ObjClass("Obj").apply { - addFn("toString") { - thisObj.asStr - } - addFn("contains") { - ObjBool(thisObj.contains(this, args.firstAndOnly())) - } - } - } + open val objClass: ObjClass = rootObjectType open suspend fun plus(context: Context, other: Obj): Obj { context.raiseNotImplemented() @@ -253,6 +248,31 @@ open class Obj { companion object { + + val rootObjectType = ObjClass("Obj").apply { + addFn("toString") { + thisObj.asStr + } + addFn("contains") { + ObjBool(thisObj.contains(this, args.firstAndOnly())) + } + // utilities + addFn("let") { + args.firstAndOnly().callOn(copy(Arguments(thisObj))) + } + addFn("apply") { + val newContext = ( thisObj as? ObjInstance)?.instanceContext ?: this + args.firstAndOnly() + .callOn(newContext) + thisObj + } + addFn("also") { + args.firstAndOnly().callOn(copy(Arguments(thisObj))) + thisObj + } + } + + inline fun from(obj: Any?): Obj { @Suppress("UNCHECKED_CAST") return when (obj) { @@ -272,6 +292,7 @@ open class Obj { obj as MutableMap.MutableEntry ObjMapEntry(obj.key, obj.value) } + else -> throw IllegalArgumentException("cannot convert to Obj: $obj") } } @@ -371,7 +392,12 @@ data class ObjNamespace(val name: String) : Obj() { } open class ObjException(exceptionClass: ExceptionClass, val context: Context, val message: String) : Obj() { - constructor(name: String,context: Context, message: String) : this(getOrCreateExceptionClass(name), context, message) + constructor(name: String, context: Context, message: String) : this( + getOrCreateExceptionClass(name), + context, + message + ) + constructor(context: Context, message: String) : this(Root, context, message) fun raise(): Nothing { @@ -386,13 +412,15 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va companion object { - class ExceptionClass(val name: String,vararg parents: ObjClass) : ObjClass(name, *parents) { + class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) { override suspend fun callOn(context: Context): Obj { val message = context.args.getOrNull(0)?.toString() ?: name return ObjException(this, context, message) } + override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}" } + val Root = ExceptionClass("Throwable").apply { addConst("message", statement { (thisObj as ObjException).message.toObj() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt index af5ddb1..ab4f7a4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt @@ -9,9 +9,10 @@ open class ObjClass( var instanceConstructor: Statement? = null - val allParentsSet: Set = parents.flatMap { - listOf(it) + it.allParentsSet - }.toSet() + val allParentsSet: Set = + parents.flatMap { + listOf(it) + it.allParentsSet + }.toMutableSet() override val objClass: ObjClass by lazy { ObjClassType } @@ -61,7 +62,7 @@ open class ObjClass( fun getInstanceMemberOrNull(name: String): ObjRecord? { members[name]?.let { return it } allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } } - return null + return rootObjectType.members[name] } fun getInstanceMember(atPos: Pos, name: String): ObjRecord = diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 8f11941..ebcbc5b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -146,6 +146,7 @@ class Script( delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong()) } + addConst("Object", rootObjectType) addConst("Real", ObjReal.type) addConst("String", ObjString.type) addConst("Int", ObjInt.type) @@ -163,13 +164,14 @@ class Script( addConst("Collection", ObjCollection) addConst("Array", ObjArray) addConst("Class", ObjClassType) - addConst("Object", Obj().objClass) val pi = ObjReal(PI) addConst("π", pi) getOrCreateNamespace("Math").apply { addConst("PI", pi) } + + } } } \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 1f300ea..74a31cf 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -1267,7 +1267,12 @@ class ScriptTest { eval( """ val x = { x, y, z -> + println("-- x=",x) + println("-- y=",y) + println("-- z=",z) + println([x,y,z]) assert( [x, y, z] == [1,2,"end"]) + println("----:") } assert( x(1, 2, "end") == void) """.trimIndent() @@ -2149,7 +2154,6 @@ class ScriptTest { assertEquals( null, s?.length ?{ "test" } ) assertEquals( null, s?[1] ) assertEquals( null, s ?{ "test" } ) - assertEquals( null, s.test ?{ "test" } ) s = "xx" assert(s.lower().size == 2) @@ -2242,4 +2246,43 @@ class ScriptTest { """.trimIndent() ) } + + @Test + fun testLet() = runTest { + eval(""" + class Point(x=0,y=0) + assert( Point() is Object) + Point().let { println(it.x, it.y) } + val x = null + x?.let { println(it.x, it.y) } + """.trimIndent()) + } + + @Test + fun testApply() = runTest { + eval(""" + class Point(x,y) + // see the difference: apply changes this to newly created Point: + val p = Point(1,2).apply { + x++; y++ + } + assertEquals(p, Point(2,3)) + >>> void + + """.trimIndent()) + } + + @Test + fun testApplyThis() = runTest { + eval(""" + class Point(x,y) + // see the difference: apply changes this to newly created Point: + val p = Point(1,2).apply { + this.x++; this.y++ + } + assertEquals(p, Point(2,3)) + >>> void + + """.trimIndent()) + } } \ No newline at end of file