+for-else-labels-break is running
+String now is indexed with Char instances, size supported +Char type and char constants
This commit is contained in:
parent
f7b35c7576
commit
3908b8ee9f
@ -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
|
||||
|
@ -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
|
||||
|
@ -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("<eval>", code))
|
||||
|
||||
/**
|
||||
* The keywords that stop processing of expression term
|
||||
*/
|
||||
val stopKeywords = setOf("break", "continue", "return", "if", "when", "do", "while", "for")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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<ObjChar>().value.code.toLong()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class ObjNamespace(val name: String) : Obj() {
|
||||
override fun toString(): String {
|
||||
|
@ -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<ObjString>().value.length.toLong()) }
|
||||
)
|
||||
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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("""
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user