fix #29 mapentries, map iterators, => operator and .toMap()

This commit is contained in:
Sergey Chernov 2025-06-16 02:11:47 +04:00
parent c002204420
commit f9416105ec
15 changed files with 213 additions and 16 deletions

View File

@ -53,11 +53,57 @@ To iterate maps it is convenient to use `keys` method that returns [Set] of keys
>>> answer
>>> void
Or iterate its key-value pairs that are instances of [MapEntry] class:
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
for( entry in map ) {
println("map[%s] = %s"(entry.key, entry.value))
}
void
>>> map[foo] = 1
>>> map[bar] = buzz
>>> map[42] = answer
>>> void
There is a shortcut to use `MapEntry` to create maps: operator `=>` which creates `MapEntry`:
val entry = "answer" => 42
assert( entry is MapEntry )
>>> void
And you can use it to construct maps:
val map = Map( "foo" => 1, "bar" => 22)
assertEquals(1, map["foo"])
assertEquals(22, map["bar"])
>>> void
Or use `.toMap` on anything that implements [Iterable] and which elements implements [Array] with 2 elements size, for example, `MapEntry`:
val map = ["foo" => 1, "bar" => 22].toMap()
assert( map is Map )
assertEquals(1, map["foo"])
assertEquals(22, map["bar"])
>>> 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
Map could be tested to be equal: when all it key-value pairs are equal, the map
is equal.
val m1 = Map(["foo", 1])
val m2 = Map(["foo", 1])
val m3 = Map(["foo", 2])
assert( m1 == m2 )
// but the references are different:
assert( m1 !== m2 )
// different maps:
assert( m1 != m3 )
>>> void
[Collection](Collection.md)

View File

@ -14,7 +14,7 @@ __Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md)
- Some class references: [List], [Real], [Range], [Iterable], [Iterator]
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator]
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
# Expressions
@ -583,6 +583,28 @@ You can get ranges to extract a portion from a list:
assertEquals( [2,3], list[1..<3])
>>> void
# Sets
Set are unordered collection of unique elements, see [Set]. Sets are [Iterable] but have no indexing access.
assertEquals( Set(3, 2, 1), Set( 1, 2, 3))
assert( 5 !in Set(1, 2, 6) )
>>> void
Please see [Set] for detailed description.
# Maps
Maps are unordered collection of key-value pairs, where keys are unique. See [Map] for details. Map also
are [Iterable]:
val m = Map( "foo" => 77, "bar" => "buzz" )
assertEquals( m["foo"], 77 )
>>> void
Please see [Map] reference for detailed description on using Maps.
# Flow control operators
## if-then-else
@ -1172,3 +1194,5 @@ See [math functions](math.md). Other general purpose functions are:
[Range]: Range.md
[String]: String.md
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
[Set]: Set.md
[Map]: Map.md

View File

