diff --git a/docs/OOP.md b/docs/OOP.md index 0bcb667..5166bf4 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -266,7 +266,7 @@ The same we can provide writable dynamic fields (var-type), adding set method: // constant field "foo" -> "bar" // mutable field - "bar" -> setValueForBar + "bar" -> storedValueForBar else -> throw SymbolNotFoundException() } @@ -275,8 +275,8 @@ The same we can provide writable dynamic fields (var-type), adding set method: // only 'bar' is mutable: if( name == "bar" ) storedValueForBar = value - // the rest is immotable. consider throw also - // SymbolNotFoundException when needed. + // the rest is immotable. consider throw also + // SymbolNotFoundException when needed. else throw IllegalAssignmentException("Can't assign "+name) } } @@ -289,9 +289,56 @@ The same we can provide writable dynamic fields (var-type), adding set method: assertThrows { accessor.bad = "!23" } + void + >>> void Of course, you can return any object from dynamic fields; returning lambdas let create _dynamic methods_ - the callable method. It is very convenient to implement libraries with dynamic remote interfaces, etc. +### Dynamic indexers + +Index access for dynamics is passed to the same getter and setter, so it is +generally the same: + + var storedValue = "bar" + val x = dynamic { + get { + if( it == "foo" ) storedValue + else null + } + } + assertEquals("bar", x["foo"] ) + assertEquals("bar", x.foo ) + >>> void + +And assigning them works the same. You can make it working +mimicking arrays, but remember, it is not Collection so +collection's sugar won't work with it: + + var storedValue = "bar" + val x = dynamic { + get { + when(it) { + "size" -> 1 + 0 -> storedValue + else -> null + } + } + set { index, value -> + if( index == 0 ) storedValue = value + else throw "Illegal index: "+index + } + } + assertEquals("bar", x[0] ) + assertEquals(1, x.size ) + x[0] = "buzz" + assertThrows { x[1] = 1 } + assertEquals("buzz", storedValue) + assertEquals("buzz", x[0]) + >>> void + +If you want dynamic to function like an array, create a [feature +request](https://gitea.sergeych.net/SergeychWorks/lyng/issues). + # Theory ## Basic principles: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 71ae0d9..55677c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ clikt = "5.0.3" kotlin = "2.2.20" android-minSdk = "24" android-compileSdk = "34" -kotlinx-coroutines = "1.9.0" +kotlinx-coroutines = "1.10.2" mp_bintools = "0.1.12" firebaseCrashlyticsBuildtools = "3.0.3" okioVersion = "3.10.2" diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 51fc564..01e1165 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -888,6 +888,7 @@ class Compiler( } } + @Suppress("SameParameterValue") private fun parseNumber(isPlus: Boolean): Obj { return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number") } @@ -1681,7 +1682,7 @@ class Compiler( cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) // could be else block: - val t2 = cc.next() + val t2 = cc.nextNonWhitespace() // we generate different statements: optimization return if (t2.type == Token.Type.ID && t2.value == "else") { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt index 7aafdbc..dcf1bce 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt @@ -28,7 +28,9 @@ data class Pos(val source: Source, val line: Int, val column: Int) { else if( line > 0) Pos(source, line-1, source.lines[line-1].length - 1) else throw IllegalStateException("can't go back from line 0, column 0") - val currentLine: String get() = if( end ) "EOF" else source.lines[line] + val currentLine: String get() = + if( end ) "EOF" + else if( line >= 0 ) source.lines[line] else "" val end: Boolean get() = line >= source.lines.size diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt index 5c05f28..9c93de6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt @@ -26,7 +26,7 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable? $pos: Error: $errorMessage ${pos.currentLine} - ${"-".repeat(pos.column)}^ + ${if( pos.column >= 0 ) "-".repeat(pos.column) + "^" else ""} """.trimIndent(), cause ) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt index 3ee5fc2..c5cfe68 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt @@ -67,6 +67,16 @@ class ObjDynamic : Obj() { ?: super.writeField(scope, name, newValue) } + override suspend fun getAt(scope: Scope, index: Obj): Obj { + return readCallback?.execute( scope.copy(Arguments(index))) + ?: super.getAt(scope, index) + } + + override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) { + writeCallback?.execute(scope.copy(Arguments(index, newValue))) + ?: super.putAt(scope, index, newValue) + } + companion object { suspend fun create(scope: Scope, builder: Statement): ObjDynamic { @@ -76,7 +86,9 @@ class ObjDynamic : Obj() { return delegate } - val type = object : ObjClass("Delegate") {} + val type = object : ObjClass("Delegate") {}.apply { +// addClassConst("IndexGetName", operatorGetName) + } } } \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/OOTest.kt b/lynglib/src/commonTest/kotlin/OOTest.kt index 77cff71..f790d0a 100644 --- a/lynglib/src/commonTest/kotlin/OOTest.kt +++ b/lynglib/src/commonTest/kotlin/OOTest.kt @@ -107,6 +107,26 @@ class OOTest { """.trimIndent()) } + @Test + fun testDynamicIndexAccess() = runTest { + eval(""" + val store = Map() + val accessor = dynamic { + get { name -> + store[name] + } + set { name, value -> + store[name] = value + } + } + assertEquals(null, accessor["foo"]) + assertEquals(null, accessor.foo) + accessor["foo"] = "bar" + assertEquals("bar", accessor["foo"]) + assertEquals("bar", accessor.foo) + """.trimIndent()) + } + @Test fun testMultilineConstructor() = runTest { eval("""