fix #17 do-while loops

+mode string functions
+val/var cold be initialized with statement and return assigned values
This commit is contained in:
Sergey Chernov 2025-06-12 11:22:27 +04:00
parent 55fd3ea716
commit a1267c4395
8 changed files with 194 additions and 14 deletions

View File

@ -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:

View File

@ -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
}
}

View File

@ -134,6 +134,13 @@ internal class CompilerContext(val tokens: List<Token>) {
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

View File

@ -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,

View File

@ -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 {

View File

@ -40,6 +40,32 @@ data class ObjString(val value: String) : Obj() {
addConst("length",
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) }
)
addFn("takeLast") {
thisAs<ObjString>().value.takeLast(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("take") {
thisAs<ObjString>().value.take(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("drop") {
thisAs<ObjString>().value.drop(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("dropLast") {
thisAs<ObjString>().value.dropLast(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("lower") {
thisAs<ObjString>().value.lowercase().let(::ObjString)
}
addFn("upper") {
thisAs<ObjString>().value.uppercase().let(::ObjString)
}
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
}
}

View File

@ -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 {

View File

@ -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())
}
}