diff --git a/docs/Range.md b/docs/Range.md new file mode 100644 index 0000000..6a0b9f7 --- /dev/null +++ b/docs/Range.md @@ -0,0 +1,61 @@ +# Range + +Range is diapason between two values. Open range has at least one end open, e.g. ±∞, closed range has both ends open. + +## Closed ranges + +The syntax is intuitive and adopted from Kotlin: + + // end inclusive: + val r = 1..5 + assert(5 in r) + assert(6 !in r) + assert(0 !in r) + assert(2 in r) + >>> void + +Exclusive end ranges are adopted from kotlin either: + + // end inclusive: + val r = 1..<5 + assert(5 !in r) + assert(6 !in r) + assert(0 !in r) + assert(2 in r) + assert(4 in r) + >>> void + +In any case, we can test an object to belong to using `in` and `!in` and +access limits: + + val r = 0..5 + (r.end - r.start)/2 + >>> 2 + +Notice, start and end are ints, so midpoint here is int too. + +It is possible to test that one range is included in another range too, +one range is defined as _contained_ in another ifm and only if, it begin and end +are equal or within another, taking into account the end-inclusiveness: + + assert( (1..3) in (1..3) ) + assert( (0..3) !in (1..3) ) + assert( (1..2) in (1..<3) ) + assert( (1..<2) in (1..<3) ) + assert( (1..<3) in (1..3) ) + >>> void + + +# Instance members + +| member | description | args | +|-----------------|----------------------------|---------------| +| contains(other) | used in `in` | Range, or Any | +| inclusiveEnd | true for '..' | Bool | +| isOpen | at any end | Bool | +| isIntRange | both start and end are Int | Bool | +| start | | Bool | +| end | | Bool | +| | | | +| | | | +| | | | diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 4bdd587..308e5c3 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -23,9 +23,9 @@ class Compiler( return Script(start, statements) } - private fun parseStatement(tokens: CompilerContext): Statement? { + private fun parseStatement(cc: CompilerContext): Statement? { while (true) { - val t = tokens.next() + val t = cc.next() return when (t.type) { Token.Type.ID -> { // could be keyword, assignment or just the expression @@ -44,16 +44,16 @@ class Compiler( // get back the token which is not '=': // tokens.previous() // try keyword statement - parseKeywordStatement(t, tokens) + parseKeywordStatement(t, cc) ?: run { - tokens.previous() - parseExpression(tokens) + cc.previous() + parseExpression(cc) } } Token.Type.PLUS2, Token.Type.MINUS2 -> { - tokens.previous() - parseExpression(tokens) + cc.previous() + parseExpression(cc) } Token.Type.LABEL -> continue @@ -64,12 +64,12 @@ class Compiler( Token.Type.SEMICOLON -> continue Token.Type.LBRACE -> { - tokens.previous() - parseBlock(tokens) + cc.previous() + parseBlock(cc) } Token.Type.RBRACE -> { - tokens.previous() + cc.previous() return null } @@ -77,8 +77,8 @@ class Compiler( else -> { // could be expression - tokens.previous() - parseExpression(tokens) + cc.previous() + parseExpression(cc) } } } @@ -299,7 +299,20 @@ class Compiler( }.value.decrementAndGet(ctx).asReadonly } } + } + Token.Type.DOTDOT, Token.Type.DOTDOTLT -> { + // closed-range operator + val inclusiveEnd = t.type == Token.Type.DOTDOT + val left = operand + val right = parseStatement(cc) + operand = Accessor { + ObjRange( + left?.getter?.invoke(it)?.value ?: ObjNull, + right?.execute(it) ?: ObjNull, + inclusiveEnd = inclusiveEnd + ).asReadonly + } } @@ -560,7 +573,7 @@ class Compiler( current = sourceObj.getAt(forContext, index) } } - if( !breakCaught && elseStatement != null) { + if (!breakCaught && elseStatement != null) { result = elseStatement.execute(it) } result @@ -859,7 +872,7 @@ class Compiler( private var lastPrty = 0 val allOps = listOf( - // assignments + // assignments, lowest priority Operator(Token.Type.ASSIGN, lastPrty) { pos, a, b -> Accessor { val value = b.getter(it).value @@ -942,6 +955,9 @@ class Compiler( Operator.simple(Token.Type.LT, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) }, Operator.simple(Token.Type.GTE, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) >= 0) }, Operator.simple(Token.Type.GT, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) > 0) }, + // in, is: + Operator.simple(Token.Type.IN, lastPrty) { c, a, b -> ObjBool(b.contains(c, a)) }, + Operator.simple(Token.Type.NOTIN, lastPrty) { c, a, b -> ObjBool(!b.contains(c, a)) }, // shuttle <=> 6 // bit shifts 7 Operator.simple(Token.Type.PLUS, ++lastPrty) { ctx, a, b -> a.plus(ctx, b) }, diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index fdc54cb..5a840a6 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -4,8 +4,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlin.math.floor -import kotlin.math.roundToLong //typealias InstanceMethod = (Context, Obj) -> Obj @@ -67,6 +65,10 @@ sealed class Obj { context.raiseNotImplemented() } + open suspend fun contains(context: Context, other: Obj): Boolean { + context.raiseNotImplemented() + } + open val asStr: ObjString by lazy { if (this is ObjString) this else ObjString(this.toString()) } @@ -75,7 +77,16 @@ sealed class Obj { * Class of the object: definition of member functions (top-level), etc. * Note that using lazy allows to avoid endless recursion here */ - open val objClass: ObjClass by lazy { ObjClass("Obj") } + open val objClass: ObjClass by lazy { + ObjClass("Obj").apply { + addFn("toString") { + thisObj.asStr + } + addFn("contains") { + ObjBool(thisObj.contains(this, args.firstAndOnly())) + } + } + } open suspend fun plus(context: Context, other: Obj): Obj { context.raiseNotImplemented() @@ -198,6 +209,8 @@ sealed class Obj { suspend fun invoke(context: Context, atPos: Pos, thisObj: Obj, args: Arguments): Obj = callOn(context.copy(atPos, args = args, newThisObj = thisObj)) + + val asReadonly: WithAccess by lazy { WithAccess(this, false) } val asMutable: WithAccess by lazy { WithAccess(this, true) } @@ -274,176 +287,83 @@ fun Obj.toBool(): Boolean = (this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean $this") -data class ObjReal(val value: Double) : Obj(), Numeric { - override val asStr by lazy { ObjString(value.toString()) } - override val longValue: Long by lazy { floor(value).toLong() } - override val doubleValue: Double by lazy { value } - override val toObjInt: ObjInt by lazy { ObjInt(longValue) } - override val toObjReal: ObjReal by lazy { ObjReal(value) } - - override fun byValueCopy(): Obj = ObjReal(value) - - override suspend fun compareTo(context: Context, other: Obj): Int { - if (other !is Numeric) return -2 - return value.compareTo(other.doubleValue) - } - - override fun toString(): String = value.toString() +class ObjRange(val start: Obj?, val end: Obj?,val inclusiveEnd: Boolean) : Obj() { override val objClass: ObjClass = type - override suspend fun plus(context: Context, other: Obj): Obj = - ObjReal(this.value + other.toDouble()) + override fun toString(): String { + val result = StringBuilder() + result.append("${start ?: '∞'} ..") + if( !inclusiveEnd) result.append('<') + result.append(" ${end ?: '∞'}") + return result.toString() + } - override suspend fun minus(context: Context, other: Obj): Obj = - ObjReal(this.value - other.toDouble()) + suspend fun containsRange(context: Context, other: ObjRange): Boolean { + if( start != null ) { + // our start is not -∞ so other start should be GTE or is not contained: + if( other.start != null && start.compareTo(context, other.start) > 0) return false + } + if( end != null ) { + // same with the end: if it is open, it can't be contained in ours: + if( other.end == null ) return false + // both exists, now there could be 4 cases: + return when { + other.inclusiveEnd && inclusiveEnd -> + end.compareTo(context, other.end) >= 0 + !other.inclusiveEnd && !inclusiveEnd -> + end.compareTo(context, other.end) >= 0 + other.inclusiveEnd && !inclusiveEnd -> + end.compareTo(context, other.end) > 0 + !other.inclusiveEnd && inclusiveEnd -> + end.compareTo(context, other.end) >= 0 + else -> throw IllegalStateException("unknown comparison") + } + } + return true + } - override suspend fun mul(context: Context, other: Obj): Obj = - ObjReal(this.value * other.toDouble()) + override suspend fun contains(context: Context, other: Obj): Boolean { - override suspend fun div(context: Context, other: Obj): Obj = - ObjReal(this.value / other.toDouble()) + if( other is ObjRange) + return containsRange(context, other) - override suspend fun mod(context: Context, other: Obj): Obj = - ObjReal(this.value % other.toDouble()) + if (start == null && end == null) return true + if (start != null) { + if (start.compareTo(context, other) > 0) return false + } + if (end != null) { + val cmp = end.compareTo(context, other) + if (inclusiveEnd && cmp < 0 || !inclusiveEnd && cmp <= 0) return false + } + return true + } + + val isIntRange: Boolean by lazy { + start is ObjInt && end is ObjInt + } companion object { - val type: ObjClass = ObjClass("Real").apply { - createField( - "roundToInt", - statement(Pos.builtIn) { - (it.thisObj as ObjReal).value.roundToLong().toObj() - }, - ) + val type = ObjClass("Range").apply { + addFn("start" ) { + thisAs().start ?: ObjNull + } + addFn("end") { + thisAs().end ?: ObjNull + } + addFn("isOpen") { + thisAs().let { it.start == null || it.end == null }.toObj() + } + addFn("isIntRange") { + thisAs().isIntRange.toObj() + } + addFn("inclusiveEnd") { + thisAs().inclusiveEnd.toObj() + } } } } -data class ObjInt(var value: Long) : Obj(), Numeric { - override val asStr get() = ObjString(value.toString()) - override val longValue get() = value - override val doubleValue get() = value.toDouble() - override val toObjInt get() = this - override val toObjReal = ObjReal(doubleValue) - - override fun byValueCopy(): Obj = ObjInt(value) - - override suspend fun getAndIncrement(context: Context): Obj { - return ObjInt(value).also { value++ } - } - - override suspend fun getAndDecrement(context: Context): Obj { - return ObjInt(value).also { value-- } - } - - override suspend fun incrementAndGet(context: Context): Obj { - return ObjInt(++value) - } - - override suspend fun decrementAndGet(context: Context): Obj { - return ObjInt(--value) - } - - override suspend fun compareTo(context: Context, other: Obj): Int { - if (other !is Numeric) return -2 - return value.compareTo(other.doubleValue) - } - - override fun toString(): String = value.toString() - - override val objClass: ObjClass = type - - override suspend fun plus(context: Context, other: Obj): Obj = - if (other is ObjInt) - ObjInt(this.value + other.value) - else - ObjReal(this.doubleValue + other.toDouble()) - - override suspend fun minus(context: Context, other: Obj): Obj = - if (other is ObjInt) - ObjInt(this.value - other.value) - else - ObjReal(this.doubleValue - other.toDouble()) - - override suspend fun mul(context: Context, other: Obj): Obj = - if (other is ObjInt) { - ObjInt(this.value * other.value) - } else ObjReal(this.value * other.toDouble()) - - override suspend fun div(context: Context, other: Obj): Obj = - if (other is ObjInt) - ObjInt(this.value / other.value) - else ObjReal(this.value / other.toDouble()) - - override suspend fun mod(context: Context, other: Obj): Obj = - if (other is ObjInt) - ObjInt(this.value % other.value) - else ObjReal(this.value.toDouble() % other.toDouble()) - - /** - * We are by-value type ([byValueCopy] is implemented) so we can do in-place - * assignment - */ - override suspend fun assign(context: Context, other: Obj): Obj? { - return if (other is ObjInt) { - value = other.value - this - } else null - } - - companion object { - val type = ObjClass("Int") - } -} - -data class ObjBool(val value: Boolean) : Obj() { - override val asStr by lazy { ObjString(value.toString()) } - - override suspend fun compareTo(context: Context, other: Obj): Int { - if (other !is ObjBool) return -2 - return value.compareTo(other.value) - } - - override fun toString(): String = value.toString() - - override val objClass: ObjClass = type - - override suspend fun logicalNot(context: Context): Obj = ObjBool(!value) - - override suspend fun logicalAnd(context: Context, other: Obj): Obj = ObjBool(value && other.toBool()) - - override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool()) - - companion object { - val type = ObjClass("Bool") - } -} - -//open class ObjProperty(var value: Obj =ObjVoid) { -// open suspend fun get(context: Context): Obj = value -// open suspend fun set(context: Context,newValue: Obj): Obj { -// return value.also { value = newValue } -// } -//} -class ObjChar(val value: Char): Obj() { - - override val objClass: ObjClass = type - - override suspend fun compareTo(context: Context, other: Obj): Int = - (other as? ObjChar)?.let { value.compareTo(it.value) } ?: -1 - - override fun toString(): String = value.toString() - - override fun inspect(): String = "'$value'" - - companion object { - val type = ObjClass("Char").apply { - addFn("toInt") { ObjInt(thisAs().value.code.toLong()) } - } - } -} - - data class ObjNamespace(val name: String) : Obj() { override fun toString(): String { return "namespace ${name}" diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjBool.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjBool.kt new file mode 100644 index 0000000..3a55795 --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjBool.kt @@ -0,0 +1,24 @@ +package net.sergeych.ling + +data class ObjBool(val value: Boolean) : Obj() { + override val asStr by lazy { ObjString(value.toString()) } + + override suspend fun compareTo(context: Context, other: Obj): Int { + if (other !is ObjBool) return -2 + return value.compareTo(other.value) + } + + override fun toString(): String = value.toString() + + override val objClass: ObjClass = type + + override suspend fun logicalNot(context: Context): Obj = ObjBool(!value) + + override suspend fun logicalAnd(context: Context, other: Obj): Obj = ObjBool(value && other.toBool()) + + override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool()) + + companion object { + val type = ObjClass("Bool") + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjInt.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjInt.kt new file mode 100644 index 0000000..99b76ad --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjInt.kt @@ -0,0 +1,78 @@ +package net.sergeych.ling + +data class ObjInt(var value: Long) : Obj(), Numeric { + override val asStr get() = ObjString(value.toString()) + override val longValue get() = value + override val doubleValue get() = value.toDouble() + override val toObjInt get() = this + override val toObjReal = ObjReal(doubleValue) + + override fun byValueCopy(): Obj = ObjInt(value) + + override suspend fun getAndIncrement(context: Context): Obj { + return ObjInt(value).also { value++ } + } + + override suspend fun getAndDecrement(context: Context): Obj { + return ObjInt(value).also { value-- } + } + + override suspend fun incrementAndGet(context: Context): Obj { + return ObjInt(++value) + } + + override suspend fun decrementAndGet(context: Context): Obj { + return ObjInt(--value) + } + + override suspend fun compareTo(context: Context, other: Obj): Int { + if (other !is Numeric) return -2 + return value.compareTo(other.doubleValue) + } + + override fun toString(): String = value.toString() + + override val objClass: ObjClass = type + + override suspend fun plus(context: Context, other: Obj): Obj = + if (other is ObjInt) + ObjInt(this.value + other.value) + else + ObjReal(this.doubleValue + other.toDouble()) + + override suspend fun minus(context: Context, other: Obj): Obj = + if (other is ObjInt) + ObjInt(this.value - other.value) + else + ObjReal(this.doubleValue - other.toDouble()) + + override suspend fun mul(context: Context, other: Obj): Obj = + if (other is ObjInt) { + ObjInt(this.value * other.value) + } else ObjReal(this.value * other.toDouble()) + + override suspend fun div(context: Context, other: Obj): Obj = + if (other is ObjInt) + ObjInt(this.value / other.value) + else ObjReal(this.value / other.toDouble()) + + override suspend fun mod(context: Context, other: Obj): Obj = + if (other is ObjInt) + ObjInt(this.value % other.value) + else ObjReal(this.value.toDouble() % other.toDouble()) + + /** + * We are by-value type ([byValueCopy] is implemented) so we can do in-place + * assignment + */ + override suspend fun assign(context: Context, other: Obj): Obj? { + return if (other is ObjInt) { + value = other.value + this + } else null + } + + companion object { + val type = ObjClass("Int") + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjReal.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjReal.kt new file mode 100644 index 0000000..7e6fc73 --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjReal.kt @@ -0,0 +1,49 @@ +package net.sergeych.ling + +import kotlin.math.floor +import kotlin.math.roundToLong + +data class ObjReal(val value: Double) : Obj(), Numeric { + override val asStr by lazy { ObjString(value.toString()) } + override val longValue: Long by lazy { floor(value).toLong() } + override val doubleValue: Double by lazy { value } + override val toObjInt: ObjInt by lazy { ObjInt(longValue) } + override val toObjReal: ObjReal by lazy { ObjReal(value) } + + override fun byValueCopy(): Obj = ObjReal(value) + + override suspend fun compareTo(context: Context, other: Obj): Int { + if (other !is Numeric) return -2 + return value.compareTo(other.doubleValue) + } + + override fun toString(): String = value.toString() + + override val objClass: ObjClass = type + + override suspend fun plus(context: Context, other: Obj): Obj = + ObjReal(this.value + other.toDouble()) + + override suspend fun minus(context: Context, other: Obj): Obj = + ObjReal(this.value - other.toDouble()) + + override suspend fun mul(context: Context, other: Obj): Obj = + ObjReal(this.value * other.toDouble()) + + override suspend fun div(context: Context, other: Obj): Obj = + ObjReal(this.value / other.toDouble()) + + override suspend fun mod(context: Context, other: Obj): Obj = + ObjReal(this.value % other.toDouble()) + + companion object { + val type: ObjClass = ObjClass("Real").apply { + createField( + "roundToInt", + statement(Pos.builtIn) { + (it.thisObj as ObjReal).value.roundToLong().toObj() + }, + ) + } + } +} \ 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 440f643..db71138 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -45,11 +45,10 @@ private class Parser(fromPos: Pos) { '=' -> { if (pos.currentChar == '=') { pos.advance() - if( currentChar == '=' ) { + if (currentChar == '=') { pos.advance() Token("===", from, Token.Type.REF_EQ) - } - else + } else Token("==", from, Token.Type.EQ) } else Token("=", from, Token.Type.ASSIGN) @@ -127,9 +126,9 @@ private class Parser(fromPos: Pos) { pos.advance() Token("...", from, Token.Type.ELLIPSIS) } else if (currentChar == '<') { + pos.advance() Token("..<", from, Token.Type.DOTDOTLT) } else { - pos.back() Token("..", from, Token.Type.DOTDOT) } } else @@ -153,16 +152,25 @@ private class Parser(fromPos: Pos) { } '!' -> { - if (currentChar == '=') { + if (currentChar == 'i') { pos.advance() - if( currentChar == '=' ) { + if( currentChar == 'n') { pos.advance() - Token("!==", from, Token.Type.REF_NEQ) + Token("!in", from, Token.Type.NOTIN) + } else { + pos.back() + Token("!", from, Token.Type.NOT) } - else - Token("!=", from, Token.Type.NEQ) } else - Token("!", from, Token.Type.NOT) + if (currentChar == '=') { + pos.advance() + if (currentChar == '=') { + pos.advance() + Token("!==", from, Token.Type.REF_NEQ) + } else + Token("!=", from, Token.Type.NEQ) + } else + Token("!", from, Token.Type.NOT) } '|' -> { @@ -210,7 +218,7 @@ private class Parser(fromPos: Pos) { if (currentChar == '\\') { value = currentChar pos.advance() - value = when(value) { + value = when (value) { 'n' -> '\n' 'r' -> '\r' 't' -> '\t' @@ -218,12 +226,13 @@ private class Parser(fromPos: Pos) { else -> throw ScriptError(currentPos, "unsupported escape character: $value") } } - if( currentChar != '\'' ) throw ScriptError(currentPos, "expected end of character literal: '") + if (currentChar != '\'') throw ScriptError(currentPos, "expected end of character literal: '") pos.advance() Token(value.toString(), start, Token.Type.CHAR) } else -> { + // text infix operators: // Labels processing is complicated! // some@ statement: label 'some', ID 'statement' // statement@some: ID 'statement', LABEL 'some'! @@ -238,7 +247,10 @@ private class Parser(fromPos: Pos) { } else Token(text, from, Token.Type.LABEL) } else - Token(text, from, Token.Type.ID) + when (text) { + "in" -> Token("in", from, Token.Type.IN) + else -> Token(text, from, Token.Type.ID) + } } else raise("can't parse token") } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 616ab82..07448c4 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -59,6 +59,7 @@ class Script( addConst("Bool", ObjBool.type) addConst("Char", ObjChar.type) addConst("List", ObjList.type) + addConst("Range", ObjRange.type) val pi = ObjReal(PI) addConst("π", pi) getOrCreateNamespace("Math").apply { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index ee05953..094f92a 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -11,6 +11,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, + IN, NOTIN, IS, NOTIS, 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, diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 9ff4386..3ec5564 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -65,6 +65,36 @@ class ScriptTest { } + @Test + fun parseRangeTest() { + var tt = parseLing("5 .. 4".toSource()) + + assertEquals(Token.Type.INT, tt[0].type) + assertEquals(Token.Type.DOTDOT, tt[1].type) + assertEquals(Token.Type.INT, tt[2].type) + + tt = parseLing("5 ..< 4".toSource()) + + assertEquals(Token.Type.INT, tt[0].type) + assertEquals(Token.Type.DOTDOTLT, tt[1].type) + assertEquals(Token.Type.INT, tt[2].type) + } + + @Test + fun parseInTest() { + var tt = parseLing("5 in 4".toSource()) + + assertEquals(Token.Type.INT, tt[0].type) + assertEquals(Token.Type.IN, tt[1].type) + assertEquals(Token.Type.INT, tt[2].type) + + tt = parseLing("5 ..< 4".toSource()) + + assertEquals(Token.Type.INT, tt[0].type) + assertEquals(Token.Type.DOTDOTLT, tt[1].type) + assertEquals(Token.Type.INT, tt[2].type) + } + @Test fun parserLabelsTest() { val src = "label@ break@label".toSource() @@ -748,35 +778,42 @@ class ScriptTest { @Test fun testListLiteral() = runTest { - eval(""" + eval( + """ val list = [1,22,3] assert(list[0] == 1) assert(list[1] == 22) assert(list[2] == 3) - """.trimIndent()) + """.trimIndent() + ) - eval(""" + eval( + """ val x0 = 100 val list = [x0 + 1, x0 * 10, 3] assert(list[0] == 101) assert(list[1] == 1000) assert(list[2] == 3) - """.trimIndent()) + """.trimIndent() + ) - eval(""" + eval( + """ val x0 = 100 val list = [x0 + 1, x0 * 10, if(x0 < 100) "low" else "high", 5] assert(list[0] == 101) assert(list[1] == 1000) assert(list[2] == "high") assert(list[3] == 5) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testListLiteralSpread() = runTest { - eval(""" + eval( + """ val list1 = [1,22,3] val list = ["start", ...list1, "end"] assert(list[0] == "start") @@ -784,40 +821,48 @@ class ScriptTest { assert(list[2] == 22) assert(list[3] == 3) assert(list[4] == "end") - """.trimIndent()) + """.trimIndent() + ) } @Test fun testListSize() = runTest { - eval(""" + eval( + """ val a = [4,3] assert(a.size == 2) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testArrayCompare() = runTest { - eval(""" + eval( + """ val a = [4,3] val b = [4,3] assert(a == b) assert( a === a ) assert( !(a === b) ) assert( a !== b ) - """.trimIndent()) + """.trimIndent() + ) } @Test fun forLoop1() = runTest { - eval(""" + eval( + """ var sum = 0 for(i in [1,2,3]) { println(i) sum += i } assert(sum == 6) - """.trimIndent()) - eval(""" + """.trimIndent() + ) + eval( + """ fun test1(array) { var sum = 0 for(i in array) { @@ -827,13 +872,15 @@ class ScriptTest { } println("result=",test1([1,2])) println("result=",test1([1,2,3])) - """.trimIndent()) + """.trimIndent() + ) } @Test fun forLoop2() = runTest { - println(eval( - """ + println( + eval( + """ fun search(haystack, needle) { for(ch in haystack) { if( ch == needle) @@ -844,7 +891,88 @@ class ScriptTest { assert( search("hello", 'l') == "found") assert( search("hello", 'z') == null) """.trimIndent() - ).toString()) + ).toString() + ) + } + + @Test + fun testIntOpenRangeInclusive() = runTest { + eval( + """ + val r = 10 .. 20 + assert( r::class == Range) + assert(r.isOpen == false) + assert(r.start == 10) + assert(r.end == 20) + assert(r.inclusiveEnd == true) + assert(r.isIntRange) + + assert(12 in r) + assert(10 in r) + assert(20 in r) + + assert(9 !in r) + assert(21 !in r) + + assert( (11..12) in r) + assert( (10..11) in r) + assert( (11..20) in r) + assert( (10..20) in r) + + assert( (9..12) !in r) + assert( (1..9) !in r) + assert( (17..22) !in r) + assert( (21..22) !in r) + +// assert(r.size == 11) + """.trimIndent() + ) + } + + @Test + fun testIntOpenRangeExclusive() = runTest { + eval( + """ + val r = 10 ..< 20 + assert( r::class == Range) + assert(r.isOpen == false) + assert(r.start == 10) + assert(r.end == 20) + assert(r.inclusiveEnd == false) + assert(r.isIntRange) + + assert(12 in r) + assert(10 in r) + assert(20 !in r) + + assert(9 !in r) + assert(21 !in r) + + assert( (11..12) in r) + assert( (10..11) in r) + assert( (11..20) !in r) + assert( (10..20) !in r) + + assert( (10..<20) in r) + + assert( (9..12) !in r) + assert( (1..9) !in r) + assert( (17..22) !in r) + assert( (21..22) !in r) + + + """.trimIndent() + ) + } + + @Test + fun testIntOpenRangeInExclusive() = runTest { + eval( + """ + assert( (1..3) !in (1..<3) ) + assert( (1..<3) in (1..3) ) + """.trimIndent() + ) } // @Test diff --git a/library/src/jvmTest/kotlin/BookTest.kt b/library/src/jvmTest/kotlin/BookTest.kt index 011c740..962bf2e 100644 --- a/library/src/jvmTest/kotlin/BookTest.kt +++ b/library/src/jvmTest/kotlin/BookTest.kt @@ -205,4 +205,9 @@ class BookTest { runDocTests("../docs/List.md") } + @Test + fun testFromRange() = runTest { + runDocTests("../docs/Range.md") + } + } \ No newline at end of file