diff --git a/docs/OOP.md b/docs/OOP.md index faef0a3..ae6026e 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -51,6 +51,7 @@ Note `Real` class: it is global variable for Real class; there are such class in assert("Hello"::class == String) assert(1970::class == Int) assert(true::class == Bool) + assert('$'::class == Char) >>> void More complex is singleton classes, because you don't need to compare their class diff --git a/docs/tutorial.md b/docs/tutorial.md index 202d34e..3e75725 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -507,34 +507,51 @@ We can skip the rest of the loop and restart it, as usual, with `continue` opera Notice that `total` remains 0 as the end of the outerLoop@ is not reachable: `continue` is always called and always make Ling to skip it. -## Labels@ +## else statement -The label can be any valid identifier, even a keyword, labels exist in their own, isolated world, so no risk of -occasional clash. Labels are also scoped to their context and do not exist outside it. - -Right now labels are implemented only for the while loop. It is intended to be implemented for all loops and returns. - -## while - else statement - -The while loop can be followed by the else block, which is executed when the loop +The while and for loops can be followed by the else block, which is executed when the loop ends normally, without breaks. It allows override loop result value, for example, -to not calculate it in every iteration. Here is the sample: +to not calculate it in every iteration. See for loop example just below. -### Else, labels, and break practical sample +## For loops - // Get a first word that starts with a given previx and return it: - fun findPrefix(prefix,words) { - var index = 0 - while( index < words.size ) { - val w = words[index++] - if( w.startsWith(prefix) ) break w - } +For loop are intended to traverse collections, and all other objects that supports +size and index access, like lists: + + var letters = 0 + for( w in ["hello", "wolrd"]) { + letters += w.length + } + "total letters: "+letters + >>> "total letters: 10" + +For loop support breaks the same as while loops above: + + fun search(haystack, needle) { + for(ch in haystack) { + if( ch == needle) + break "found" + } else null } - val words = ["hello", "world", "foobar", "end"] - assert( findPrefix("bad", words) == null ) - findPrefix("foo", words ) - >>> "foobar" + assert( search("hello", 'l') == "found") + assert( search("hello", 'z') == null) + >>> void + +We can use labels too: + + fun search(haystacks, needle) { + exit@ for( hs in haystacks ) { + for(ch in hs ) { + if( ch == needle) + break@exit "found" + } + } + else null + } + assert( search(["hello", "world"], 'l') == "found") + assert( search(["hello", "world"], 'z') == null) + >>> void # Self-assignments in expression @@ -581,6 +598,7 @@ There are self-assigning version for operators too: | Int | 64 bit signed | `1` `-22` `0x1FF` | | Real | 64 bit double | `1.0`, `2e-11` | | Bool | boolean | `true` `false` | +| Char | single unicode character | `'S'`, `'\n'` | | String | unicode string, no limits | "hello" (see below) | | List | mutable list | [1, "two", 3] | | Void | no value could exist, singleton | void | @@ -589,6 +607,29 @@ There are self-assigning version for operators too: See also [math operations](math.md) +## Character details + +The type for the character objects is `Char`. + +### Char literal escapes + +Are the same as in string literals with little difference: + +| escape | ASCII value | +|--------|-------------------| +| \n | 0x10, newline | +| \t | 0x07, tabulation | +| \\ | \ slash character | +| \' | ' apostrophe | + +### Char instance members + +| member | type | meaning | +|--------|------|--------------------------------| +| code | Int | Unicode code for the character | +| | | | + + ## String details ### String operations diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 0d6d03c..4bdd587 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -112,7 +112,6 @@ class Compiler( return lvalue } - private fun parseTerm(cc: CompilerContext): Accessor? { var operand: Accessor? = null @@ -227,7 +226,7 @@ class Compiler( Token.Type.ID -> { // there could be terminal operators or keywords:// variable to read or like when (t.value) { - "if", "when", "do", "while", "return" -> { + in stopKeywords -> { if (operand != null) throw ScriptError(t.pos, "unexpected keyword") cc.previous() val s = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting valid statement") @@ -405,6 +404,8 @@ class Compiler( Token.Type.STRING -> Accessor { ObjString(t.value).asReadonly } + Token.Type.CHAR -> Accessor { ObjChar(t.value[0]).asReadonly } + Token.Type.PLUS -> { val n = parseNumber(true, cc) Accessor { n.asReadonly } @@ -470,6 +471,7 @@ class Compiler( "val" -> parseVarDeclaration(id.value, false, cc) "var" -> parseVarDeclaration(id.value, true, cc) "while" -> parseWhileStatement(cc) + "for" -> parseForStatement(cc) "break" -> parseBreakStatement(id.pos, cc) "continue" -> parseContinueStatement(id.pos, cc) "fn", "fun" -> parseFunctionDeclaration(cc) @@ -492,6 +494,83 @@ class Compiler( return found } + private fun parseForStatement(cc: CompilerContext): Statement { + val label = getLabel(cc)?.also { cc.labels += it } + val start = ensureLparen(cc) + + // for - in? + val tVar = cc.next() + if (tVar.type != Token.Type.ID) + throw ScriptError(tVar.pos, "Bad for statement: expected loop variable") + val tOp = cc.next() + if (tOp.value == "in") { + // in loop + val source = parseStatement(cc) ?: throw ScriptError(start, "Bad for statement: expected expression") + ensureRparen(cc) + val body = parseStatement(cc) ?: throw ScriptError(start, "Bad for statement: expected loop body") + + // possible else clause + 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) { + val forContext = it.copy(start) + + // loop var: StoredObject + val loopSO = forContext.addItem(tVar.value, true, ObjNull) + + // insofar we suggest source object is enumerable. Later we might need to add checks + val sourceObj = source.execute(forContext) + val size = runCatching { sourceObj.callInstanceMethod(forContext, "size").toInt() } + .getOrElse { throw ScriptError(tOp.pos, "object is not enumerable: no size") } + var result: Obj = ObjVoid + var breakCaught = false + if (size > 0) { + var current = runCatching { sourceObj.getAt(forContext, 0) } + .getOrElse { + throw ScriptError( + tOp.pos, + "object is not enumerable: no index access for ${sourceObj.inspect()}", + it + ) + } + var index = 0 + while (true) { + loopSO.value = current + try { + result = body.execute(forContext) + } catch (lbe: LoopBreakContinueException) { + if (lbe.label == label || lbe.label == null) { + breakCaught = true + if (lbe.doContinue) continue + else { + result = lbe.result + break + } + } else + throw lbe + } + if (++index >= size) break + current = sourceObj.getAt(forContext, index) + } + } + if( !breakCaught && elseStatement != null) { + result = elseStatement.execute(it) + } + result + } + } else { + // maybe other loops? + throw ScriptError(tOp.pos, "Unsupported for-loop syntax") + } + } + private fun parseWhileStatement(cc: CompilerContext): Statement { val label = getLabel(cc)?.also { cc.labels += it } val start = ensureLparen(cc) @@ -887,6 +966,11 @@ class Compiler( } fun compile(code: String): Script = Compiler().compile(Source("", code)) + + /** + * The keywords that stop processing of expression term + */ + val stopKeywords = setOf("break", "continue", "return", "if", "when", "do", "while", "for") } } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 1d42d4c..1d30f50 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -38,6 +38,7 @@ class Context( return requiredArg(0) } + @Suppress("unused") fun requireExactCount(count: Int) { if( args.list.size != count ) { raiseError("Expected exactly $count arguments, got ${args.list.size}") @@ -56,8 +57,8 @@ class Context( fun copy(pos: Pos, args: Arguments = Arguments.EMPTY,newThisObj: Obj? = null): Context = Context(this, args, pos, newThisObj ?: thisObj) - fun addItem(name: String, isMutable: Boolean, value: Obj?) { - objects.put(name, StoredObj(value, isMutable)) + fun addItem(name: String, isMutable: Boolean, value: Obj?): StoredObj { + return StoredObj(value, isMutable).also { objects.put(name, it) } } fun getOrCreateNamespace(name: String): ObjNamespace = diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index dfff099..fdc54cb 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -54,7 +54,10 @@ sealed class Obj { getInstanceMemberOrNull(name) ?: throw ScriptError(atPos, "symbol doesn't exist: $name") - suspend fun callInstanceMethod(context: Context, name: String, args: Arguments): Obj = + suspend fun callInstanceMethod(context: Context, + name: String, + args: Arguments = Arguments.EMPTY + ): Obj = // note that getInstanceMember traverses the hierarchy objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args) @@ -422,6 +425,24 @@ data class ObjBool(val value: Boolean) : 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 { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjString.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjString.kt index 3709c1a..bc2949a 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ObjString.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjString.kt @@ -27,6 +27,10 @@ data class ObjString(val value: String) : Obj() { return ObjString(value + other.asStr.value) } + override suspend fun getAt(context: Context, index: Int): Obj { + return ObjChar(value[index]) + } + companion object { val type = ObjClass("String").apply { addConst("startsWith", @@ -36,6 +40,7 @@ data class ObjString(val value: String) : Obj() { addConst("length", statement { ObjInt(thisAs().value.length.toLong()) } ) + addFn("size") { ObjInt(thisAs().value.length.toLong()) } } } } \ 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 87c46ee..440f643 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -203,6 +203,26 @@ private class Parser(fromPos: Pos) { decodeNumber(loadChars(digits), from) } + '\'' -> { + val start = pos.toPos() + var value = currentChar + pos.advance() + if (currentChar == '\\') { + value = currentChar + pos.advance() + value = when(value) { + 'n' -> '\n' + 'r' -> '\r' + 't' -> '\t' + '\'', '\\' -> value + else -> throw ScriptError(currentPos, "unsupported escape character: $value") + } + } + if( currentChar != '\'' ) throw ScriptError(currentPos, "expected end of character literal: '") + pos.advance() + Token(value.toString(), start, Token.Type.CHAR) + } + else -> { // Labels processing is complicated! // some@ statement: label 'some', ID 'statement' diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 183405d..616ab82 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -57,6 +57,7 @@ class Script( addConst("String", ObjString.type) addConst("Int", ObjInt.type) addConst("Bool", ObjBool.type) + addConst("Char", ObjChar.type) addConst("List", ObjList.type) val pi = ObjReal(PI) addConst("π", pi) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ScriptError.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ScriptError.kt index 6d17d8d..d8f59e8 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ScriptError.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ScriptError.kt @@ -2,12 +2,13 @@ package net.sergeych.ling -open class ScriptError(val pos: Pos, val errorMessage: String) : Exception( +open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=null) : Exception( """ $pos: Error: $errorMessage ${pos.currentLine} ${"-".repeat(pos.column)}^ - """.trimIndent() + """.trimIndent(), + cause ) class ExecutionError(val errorObject: ObjError) : ScriptError(errorObject.context.pos, errorObject.message) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index 503ea3c..ee05953 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -5,7 +5,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) { @Suppress("unused") enum class Type { - ID, INT, REAL, HEX, STRING, LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA, + ID, INT, REAL, HEX, STRING, CHAR, + LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA, SEMICOLON, COLON, PLUS, MINUS, STAR, SLASH, PERCENT, ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index f234a66..9ff4386 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -807,6 +807,46 @@ class ScriptTest { """.trimIndent()) } + @Test + fun forLoop1() = runTest { + eval(""" + var sum = 0 + for(i in [1,2,3]) { + println(i) + sum += i + } + assert(sum == 6) + """.trimIndent()) + eval(""" + fun test1(array) { + var sum = 0 + for(i in array) { + if( i > 2 ) break "too much" + sum += i + } + } + println("result=",test1([1,2])) + println("result=",test1([1,2,3])) + """.trimIndent()) + } + + @Test + fun forLoop2() = runTest { + println(eval( + """ + fun search(haystack, needle) { + for(ch in haystack) { + if( ch == needle) + break "found" + } + else null + } + assert( search("hello", 'l') == "found") + assert( search("hello", 'z') == null) + """.trimIndent() + ).toString()) + } + // @Test // fun testLambda1() = runTest { // val l = eval(""" diff --git a/library/src/jvmTest/kotlin/BookTest.kt b/library/src/jvmTest/kotlin/BookTest.kt index 33e7e30..011c740 100644 --- a/library/src/jvmTest/kotlin/BookTest.kt +++ b/library/src/jvmTest/kotlin/BookTest.kt @@ -158,7 +158,9 @@ suspend fun DocTest.test() { ) { println("Test failed: ${this.detailedString}") } - error?.let { fail(it.toString()) } + error?.let { + fail(it.toString(), it) + } assertEquals(expectedOutput, collectedOutput.toString(), "script output do not match") assertEquals(expectedResult, result.toString(), "script result does not match") // println("OK: $this")