diff --git a/docs/tutorial.md b/docs/tutorial.md index cb82829..ce880b4 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -552,6 +552,89 @@ Or, more neat: >>> just 3 >>> void +## When + +It is very much like the kotlin's: + + fun type(x) { + when(x) { + in 'a'..'z', in 'A'..'Z' -> "letter" + in '0'..'9' -> "digit" + '$' -> "dollar" + "EUR" -> "crap" + in ['@', '#', '^'] -> "punctuation1" + in "*&.," -> "punctuation2" + else -> "unknown" + } + } + assertEquals("digit", type('3')) + assertEquals("dollar", type('$')) + assertEquals("crap", type("EUR")) + >>> void + +Notice, several conditions can be grouped with a comma. +Also, you can check the type too: + + fun type(x) { + when(x) { + "42", 42 -> "answer to the great question" + is Real, is Int -> "number" + is String -> { + for( d in x ) { + if( d !in '0'..'9' ) + break "unknown" + } + else "number" + } + } + } + assertEquals("number", type(5)) + assertEquals("number", type("153")) + assertEquals("number", type(π/2)) + assertEquals("unknown", type("12%")) + assertEquals("answer to the great question", type(42)) + assertEquals("answer to the great question", type("42")) + >>> void + +### supported when conditions: + +#### Contains: + +You can thest that _when expression_ is _contained_, or not contained, in some object using `in container` and `!in container`. The container is any object that provides `contains` method, otherwise the runtime exception will be thrown. + +Typical builtin types that are containers (e.g. support `conain`): + +| class | notes | +|------------|--------------------------------------------| +| Collection | contains an element (1) | +| Array | faster maybe that Collection's | +| List | faster than Array's | +| String | character in string or substring in string | +| Range | object is included in the range (2) | + +(1) +: Iterable is not the container as it can be infinite + +(2) +: Depending on the inclusivity and open/closed range parameters. BE careful here: String range is allowed, but it is usually not what you expect of it: + + assert( "more" in "a".."z") // string range ok + assert( 'x' !in "a".."z") // char in string range: probably error + assert( 'x' in 'a'..'z') // character range: ok + assert( "x" !in 'a'..'z') // string in character range: could be error + >>> void + +So we recommend not to mix characters and string ranges; use `ch in str` that works +as expected: + + "foo" in "foobar" + >>> true + +and also character inclusion: + + 'o' in "foobar" + >>> true + ## while Regular pre-condition while loop, as expression, loop returns the last expression as everything else: diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 97df154..2605880 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "0.5.2-SNAPSHOT" +version = "0.6.0-SNAPSHOT" buildscript { repositories { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index c4d65d8..55aa8d6 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -454,7 +454,7 @@ class Compiler( Token.Type.ID -> { // visibility - val visibility = if( isClassDeclaration && t.value == "private" ) { + val visibility = if (isClassDeclaration && t.value == "private") { t = cc.next() Visibility.Private } else Visibility.Public @@ -650,7 +650,7 @@ class Compiler( "void" -> Accessor { ObjVoid.asReadonly } "null" -> Accessor { ObjNull.asReadonly } "true" -> Accessor { ObjBool(true).asReadonly } - "false" -> Accessor { ObjBool(false).asReadonly } + "false" -> Accessor { ObjFalse.asReadonly } else -> { Accessor({ it.pos = t.pos @@ -709,6 +709,7 @@ class Compiler( "class" -> parseClassDeclaration(cc, false) "try" -> parseTryStatement(cc) "throw" -> parseThrowStatement(cc) + "when" -> parseWhenStatement(cc) else -> { // triples cc.previous() @@ -729,6 +730,113 @@ class Compiler( } } + data class WhenCase(val condition: Statement, val block: Statement) + + private fun parseWhenStatement(cc: CompilerContext): Statement { + // has a value, when(value) ? + var t = cc.skipWsTokens() + return if (t.type == Token.Type.LPAREN) { + // when(value) + val value = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when(value) expected") + cc.skipTokenOfType(Token.Type.RPAREN) + t = cc.next() + if (t.type != Token.Type.LBRACE) throw ScriptError(t.pos, "when { ... } expected") + val cases = mutableListOf() + var elseCase: Statement? = null + lateinit var whenValue: Obj + + // there could be 0+ then clauses + // condition could be a value, in and is clauses: + // parse several conditions for one then clause + + // loop cases + outer@ while (true) { + + var skipParseBody = false + val currentCondition = mutableListOf() + + // loop conditions + while (true) { + t = cc.skipWsTokens() + + when (t.type) { + Token.Type.IN, + Token.Type.NOTIN -> { + // we need a copy in the closure: + val isIn = t.type == Token.Type.IN + val container = parseExpression(cc) ?: throw ScriptError(cc.currentPos(), "type expected") + currentCondition += statement { + val r = container.execute(this).contains(this, whenValue) + ObjBool(if (isIn) r else !r) + } + } + + Token.Type.IS, Token.Type.NOTIS -> { + // we need a copy in the closure: + val isIn = t.type == Token.Type.IS + val caseType = parseExpression(cc) ?: throw ScriptError(cc.currentPos(), "type expected") + currentCondition += statement { + val r = whenValue.isInstanceOf(caseType.execute(this)) + ObjBool(if (isIn) r else !r) + } + } + + Token.Type.COMMA -> + continue + + Token.Type.ARROW -> + break + + Token.Type.RBRACE -> + break@outer + + else -> { + if (t.value == "else") { + cc.skipTokens(Token.Type.ARROW) + if (elseCase != null) throw ScriptError( + cc.currentPos(), + "when else block already defined" + ) + elseCase = + parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when else block expected") + skipParseBody = true + } else { + cc.previous() + val x = parseExpression(cc) + ?: throw ScriptError(cc.currentPos(), "when case condition expected") + currentCondition += statement { + ObjBool(x.execute(this).compareTo(this, whenValue) == 0) + } + } + } + } + } + // parsed conditions? + if (!skipParseBody) { + val block = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when case block expected") + for (c in currentCondition) cases += WhenCase(c, block) + } + } + statement { + var result: Obj = ObjVoid + // in / is and like uses whenValue from closure: + whenValue = value.execute(this) + var found = false + for (c in cases) + if (c.condition.execute(this).toBool()) { + result = c.block.execute(this) + found = true + break + } + if (!found && elseCase != null) result = elseCase.execute(this) + result + } + } else { + // when { cond -> ... } + TODO("when without object is not yet implemented") + } + } + private fun parseThrowStatement(cc: CompilerContext): Statement { val throwStatement = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "throw object expected") return statement { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt index 52ec1cf..0cab7fc 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt @@ -21,7 +21,11 @@ internal class CompilerContext(val tokens: List) { fun hasNext() = currentIndex < tokens.size fun hasPrevious() = currentIndex > 0 - fun next() = tokens.getOrElse(currentIndex) { throw IllegalStateException("No next token") }.also { currentIndex++ } + fun next() = + if( currentIndex < tokens.size ) tokens[currentIndex++] + else Token("", tokens.last().pos, Token.Type.EOF) +// throw IllegalStateException("No more tokens") + fun previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex] fun savePos() = currentIndex @@ -47,9 +51,7 @@ internal class CompilerContext(val tokens: List) { throw ScriptError(at, message) } - fun currentPos() = - if (hasNext()) next().pos.also { previous() } - else previous().pos.also { next() } + fun currentPos(): Pos = tokens[currentIndex].pos /** * Skips next token if its type is `tokenType`, returns `true` if so. @@ -145,19 +147,16 @@ internal class CompilerContext(val tokens: List) { } } -// fun expectKeyword(vararg keyword: String): String { -// val t = next() -// if (t.type != Token.Type.ID && t.value !in keyword) { -// throw ScriptError(t.pos, "expected one of ${keyword.joinToString()}") -// -// } - -// data class ReturnScope(val needCatch: Boolean = false) - -// private val - -// fun startReturnScope(): ReturnScope { -// return ReturnScope() -// } + /** + * Skip newlines and comments. Returns (and reads) first non-whitespace token. + * Note that [Token.Type.EOF] is not considered a whitespace token. + */ + fun skipWsTokens(): Token { + while( current().type in wstokens ) next() + return next() + } + companion object { + val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT) + } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index 3f7a44e..c2fc4c9 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -89,7 +89,7 @@ open class Obj { } open suspend fun contains(context: Context, other: Obj): Boolean { - context.raiseNotImplemented() + return invokeInstanceMethod(context, "contains", other).toBool() } open val asStr: ObjString by lazy { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt index 4c7f777..68cf650 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt @@ -42,7 +42,8 @@ open class ObjClass( visibility: Visibility = Visibility.Public, pos: Pos = Pos.builtIn ) { - if (name in members || allParentsSet.any { name in it.members }) + val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] } + if( existing?.isMutable == false) throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes") members[name] = ObjRecord(initialValue, isMutable, visibility) } @@ -97,7 +98,19 @@ val ObjIterable by lazy { */ val ObjCollection by lazy { val i: ObjClass = ObjIterable - ObjClass("Collection", i) + ObjClass("Collection", i).apply { + // it is not effective, but it is open: + addFn("contains", isOpen = true) { + val obj = args.firstAndOnly() + val it = thisObj.invokeInstanceMethod(this, "iterator") + while (it.invokeInstanceMethod(this, "hasNext").toBool()) { + if( obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0 ) + return@addFn ObjTrue + } + ObjFalse + } + + } } val ObjIterator by lazy { ObjClass("Iterator") } @@ -145,6 +158,15 @@ val ObjArray by lazy { addFn("iterator") { ObjArrayIterator(thisObj).also { it.init(this) } } + + addFn("contains", isOpen = true) { + val obj = args.firstAndOnly() + for( i in 0..< thisObj.invokeInstanceMethod(this, "size").toInt()) { + if( thisObj.getAt(this, i).compareTo(this, obj) == 0 ) return@addFn ObjTrue + } + ObjFalse + } + addFn("isample") { "ok".toObj() } } } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt index f3f57b6..00f2b49 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt @@ -75,6 +75,10 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { return this } + override suspend fun contains(context: Context, other: Obj): Boolean { + return list.contains(other) + } + override val objClass: ObjClass get() = type diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt index 790d2f1..e4acde4 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt @@ -31,6 +31,14 @@ data class ObjString(val value: String) : Obj() { return ObjChar(value[index]) } + override suspend fun contains(context: Context, other: Obj): Boolean { + return if (other is ObjString) + value.contains(other.value) + else if (other is ObjChar) + value.contains(other.value) + else context.raiseArgumentError("String.contains can't take $other") + } + companion object { val type = ObjClass("String").apply { addConst("startsWith", diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 2c73a75..32e292b 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -298,9 +298,9 @@ class ScriptTest { @Test fun eqNeqTest() = runTest { assertEquals(ObjBool(true), eval("val x = 2; x == 2")) - assertEquals(ObjBool(false), eval("val x = 3; x == 2")) + assertEquals(ObjFalse, eval("val x = 3; x == 2")) assertEquals(ObjBool(true), eval("val x = 3; x != 2")) - assertEquals(ObjBool(false), eval("val x = 3; x != 3")) + assertEquals(ObjFalse, eval("val x = 3; x != 3")) assertTrue { eval("1 == 1").toBool() } assertTrue { eval("true == true").toBool() } @@ -313,17 +313,17 @@ class ScriptTest { @Test fun logicTest() = runTest { - assertEquals(ObjBool(false), eval("true && false")) - assertEquals(ObjBool(false), eval("false && false")) - assertEquals(ObjBool(false), eval("false && true")) + assertEquals(ObjFalse, eval("true && false")) + assertEquals(ObjFalse, eval("false && false")) + assertEquals(ObjFalse, eval("false && true")) assertEquals(ObjBool(true), eval("true && true")) assertEquals(ObjBool(true), eval("true || false")) - assertEquals(ObjBool(false), eval("false || false")) + assertEquals(ObjFalse, eval("false || false")) assertEquals(ObjBool(true), eval("false || true")) assertEquals(ObjBool(true), eval("true || true")) - assertEquals(ObjBool(false), eval("!true")) + assertEquals(ObjFalse, eval("!true")) assertEquals(ObjBool(true), eval("!false")) } @@ -1979,4 +1979,138 @@ class ScriptTest { """.trimIndent() ) } + + @Test + fun testSimpleWhen() = runTest { + eval( + """ + var result = when("a") { + "a" -> "ok" + else -> "fail" + } + assertEquals(result, "ok") + result = when(5) { + 3 -> "fail1" + 4 -> "fail2" + else -> "ok2" + } + assert(result == "ok2") + result = when(5) { + 3 -> "fail" + 4 -> "fail2" + } + assert(result == void) + """.trimIndent() + ) + } + + @Test + fun testWhenIs() = runTest { + eval( + """ + var result = when("a") { + is Int -> "fail2" + is String -> "ok" + else -> "fail" + } + assertEquals(result, "ok") + result = when(5) { + 3 -> "fail1" + 4 -> "fail2" + else -> "ok2" + } + assert(result == "ok2") + result = when(5) { + 3 -> "fail" + 4 -> "fail2" + } + assert(result == void) + result = when(5) { + !is String -> "ok" + 4 -> "fail2" + } + assert(result == "ok") + """.trimIndent() + ) + } + + @Test + fun testWhenIn() = runTest { + eval( + """ + var result = when('e') { + in 'a'..'c' -> "fail2" + in 'a'..'z' -> "ok" + else -> "fail" + } +// assertEquals(result, "ok") + result = when(5) { + in [1,2,3,4,6] -> "fail1" + in [7, 0, 9] -> "fail2" + else -> "ok2" + } + assert(result == "ok2") + result = when(5) { + in [1,2,3,4,6] -> "fail1" + in [7, 0, 9] -> "fail2" + in [-1, 5, 11] -> "ok3" + else -> "fail3" + } + assert(result == "ok3") + result = when(5) { + !in [1,2,3,4,6, 5] -> "fail1" + !in [7, 0, 9, 5] -> "fail2" + !in [-1, 15, 11] -> "ok4" + else -> "fail3" + } + assert(result == "ok4") + result = when(5) { + in [1,3] -> "fail" + in 2..4 -> "fail2" + } + assert(result == void) + """.trimIndent() + ) + } + + @Test + fun testWhenSample1() = runTest { + eval( + """ + fun type(x) { + when(x) { + in 'a'..'z', in 'A'..'Z' -> "letter" + in '0'..'9' -> "digit" + in "$%&" -> "hate char" + else -> "unknown" + } + } + assertEquals("digit", type('3')) + assertEquals("letter", type('E')) + assertEquals("hate char", type('%')) + """.trimIndent() + ) + } + + @Test + fun testWhenSample2() = runTest { + eval( + """ + fun type(x) { + when(x) { + "42", 42 -> "answer to the great question" + is Real, is Int -> "number" + is String -> { + for( d in x ) { + if( d !in '0'..'9' ) + break "unknown" + } + else "number" + } + } + } + assertEquals("number", type(5)) + """.trimIndent() + ) + } } \ No newline at end of file