From c002204420ffaa7ab4088c7d75017e74e8c0d32c Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 16 Jun 2025 01:12:04 +0400 Subject: [PATCH] fix #28 basic map supports (still no iterators) --- docs/Map.md | 63 ++++++++++++++++ .../net/sergeych/lyng/ArgsDeclaration.kt | 6 +- .../kotlin/net/sergeych/lyng/Context.kt | 6 +- .../kotlin/net/sergeych/lyng/Obj.kt | 8 ++- .../kotlin/net/sergeych/lyng/ObjInt.kt | 6 ++ .../kotlin/net/sergeych/lyng/ObjList.kt | 7 +- .../kotlin/net/sergeych/lyng/ObjMap.kt | 71 +++++++++++++++++++ .../kotlin/net/sergeych/lyng/ObjReal.kt | 6 +- .../kotlin/net/sergeych/lyng/ObjSet.kt | 6 +- .../kotlin/net/sergeych/lyng/ObjString.kt | 8 ++- .../kotlin/net/sergeych/lyng/Script.kt | 1 + lynglib/src/commonTest/kotlin/ScriptTest.kt | 2 +- lynglib/src/jvmTest/kotlin/BookTest.kt | 5 ++ 13 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 docs/Map.md create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt diff --git a/docs/Map.md b/docs/Map.md new file mode 100644 index 0000000..df5f7c8 --- /dev/null +++ b/docs/Map.md @@ -0,0 +1,63 @@ +# Map + +Map is a mutable collection of key-value pars, where keys are unique. Maps could be created with +constructor or `.toMap` methods. When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List]. + +Constructed map instance is of class `Map` and implements `Collection` (and therefore `Iterable`) + + val map = Map( ["foo", 1], ["bar", "buzz"] ) + assert(map is Map) + assert(map.size == 2) + assert(map is Iterable) + >>> void + +Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of standard types are). You can access elements with indexing operator: + + val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] ) + assert( map["bar"] == "buzz") + assert( map[42] == "answer" ) + assertThrows { map["nonexistent"] } + assert( map.getOrNull(101) == null ) + assert( map.getOrPut(911) { "nine-eleven" } == "nine-eleven" ) + // now 91 entry is set: + assert( map[911] == "nine-eleven" ) + map["foo"] = -1 + assert( map["foo"] == -1) + >>> void + +To remove item from the collection. use `remove`. It returns last removed item or null. Be careful if you +hold nulls in the map - this is not a recommended practice when using `remove` returned value. `clear()` +removes all. + + val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] ) + assertEquals( 1, map.remove("foo") ) + assert( map.getOrNull("foo") == null) + assert( map.size == 2 ) + map.clear() + assert( map.size == 0 ) + >>> void + +Map implements [contains] method that checks _the presence of the key_ in the map: + + val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] ) + assert( "foo" in map ) + assert( "answer" !in map ) + >>> void + +To iterate maps it is convenient to use `keys` method that returns [Set] of keys (keys are unique: + + val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] ) + for( k in map.keys ) println(map[k]) + >>> 1 + >>> buzz + >>> answer + >>> void + +It is possible also to get values as [List] (values are not unique): + + val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] ) + assertEquals(map.values, [1, "buzz", "answer"] ) + >>> void + + +[Collection](Collection.md) \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index a0aa1da..c8d82eb 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -56,7 +56,7 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) else -> { println("callArgs: ${callArgs.joinToString()}") println("tailBlockMode: ${arguments.tailBlockMode}") - context.raiseArgumentError("too few arguments for the call") + context.raiseIllegalArgument("too few arguments for the call") } } assign(a, value) @@ -77,7 +77,7 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) } a.defaultValue != null -> a.defaultValue.execute(context) - else -> context.raiseArgumentError("too few arguments for the call") + else -> context.raiseIllegalArgument("too few arguments for the call") } assign(a, value) i-- @@ -98,7 +98,7 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) processEllipsis(leftIndex, end) } else { if (leftIndex < callArgs.size) - context.raiseArgumentError("too many arguments for the call") + context.raiseIllegalArgument("too many arguments for the call") } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt index ff5ea25..2748223 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt @@ -23,7 +23,11 @@ class Context( raiseError(ObjIndexOutOfBoundsException(this, message)) @Suppress("unused") - fun raiseArgumentError(message: String = "Illegal argument error"): Nothing = + fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing = + raiseError(ObjIllegalArgumentException(this, message)) + + @Suppress("unused") + fun raiseNoSuchElement(message: String = "No such element"): Nothing = raiseError(ObjIllegalArgumentException(this, message)) fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg)) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index 940e545..0b6249d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -254,6 +254,7 @@ open class Obj { companion object { inline fun from(obj: T): Obj { + @Suppress("UNCHECKED_CAST") return when (obj) { is Obj -> obj is Double -> ObjReal(obj) @@ -263,6 +264,7 @@ open class Obj { is String -> ObjString(obj) is CharSequence -> ObjString(obj.toString()) is Boolean -> ObjBool(obj) + is Set<*> -> ObjSet((obj as Set).toMutableSet()) Unit -> ObjVoid null -> ObjNull is Iterator<*> -> ObjKotlinIterator(obj) @@ -423,6 +425,7 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va "ClassCastException", "IndexOutOfBoundsException", "IllegalArgumentException", + "NoSuchElementException", "IllegalAssignmentException", "SymbolNotDefinedException", "IterationEndException", @@ -447,8 +450,11 @@ class ObjIndexOutOfBoundsException(context: Context, message: String = "index ou class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") : ObjException("IllegalArgumentException", context, message) +class ObjNoSuchElementException(context: Context, message: String = "no such element") : + ObjException("IllegalArgumentException", context, message) + class ObjIllegalAssignmentException(context: Context, message: String = "illegal assignment") : - ObjException("IllegalAssignmentException", context, message) + ObjException("NoSuchElementException", context, message) class ObjSymbolNotDefinedException(context: Context, message: String = "symbol is not defined") : ObjException("SymbolNotDefinedException", context, message) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInt.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInt.kt index 455c032..9d02248 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInt.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInt.kt @@ -9,6 +9,10 @@ data class ObjInt(var value: Long) : Obj(), Numeric { override fun byValueCopy(): Obj = ObjInt(value) + override fun hashCode(): Int { + return value.hashCode() + } + override suspend fun getAndIncrement(context: Context): Obj { return ObjInt(value).also { value++ } } @@ -77,6 +81,8 @@ data class ObjInt(var value: Long) : Obj(), Numeric { } companion object { + val Zero = ObjInt(0) + val One = ObjInt(1) val type = ObjClass("Int") } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt index 887b185..62bc5ca 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt @@ -47,7 +47,7 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { } } - else -> context.raiseArgumentError("Illegal index object for a list: ${index.inspect()}") + else -> context.raiseIllegalArgument("Illegal index object for a list: ${index.inspect()}") } } @@ -114,6 +114,11 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { return list.map { it.toKotlin(context) } } + override fun hashCode(): Int { + // check? + return list.hashCode() + } + companion object { val type = ObjClass("List", ObjArray).apply { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt new file mode 100644 index 0000000..63b3593 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt @@ -0,0 +1,71 @@ +package net.sergeych.lyng + +class ObjMap(val map: MutableMap): Obj() { + + override val objClass = type + + override suspend fun getAt(context: Context, index: Obj): Obj = + map.getOrElse(index) { context.raiseNoSuchElement() } + + override suspend fun contains(context: Context, other: Obj): Boolean { + return other in map + } + override fun toString(): String = map.toString() + companion object { + + suspend fun listToMap(context: Context, list: List): MutableMap { + val map = mutableMapOf() + if (list.isEmpty()) return map + + val first = list.first() + if (first.isInstanceOf(ObjArray)) { + if( first.invokeInstanceMethod(context,"size").toInt() != 2) + context.raiseIllegalArgument( + "list to construct map entry should exactly be 2 element Array like [key,value], got $list" + ) + } else context.raiseIllegalArgument("first element of map list be a Collection of 2 elements; got $first") + + + + list.forEach { + map[it.getAt(context,ObjInt.Zero)] = it.getAt(context,ObjInt.One) + } + return map + } + + + val type = object: ObjClass("Map", ObjCollection) { + override suspend fun callOn(context: Context): Obj { + return ObjMap(listToMap(context, context.args.list)) + } + }.apply { + addFn("getOrNull") { + val key = args.firstAndOnly(pos) + thisAs().map.getOrElse(key) { ObjNull } + } + addFn("getOrPut") { + val key = requiredArg(0) + thisAs().map.getOrPut(key) { + val lambda = requiredArg(1) + lambda.execute(this) + } + } + addFn("size") { + thisAs().map.size.toObj() + } + addFn("remove") { + thisAs().map.remove(requiredArg(0))?.toObj() ?: ObjNull + } + addFn("clear") { + thisAs().map.clear() + thisObj + } + addFn("keys") { + thisAs().map.keys.toObj() + } + addFn("values") { + ObjList(thisAs().map.values.toMutableList()) + } + } + } +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjReal.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjReal.kt index 3717b70..4e5b50a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjReal.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjReal.kt @@ -10,6 +10,8 @@ data class ObjReal(val value: Double) : Obj(), Numeric { override val toObjInt: ObjInt by lazy { ObjInt(longValue) } override val toObjReal: ObjReal by lazy { ObjReal(value) } + override val objClass: ObjClass = type + override fun byValueCopy(): Obj = ObjReal(value) override suspend fun compareTo(context: Context, other: Obj): Int { @@ -19,7 +21,9 @@ data class ObjReal(val value: Double) : Obj(), Numeric { override fun toString(): String = value.toString() - override val objClass: ObjClass = type + override fun hashCode(): Int { + return value.hashCode() + } override suspend fun plus(context: Context, other: Obj): Obj = ObjReal(this.value + other.toDouble()) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjSet.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjSet.kt index 2f901b3..6cc270b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjSet.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjSet.kt @@ -44,12 +44,12 @@ class ObjSet(val set: MutableSet = mutableSetOf()) : Obj() { return if (other is ObjSet) { ObjSet(set.intersect(other.set).toMutableSet()) } else - context.raiseArgumentError("set operator * requires another set") + context.raiseIllegalArgument("set operator * requires another set") } override suspend fun minus(context: Context, other: Obj): Obj { if (other !is ObjSet) - context.raiseArgumentError("set operator - requires another set") + context.raiseIllegalArgument("set operator - requires another set") return ObjSet(set.minus(other.set).toMutableSet()) } @@ -66,6 +66,8 @@ class ObjSet(val set: MutableSet = mutableSetOf()) : Obj() { } companion object { + + val type = object : ObjClass("Set", ObjCollection) { override suspend fun callOn(context: Context): Obj { return ObjSet(context.args.list.toMutableSet()) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt index 9c66116..3533d42 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt @@ -46,7 +46,11 @@ data class ObjString(val value: String) : Obj() { } return ObjString(value.substring(start, end)) } - context.raiseArgumentError("String index must be Int or Range") + context.raiseIllegalArgument("String index must be Int or Range") + } + + override fun hashCode(): Int { + return value.hashCode() } override suspend fun callOn(context: Context): Obj { @@ -58,7 +62,7 @@ data class ObjString(val value: String) : Obj() { value.contains(other.value) else if (other is ObjChar) value.contains(other.value) - else context.raiseArgumentError("String.contains can't take $other") + else context.raiseIllegalArgument("String.contains can't take $other") } companion object { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index a8d2f1b..af210a7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -154,6 +154,7 @@ class Script( addConst("List", ObjList.type) addConst("Set", ObjSet.type) addConst("Range", ObjRange.type) + addConst("Map", ObjMap.type) @Suppress("RemoveRedundantQualifierName") addConst("Callable", Statement.type) // interfaces diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 6ce1d29..1f300ea 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -1890,7 +1890,7 @@ class ScriptTest { fun testThrowFromKotlin() = runTest { val c = Context() c.addFn("callThrow") { - raiseArgumentError("fromKotlin") + raiseIllegalArgument("fromKotlin") } c.eval( """ diff --git a/lynglib/src/jvmTest/kotlin/BookTest.kt b/lynglib/src/jvmTest/kotlin/BookTest.kt index 9d72774..d7000b0 100644 --- a/lynglib/src/jvmTest/kotlin/BookTest.kt +++ b/lynglib/src/jvmTest/kotlin/BookTest.kt @@ -260,6 +260,11 @@ class BookTest { runDocTests("../docs/Set.md") } + @Test + fun testMap() = runTest { + runDocTests("../docs/Map.md") + } + @Test fun testSampleBooks() = runTest { for (bt in Files.list(Paths.get("../docs/samples")).toList()) {