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"
>>> "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:

View File

@ -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 {

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 {
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()

View File

@ -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")
}
}

View File

@ -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 <reified T : Obj> thisAs(): T = (thisObj as? T)
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
internal var appliedContext: Context? = null
internal val objects = mutableMapOf<String, ObjRecord>()
operator fun get(name: String): ObjRecord? =
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)

View File

@ -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<Obj, Obj>
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 {
@ -391,8 +417,10 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
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()

View File

@ -9,9 +9,10 @@ open class ObjClass(
var instanceConstructor: Statement? = null
val allParentsSet: Set<ObjClass> = parents.flatMap {
val allParentsSet: Set<ObjClass> =
parents.flatMap {
listOf(it) + it.allParentsSet
}.toSet()
}.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 =

View File

@ -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)
}
}
}
}

View File

@ -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())
}
}