fix #30: let, apply, also. Fix in context combining for lambda calls.
This commit is contained in:
parent
f9416105ec
commit
2d4c4d345d
@ -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:
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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 =
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user