From eba29aedb767fe4307cb8a261189bd4018197cb4 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 26 May 2025 18:51:42 +0400 Subject: [PATCH] big refactoring towards OO support just started --- .../kotlin/net/sergeych/ling/Compiler.kt | 572 +++++++++++++----- .../kotlin/net/sergeych/ling/Context.kt | 3 +- .../kotlin/net/sergeych/ling/Obj.kt | 158 +++-- .../kotlin/net/sergeych/ling/Parser.kt | 33 +- .../kotlin/net/sergeych/ling/StoredObj.kt | 1 - .../kotlin/net/sergeych/ling/Token.kt | 5 +- .../kotlin/net/sergeych/ling/statements.kt | 17 +- library/src/commonTest/kotlin/ScriptTest.kt | 38 +- 8 files changed, 605 insertions(+), 222 deletions(-) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index b8c75ce..261b1cc 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -45,13 +45,50 @@ type alias has target type name. So we have to have something that denotes a _ty //} -class CompilerContext(tokens: List) : ListIterator by tokens.listIterator() { +class CompilerContext(val tokens: List) : ListIterator by tokens.listIterator() { val labels = mutableSetOf() fun ensureLabelIsValid(pos: Pos, label: String) { if (label !in labels) throw ScriptError(pos, "Undefined label '$label'") } + + @Suppress("unused") + fun requireId() = requireToken(Token.Type.ID, "identifier is required") + + fun requireToken(type: Token.Type, message: String = "required ${type.name}"): Token = + next().also { + if (type != it.type) throw ScriptError(it.pos, message) + } + + fun syntaxError(at: Pos, message: String = "Syntax error"): Nothing { + throw ScriptError(at, message) + } + + fun currentPos() = + if (hasNext()) next().pos.also { previous() } + else previous().pos.also { next() } + + /** + * Skips next token if its type is `tokenType`, returns `true` if so. + * @param errorMessage message to throw if next token is not `tokenType` + * @param isOptional if `true` and token is not of `tokenType`, just return `false` and does not skip it + * @return `true` if the token was skipped + * @throws ScriptError if [isOptional] is `false` and next token is not of [tokenType] + */ + fun skipTokenOfType(tokenType: Token.Type, errorMessage: String="expected ${tokenType.name}", isOptional: Boolean = false): Boolean { + val t = next() + return if (t.type != tokenType) { + if (!isOptional) { + println("unexpected: $t (needed $tokenType)") + throw ScriptError(t.pos, errorMessage) + } else { + previous() + false + } + } else true + } + } @@ -97,6 +134,7 @@ class Compiler { parseExpression(tokens) } } + Token.Type.PLUS2, Token.Type.MINUS2 -> { tokens.previous() parseExpression(tokens) @@ -132,7 +170,7 @@ class Compiler { private fun parseExpression(tokens: CompilerContext, level: Int = 0): Statement? { if (level == lastLevel) - return parseTerm(tokens) + return parseTerm3(tokens) var lvalue = parseExpression(tokens, level + 1) if (lvalue == null) return null @@ -153,153 +191,404 @@ class Compiler { return lvalue } - fun parseTerm(tokens: CompilerContext): Statement? { - // call op - // index op - // unary op - // parenthesis - // number or string - val t = tokens.next() - // todoL var? + + /* + Term compiler + + Fn calls could be: + + 1) Fn(...) + 2) thisObj.method(...) + + 1 is a shortcut to this.Fn(...) + + In general, we can assume any Fn to be of the same king, with `this` that always exist, and set by invocation. + + In the case of (1), so called regular, or not bound function, it takes current this from the context. + In the case of (2), bound function, it creates sub-context binding thisObj to `this` in it. + + Suppose we do regular parsing. THen we get lparam = statement, and parse to the `(`. Now we have to + compile the invocation of lparam, which can be thisObj.method or just method(). Unfortunately we + already compiled it and can't easily restore its type, so we have to parse it different way. + + EBNF to parse term having lparam. + + boundcall = "." , identifier, "(" + + We then call instance method bound to `lparam`. + + call = "(', args, ") + + we treat current lparam as callable and invoke it on the current context with current value of 'this. + + Just traversing fields: + + traverse = ".", not (identifier , ".") + + Other cases to parse: + + index = lparam, "[" , ilist , "]" + + + */ + + /** + * Lower level of expr: + * + * assigning expressions: + * + * expr = expr: assignment + * ++expr, expr++, --expr, expr--, + * + * update-assigns: + * expr += expr, ... + * + * Dot!: expr , '.', ID + * Lambda: { } + * index: expr[ ilist ] + * call: ( ilist ) + * self updating: ++expr, expr++, --expr, expr--, expr+=, + * expr-=, expr*=, expr/= + * read expr: + */ + fun parseTerm3(cc: CompilerContext): Statement? { + var operand: Accessor? = null + + while (true) { + val t = cc.next() + val startPos = t.pos + when (t.type) { + Token.Type.NEWLINE, Token.Type.SEMICOLON, Token.Type.EOF -> { + cc.previous() + return operand?.let { op -> statement(startPos) { op.getter(it) } } + } + + Token.Type.DOT -> { + if (operand == null) + throw ScriptError(t.pos, "Expecting expression before dot") + continue + } + + Token.Type.LPAREN -> { + operand?.let { left -> + // this is function call from + operand = parseFunctionCall( + cc, + left, + thisObj = null, + ) + } ?: run { + // Expression in parentheses + val statement = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting expression") + operand = Accessor { + statement.execute(it) + } + cc.skipTokenOfType(Token.Type.RPAREN, "missing ')'") + } + } + + Token.Type.ID -> { + operand?.let { left -> + // selector: , '.' , + // we replace operand with selector code, that + // is RW: + operand = Accessor({ + it.pos = t.pos + left.getter(it).readField(it, t.value) + }) { cxt, newValue -> + cxt.pos = t.pos + left.getter(cxt).writeField(cxt, t.value, newValue) + } + } ?: run { + // variable to read or like + cc.previous() + operand = parseAccessor(cc) + } + } + + Token.Type.PLUS2 -> { + // note: post-increment result is not assignable (truly lvalue) + operand?.let { left -> + // post increment + left.setter(startPos) + operand = Accessor({ ctx -> + left.getter(ctx).getAndIncrement(ctx) + }) + } ?: run { + // no lvalue means pre-increment, expression to increment follows + val next = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression") + operand = Accessor({ ctx -> next.getter(ctx).incrementAndGet(ctx) }) + } + } + + Token.Type.MINUS2 -> { + // note: post-decrement result is not assignable (truly lvalue) + operand?.let { left -> + // post decrement + left.setter(startPos) + operand = Accessor { ctx -> + left.getter(ctx).getAndDecrement(ctx) + } + }?: run { + // no lvalue means pre-decrement, expression to decrement follows + val next = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression") + operand = Accessor { ctx -> next.getter(ctx).decrementAndGet(ctx) } + } + + } + + + else -> { + cc.previous() + operand?.let { op -> + return statement(startPos) { op.getter(it) } + } + operand = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression") + } + } + } + } + + fun parseFunctionCall(cc: CompilerContext, left: Accessor, thisObj: Statement?): Accessor { + // insofar, functions always return lvalue + val args = mutableListOf() + do { + val t = cc.next() + when (t.type) { + Token.Type.RPAREN, Token.Type.COMMA -> {} + else -> { + cc.previous() + parseStatement(cc)?.let { args += Arguments.Info(it, t.pos) } + ?: throw ScriptError(t.pos, "Expecting arguments list") + } + } + } while (t.type != Token.Type.RPAREN) + + return Accessor { context -> + val v = left.getter(context) + v.callOn(context.copy( + context.pos, + Arguments( + context.pos, + args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) } + ), + ) + ) + } + } + +// fun parseReservedWord(word: String): Accessor? = +// when(word) { +// "true" -> Accessor { ObjBool(true) } +// "false" -> Accessor { ObjBool(false) } +// "void" -> Accessor { ObjVoid } +// "null" -> Accessor { ObjNull } +// else -> null +// } + + + fun parseAccessor(cc: CompilerContext): Accessor? { + // could be: literal + val t = cc.next() return when (t.type) { - Token.Type.ID -> { - when (t.value) { - "void" -> statement(t.pos, true) { ObjVoid } - "null" -> statement(t.pos, true) { ObjNull } - "true" -> statement(t.pos, true) { ObjBool(true) } - "false" -> statement(t.pos, true) { ObjBool(false) } - else -> parseVarAccess(t, tokens) - } + Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> { + cc.previous() + val n = parseNumber(true, cc) + Accessor({ n }) } - Token.Type.STRING -> statement(t.pos, true) { ObjString(t.value) } - - Token.Type.LPAREN -> { - // ( subexpr ) - parseExpression(tokens)?.also { - val tl = tokens.next() - if (tl.type != Token.Type.RPAREN) - throw ScriptError(t.pos, "unbalanced parenthesis: no ')' for it") - } - } + Token.Type.STRING -> Accessor({ ObjString(t.value) }) Token.Type.PLUS -> { - val n = parseNumber(true, tokens) - statement(t.pos, true) { n } + val n = parseNumber(true, cc) + Accessor { n } } Token.Type.MINUS -> { - val n = parseNumber(false, tokens) - statement(t.pos, true) { n } + val n = parseNumber(false, cc) + Accessor { n } } - Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> { - tokens.previous() - val n = parseNumber(true, tokens) - statement(t.pos, true) { n } + Token.Type.ID -> { + when (t.value) { + "void" -> Accessor { ObjVoid } + "null" -> Accessor { ObjNull } + "true" -> Accessor { ObjBool(true) } + "false" -> Accessor { ObjBool(false) } + else -> { + Accessor({ + it.pos = t.pos + it.get(t.value)?.value ?: it.raiseError("symbol not defined: '${t.value}'") + }) { ctx, newValue -> + ctx.get(t.value)?.let { stored -> + ctx.pos = t.pos + if (stored.isMutable) + stored.value = newValue + else + ctx.raiseError("Cannot assign to immutable value") + } ?: ctx.raiseError("symbol not defined: '${t.value}'") + } + } + } } else -> null } - } - fun parseVarAccess(id: Token, tokens: CompilerContext, path: List = emptyList()): Statement { - val nt = tokens.next() - fun resolve(context: Context): Context { - var targetContext = context - for (n in path) { - val x = targetContext[n] ?: throw ScriptError(id.pos, "undefined symbol: $n") - (x.value as? ObjNamespace)?.let { targetContext = it.context } - ?: throw ScriptError(id.pos, "Invalid symbolic path (wrong type of ${x.name}: ${x.value}") - } - return targetContext - } - return when (nt.type) { - Token.Type.DOT -> { - // selector - val t = tokens.next() - if (t.type == Token.Type.ID) { - parseVarAccess(t, tokens, path + id.value) - } else - throw ScriptError(t.pos, "Expected identifier after '.'") - } - - Token.Type.PLUS2 -> { - statement(id.pos) { context -> - context.pos = id.pos - val v = resolve(context).get(id.value) - ?: throw ScriptError(id.pos, "Undefined symbol: ${id.value}") - v.value?.getAndIncrement(context) - ?: context.raiseNPE() - } - } - - Token.Type.MINUS2 -> { - statement(id.pos) { context -> - context.pos = id.pos - val v = resolve(context).get(id.value) - ?: throw ScriptError(id.pos, "Undefined symbol: ${id.value}") - v.value?.getAndDecrement(context) - ?: context.raiseNPE() - } - } - Token.Type.LPAREN -> { - // function call - // Load arg list - val args = mutableListOf() - do { - val t = tokens.next() - when (t.type) { - Token.Type.RPAREN, Token.Type.COMMA -> {} - else -> { - tokens.previous() - parseStatement(tokens)?.let { args += Arguments.Info(it, t.pos) } - ?: throw ScriptError(t.pos, "Expecting arguments list") - } - } - } while (t.type != Token.Type.RPAREN) - - statement(id.pos) { context -> - val v = - resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined function: ${id.value}") - (v.value as? Statement)?.execute( - context.copy( - id.pos, - Arguments( - nt.pos, - args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) } - ), - ) - ) - ?: throw ScriptError(id.pos, "Variable $id is not callable ($id)") - } - } - - Token.Type.LBRACKET -> { - TODO("indexing") - } - - else -> { - // just access the var - tokens.previous() -// val t = tokens.next() +// fun parseTerm(tokens: CompilerContext): Statement? { +// // call op +// // index op +// // unary op +// // parenthesis +// // number or string +// val t = tokens.next() +// // todoL var? +// return when (t.type) { +// Token.Type.ID -> { +// when (t.value) { +// "void" -> statement(t.pos, true) { ObjVoid } +// "null" -> statement(t.pos, true) { ObjNull } +// "true" -> statement(t.pos, true) { ObjBool(true) } +// "false" -> statement(t.pos, true) { ObjBool(false) } +// else -> parseVarAccess(t, tokens) +// } +// } +// +// Token.Type.STRING -> statement(t.pos, true) { ObjString(t.value) } +// +// Token.Type.LPAREN -> { +// // ( subexpr ) +// parseExpression(tokens)?.also { +// val tl = tokens.next() +// if (tl.type != Token.Type.RPAREN) +// throw ScriptError(t.pos, "unbalanced parenthesis: no ')' for it") +// } +// } +// +// Token.Type.PLUS -> { +// val n = parseNumber(true, tokens) +// statement(t.pos, true) { n } +// } +// +// Token.Type.MINUS -> { +// val n = parseNumber(false, tokens) +// statement(t.pos, true) { n } +// } +// +// Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> { // tokens.previous() -// println(t) -// if( path.isEmpty() ) { -// statement? -// tokens.previous() -// parseStatement(tokens) ?: throw ScriptError(id.pos, "Expecting expression/statement") -// } else - statement(id.pos) { - val v = resolve(it).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: ${id.value}") - v.value ?: throw ScriptError(id.pos, "Variable $id is not initialized") - } - } - } - } +// val n = parseNumber(true, tokens) +// statement(t.pos, true) { n } +// } +// +// else -> null +// } +// +// } +// fun parseVarAccess(id: Token, tokens: CompilerContext, path: List = emptyList()): Statement { +// val nt = tokens.next() +// +// fun resolve(context: Context): Context { +// var targetContext = context +// for (n in path) { +// val x = targetContext[n] ?: throw ScriptError(id.pos, "undefined symbol: $n") +// (x.value as? ObjNamespace)?.let { targetContext = it.context } +// ?: throw ScriptError(id.pos, "Invalid symbolic path (wrong type of ${n}: ${x.value}") +// } +// return targetContext +// } +// return when (nt.type) { +// Token.Type.DOT -> { +// // selector +// val t = tokens.next() +// if (t.type == Token.Type.ID) { +// parseVarAccess(t, tokens, path + id.value) +// } else +// throw ScriptError(t.pos, "Expected identifier after '.'") +// } +// +// Token.Type.PLUS2 -> { +// statement(id.pos) { context -> +// context.pos = id.pos +// val v = resolve(context).get(id.value) +// ?: throw ScriptError(id.pos, "Undefined symbol: ${id.value}") +// v.value?.getAndIncrement(context) +// ?: context.raiseNPE() +// } +// } +// +// Token.Type.MINUS2 -> { +// statement(id.pos) { context -> +// context.pos = id.pos +// val v = resolve(context).get(id.value) +// ?: throw ScriptError(id.pos, "Undefined symbol: ${id.value}") +// v.value?.getAndDecrement(context) +// ?: context.raiseNPE() +// } +// } +// +// Token.Type.LPAREN -> { +// // function call +// // Load arg list +// val args = mutableListOf() +// do { +// val t = tokens.next() +// when (t.type) { +// Token.Type.RPAREN, Token.Type.COMMA -> {} +// else -> { +// tokens.previous() +// parseStatement(tokens)?.let { args += Arguments.Info(it, t.pos) } +// ?: throw ScriptError(t.pos, "Expecting arguments list") +// } +// } +// } while (t.type != Token.Type.RPAREN) +// +// statement(id.pos) { context -> +// val v = +// resolve(context).get(id.value) ?: throw ScriptError( +// id.pos, +// "Undefined function: ${id.value}" +// ) +// (v.value as? Statement)?.execute( +// context.copy( +// id.pos, +// Arguments( +// nt.pos, +// args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) } +// ), +// ) +// ) +// ?: throw ScriptError(id.pos, "Variable $id is not callable ($id)") +// } +// } +// +// Token.Type.LBRACKET -> { +// TODO("indexing") +// } +// +// else -> { +// // just access the var +// tokens.previous() +//// val t = tokens.next() +//// tokens.previous() +//// println(t) +//// if( path.isEmpty() ) { +//// statement? +//// tokens.previous() +//// parseStatement(tokens) ?: throw ScriptError(id.pos, "Expecting expression/statement") +//// } else +// statement(id.pos) { +// val v = +// resolve(it).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: ${id.value}") +// v.value ?: throw ScriptError(id.pos, "Variable $id is not initialized") +// } +// } +// } +// } +// fun parseNumber(isPlus: Boolean, tokens: CompilerContext): Obj { val t = tokens.next() @@ -458,12 +747,14 @@ class Compiler { val ifBody = parseStatement(tokens) ?: throw ScriptError(pos, "Bad if statement: expected statement") + tokens.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) // could be else block: val t2 = tokens.next() // we generate different statements: optimization return if (t2.type == Token.Type.ID && t2.value == "else") { - val elseBody = parseStatement(tokens) ?: throw ScriptError(pos, "Bad else statement: expected statement") + val elseBody = + parseStatement(tokens) ?: throw ScriptError(pos, "Bad else statement: expected statement") return statement(start) { if (condition.execute(it).toBool()) ifBody.execute(it) @@ -626,13 +917,13 @@ class Compiler { // bitwise or 2 // bitwise and 3 // equality/ne 4 - LogicalOp(Token.Type.EQ, 4) { a, b -> a == b }, - LogicalOp(Token.Type.NEQ, 4) { a, b -> a != b }, + LogicalOp(Token.Type.EQ, 4) { c, a, b -> a.compareTo(c, b) == 0 }, + LogicalOp(Token.Type.NEQ, 4) { c, a, b -> a.compareTo(c, b) != 0 }, // relational <=,... 5 - LogicalOp(Token.Type.LTE, 5) { a, b -> a <= b }, - LogicalOp(Token.Type.LT, 5) { a, b -> a < b }, - LogicalOp(Token.Type.GTE, 5) { a, b -> a >= b }, - LogicalOp(Token.Type.GT, 5) { a, b -> a > b }, + LogicalOp(Token.Type.LTE, 5) { c, a, b -> a.compareTo(c, b) <= 0 }, + LogicalOp(Token.Type.LT, 5) { c, a, b -> a.compareTo(c, b) < 0 }, + LogicalOp(Token.Type.GTE, 5) { c, a, b -> a.compareTo(c, b) >= 0 }, + LogicalOp(Token.Type.GT, 5) { c, a, b -> a.compareTo(c, b) > 0 }, // shuttle <=> 6 // bitshhifts 7 Operator(Token.Type.PLUS, 8, 2) { pos, a, b -> @@ -657,14 +948,17 @@ class Compiler { suspend fun eval(code: String) = Compiler.compile(code).execute() -fun LogicalOp(tokenType: Token.Type, priority: Int, f: (Obj, Obj) -> Boolean) = Compiler.Operator( +fun LogicalOp( + tokenType: Token.Type, priority: Int, + f: suspend (Context, Obj, Obj) -> Boolean +) = Compiler.Operator( tokenType, priority, 2 ) { pos, a, b -> statement(pos) { ObjBool( - f(a.execute(it), b.execute(it)) + f(it, a.execute(it), b.execute(it)) ) } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 47e8a94..27c210f 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -32,13 +32,12 @@ class Context( fun copy(pos: Pos, args: Arguments = Arguments.EMPTY): Context = Context(this, args, pos) fun addItem(name: String, isMutable: Boolean, value: Obj?) { - objects.put(name, StoredObj(name, value, isMutable)) + objects.put(name, StoredObj(value, isMutable)) } fun getOrCreateNamespace(name: String) = (objects.getOrPut(name) { StoredObj( - name, ObjNamespace(name, copy(pos)), isMutable = false ) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index d9c8a74..f9c004b 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -8,41 +8,65 @@ import kotlin.math.floor typealias InstanceMethod = (Context, Obj) -> Obj -data class Item(var value: T, val isMutable: Boolean = false) +data class WithAccess(var value: T, val isMutable: Boolean) + +data class Accessor( + val getter: suspend (Context) -> Obj, + val setterOrNull: (suspend (Context, Obj) -> Unit)? +) { + constructor(getter: suspend (Context) -> Obj) : this(getter, null) + + fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos,"can't assign value") +} -@Serializable sealed class ClassDef( val className: String ) { val baseClasses: List get() = emptyList() - private val instanceMethods: MutableMap> get() = mutableMapOf() + protected val instanceMembers: MutableMap> = mutableMapOf() + private val monitor = Mutex() - private val instanceLock = Mutex() suspend fun addInstanceMethod( + context: Context, name: String, - freeze: Boolean = false, - pos: Pos = Pos.builtIn, - body: InstanceMethod + isOpen: Boolean = false, + body: Obj ) { - instanceLock.withLock { - instanceMethods[name]?.let { + monitor.withLock { + instanceMembers[name]?.let { if (!it.isMutable) - throw ScriptError(pos, "existing method $name is frozen and can't be updated") + context.raiseError("method $name is not open and can't be overridden") it.value = body - } ?: instanceMethods.put(name, Item(body, freeze)) + } ?: instanceMembers.put(name, WithAccess(body, isOpen)) } } - //suspend fun callInstanceMethod(context: Context, self: Obj,args: Arguments): Obj { -// - // } + suspend fun getInstanceMethodOrNull(name: String): Obj? = + monitor.withLock { instanceMembers[name]?.value } + + suspend fun getInstanceMethod(context: Context, name: String): Obj = + getInstanceMethodOrNull(name) ?: context.raiseError("no method found: $name") + +// suspend fun callInstanceMethod(context: Context, name: String, self: Obj,args: Arguments): Obj { +// getInstanceMethod(context, name).invoke(context, self,args) +// } } + object ObjClassDef : ClassDef("Obj") -@Serializable -sealed class Obj : Comparable { +sealed class Obj { + open val classDef: ClassDef = ObjClassDef + var isFrozen: Boolean = false + + protected val instanceMethods: Map> = mutableMapOf() + private val monitor = Mutex() + + open suspend fun compareTo(context: Context, other: Obj): Int { + context.raiseNotImplemented() + } + open val asStr: ObjString by lazy { if (this is ObjString) this else ObjString(this.toString()) } @@ -53,39 +77,49 @@ sealed class Obj : Comparable { context.raiseNotImplemented() } + open fun assign(context: Context, other: Obj): Obj { + context.raiseNotImplemented() + } + + open fun plusAssign(context: Context, other: Obj): Obj { + assign(context, plus(context, other)) + return this + } + open fun getAndIncrement(context: Context): Obj { context.raiseNotImplemented() } + open fun incrementAndGet(context: Context): Obj { + context.raiseNotImplemented() + } + + open fun decrementAndGet(context: Context): Obj { + context.raiseNotImplemented() + } + open fun getAndDecrement(context: Context): Obj { context.raiseNotImplemented() } - @Suppress("unused") - enum class Type { - @SerialName("Void") - Void, + fun willMutate(context: Context) { + if (isFrozen) context.raiseError("attempt to mutate frozen object") + } - @SerialName("Null") - Null, + suspend fun getInstanceMember(context: Context, name: String): Obj? = definition.getInstanceMethodOrNull(name) - @SerialName("String") - String, + suspend fun sync(block: () -> T): T = monitor.withLock { block() } - @SerialName("Int") - Int, + open suspend fun readField(context: Context, name: String): Obj { + context.raiseNotImplemented() + } - @SerialName("Real") - Real, + open suspend fun writeField(context: Context,name: String, newValue: Obj) { + context.raiseNotImplemented() + } - @SerialName("Bool") - Bool, - - @SerialName("Fn") - Fn, - - @SerialName("Any") - Any, + open suspend fun callOn(context: Context): Obj { + context.raiseNotImplemented() } companion object { @@ -117,7 +151,7 @@ object ObjVoid : Obj() { return other is ObjVoid || other is Unit } - override fun compareTo(other: Obj): Int { + override suspend fun compareTo(context: Context, other: Obj): Int { return if (other === this) 0 else -1 } @@ -127,7 +161,7 @@ object ObjVoid : Obj() { @Serializable @SerialName("null") object ObjNull : Obj() { - override fun compareTo(other: Obj): Int { + override suspend fun compareTo(context: Context, other: Obj): Int { return if (other === this) 0 else -1 } @@ -140,8 +174,8 @@ object ObjNull : Obj() { @SerialName("string") data class ObjString(val value: String) : Obj() { - override fun compareTo(other: Obj): Int { - if (other !is ObjString) throw IllegalArgumentException("cannot compare string with $other") + override suspend fun compareTo(context: Context, other: Obj): Int { + if (other !is ObjString) context.raiseError("cannot compare string with $other") return this.value.compareTo(other.value) } @@ -181,8 +215,8 @@ data class ObjReal(val value: Double) : Obj(), Numeric { override val toObjInt: ObjInt by lazy { ObjInt(longValue) } override val toObjReal: ObjReal by lazy { ObjReal(value) } - override fun compareTo(other: Obj): Int { - if (other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other") + override suspend fun compareTo(context: Context, other: Obj): Int { + if (other !is Numeric) context.raiseError("cannot compare $this with $other") return value.compareTo(other.doubleValue) } @@ -206,15 +240,16 @@ data class ObjInt(var value: Long) : Obj(), Numeric { return ObjInt(value).also { value-- } } -// override fun plus(context: Context, other: Obj): Obj { -// if (other !is Numeric) -// context.raiseError("cannot add $this with $other") -// return if (other is ObjInt) -// -// } + override fun incrementAndGet(context: Context): Obj { + return ObjInt(++value) + } - override fun compareTo(other: Obj): Int { - if (other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other") + override fun decrementAndGet(context: Context): Obj { + return ObjInt(--value) + } + + override suspend fun compareTo(context: Context, other: Obj): Int { + if (other !is Numeric) context.raiseError("cannot compare $this with $other") return value.compareTo(other.doubleValue) } @@ -226,8 +261,8 @@ data class ObjInt(var value: Long) : Obj(), Numeric { data class ObjBool(val value: Boolean) : Obj() { override val asStr by lazy { ObjString(value.toString()) } - override fun compareTo(other: Obj): Int { - if (other !is ObjBool) throw IllegalArgumentException("cannot compare $this with $other") + override suspend fun compareTo(context: Context, other: Obj): Int { + if (other !is ObjBool) context.raiseError("cannot compare $this with $other") return value.compareTo(other.value) } @@ -239,16 +274,25 @@ data class ObjNamespace(val name: String, val context: Context) : Obj() { return "namespace ${name}" } - override fun compareTo(other: Obj): Int { - throw IllegalArgumentException("cannot compare namespaces") + override suspend fun readField(callerContext: Context,name: String): Obj { + return context[name]?.value ?: callerContext.raiseError("not found: $name") } + } open class ObjError(val context: Context, val message: String) : Obj() { override val asStr: ObjString by lazy { ObjString("Error: $message") } - override fun compareTo(other: Obj): Int { - if (other === this) return 0 else return -1 - } } class ObjNullPointerError(context: Context) : ObjError(context, "object is null") + +class ObjClass(override val definition: ClassDef) : Obj() { + + override suspend fun compareTo(context: Context, other: Obj): Int { +// definition.callInstanceMethod(":compareTo", context, other)?.let { +// it(context, this) +// } + TODO("Not yet implemented") + } + +} \ 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 c7c0752..962efd0 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -51,19 +51,31 @@ private class Parser(fromPos: Pos) { } '+' -> { - if( currentChar == '+') { - advance() - Token("+", from, Token.Type.PLUS2) + when (currentChar) { + '+' -> { + advance() + Token("+", from, Token.Type.PLUS2) + } + '=' -> { + advance() + Token("+", from, Token.Type.PLUSASSIGN) + } + else -> + Token("+", from, Token.Type.PLUS) } - else - Token("+", from, Token.Type.PLUS) } '-' -> { - if (currentChar == '-') { - advance() - Token("--", from, Token.Type.MINUS2) - } else - Token("-", from, Token.Type.MINUS) + when (currentChar) { + '-' -> { + advance() + Token("--", from, Token.Type.MINUS2) + } + '=' -> { + advance() + Token("-", from, Token.Type.MINUSASSIGN) + } + else -> Token("-", from, Token.Type.MINUS) + } } '*' -> Token("*", from, Token.Type.STAR) '/' -> { @@ -285,6 +297,7 @@ private class Parser(fromPos: Pos) { private fun skipws(): Char? { while (!pos.end) { val ch = pos.currentChar + if( ch == '\n') break if (ch.isWhitespace()) advance() else diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt index e298430..1f28201 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt @@ -4,7 +4,6 @@ package net.sergeych.ling * Whatever [Obj] stored somewhere */ data class StoredObj( - val name: String, var value: Obj?, val isMutable: Boolean = false ) \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index bcf86b0..e6f241c 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -7,10 +7,11 @@ data class Token(val value: String, val pos: Pos, val type: Type) { enum class Type { ID, INT, REAL, HEX, STRING, LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA, SEMICOLON, COLON, - PLUS, MINUS, STAR, SLASH, ASSIGN, + PLUS, MINUS, STAR, SLASH, PERCENT, + ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, PLUS2, MINUS2, EQ, NEQ, LT, LTE, GT, GTE, - AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT, + AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, SINLGE_LINE_COMMENT, MULTILINE_COMMENT, LABEL,ATLABEL, // label@ at@label NEWLINE, diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt index de34787..1c04935 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt @@ -2,21 +2,30 @@ package net.sergeych.ling fun String.toSource(name: String = "eval"): Source = Source(name, this) +sealed class ObjType { + object Any : ObjType() + object Int : ObjType() +} + @Suppress("unused") abstract class Statement( val isStaticConst: Boolean = false, val isConst: Boolean = false, - val returnType: Type = Type.Any + val returnType: ObjType = ObjType.Any ) : Obj() { abstract val pos: Pos abstract suspend fun execute(context: Context): Obj - override fun compareTo(other: Obj): Int { + override suspend fun compareTo(context: Context,other: Obj): Int { throw UnsupportedOperationException("not comparable") } + override suspend fun callOn(context: Context): Obj { + return execute(context) + } + override fun toString(): String = "Callable@${this.hashCode()}" } @@ -30,8 +39,8 @@ fun Statement.require(cond: Boolean, message: () -> String) { if (!cond) raise(message()) } -fun statement(pos: Pos, isStaticConst: Boolean = false, f: suspend (Context) -> Obj): Statement = - object : Statement(isStaticConst) { +fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend (Context) -> Obj): Statement = + object : Statement(isStaticConst, isConst) { override val pos: Pos = pos override suspend fun execute(context: Context): Obj = f(context) } diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 265ca24..266e80d 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -7,6 +7,24 @@ import kotlin.test.* class ScriptTest { + @Test + fun parseNewlines() { + fun check(expected: String, type: Token.Type, row: Int, col: Int, src: String, offset: Int = 0) { + val source = src.toSource() + assertEquals( + Token(expected, source.posAt(row, col), type), + parseLing(source)[offset] + ) + } + check("1", Token.Type.INT, 0, 0, "1 + x\n2", 0) + check("+", Token.Type.PLUS, 0, 2, "1 + x\n2", 1) + check("x", Token.Type.ID, 0, 4, "1 + x\n2", 2) + check("\n", Token.Type.NEWLINE, 0, 5, "1 + x\n2", 3) +// check("2", Token.Type.INT, 1, 0, "1 + x\n2", 4) +// check("", Token.Type.EOF, 1, 0, "1 + x\n2", 5) + + } + @Test fun parseNumbersTest() { fun check(expected: String, type: Token.Type, row: Int, col: Int, src: String, offset: Int = 0) { @@ -69,7 +87,7 @@ class ScriptTest { assertEquals(Token("(", src.posAt(0, 7), Token.Type.LPAREN), p.next()) assertEquals(Token("Hello", src.posAt(0, 8), Token.Type.STRING), p.next()) assertEquals(Token(")", src.posAt(0, 15), Token.Type.RPAREN), p.next()) - + assertEquals(Token("\n", src.posAt(0, 16), Token.Type.NEWLINE), p.next()) assertEquals(Token("println", src.posAt(1, 0), Token.Type.ID), p.next()) assertEquals(Token("(", src.posAt(1, 7), Token.Type.LPAREN), p.next()) assertEquals(Token("world", src.posAt(1, 9), Token.Type.STRING), p.next()) @@ -290,7 +308,7 @@ class ScriptTest { context.eval( """ fn test1(n) { - if( n >= 10 ) + if( n >= 10 ) "enough" else "more" @@ -301,13 +319,13 @@ class ScriptTest { assertEquals("more", context.eval("test1(1)").toString()) // if/else with blocks - context = Context(pos = Pos.builtIn ) + context = Context(pos = Pos.builtIn) context.eval( """ fn test1(n) { if( n > 20 ) { "too much" - } else if( n >= 10 ) { + } else if( n >= 10 ) { "enough" } else { @@ -425,6 +443,7 @@ class ScriptTest { """ val count = 3 val res = if( count > 10 ) "too much" else "just " + count + println(res) res """.trimIndent() ) @@ -513,16 +532,21 @@ class ScriptTest { @Test fun testIncrAndDecr() = runTest { val c = Context() - assertEquals( "8", c.eval(""" + assertEquals( + "8", c.eval( + """ var x = 5 x-- x-- x++ x * 2 - """).toString()) + """ + ).toString() + ) - assertEquals( "4", c.eval("x").toString()) + assertEquals("4", c.eval("x").toString()) // assertEquals( "8", c.eval("x*2").toString()) // assertEquals( "4", c.eval("x+0").toString()) } + } \ No newline at end of file