diff --git a/docs/List.md b/docs/List.md index 8cf09af..f3ab0cd 100644 --- a/docs/List.md +++ b/docs/List.md @@ -12,13 +12,44 @@ you can use it's class to ensure type: []::class == List >>> true +## Indexing + +indexing is zero-based, as in C/C++/Java/Kotlin, etc. + + val list = [10, 20, 30] + list[1] + >>> 20 + +Using negative indexes has a special meaning: _offset from the end of the list_: + + val list = [10, 20, 30] + list[-1] + >>> 30 + +__Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too. + +## Concatenation + + assert( [4,5] + [1,2] == [4,5,1,2]) + >>> void + +## Comparisons + + assert( [1, 2] != [1, 3]) + assert( [1, 2, 3] > [1, 2]) + assert( [1, 3] > [1, 2, 3]) + assert( [1, 2, 3] == [1, 2, 3]) + // note that in the case above objects are referentially different: + assert( [1, 2, 3] !== [1, 2, 3]) + >>> void + ## Members -| name | meaning | type | -|---------|-------------------------------|------| -| `.size` | property returns current size | Int | -| | | | -| | | | -| | | | -| | | | -| | | | +| name | meaning | type | +|----------------------------|----------------------------------------------|----------| +| `size` | current size | Int | +| `add(elements...)` | add one or more elements to the end | Any | +| `addAt(index,elements...)` | insert elements at position | Int, Any | +| `removeAt(index)` | remove element at position | Int | +| `removeAt(start,end)` | remove range, start inclusive, end exclusive | Int, Int | +| | | | diff --git a/docs/tutorial.md b/docs/tutorial.md index 4a6e6fb..e6a1aa1 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -116,31 +116,49 @@ It is rather simple, like everywhere else: See [math](math.md) for more on it. Notice using Greek as identifier, all languages are allowed. Logical operation could be used the same - + var x = 10 ++x >= 11 >>> true ## Supported operators -| op | ass | args | -|:--------:|-----|-------------------| -| + | += | Int or Real | -| - | -= | Int or Real | -| * | *= | Int or Real | -| / | /= | Int or Real | -| % | %= | Int or Real | -| && | | Bool | -| \|\| | | Bool | -| !x | | Bool | -| < | | String, Int, Real | -| <= | | String, Int, Real | -| >= | | String, Int, Real | -| > | | String, Int, Real | -| == | | Any | -| != | | Any | -| ++a, a++ | | Int | -| --a, a-- | | Int | +| op | ass | args | comments | +|:--------:|-----|-------------------|----------| +| + | += | Int or Real | | +| - | -= | Int or Real | infix | +| * | *= | Int or Real | | +| / | /= | Int or Real | | +| % | %= | Int or Real | | +| && | | Bool | | +| \|\| | | Bool | | +| !x | | Bool | | +| < | | String, Int, Real | (1) | +| <= | | String, Int, Real | (1) | +| >= | | String, Int, Real | (1) | +| > | | String, Int, Real | (1) | +| == | | Any | (1) | +| === | | Any | (2) | +| !== | | Any | (2) | +| != | | Any | (1) | +| ++a, a++ | | Int | | +| --a, a-- | | Int | | + +(1) +: comparison are based on comparison operator which can be overloaded + +(2) +: referential equality means left and right operands references exactly same instance of some object. Nothe that all +singleton object, like `null`, are referentially equal too, while string different literals even being equal are most +likely referentially not equal: + + assert( null == null) // singletons + assert( null === null) + // but, for non-singletons: + assert( 5 == 5) + assert( 5 !== 5) + assert( "foo" !== "foo" ) + >>> void # Variables @@ -244,7 +262,7 @@ to call it: If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?) -# Lists (arrays) +# Lists (aka arrays) Ling has built-in mutable array class `List` with simple literals: @@ -259,7 +277,7 @@ Lists can contain any type of objects, lists too: assert(list[1].size == 2) >>> void -Notice usage of indexing. +Notice usage of indexing. You can use negative indexes to offset from the end of the list; see more in [Lists](List.md). When you want to "flatten" it to single array, you can use splat syntax: @@ -273,13 +291,66 @@ Of course, you can splat from anything that is List (or list-like, but it will b ["start", ...b, ...a, "end"] >>> ["start", 10.1, 20.2, "one", "two", "end"] -Of course, you can set any array element: +Of course, you can set any list element: val a = [1, 2, 3] a[1] = 200 a >>> [1, 200, 3] +Lists are comparable, as long as their respective elements are: + + assert( [1,2,3] == [1,2,3]) + + // but they are _different_ objects: + assert( [1,2,3] !== [1,2,3]) + + // when sizes are different, but common part is equal, + // longer is greater + assert( [1,2,3] > [1,2] ) + + // otherwise, where the common part is greater, the list is greater: + assert( [1,2,3] < [1,3] ) + >>> void + +All comparison operators with list are working ok. Also, you can concatenate lists: + + assert( [5, 4] + ["foo", 2] == [5, 4, "foo", 2]) + >>> void + +To add elements to the list: + + val x = [1,2] + x.add(3) + assert( x == [1,2,3]) + // same as x += ["the", "end"] but faster: + x.add("the", "end") + assert( x == [1, 2, 3, "the", "end"]) + >>> void + +Self-modifying concatenation by `+=` also works: + + val x = [1, 2] + x += [3, 4] + assert( x == [1, 2, 3, 4]) + >>> void + +You can insert elements at any position using `addAt`: + + val x = [1,2,3] + x.addAt(1, "foo", "bar") + assert( x == [1, "foo", "bar", 2, 3]) + >>> void + +## Removing list items + + val x = [1, 2, 3, 4, 5] + x.removeAt(2) + assert( x == [1, 2, 4, 5]) + // or remove range (start inclusive, end exclusive): + x.removeAt(1,3) + assert( x == [1, 5]) + >>> void # Flow control operators diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt index e44c906..5fb79fa 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt @@ -13,12 +13,6 @@ data class Arguments(val list: List): Iterable { return list.first().value } - inline fun required(index: Int, context: Context): T { - if( list.size <= index ) context.raiseError("Expected at least ${index+1} argument, got ${list.size}") - return (list[index].value as? T) - ?: context.raiseError("Expected type ${T::class.simpleName}, got ${list[index].value::class.simpleName}") - } - companion object { val EMPTY = Arguments(emptyList()) } @@ -27,5 +21,3 @@ data class Arguments(val list: List): Iterable { return list.map { it.value }.iterator() } } - -fun List.toArguments() = Arguments(this ) \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index ff89023..2068f03 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -148,7 +148,10 @@ class Compiler( v.callInstanceMethod( context, next.value, - args.toArguments() + Arguments(args.map { + val st = it.value as Statement + Arguments.Info(st.execute(context),it.pos) } + ) ), isMutable = false ) } @@ -838,6 +841,8 @@ class Compiler( // equality/ne 4 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) }, + Operator.simple(Token.Type.REF_NEQ, lastPrty) { _, a, b -> ObjBool(a !== b) }, // relational <=,... 5 Operator.simple(Token.Type.LTE, ++lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) }, Operator.simple(Token.Type.LT, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) }, diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 40dcdde..a1ec5df 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -17,6 +17,8 @@ class Context( @Suppress("unused") fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this)) + fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg)) + fun raiseError(message: String): Nothing { throw ExecutionError(ObjError(this, message)) } @@ -25,6 +27,15 @@ class Context( throw ExecutionError(obj) } + inline fun requiredArg(index: Int): T { + if( args.list.size <= index ) raiseError("Expected at least ${index+1} argument, got ${args.list.size}") + return (args.list[index].value as? T) + ?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index].value::class.simpleName}") + } + + inline fun thisAs(): T = (thisObj as? T) + ?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") + private val objects = mutableMapOf() operator fun get(name: String): StoredObj? = diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index 0eef63c..4bfdadc 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -31,6 +31,8 @@ sealed class Obj { // private val memberMutex = Mutex() private val parentInstances = listOf() + open fun inspect(): String = toString() + /** * Some objects are by-value, historically [ObjInt] and [ObjReal] are usually treated as such. * When initializing a var with it, by value objects must be copied. By-reference ones aren't. @@ -255,7 +257,13 @@ data class ObjString(val value: String) : Obj() { return this.value.compareTo(other.value) } - override fun toString(): String = "\"$value\"" + override fun toString(): String = value + + override val asStr: ObjString by lazy { this } + + override fun inspect(): String { + return "\"$value\"" + } override val objClass: ObjClass get() = type @@ -445,33 +453,6 @@ data class ObjBool(val value: Boolean) : Obj() { // } //} -class ObjList(val list: MutableList) : Obj() { - - override fun toString(): String = "[${list.joinToString(separator = ", ")}]" - - override suspend fun getAt(context: Context, index: Int): Obj { - return list[index] - } - - override suspend fun putAt(context: Context, index: Int, newValue: Obj) { - list[index] = newValue - } - - override val objClass: ObjClass - get() = type - - companion object { - val type = ObjClass("List").apply { - createField("size", - statement(Pos.builtIn) { - (it.thisObj as ObjList).list.size.toObj() - }, - false - ) - } - } -} - data class ObjNamespace(val name: String) : Obj() { override fun toString(): String { return "namespace ${name}" @@ -485,3 +466,4 @@ open class ObjError(val context: Context, val message: String) : Obj() { class ObjNullPointerError(context: Context) : ObjError(context, "object is null") class ObjAssertionError(context: Context, message: String) : ObjError(context, message) +class ObjClassCastError(context: Context, message: String) : ObjError(context, message) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt new file mode 100644 index 0000000..ce5e0bf --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt @@ -0,0 +1,93 @@ +package net.sergeych.ling + +class ObjList(val list: MutableList) : Obj() { + + override fun toString(): String = "[${ + list.joinToString(separator = ", ") { it.inspect() } + }]" + + fun normalize(context: Context, index: Int): Int { + val i = if (index < 0) list.size + index else index + if (i !in list.indices) context.raiseError("index $index out of bounds for size ${list.size}") + return i + } + + override suspend fun getAt(context: Context, index: Int): Obj { + val i = normalize(context, index) + return list[i] + } + + override suspend fun putAt(context: Context, index: Int, newValue: Obj) { + val i = normalize(context, index) + list[i] = newValue + } + + override suspend fun compareTo(context: Context, other: Obj): Int { + if (other !is ObjList) context.raiseError("cannot compare $this with $other") + val mySize = list.size + val otherSize = other.list.size + val commonSize = minOf(mySize, otherSize) + for (i in 0.. -1 + mySize > otherSize -> 1 + else -> 0 + } + } + + override suspend fun plus(context: Context, other: Obj): Obj { + (other as? ObjList) ?: context.raiseError("cannot concatenate $this with $other") + return ObjList((list + other.list).toMutableList()) + } + + override suspend fun plusAssign(context: Context, other: Obj): Obj { + (other as? ObjList) ?: context.raiseError("cannot concatenate $this with $other") + list += other.list + return this + } + + override val objClass: ObjClass + get() = type + + companion object { + val type = ObjClass("List").apply { + createField("size", + statement { + (thisObj as ObjList).list.size.toObj() + } + ) + createField("add", + statement { + val l = thisAs().list + for (a in args) l.add(a) + ObjVoid + } + ) + createField("addAt", + statement { + if (args.size < 2) raiseError("addAt takes 2+ arguments") + val l = thisAs() + var index = l.normalize(this, requiredArg(0).value.toInt()) + for (i in 1..() + val start = self.normalize(this, requiredArg(0).value.toInt()) + if (args.size == 2) { + val end = requiredArg(1).value.toInt() + self.list.subList(start, self.normalize(this, end)).clear() + } else + self.list.removeAt(start) + self + }) + } + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index ef5cb16..87c46ee 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -45,7 +45,12 @@ private class Parser(fromPos: Pos) { '=' -> { if (pos.currentChar == '=') { pos.advance() - Token("==", from, Token.Type.EQ) + if( currentChar == '=' ) { + pos.advance() + Token("===", from, Token.Type.REF_EQ) + } + else + Token("==", from, Token.Type.EQ) } else Token("=", from, Token.Type.ASSIGN) } @@ -150,7 +155,12 @@ private class Parser(fromPos: Pos) { '!' -> { if (currentChar == '=') { pos.advance() - Token("!=", from, Token.Type.NEQ) + if( currentChar == '=' ) { + pos.advance() + Token("!==", from, Token.Type.REF_NEQ) + } + else + Token("!=", from, Token.Type.NEQ) } else Token("!", from, Token.Type.NOT) } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 1706064..183405d 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -48,7 +48,7 @@ class Script( } addVoidFn("assert") { - val cond = args.required(0, this) + val cond = requiredArg(0) if( !cond.value == true ) raiseError(ObjAssertionError(this,"Assertion failed")) } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index 27a8063..503ea3c 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -10,7 +10,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) { PLUS, MINUS, STAR, SLASH, PERCENT, ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, PLUS2, MINUS2, - EQ, NEQ, LT, LTE, GT, GTE, + EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON, SINLGE_LINE_COMMENT, MULTILINE_COMMENT, LABEL, ATLABEL, // label@ at@label diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt index 3f18f76..da4f64d 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt @@ -45,4 +45,10 @@ fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false override suspend fun execute(context: Context): Obj = f(context) } +fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend Context.() -> Obj): Statement = + object : Statement(isStaticConst, isConst) { + override val pos: Pos = Pos.builtIn + override suspend fun execute(context: Context): Obj = f(context) + } + diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 244e575..f234a66 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -795,6 +795,18 @@ class ScriptTest { """.trimIndent()) } + @Test + fun testArrayCompare() = runTest { + eval(""" + val a = [4,3] + val b = [4,3] + assert(a == b) + assert( a === a ) + assert( !(a === b) ) + assert( a !== b ) + """.trimIndent()) + } + // @Test // fun testLambda1() = runTest { // val l = eval(""" diff --git a/library/src/jvmTest/kotlin/BookTest.kt b/library/src/jvmTest/kotlin/BookTest.kt index 5a99d43..33e7e30 100644 --- a/library/src/jvmTest/kotlin/BookTest.kt +++ b/library/src/jvmTest/kotlin/BookTest.kt @@ -151,7 +151,7 @@ suspend fun DocTest.test() { } catch (e: Throwable) { error = e null - }?.toString()?.replace(Regex("@\\d+"), "@...") + }?.inspect()?.replace(Regex("@\\d+"), "@...") if (error != null || expectedOutput != collectedOutput.toString() || expectedResult != result