fix #30: let, apply, also. Fix in context combining for lambda calls.

This commit is contained in:
Sergey Chernov 2025-06-16 15:44:22 +04:00
parent f9416105ec
commit 2d4c4d345d
9 changed files with 176 additions and 27 deletions

View File

@ -163,6 +163,64 @@ There is also "elvis operator", null-coalesce infix operator '?:' that returns r
null ?: "nothing" null ?: "nothing"
>>> "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 ## Math
It is rather simple, like everywhere else: It is rather simple, like everywhere else:

View File

@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "0.6.5-SNAPSHOT" version = "0.6.7-SNAPSHOT"
buildscript { buildscript {
repositories { repositories {

View File

@ -28,6 +28,8 @@ suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMod
data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : List<Obj> by list { data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : List<Obj> by list {
constructor(vararg values: Obj) : this(values.toList())
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
return list.first() return list.first()

View File

@ -122,7 +122,7 @@ class Compiler(
} }
Token.Type.DOT, Token.Type.NULL_COALESCE -> { 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 -> operand?.let { left ->
// dotcall: calling method on the operand, if next is ID, "(" // dotcall: calling method on the operand, if next is ID, "("
var isCall = false var isCall = false
@ -154,7 +154,7 @@ class Compiler(
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { 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 { ... } // single lambda arg, like assertTrows { ... }
cc.next() cc.next()
isCall = true isCall = true
@ -393,7 +393,8 @@ class Compiler(
var closure: Context? = null var closure: Context? = null
val callStatement = statement { 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) { if (argsDeclaration == null) {
// no args: automatic var 'it' // no args: automatic var 'it'
val l = args.list val l = args.list
@ -1667,7 +1668,7 @@ class Compiler(
* The keywords that stop processing of expression term * The keywords that stop processing of expression term
*/ */
val stopKeywords = 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")
} }
} }

View File

@ -4,7 +4,7 @@ class Context(
val parent: Context?, val parent: Context?,
val args: Arguments = Arguments.EMPTY, val args: Arguments = Arguments.EMPTY,
var pos: Pos = Pos.builtIn, var pos: Pos = Pos.builtIn,
val thisObj: Obj = ObjVoid, var thisObj: Obj = ObjVoid,
var skipContextCreation: Boolean = false, var skipContextCreation: Boolean = false,
) { ) {
constructor( constructor(
@ -13,6 +13,15 @@ class Context(
) )
: this(Script.defaultContext, args, pos) : 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") fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused") @Suppress("unused")
@ -65,11 +74,16 @@ class Context(
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T) inline fun <reified T : Obj> thisAs(): T = (thisObj as? T)
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") ?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
internal var appliedContext: Context? = null
internal val objects = mutableMapOf<String, ObjRecord>() internal val objects = mutableMapOf<String, ObjRecord>()
operator fun get(name: String): ObjRecord? = operator fun get(name: String): ObjRecord? =
objects[name] if (name == "this") thisObj.asReadonly
?: parent?.get(name) else {
objects[name]
?: parent?.get(name)
?: appliedContext?.get(name)
}
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context = fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj) Context(this, args, pos, newThisObj ?: thisObj)

View File

@ -64,7 +64,11 @@ open class Obj {
*/ */
open fun byValueCopy(): Obj = this 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 = suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj =
invokeInstanceMethod(context, name, Arguments(args.toList())) invokeInstanceMethod(context, name, Arguments(args.toList()))
@ -103,16 +107,7 @@ open class Obj {
* Class of the object: definition of member functions (top-level), etc. * Class of the object: definition of member functions (top-level), etc.
* Note that using lazy allows to avoid endless recursion here * Note that using lazy allows to avoid endless recursion here
*/ */
open val objClass: ObjClass by lazy { open val objClass: ObjClass = rootObjectType
ObjClass("Obj").apply {
addFn("toString") {
thisObj.asStr
}
addFn("contains") {
ObjBool(thisObj.contains(this, args.firstAndOnly()))
}
}
}
open suspend fun plus(context: Context, other: Obj): Obj { open suspend fun plus(context: Context, other: Obj): Obj {
context.raiseNotImplemented() context.raiseNotImplemented()
@ -253,6 +248,31 @@ open class Obj {
companion object { 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 { inline fun from(obj: Any?): Obj {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return when (obj) { return when (obj) {
@ -272,6 +292,7 @@ open class Obj {
obj as MutableMap.MutableEntry<Obj, Obj> obj as MutableMap.MutableEntry<Obj, Obj>
ObjMapEntry(obj.key, obj.value) ObjMapEntry(obj.key, obj.value)
} }
else -> throw IllegalArgumentException("cannot convert to Obj: $obj") 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() { 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) constructor(context: Context, message: String) : this(Root, context, message)
fun raise(): Nothing { fun raise(): Nothing {
@ -386,13 +412,15 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
companion object { 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 { override suspend fun callOn(context: Context): Obj {
val message = context.args.getOrNull(0)?.toString() ?: name val message = context.args.getOrNull(0)?.toString() ?: name
return ObjException(this, context, message) return ObjException(this, context, message)
} }
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}" override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
} }
val Root = ExceptionClass("Throwable").apply { val Root = ExceptionClass("Throwable").apply {
addConst("message", statement { addConst("message", statement {
(thisObj as ObjException).message.toObj() (thisObj as ObjException).message.toObj()

View File

@ -9,9 +9,10 @@ open class ObjClass(
var instanceConstructor: Statement? = null var instanceConstructor: Statement? = null
val allParentsSet: Set<ObjClass> = parents.flatMap { val allParentsSet: Set<ObjClass> =
listOf(it) + it.allParentsSet parents.flatMap {
}.toSet() listOf(it) + it.allParentsSet
}.toMutableSet()
override val objClass: ObjClass by lazy { ObjClassType } override val objClass: ObjClass by lazy { ObjClassType }
@ -61,7 +62,7 @@ open class ObjClass(
fun getInstanceMemberOrNull(name: String): ObjRecord? { fun getInstanceMemberOrNull(name: String): ObjRecord? {
members[name]?.let { return it } members[name]?.let { return it }
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(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 = fun getInstanceMember(atPos: Pos, name: String): ObjRecord =

View File

@ -146,6 +146,7 @@ class Script(
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong()) delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
} }
addConst("Object", rootObjectType)
addConst("Real", ObjReal.type) addConst("Real", ObjReal.type)
addConst("String", ObjString.type) addConst("String", ObjString.type)
addConst("Int", ObjInt.type) addConst("Int", ObjInt.type)
@ -163,13 +164,14 @@ class Script(
addConst("Collection", ObjCollection) addConst("Collection", ObjCollection)
addConst("Array", ObjArray) addConst("Array", ObjArray)
addConst("Class", ObjClassType) addConst("Class", ObjClassType)
addConst("Object", Obj().objClass)
val pi = ObjReal(PI) val pi = ObjReal(PI)
addConst("π", pi) addConst("π", pi)
getOrCreateNamespace("Math").apply { getOrCreateNamespace("Math").apply {
addConst("PI", pi) addConst("PI", pi)
} }
} }
} }
} }

View File

@ -1267,7 +1267,12 @@ class ScriptTest {
eval( eval(
""" """
val x = { x, y, z -> 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"]) assert( [x, y, z] == [1,2,"end"])
println("----:")
} }
assert( x(1, 2, "end") == void) assert( x(1, 2, "end") == void)
""".trimIndent() """.trimIndent()
@ -2149,7 +2154,6 @@ class ScriptTest {
assertEquals( null, s?.length ?{ "test" } ) assertEquals( null, s?.length ?{ "test" } )
assertEquals( null, s?[1] ) assertEquals( null, s?[1] )
assertEquals( null, s ?{ "test" } ) assertEquals( null, s ?{ "test" } )
assertEquals( null, s.test ?{ "test" } )
s = "xx" s = "xx"
assert(s.lower().size == 2) assert(s.lower().size == 2)
@ -2242,4 +2246,43 @@ class ScriptTest {
""".trimIndent() """.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())
}
} }