+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:
Sergey Chernov 2025-05-31 15:08:11 +04:00
parent f7b35c7576
commit 3908b8ee9f
12 changed files with 249 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("""

View File

@ -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")