@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "0.6.4-SNAPSHOT"
version = "0.6.5-SNAPSHOT"
buildscript {
repositories {

View File

@ -1616,6 +1616,8 @@ class Compiler(
// bitwise or 2
// bitwise and 3
// equality/ne 4
Operator.simple(Token.Type.EQARROW, ++lastPrty) { c, a, b -> ObjMapEntry(a,b) },
//
Operator.simple(Token.Type.EQ, ++lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) == 0) },
Operator.simple(Token.Type.NEQ, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
Operator.simple(Token.Type.REF_EQ, lastPrty) { _, a, b -> ObjBool(a === b) },

View File

@ -253,7 +253,7 @@ open class Obj {
companion object {
inline fun <reified T> from(obj: T): Obj {
inline fun from(obj: Any?): Obj {
@Suppress("UNCHECKED_CAST")
return when (obj) {
is Obj -> obj
@ -268,6 +268,10 @@ open class Obj {
Unit -> ObjVoid
null -> ObjNull
is Iterator<*> -> ObjKotlinIterator(obj)
is Map.Entry<*, *> -> {
obj as MutableMap.MutableEntry<Obj, Obj>
ObjMapEntry(obj.key, obj.value)
}
else -> throw IllegalArgumentException("cannot convert to Obj: $obj")
}
}
@ -450,6 +454,7 @@ class ObjIndexOutOfBoundsException(context: Context, message: String = "index ou
class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") :
ObjException("IllegalArgumentException", context, message)
@Suppress("unused")
class ObjNoSuchElementException(context: Context, message: String = "no such element") :
ObjException("IllegalArgumentException", context, message)

View File

@ -80,6 +80,15 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
return value
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjInt
return value == other.value
}
companion object {
val Zero = ObjInt(0)
val One = ObjInt(1)

View File

@ -47,6 +47,15 @@ val ObjIterable by lazy {
ObjSet(result)
}
addFn("toMap") {
val result = mutableListOf<Obj>()
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
result += it.invokeInstanceMethod(this, "next")
}
ObjMap(ObjMap.listToMap(this, result))
}
addFn("forEach", isOpen = true) {
val it = thisObj.invokeInstanceMethod(this, "iterator")
val fn = requiredArg<Statement>(0)

View File

@ -1,6 +1,12 @@
@file:Suppress("unused")
package net.sergeych.lyng
class ObjKotlinIterator(val iterator: Iterator<Any?>): Obj() {
/**
* Iterator wrapper to allow Kotlin collections to be returned from Lyng objects;
* each object is converted to a Lyng object.
*/
class ObjKotlinIterator(val iterator: Iterator<Any?>) : Obj() {
override val objClass = type
@ -10,5 +16,24 @@ class ObjKotlinIterator(val iterator: Iterator<Any?>): Obj() {
addFn("hasNext") { thisAs<ObjKotlinIterator>().iterator.hasNext().toObj() }
}
}
}
/**
* Propagate kotlin iterator that already produces Lyng objects, no conversion
* is applied
*/
class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
override val objClass = type
companion object {
val type = ObjClass("KotlinIterator", ObjIterator).apply {
addFn("next") {
thisAs<ObjKotlinObjIterator>().iterator.next()
}
addFn("hasNext") { thisAs<ObjKotlinIterator>().iterator.hasNext().toObj() }
}
}
}

View File

@ -119,6 +119,15 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
return list.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjList
return list == other.list
}
companion object {
val type = ObjClass("List", ObjArray).apply {

View File

@ -1,6 +1,40 @@
package net.sergeych.lyng
class ObjMap(val map: MutableMap<Obj,Obj>): Obj() {
class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjMapEntry) return -1
val c = key.compareTo(context, other.key)
if (c != 0) return c
return value.compareTo(context, other.value)
}
override suspend fun getAt(context: Context, index: Obj): Obj = when (index.toInt()) {
0 -> key
1 -> value
else -> context.raiseIndexOutOfBounds()
}
override fun toString(): String {
return "$key=>$value"
}
override val objClass = type
companion object {
val type = object : ObjClass("MapEntry", ObjArray) {
override suspend fun callOn(context: Context): Obj {
return ObjMapEntry(context.requiredArg<Obj>(0), context.requiredArg<Obj>(1))
}
}.apply {
addFn("key") { thisAs<ObjMapEntry>().key }
addFn("value") { thisAs<ObjMapEntry>().value }
addFn("size") { 2.toObj() }
}
}
}
class ObjMap(val map: MutableMap<Obj, Obj>) : Obj() {
override val objClass = type
@ -10,16 +44,22 @@ class ObjMap(val map: MutableMap<Obj,Obj>): Obj() {
override suspend fun contains(context: Context, other: Obj): Boolean {
return other in map
}
override suspend fun compareTo(context: Context, other: Obj): Int {
if( other is ObjMap && other.map == map) return 0
return -1
}
override fun toString(): String = map.toString()
companion object {
suspend fun listToMap(context: Context, list: List<Obj>): MutableMap<Obj,Obj> {
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)
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"
)
@ -28,13 +68,13 @@ class ObjMap(val map: MutableMap<Obj,Obj>): Obj() {
list.forEach {
map[it.getAt(context,ObjInt.Zero)] = it.getAt(context,ObjInt.One)
map[it.getAt(context, ObjInt.Zero)] = it.getAt(context, ObjInt.One)
}
return map
}
val type = object: ObjClass("Map", ObjCollection) {
val type = object : ObjClass("Map", ObjCollection) {
override suspend fun callOn(context: Context): Obj {
return ObjMap(listToMap(context, context.args.list))
}
@ -66,6 +106,9 @@ class ObjMap(val map: MutableMap<Obj,Obj>): Obj() {
addFn("values") {
ObjList(thisAs<ObjMap>().map.values.toMutableList())
}
addFn("iterator") {
ObjKotlinIterator(thisAs<ObjMap>().map.entries.iterator())
}
}
}
}

View File

@ -47,6 +47,15 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
return value
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjReal
return value == other.value
}
companion object {
val type: ObjClass = ObjClass("Real").apply {
createField(

View File

@ -65,6 +65,15 @@ data class ObjString(val value: String) : Obj() {
else context.raiseIllegalArgument("String.contains can't take $other")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjString
return value == other.value
}
companion object {
val type = ObjClass("String").apply {
addFn("startsWith") {

View File

@ -45,12 +45,18 @@ private class Parser(fromPos: Pos) {
'=' -> {
if (pos.currentChar == '=') {
pos.advance()
if (currentChar == '=') {
pos.advance()
Token("===", from, Token.Type.REF_EQ)
} else
Token("==", from, Token.Type.EQ)
} else
when (currentChar) {
'=' -> {
pos.advance()
Token("===", from, Token.Type.REF_EQ)
}
else -> Token("==", from, Token.Type.EQ)
}
} else if( currentChar == '>' ) {
pos.advance()
Token("=>", from, Token.Type.EQARROW)
}
else
Token("=", from, Token.Type.ASSIGN)
}

View File

@ -155,6 +155,7 @@ class Script(
addConst("Set", ObjSet.type)
addConst("Range", ObjRange.type)
addConst("Map", ObjMap.type)
addConst("MapEntry", ObjMapEntry.type)
@Suppress("RemoveRedundantQualifierName")
addConst("Callable", Statement.type)
// interfaces

View File

@ -14,7 +14,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
IN, NOTIN, IS, NOTIS,
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ,
SHUTTLE,
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,