diff --git a/docs/tutorial.md b/docs/tutorial.md index 0114da3..bf68143 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -554,16 +554,16 @@ Or, more neat: ## while -Regular pre-condition while loop, as expression, loop returns it's last line result: +Regular pre-condition while loop, as expression, loop returns the last expression as everything else: var count = 0 - while( count < 5 ) { - count++ - count * 10 - } - >>> 50 + val result = while( count < 5 ) count++ + result + >>> 4 -We can break as usual: +Notice it _is 4 because when count became 5, the loop body was not executed!_. + +We can break while as usual: var count = 0 while( count < 5 ) { @@ -682,6 +682,36 @@ So the returned value, as seen from diagram could be one of: - value returned from the `else` clause, of the loop was not broken - value returned from the last execution of loop body, if there was no `break` and no `else` clause. +## do-while loops + +There works exactly as while loops but the body is executed prior to checking the while condition: + + var i = 0 + do { i++ } while( i < 1 ) + i + >>> 1 + +The important feature of the do-while loop is that the condition expression is +evaluated on the body scope, e.g., variables, intruduced in the loop body are +available in the condition: + + do { + var continueLoop = false + "OK" + } while( continueLoop ) + >>> "OK" + +This is sometimes convenient when condition is complex and has to be calculated inside the loop body. Notice the value returning by the loop: + + fun readLine() { "done: result" } + val result = do { + val line = readLine() + } while( !line.startsWith("done:") ) + result.drop(6) + >>> "result" + +Suppose readLine() here reads some stream of lines. + ## For loops @@ -791,7 +821,7 @@ See [Ranges](Range.md) for detailed documentation on it. // single line comment var result = null // here we will store the result - >>> void + >>> null # Integral data types @@ -837,10 +867,36 @@ Are the same as in string literals with little difference: ## String details +Strings are much like Kotlin ones: + + "Hello".length + >>> 5 +And supports growing set of kotlin-borrowed operations, see below, for example: + + assertEquals("Hell", "Hello".dropLast(1)) + >>> void + ### String operations Concatenation is a `+`: `"hello " + name` works as expected. No confusion. +Typical set of String functions includes: + +| fun/prop | description / notes | +|------------------|------------------------------------------------------------| +| lower() | change case to unicode upper | +| upper() | change case to unicode lower | +| startsWith(prefix) | true if starts with a prefix | +| take(n) | get a new string from up to n first characters | +| takeLast(n) | get a new string from up to n last characters | +| drop(n) | get a new string dropping n first chars, or empty string | +| dropLast(n) | get a new string dropping n last chars, or empty string | +| size | size in characters like `length` because String is [Array] | + + + + + ### Literals String literal could be multiline: diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 059b120..85dedba 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -710,6 +710,7 @@ class Compiler( "val" -> parseVarDeclaration(id.value, false, cc) "var" -> parseVarDeclaration(id.value, true, cc) "while" -> parseWhileStatement(cc) + "do" -> parseDoWhileStatement(cc) "for" -> parseForStatement(cc) "break" -> parseBreakStatement(id.pos, cc) "continue" -> parseContinueStatement(id.pos, cc) @@ -944,6 +945,59 @@ class Compiler( return elseStatement?.execute(forContext) ?: result } + @Suppress("UNUSED_VARIABLE") + private fun parseDoWhileStatement(cc: CompilerContext): Statement { + val label = getLabel(cc)?.also { cc.labels += it } + val (breakFound, body) = cc.parseLoop { + parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "Bad while statement: expected statement") + } + label?.also { cc.labels -= it } + + cc.skipTokens(Token.Type.NEWLINE) + + val t = cc.next() + if( t.type != Token.Type.ID && t.value != "while" ) + cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here") + + val conditionStart = ensureLparen(cc) + val condition = + parseExpression(cc) ?: throw ScriptError(conditionStart, "Bad while statement: expected expression") + ensureRparen(cc) + + cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) + val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) { + parseStatement(cc) + } else { + cc.previous() + null + } + + + return statement(body.pos) { + var wasBroken = false + var result: Obj = ObjVoid + lateinit var doContext: Context + do { + doContext = it.copy().apply { skipContextCreation = true } + try { + result = body.execute(doContext) + } + catch( e: LoopBreakContinueException) { + if( e.label == label || e.label == null ) { + if( e.doContinue ) continue + else { + result = e.result + wasBroken = true + break + } + } + } + } while( condition.execute(doContext).toBool() ) + if( !wasBroken ) elseStatement?.let { s -> result = s.execute(it) } + result + } + } + private fun parseWhileStatement(cc: CompilerContext): Statement { val label = getLabel(cc)?.also { cc.labels += it } val start = ensureLparen(cc) @@ -1149,7 +1203,7 @@ class Compiler( val block = parseScript(startPos, cc) return statement(startPos) { // block run on inner context: - block.execute(it.copy(startPos)) + block.execute(if( it.skipContextCreation ) it else it.copy(startPos)) }.also { val t1 = cc.next() if (t1.type != Token.Type.RBRACE) @@ -1184,7 +1238,7 @@ class Compiler( } } - val initialExpression = if (setNull) null else parseExpression(tokens) + val initialExpression = if (setNull) null else parseStatement(tokens, true) ?: throw ScriptError(eqToken.pos, "Expected initializer expression") return statement(nameToken.pos) { context -> @@ -1196,7 +1250,7 @@ class Compiler( val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull context.addItem(name, mutable, initValue, visibility) - ObjVoid + initValue } } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt index acb955e..47eabca 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt @@ -134,6 +134,13 @@ internal class CompilerContext(val tokens: List) { return default } +// 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 diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt index 0a3f0ae..5795be4 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt @@ -4,7 +4,8 @@ class Context( val parent: Context?, val args: Arguments = Arguments.EMPTY, var pos: Pos = Pos.builtIn, - val thisObj: Obj = ObjVoid + val thisObj: Obj = ObjVoid, + var skipContextCreation: Boolean = false, ) { constructor( args: Arguments = Arguments.EMPTY, diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index 64e02b0..10a873e 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -286,6 +286,8 @@ object ObjNull : Obj() { override fun equals(other: Any?): Boolean { return other is ObjNull || other == null } + + override fun toString(): String = "null" } interface Numeric { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt index 7c3e031..790d2f1 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt @@ -40,6 +40,32 @@ data class ObjString(val value: String) : Obj() { addConst("length", statement { ObjInt(thisAs().value.length.toLong()) } ) + addFn("takeLast") { + thisAs().value.takeLast( + requiredArg(0).toInt() + ).let(::ObjString) + } + addFn("take") { + thisAs().value.take( + requiredArg(0).toInt() + ).let(::ObjString) + } + addFn("drop") { + thisAs().value.drop( + requiredArg(0).toInt() + ).let(::ObjString) + } + addFn("dropLast") { + thisAs().value.dropLast( + requiredArg(0).toInt() + ).let(::ObjString) + } + addFn("lower") { + thisAs().value.lowercase().let(::ObjString) + } + addFn("upper") { + thisAs().value.uppercase().let(::ObjString) + } addFn("size") { ObjInt(thisAs().value.length.toLong()) } } } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 47ab882..632dccb 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -17,7 +17,7 @@ class Script( return lastResult } - suspend fun execute() = execute(defaultContext.copy(pos)) + suspend fun execute() = execute(defaultContext.copy(pos = pos)) companion object { val defaultContext: Context = Context().apply { diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index b306bf3..1eb78bd 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -174,7 +174,7 @@ class ScriptTest { fun varsAndConstsTest() = runTest { val context = Context(pos = Pos.builtIn) assertEquals( - ObjVoid, context.eval( + ObjInt(3L), context.eval( """ val a = 17 var b = 3 @@ -434,6 +434,16 @@ class ScriptTest { } + @Test + fun whileAssignTest() = runTest { + eval(""" + var t = 0 + val x = while( t < 5 ) { t++ } + // last returned value is 4 - when t was 5 body was not executed + assertEquals( 4, x ) + """.trimIndent()) + } + @Test fun whileTest() = runTest { assertEquals( @@ -1662,4 +1672,28 @@ class ScriptTest { """.trimIndent()) } + @Test + fun doWhileSimpleTest() = runTest { + eval(""" + var sum = 0 + var x = do { + val s = sum + sum += 1 + } while( s < 10 ) + assertEquals(11, x) + """.trimIndent()) + } + + @Test + fun testFailDoWhileSample1() = runTest { + eval(""" + fun readLine() { "done: result" } + val result = do { + val line = readLine() + } while( !line.startsWith("done:") ) + assertEquals("result", result.drop(6)) + result + """.trimIndent()) + } + } \ No newline at end of file