From 6d8eed7b8c439b4e86d4fa78e6a5aa22479afd0a Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 12 Aug 2025 00:15:45 +0300 Subject: [PATCH] added "dynamic" fields (get/set/call fields by name using `dynamic` standard function --- README.md | 5 +- docs/OOP.md | 57 ++++++++++++++++ .../kotlin/net/sergeych/lyng/Script.kt | 5 ++ .../net/sergeych/lyng/obj/ObjDynamic.kt | 65 +++++++++++++++++++ lynglib/src/commonTest/kotlin/OOTest.kt | 51 +++++++++++---- 5 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt diff --git a/README.md b/README.md index 74fd3ba..02e476c 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,9 @@ Ready features: - [x] multiline strings - [x] typesafe bit-effective serialization - [x] compression/decompression (integrated in serialization) -- - Under way: +- [x] dynamic fields + +### Under way: - [ ] regular exceptions - [ ] multiple inheritance for user classes diff --git a/docs/OOP.md b/docs/OOP.md index a958930..0bcb667 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -235,6 +235,63 @@ as they are modifying the type, not the context. Beware of it. We might need to reconsider it later. +## dynamic symbols + +Sometimes it is convenient to provide methods and variables whose names are not known at compile time. For example, it could be external interfaces not known to library code, user-defined data fields, etc. You can use `dynamic` function to create such: + + // val only dynamic object + val accessor = dynamic { + // all symbol reads are redirected here: + get { name -> + // lets provide one dynamic symbol: + if( name == "foo" ) "bar" else null + // consider also throw SymbolNotDefinedException + } + } + + // now we can access dynamic "fields" of accessor: + assertEquals("bar", accessor.foo) + assertEquals(null, accessor.bar) + >>> void + +The same we can provide writable dynamic fields (var-type), adding set method: + + // store one dynamic field here + var storedValueForBar = null + + // create dynamic object with 2 fields: + val accessor = dynamic { + get { name -> + when(name) { + // constant field + "foo" -> "bar" + // mutable field + "bar" -> setValueForBar + + else -> throw SymbolNotFoundException() + } + } + set { name, value -> + // only 'bar' is mutable: + if( name == "bar" ) + storedValueForBar = value + // the rest is immotable. consider throw also + // SymbolNotFoundException when needed. + else throw IllegalAssignmentException("Can't assign "+name) + } + } + + assertEquals("bar", accessor.foo) + assertEquals(null, accessor.bar) + accessor.bar = "buzz" + assertEquals("buzz", accessor.bar) + + assertThrows { + accessor.bad = "!23" + } + +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. + # Theory ## Basic principles: diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 8fa36bb..c003129 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -170,6 +170,11 @@ class Script( } result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown")) } + + addFn("dynamic") { + ObjDynamic.create(this, requireOnlyArg()) + } + addFn("require") { val condition = requiredArg(0) if( !condition.value ) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt new file mode 100644 index 0000000..c05d191 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt @@ -0,0 +1,65 @@ +package net.sergeych.lyng.obj + +import net.sergeych.lyng.Arguments +import net.sergeych.lyng.Scope +import net.sergeych.lyng.Statement + +class ObjDynamicContext(val delegate: ObjDynamic) : Obj() { + override val objClass: ObjClass = type + + companion object { + val type = ObjClass("DelegateContext").apply { + addFn("get") { + val d = thisAs().delegate + if (d.readCallback != null) + raiseIllegalState("get already defined") + d.readCallback = requireOnlyArg() + ObjVoid + } + + addFn("set") { + val d = thisAs().delegate + if (d.writeCallback != null) + raiseIllegalState("set already defined") + d.writeCallback = requireOnlyArg() + ObjVoid + } + + } + + } +} + +class ObjDynamic : Obj() { + + internal var readCallback: Statement? = null + internal var writeCallback: Statement? = null + + override suspend fun readField(scope: Scope, name: String): ObjRecord { + return readCallback?.execute(scope.copy(Arguments(ObjString(name))))?.let { + if (writeCallback != null) + it.asMutable + else + it.asReadonly + } + ?: super.readField(scope, name) + } + + override suspend fun writeField(scope: Scope, name: String, newValue: Obj) { + writeCallback?.execute(scope.copy(Arguments(ObjString(name), newValue))) + ?: super.writeField(scope, name, newValue) + } + + companion object { + + suspend fun create(scope: Scope, builder: Statement): ObjDynamic { + val delegate = ObjDynamic() + val context = ObjDynamicContext(delegate) + builder.execute(scope.copy(newThisObj = context)) + return delegate + } + + val type = object : ObjClass("Delegate") {} + } + +} \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/OOTest.kt b/lynglib/src/commonTest/kotlin/OOTest.kt index ac40bda..30b1620 100644 --- a/lynglib/src/commonTest/kotlin/OOTest.kt +++ b/lynglib/src/commonTest/kotlin/OOTest.kt @@ -44,20 +44,49 @@ class OOTest { """.trimIndent()) } -// @Test - fun testDynamic() = runTest { + @Test + fun testDynamicGet() = runTest { eval(""" - println("0") - class DynamicTest : Dynamic { - - fun getDynamic(name) { - if (name == "foo") "bar" else null + val accessor = dynamic { + get { name -> + if( name == "foo" ) "bar" else null } } - println("1") - val d = DynamicTest() - println(d) - println("2") + + println("-- " + accessor.foo) + assertEquals("bar", accessor.foo) + assertEquals(null, accessor.bar) + + """.trimIndent()) + } + + @Test + fun testDelegateSet() = runTest { + eval(""" + var setValueForBar = null + val accessor = dynamic { + get { name -> + when(name) { + "foo" -> "bar" + "bar" -> setValueForBar + else -> null + } + } + set { name, value -> + if( name == "bar" ) + setValueForBar = value + else throw IllegalAssignmentException("Can't assign "+name) + } + } + + assertEquals("bar", accessor.foo) + assertEquals(null, accessor.bar) + accessor.bar = "buzz" + assertEquals("buzz", accessor.bar) + + assertThrows { + accessor.bad = "!23" + } """.trimIndent()) } } \ No newline at end of file