+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:
sergeych committed 2025-05-31 15:08:11 +04:00
1 parent f7b35c7576
commit 3908b8ee9f
12 files changed
+249 -31

No files matched your search

@@ -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("""
+3 -1
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")