fix #28 basic map supports (still no iterators)

This commit is contained in:
Sergey Chernov 2025-06-16 01:12:04 +04:00
parent a4448ab2ff
commit c002204420
13 changed files with 183 additions and 12 deletions

63
docs/Map.md Normal file
View File

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

View File

@ -56,7 +56,7 @@ data class ArgsDeclaration(val params: List<Item>, 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<Item>, 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<Item>, 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")
}
}

View File

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

View File

@ -254,6 +254,7 @@ open class Obj {
companion object {
inline fun <reified T> 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<Obj>).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)

View File

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

View File

@ -47,7 +47,7 @@ class ObjList(val list: MutableList<Obj> = 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<Obj> = mutableListOf()) : Obj() {
return list.map { it.toKotlin(context) }
}
override fun hashCode(): Int {
// check?
return list.hashCode()
}
companion object {
val type = ObjClass("List", ObjArray).apply {

View File

@ -0,0 +1,71 @@
package net.sergeych.lyng
class ObjMap(val map: MutableMap<Obj,Obj>): 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<Obj>): MutableMap<Obj,Obj> {
val map = mutableMapOf<Obj, Obj>()
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<ObjMap>().map.getOrElse(key) { ObjNull }
}
addFn("getOrPut") {
val key = requiredArg<Obj>(0)
thisAs<ObjMap>().map.getOrPut(key) {
val lambda = requiredArg<Statement>(1)
lambda.execute(this)
}
}
addFn("size") {
thisAs<ObjMap>().map.size.toObj()
}
addFn("remove") {
thisAs<ObjMap>().map.remove(requiredArg<Obj>(0))?.toObj() ?: ObjNull
}
addFn("clear") {
thisAs<ObjMap>().map.clear()
thisObj
}
addFn("keys") {
thisAs<ObjMap>().map.keys.toObj()
}
addFn("values") {
ObjList(thisAs<ObjMap>().map.values.toMutableList())
}
}
}
}

View File

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

View File

@ -44,12 +44,12 @@ class ObjSet(val set: MutableSet<Obj> = 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<Obj> = mutableSetOf()) : Obj() {
}
companion object {
val type = object : ObjClass("Set", ObjCollection) {
override suspend fun callOn(context: Context): Obj {
return ObjSet(context.args.list.toMutableSet())

View File

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

View File

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

View File

@ -1890,7 +1890,7 @@ class ScriptTest {
fun testThrowFromKotlin() = runTest {
val c = Context()
c.addFn("callThrow") {
raiseArgumentError("fromKotlin")
raiseIllegalArgument("fromKotlin")
}
c.eval(
"""

View File

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