fix #28 basic map supports (still no iterators)
This commit is contained in:
parent
a4448ab2ff
commit
c002204420
63
docs/Map.md
Normal file
63
docs/Map.md
Normal 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)
|
@ -56,7 +56,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
else -> {
|
else -> {
|
||||||
println("callArgs: ${callArgs.joinToString()}")
|
println("callArgs: ${callArgs.joinToString()}")
|
||||||
println("tailBlockMode: ${arguments.tailBlockMode}")
|
println("tailBlockMode: ${arguments.tailBlockMode}")
|
||||||
context.raiseArgumentError("too few arguments for the call")
|
context.raiseIllegalArgument("too few arguments for the call")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assign(a, value)
|
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)
|
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)
|
assign(a, value)
|
||||||
i--
|
i--
|
||||||
@ -98,7 +98,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
processEllipsis(leftIndex, end)
|
processEllipsis(leftIndex, end)
|
||||||
} else {
|
} else {
|
||||||
if (leftIndex < callArgs.size)
|
if (leftIndex < callArgs.size)
|
||||||
context.raiseArgumentError("too many arguments for the call")
|
context.raiseIllegalArgument("too many arguments for the call")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,11 @@ class Context(
|
|||||||
raiseError(ObjIndexOutOfBoundsException(this, message))
|
raiseError(ObjIndexOutOfBoundsException(this, message))
|
||||||
|
|
||||||
@Suppress("unused")
|
@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))
|
raiseError(ObjIllegalArgumentException(this, message))
|
||||||
|
|
||||||
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
|
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
|
||||||
|
@ -254,6 +254,7 @@ open class Obj {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
inline fun <reified T> from(obj: T): Obj {
|
inline fun <reified T> from(obj: T): Obj {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
return when (obj) {
|
return when (obj) {
|
||||||
is Obj -> obj
|
is Obj -> obj
|
||||||
is Double -> ObjReal(obj)
|
is Double -> ObjReal(obj)
|
||||||
@ -263,6 +264,7 @@ open class Obj {
|
|||||||
is String -> ObjString(obj)
|
is String -> ObjString(obj)
|
||||||
is CharSequence -> ObjString(obj.toString())
|
is CharSequence -> ObjString(obj.toString())
|
||||||
is Boolean -> ObjBool(obj)
|
is Boolean -> ObjBool(obj)
|
||||||
|
is Set<*> -> ObjSet((obj as Set<Obj>).toMutableSet())
|
||||||
Unit -> ObjVoid
|
Unit -> ObjVoid
|
||||||
null -> ObjNull
|
null -> ObjNull
|
||||||
is Iterator<*> -> ObjKotlinIterator(obj)
|
is Iterator<*> -> ObjKotlinIterator(obj)
|
||||||
@ -423,6 +425,7 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
|
|||||||
"ClassCastException",
|
"ClassCastException",
|
||||||
"IndexOutOfBoundsException",
|
"IndexOutOfBoundsException",
|
||||||
"IllegalArgumentException",
|
"IllegalArgumentException",
|
||||||
|
"NoSuchElementException",
|
||||||
"IllegalAssignmentException",
|
"IllegalAssignmentException",
|
||||||
"SymbolNotDefinedException",
|
"SymbolNotDefinedException",
|
||||||
"IterationEndException",
|
"IterationEndException",
|
||||||
@ -447,8 +450,11 @@ class ObjIndexOutOfBoundsException(context: Context, message: String = "index ou
|
|||||||
class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") :
|
class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") :
|
||||||
ObjException("IllegalArgumentException", context, message)
|
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") :
|
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") :
|
class ObjSymbolNotDefinedException(context: Context, message: String = "symbol is not defined") :
|
||||||
ObjException("SymbolNotDefinedException", context, message)
|
ObjException("SymbolNotDefinedException", context, message)
|
||||||
|
@ -9,6 +9,10 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
|
|||||||
|
|
||||||
override fun byValueCopy(): Obj = ObjInt(value)
|
override fun byValueCopy(): Obj = ObjInt(value)
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return value.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getAndIncrement(context: Context): Obj {
|
override suspend fun getAndIncrement(context: Context): Obj {
|
||||||
return ObjInt(value).also { value++ }
|
return ObjInt(value).also { value++ }
|
||||||
}
|
}
|
||||||
@ -77,6 +81,8 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val Zero = ObjInt(0)
|
||||||
|
val One = ObjInt(1)
|
||||||
val type = ObjClass("Int")
|
val type = ObjClass("Int")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
return list.map { it.toKotlin(context) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
// check?
|
||||||
|
return list.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val type = ObjClass("List", ObjArray).apply {
|
val type = ObjClass("List", ObjArray).apply {
|
||||||
|
|
||||||
|
71
lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt
Normal file
71
lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,8 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
override val toObjInt: ObjInt by lazy { ObjInt(longValue) }
|
override val toObjInt: ObjInt by lazy { ObjInt(longValue) }
|
||||||
override val toObjReal: ObjReal by lazy { ObjReal(value) }
|
override val toObjReal: ObjReal by lazy { ObjReal(value) }
|
||||||
|
|
||||||
|
override val objClass: ObjClass = type
|
||||||
|
|
||||||
override fun byValueCopy(): Obj = ObjReal(value)
|
override fun byValueCopy(): Obj = ObjReal(value)
|
||||||
|
|
||||||
override suspend fun compareTo(context: Context, other: Obj): Int {
|
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 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 =
|
override suspend fun plus(context: Context, other: Obj): Obj =
|
||||||
ObjReal(this.value + other.toDouble())
|
ObjReal(this.value + other.toDouble())
|
||||||
|
@ -44,12 +44,12 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
|
|||||||
return if (other is ObjSet) {
|
return if (other is ObjSet) {
|
||||||
ObjSet(set.intersect(other.set).toMutableSet())
|
ObjSet(set.intersect(other.set).toMutableSet())
|
||||||
} else
|
} else
|
||||||
context.raiseArgumentError("set operator * requires another set")
|
context.raiseIllegalArgument("set operator * requires another set")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun minus(context: Context, other: Obj): Obj {
|
override suspend fun minus(context: Context, other: Obj): Obj {
|
||||||
if (other !is ObjSet)
|
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())
|
return ObjSet(set.minus(other.set).toMutableSet())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +66,8 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
|
||||||
val type = object : ObjClass("Set", ObjCollection) {
|
val type = object : ObjClass("Set", ObjCollection) {
|
||||||
override suspend fun callOn(context: Context): Obj {
|
override suspend fun callOn(context: Context): Obj {
|
||||||
return ObjSet(context.args.list.toMutableSet())
|
return ObjSet(context.args.list.toMutableSet())
|
||||||
|
@ -46,7 +46,11 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
}
|
}
|
||||||
return ObjString(value.substring(start, end))
|
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 {
|
override suspend fun callOn(context: Context): Obj {
|
||||||
@ -58,7 +62,7 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
value.contains(other.value)
|
value.contains(other.value)
|
||||||
else if (other is ObjChar)
|
else if (other is ObjChar)
|
||||||
value.contains(other.value)
|
value.contains(other.value)
|
||||||
else context.raiseArgumentError("String.contains can't take $other")
|
else context.raiseIllegalArgument("String.contains can't take $other")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -154,6 +154,7 @@ class Script(
|
|||||||
addConst("List", ObjList.type)
|
addConst("List", ObjList.type)
|
||||||
addConst("Set", ObjSet.type)
|
addConst("Set", ObjSet.type)
|
||||||
addConst("Range", ObjRange.type)
|
addConst("Range", ObjRange.type)
|
||||||
|
addConst("Map", ObjMap.type)
|
||||||
@Suppress("RemoveRedundantQualifierName")
|
@Suppress("RemoveRedundantQualifierName")
|
||||||
addConst("Callable", Statement.type)
|
addConst("Callable", Statement.type)
|
||||||
// interfaces
|
// interfaces
|
||||||
|
@ -1890,7 +1890,7 @@ class ScriptTest {
|
|||||||
fun testThrowFromKotlin() = runTest {
|
fun testThrowFromKotlin() = runTest {
|
||||||
val c = Context()
|
val c = Context()
|
||||||
c.addFn("callThrow") {
|
c.addFn("callThrow") {
|
||||||
raiseArgumentError("fromKotlin")
|
raiseIllegalArgument("fromKotlin")
|
||||||
}
|
}
|
||||||
c.eval(
|
c.eval(
|
||||||
"""
|
"""
|
||||||
|
@ -260,6 +260,11 @@ class BookTest {
|
|||||||
runDocTests("../docs/Set.md")
|
runDocTests("../docs/Set.md")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMap() = runTest {
|
||||||
|
runDocTests("../docs/Map.md")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSampleBooks() = runTest {
|
fun testSampleBooks() = runTest {
|
||||||
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
|
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user