diff --git a/docs/tutorial.md b/docs/tutorial.md index 2262b8d..8680cb7 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -4,36 +4,38 @@ Ling is a very simple language, where we take only most important and popular fe other scripts and languages. In particular, we adopt _principle of minimal confusion_[^1]. In other word, the code usually works as expected when you see it. So, nothing unusual. -# Expressions and blocks. +# Expressions Everything is an expression in Ling. Even an empty block: - { - // empty block - } + // empty block >>> void -Block returns it last expression as "return value": +any block also returns it's last expression: - { + if( true ) { 2 + 2 3 + 3 } >>> 6 -Same is without block: - - 3 + 3 - >>> 6 - If you don't want block to return anything, use `void`: - { - 3 + 4 + fn voidFunction() { + 3 + 4 // this will be ignored void } + voidFunction() >>> void +otherwise, last expression will be returned: + + fn normalize(value, minValue, maxValue) { + (value - minValue) / (maxValue-minValue) + } + normalize( 4, 0.0, 10.0) + >>> 0.4 + Every construction is an expression that returns something (or `void`): val x = 111 // or autotest will fail! @@ -54,18 +56,47 @@ You can use blocks in if statement, as expected: When putting multiple statments in the same line it is convenient and recommended to use `;`: - var from; var to; + var from; var to from = 0; to = 100 - >>> void - -Notice: returned value is `void` as assignment operator does not return its value. We might decide to change it. + >>> 100 +Notice: returned value is `100` as assignment operator returns its assigned value. Most often you can omit `;`, but improves readability and prevent some hardly seen bugs. -So the principles are: +## Assignments -- everything is an expression returning its last calculated value or `void` -- expression could be a `{ block }` +Assignemnt is an expression that changes its lvalue and return assigned value: + + var x = 100 + x = 20 + println(5 + (x=6)) // 11: x changes its value! + x + >>> 11 + >>> 6 + +As the assignment itself is an expression, you can use it in strange ways. Just remember +to use parentheses as assignment operation insofar is left-associated and will not +allow chained assignments (we might fix it later) + + var x = 0 + var y = 0 + x = (y = 5) + x + y + >>> 10 + +## Modifying arithmetics + +There is a set of assigning operations: `+=`, `-=`, `*=`, `/=` and even `%=`. + + var x = 5 + assert( 25 == (x*=5) ) + assert( 25 == x) + assert( 24 == (x-=1) ) + assert( 12 == (x/=2) ) + x + >>> 12 + +Notice the parentheses here: the assignment has low priority! ## Expression details @@ -202,7 +233,7 @@ Regular pre-condition while loop, as expression, loop returns it's last line res var count = 0 while( count < 5 ) { - count = count + 1 + count++ count * 10 } >>> 50 @@ -212,18 +243,17 @@ We can break as usual: var count = 0 while( count < 5 ) { if( count < 5 ) break - count = count + 1 - count * 10 + count = ++count * 10 } >>> void Why `void`? Because `break` drops out without the chute, not providing anything to return. Indeed, we should provide exit value in the case: var count = 0 - while( count < 5 ) { + while( count < 50 ) { if( count > 3 ) break "too much" - count = count + 1 - count * 10 + count = ++count * 10 + "wrong "+count } >>> too much @@ -288,6 +318,38 @@ The label can be any valid identifier, even a keyword, labels exist in their own Right now labels are implemented only for the while loop. It is intended to be implemented for all loops and returns. +# Self-assignments in expression + +There are auto-increments and auto-decrements: + + var counter = 0 + assert(counter++ * 100 == 0) + assert(counter == 1) + >>> void + +but: + + var counter = 0 + assert( ++counter * 100 == 100) + assert(counter == 1) + >>> void + +The same with `--`: + + var count = 100 + var sum = 0 + while( count > 0 ) sum = sum + count-- + sum + >>> 5050 + +There are self-assigning version for operators too: + + var count = 100 + var sum = 0 + while( count > 0 ) sum += count-- + sum + >>> 5050 + # Comments // single line comment diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 39a6a1b..4c76efa 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -29,20 +29,20 @@ class Compiler( return when (t.type) { Token.Type.ID -> { // could be keyword, assignment or just the expression - val next = tokens.next() - if (next.type == Token.Type.ASSIGN) { - // this _is_ assignment statement - return AssignStatement( - t.pos, t.value, - parseStatement(tokens) ?: throw ScriptError( - t.pos, - "Expecting expression for assignment operator" - ) - ) - } - // not assignment, maybe keyword statement: - // get back the token which is not '=': - tokens.previous() +// val next = tokens.next() +// if (next.type == Token.Type.ASSIGN) { +// this _is_ assignment statement +// return AssignStatement( +// t.pos, t.value, +// parseStatement(tokens) ?: throw ScriptError( +// t.pos, +// "Expecting expression for assignment operator" +// ) +// ) +// } +// not assignment, maybe keyword statement: +// get back the token which is not '=': +// tokens.previous() // try keyword statement parseKeywordStatement(t, tokens) ?: run { @@ -84,10 +84,15 @@ class Compiler( } } - private fun parseExpression(tokens: CompilerContext, level: Int = 0): Statement? { + private fun parseExpression(tokens: CompilerContext): Statement? { + val pos = tokens.currentPos() + return parseExpressionLevel(tokens)?.let { a -> statement(pos) { a.getter(it) } } + } + + private fun parseExpressionLevel(tokens: CompilerContext, level: Int = 0): Accessor? { if (level == lastLevel) return parseTerm3(tokens) - var lvalue = parseExpression(tokens, level + 1) + var lvalue = parseExpressionLevel(tokens, level + 1) if (lvalue == null) return null while (true) { @@ -99,7 +104,7 @@ class Compiler( break } - val rvalue = parseExpression(tokens, level + 1) + val rvalue = parseExpressionLevel(tokens, level + 1) ?: throw ScriptError(opToken.pos, "Expecting expression") lvalue = op.generate(opToken.pos, lvalue!!, rvalue) @@ -167,7 +172,7 @@ class Compiler( * expr-=, expr*=, expr/= * read expr: */ - private fun parseTerm3(cc: CompilerContext): Statement? { + private fun parseTerm3(cc: CompilerContext): Accessor? { var operand: Accessor? = null while (true) { @@ -176,7 +181,13 @@ class Compiler( when (t.type) { Token.Type.NEWLINE, Token.Type.SEMICOLON, Token.Type.EOF -> { cc.previous() - return operand?.let { op -> statement(startPos) { op.getter(it) } } + return operand + } + + Token.Type.NOT -> { + if( operand != null ) throw ScriptError(t.pos, "unexpected operator not '!'") + val op = parseTerm3(cc) ?: throw ScriptError(t.pos, "Expecting expression") + operand = Accessor { op.getter(it).logicalNot(it) } } Token.Type.DOT -> { @@ -233,11 +244,6 @@ class Compiler( Token.Type.ID -> { // there could be terminal operators or keywords:// variable to read or like when (t.value) { - "else" -> { - cc.previous() - return operand?.let { op -> statement(startPos) { op.getter(it) } } - } - "if", "when", "do", "while", "return" -> { if (operand != null) throw ScriptError(t.pos, "unexpected keyword") cc.previous() @@ -245,9 +251,9 @@ class Compiler( operand = Accessor { s.execute(it) } } - "break", "continue" -> { + "else", "break", "continue" -> { cc.previous() - return operand?.let { op -> statement(startPos) { op.getter(it) } } + return operand } @@ -307,9 +313,7 @@ class Compiler( else -> { cc.previous() - operand?.let { op -> - return statement(startPos) { op.getter(it) } - } + operand?.let { return it } operand = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression") } } @@ -713,53 +717,118 @@ class Compiler( } } -// fun parseStatement(parser: Parser): Statement? = -// parser.withToken { -// if (tokens.isEmpty()) null -// else { -// when (val token = tokens[0]) { -// else -> { -// rollback() -// null -// } -// } -// } -// } - data class Operator( val tokenType: Token.Type, - val priority: Int, val arity: Int, - val generate: (Pos, Statement, Statement) -> Statement - ) + val priority: Int, val arity: Int=2, + val generate: (Pos, Accessor, Accessor) -> Accessor + ) { +// fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND + + companion object { + fun simple(tokenType: Token.Type, priority: Int, f: suspend (Context, Obj, Obj) -> Obj): Operator = + Operator(tokenType, priority, 2, { _: Pos, a: Accessor, b: Accessor -> + Accessor { f(it, a.getter(it), b.getter(it)) } + }) + } + + } companion object { + private var lastPrty = 0 val allOps = listOf( - Operator(Token.Type.OR, 0, 2) { pos, a, b -> LogicalOrStatement(pos, a, b) }, - Operator(Token.Type.AND, 1, 2) { pos, a, b -> LogicalAndStatement(pos, a, b) }, + // assignments + Operator(Token.Type.ASSIGN, lastPrty) { pos, a, b -> + Accessor { + val value = b.getter(it) + a.setter(pos)(it, value) + value + } + }, + Operator(Token.Type.PLUSASSIGN, lastPrty) { pos, a, b -> + Accessor { + val x = a.getter(it) + val y = b.getter(it) + x.plusAssign(it, y) ?: run { + val result = x.plus(it, y) + a.setter(pos)(it, result) + result + } + } + }, + Operator(Token.Type.MINUSASSIGN, lastPrty) { pos, a, b -> + Accessor { + val x = a.getter(it) + val y = b.getter(it) + x.minusAssign(it, y) ?: run { + val result = x.minus(it, y) + a.setter(pos)(it, result) + result + } + } + }, + Operator(Token.Type.STARASSIGN, lastPrty) { pos, a, b -> + Accessor { + val x = a.getter(it) + val y = b.getter(it) + x.mulAssign(it, y) ?: run { + val result = x.mul(it, y) + a.setter(pos)(it, result) + result + + } + } + }, + Operator(Token.Type.SLASHASSIGN, lastPrty) { pos, a, b -> + Accessor { + val x = a.getter(it) + val y = b.getter(it) + x.divAssign(it, y) ?: run { + val result = x.div(it, y) + a.setter(pos)(it, result) + result + + } + } + }, + Operator(Token.Type.PERCENTASSIGN, lastPrty) { pos, a, b -> + Accessor { + val x = a.getter(it) + val y = b.getter(it) + x.modAssign(it, y) ?: run { + val result = x.mod(it, y) + a.setter(pos)(it, result) + result + + } + } + }, + // logical 1 + Operator.simple(Token.Type.OR, ++lastPrty) { ctx, a, b -> a.logicalOr(ctx,b) }, + // logical 2 + Operator.simple(Token.Type.AND, ++lastPrty) { ctx, a, b -> a.logicalAnd(ctx,b) }, // bitwise or 2 // bitwise and 3 // equality/ne 4 - 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 }, + Operator.simple(Token.Type.EQ, ++lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) == 0) }, + Operator.simple(Token.Type.NEQ, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) }, // relational <=,... 5 - 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 }, + Operator.simple(Token.Type.LTE, ++lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) }, + Operator.simple(Token.Type.LT, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) }, + Operator.simple(Token.Type.GTE, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) >= 0) }, + Operator.simple(Token.Type.GT, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) > 0) }, // shuttle <=> 6 - // bitshhifts 7 - Operator(Token.Type.PLUS, 8, 2) { pos, a, b -> - PlusStatement(pos, a, b) - }, - Operator(Token.Type.MINUS, 8, 2) { pos, a, b -> - MinusStatement(pos, a, b) - }, - Operator(Token.Type.STAR, 9, 2) { pos, a, b -> MulStatement(pos, a, b) }, - Operator(Token.Type.SLASH, 9, 2) { pos, a, b -> DivStatement(pos, a, b) }, - Operator(Token.Type.PERCENT, 9, 2) { pos, a, b -> ModStatement(pos, a, b) }, + // bit shifts 7 + Operator.simple(Token.Type.PLUS, ++lastPrty) { ctx, a, b -> a.plus(ctx,b) }, + Operator.simple(Token.Type.MINUS, lastPrty) { ctx, a, b -> a.minus(ctx,b) }, + + Operator.simple(Token.Type.STAR, ++lastPrty) { ctx, a, b -> a.mul(ctx,b) }, + Operator.simple(Token.Type.SLASH, lastPrty) { ctx, a, b -> a.div(ctx, b) }, + Operator.simple(Token.Type.PERCENT, lastPrty) { ctx, a, b -> a.mod(ctx, b) }, ) - val lastLevel = 10 + + val lastLevel = lastPrty + 1 + val byLevel: List> = (0.. allOps.filter { it.priority == l } .map { it.tokenType to it }.toMap() @@ -771,17 +840,3 @@ class Compiler( suspend fun eval(code: String) = Compiler.compile(code).execute() -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(it, a.execute(it), b.execute(it)) - ) - } -} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/CompilerContext.kt b/library/src/commonMain/kotlin/net/sergeych/ling/CompilerContext.kt index 5cc13b8..a6d71fb 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/CompilerContext.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/CompilerContext.kt @@ -16,6 +16,7 @@ internal class CompilerContext(val tokens: List) : ListIterator by if (type != it.type) throw ScriptError(it.pos, message) } + @Suppress("unused") fun syntaxError(at: Pos, message: String = "Syntax error"): Nothing { throw ScriptError(at, message) } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 57fab0e..53ed39b 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -14,6 +14,7 @@ class Context( fun raiseNotImplemented(): Nothing = raiseError("operation not implemented") + @Suppress("unused") fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this)) fun raiseError(message: String): Nothing { @@ -73,17 +74,6 @@ class Context( } } - inline fun addConstWithAliases(value: T, vararg names: String) { - val obj = Obj.from(value) - for (name in names) { - addItem( - name, - false, - obj - ) - } - } - fun addConst(name: String,value: Obj) = addItem(name, false, value) suspend fun eval(code: String): Obj = diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index 9697631..7c8921c 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable import kotlin.math.floor import kotlin.math.roundToLong -typealias InstanceMethod = (Context, Obj) -> Obj +//typealias InstanceMethod = (Context, Obj) -> Obj data class WithAccess(var value: T, val isMutable: Boolean) @@ -70,32 +70,70 @@ sealed class Obj { */ open val objClass: ObjClass by lazy { ObjClass("Obj") } - open fun plus(context: Context, other: Obj): Obj { + open suspend fun plus(context: Context, other: Obj): Obj { context.raiseNotImplemented() } - open fun assign(context: Context, other: Obj): Obj { + open suspend fun minus(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 { + open suspend fun mul(context: Context, other: Obj): Obj { context.raiseNotImplemented() } - open fun incrementAndGet(context: Context): Obj { + open suspend fun div(context: Context, other: Obj): Obj { context.raiseNotImplemented() } - open fun decrementAndGet(context: Context): Obj { + open suspend fun mod(context: Context, other: Obj): Obj { context.raiseNotImplemented() } - open fun getAndDecrement(context: Context): Obj { + open suspend fun logicalNot(context: Context): Obj { + context.raiseNotImplemented() + } + + open suspend fun logicalAnd(context: Context, other: Obj): Obj { + context.raiseNotImplemented() + } + + open suspend fun logicalOr(context: Context, other: Obj): Obj { + context.raiseNotImplemented() + } + + open suspend fun assign(context: Context, other: Obj): Obj { + context.raiseNotImplemented() + } + + /** + * a += b + * if( the operation is not defined, it returns null and the compiler would try + * to generate it as 'this = this + other', reassigning its variable + */ + open suspend fun plusAssign(context: Context, other: Obj): Obj? = null + + /** + * `-=` operations, see [plusAssign] + */ + open suspend fun minusAssign(context: Context, other: Obj): Obj? = null + open suspend fun mulAssign(context: Context, other: Obj): Obj? = null + open suspend fun divAssign(context: Context, other: Obj): Obj? = null + open suspend fun modAssign(context: Context, other: Obj): Obj? = null + + open suspend fun getAndIncrement(context: Context): Obj { + context.raiseNotImplemented() + } + + open suspend fun incrementAndGet(context: Context): Obj { + context.raiseNotImplemented() + } + + open suspend fun decrementAndGet(context: Context): Obj { + context.raiseNotImplemented() + } + + open suspend fun getAndDecrement(context: Context): Obj { context.raiseNotImplemented() } @@ -105,9 +143,9 @@ sealed class Obj { suspend fun sync(block: () -> T): T = monitor.withLock { block() } - suspend fun readField(context: Context, name: String): Obj = getInstanceMember(context.pos, name) + fun readField(context: Context, name: String): Obj = getInstanceMember(context.pos, name) - suspend fun writeField(context: Context, name: String, newValue: Obj) { + fun writeField(context: Context, name: String, newValue: Obj) { willMutate(context) members[name]?.let { if (it.isMutable) it.value = newValue } ?: context.raiseError("Can't reassign member: $name") @@ -194,6 +232,10 @@ data class ObjString(val value: String) : Obj() { override val objClass: ObjClass get() = type + override suspend fun plus(context: Context, other: Obj): Obj { + return ObjString(value + other.asStr.value) + } + companion object { val type = ObjClass("String") } @@ -239,6 +281,21 @@ data class ObjReal(val value: Double) : Obj(), Numeric { override val objClass: ObjClass = type + override suspend fun plus(context: Context, other: Obj): Obj = + ObjReal(this.value + other.toDouble()) + + override suspend fun minus(context: Context, other: Obj): Obj = + ObjReal(this.value - other.toDouble()) + + override suspend fun mul(context: Context, other: Obj): Obj = + ObjReal(this.value * other.toDouble()) + + override suspend fun div(context: Context, other: Obj): Obj = + ObjReal(this.value / other.toDouble()) + + override suspend fun mod(context: Context, other: Obj): Obj = + ObjReal(this.value % other.toDouble()) + companion object { val type: ObjClass = ObjClass("Real").apply { createField( @@ -258,19 +315,19 @@ data class ObjInt(var value: Long) : Obj(), Numeric { override val toObjInt get() = this override val toObjReal = ObjReal(doubleValue) - override fun getAndIncrement(context: Context): Obj { + override suspend fun getAndIncrement(context: Context): Obj { return ObjInt(value).also { value++ } } - override fun getAndDecrement(context: Context): Obj { + override suspend fun getAndDecrement(context: Context): Obj { return ObjInt(value).also { value-- } } - override fun incrementAndGet(context: Context): Obj { + override suspend fun incrementAndGet(context: Context): Obj { return ObjInt(++value) } - override fun decrementAndGet(context: Context): Obj { + override suspend fun decrementAndGet(context: Context): Obj { return ObjInt(--value) } @@ -283,6 +340,33 @@ data class ObjInt(var value: Long) : Obj(), Numeric { override val objClass: ObjClass = type + override suspend fun plus(context: Context, other: Obj): Obj = + if (other is ObjInt) + ObjInt(this.value + other.value) + else + ObjReal(this.doubleValue + other.toDouble()) + + override suspend fun minus(context: Context, other: Obj): Obj = + if (other is ObjInt) + ObjInt(this.value - other.value) + else + ObjReal(this.doubleValue - other.toDouble()) + + override suspend fun mul(context: Context, other: Obj): Obj = + if (other is ObjInt) { + ObjInt(this.value * other.value) + } else ObjReal(this.value * other.toDouble()) + + override suspend fun div(context: Context, other: Obj): Obj = + if (other is ObjInt) + ObjInt(this.value / other.value) + else ObjReal(this.value / other.toDouble()) + + override suspend fun mod(context: Context, other: Obj): Obj = + if (other is ObjInt) + ObjInt(this.value % other.value) + else ObjReal(this.value.toDouble() % other.toDouble()) + companion object { val type = ObjClass("Int") } @@ -300,6 +384,12 @@ data class ObjBool(val value: Boolean) : Obj() { override val objClass: ObjClass = type + override suspend fun logicalNot(context: Context): Obj = ObjBool(!value) + + override suspend fun logicalAnd(context: Context, other: Obj): Obj = ObjBool(value && other.toBool()) + + override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool()) + companion object { val type = ObjClass("Bool") } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index 2deb378..204eede 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -3,7 +3,7 @@ package net.sergeych.ling val digitsSet = ('0'..'9').toSet() val digits = { d: Char -> d in digitsSet } val hexDigits = digitsSet + ('a'..'f') + ('A'..'F') -val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit()} +val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit() } @Suppress("unused") val idFirstChars = { d: Char -> d.isLetter() || d == '_' } @@ -56,62 +56,82 @@ private class Parser(fromPos: Pos) { advance() Token("+", from, Token.Type.PLUS2) } + '=' -> { advance() Token("+", from, Token.Type.PLUSASSIGN) } + else -> Token("+", from, Token.Type.PLUS) } } + '-' -> { 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) - '/' -> { - if( currentChar == '/') { + + '*' -> { + if (currentChar == '=') { + advance() + Token("*=", from, Token.Type.STARASSIGN) + } else + Token("*", from, Token.Type.STAR) + } + + '/' -> when (currentChar) { + '/' -> { advance() Token(loadToEnd().trim(), from, Token.Type.SINLGE_LINE_COMMENT) } - else - Token("/", from, Token.Type.SLASH) + '=' -> { + advance() + Token("/=", from, Token.Type.SLASHASSIGN) + } + else -> Token("/", from, Token.Type.SLASH) } - '%' -> Token("%", from, Token.Type.PERCENT) + + '%' -> when(currentChar) { + '=' -> { advance(); Token("%=", from, Token.Type.PERCENTASSIGN) } + else -> Token("%", from, Token.Type.PERCENT) } + '.' -> Token(".", from, Token.Type.DOT) '<' -> { - if(currentChar == '=') { + if (currentChar == '=') { advance() Token("<=", from, Token.Type.LTE) - } - else + } else Token("<", from, Token.Type.LT) } + '>' -> { - if( currentChar == '=') { + if (currentChar == '=') { advance() Token(">=", from, Token.Type.GTE) - } - else + } else Token(">", from, Token.Type.GT) } + '!' -> { - if( currentChar == '=') { + if (currentChar == '=') { advance() Token("!=", from, Token.Type.NEQ) - } - else + } else Token("!", from, Token.Type.NOT) } + '|' -> { if (currentChar == '|') { advance() @@ -119,6 +139,7 @@ private class Parser(fromPos: Pos) { } else Token("|", from, Token.Type.BITOR) } + '&' -> { if (currentChar == '&') { advance() @@ -126,19 +147,20 @@ private class Parser(fromPos: Pos) { } else Token("&", from, Token.Type.BITAND) } + '@' -> { val label = loadChars(idNextChars) - if( label.isNotEmpty()) Token(label, from, Token.Type.ATLABEL) + if (label.isNotEmpty()) Token(label, from, Token.Type.ATLABEL) else raise("unexpected @ character") } + '\n' -> Token("\n", from, Token.Type.NEWLINE) ':' -> { - if( currentChar == ':') { + if (currentChar == ':') { advance() Token("::", from, Token.Type.COLONCOLON) - } - else + } else Token(":", from, Token.Type.COLON) } @@ -154,20 +176,17 @@ private class Parser(fromPos: Pos) { // statement@some: ID 'statement', LABEL 'some'! if (ch.isLetter() || ch == '_') { val text = ch + loadChars(idNextChars) - if( currentChar == '@') { + if (currentChar == '@') { advance() - if( currentChar.isLetter()) { + if (currentChar.isLetter()) { // break@label or like pos.back() Token(text, from, Token.Type.ID) - } - else + } else Token(text, from, Token.Type.LABEL) - } - else + } else Token(text, from, Token.Type.ID) - } - else + } else raise("can't parse token") } } @@ -209,7 +228,7 @@ private class Parser(fromPos: Pos) { // could be integer, also hex: if (currentChar == 'x' && p1 == "0") { advance() - Token(loadChars({ it in hexDigits}), start, Token.Type.HEX).also { + Token(loadChars({ it in hexDigits }), start, Token.Type.HEX).also { if (currentChar.isLetter()) raise("invalid hex literal") } @@ -261,7 +280,7 @@ private class Parser(fromPos: Pos) { * * @return the string of valid characters, could be empty */ - private fun loadChars(isValidChar: (Char)->Boolean): String { + private fun loadChars(isValidChar: (Char) -> Boolean): String { val startLine = pos.line val result = StringBuilder() while (!pos.end && pos.line == startLine) { @@ -306,7 +325,7 @@ private class Parser(fromPos: Pos) { private fun skipws(): Char? { while (!pos.end) { val ch = pos.currentChar - if( ch == '\n') break + if (ch == '\n') break if (ch.isWhitespace()) advance() else diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 9e6ec7c..7e5020e 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -31,17 +31,17 @@ class Script( addFn("floor") { val x = args.firstAndOnly() (if (x is ObjInt) x - else ObjReal(floor(x.toDouble()))) as Obj + else ObjReal(floor(x.toDouble()))) } addFn("ceil") { val x = args.firstAndOnly() (if (x is ObjInt) x - else ObjReal(ceil(x.toDouble()))) as Obj + else ObjReal(ceil(x.toDouble()))) } addFn("round") { val x = args.firstAndOnly() (if (x is ObjInt) x - else ObjReal(round(x.toDouble()))) as Obj + else ObjReal(round(x.toDouble()))) } addFn("sin") { ObjReal(sin(args.firstAndOnly().toDouble())) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index e6f241c..d687aca 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -11,7 +11,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) { ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, PLUS2, MINUS2, EQ, NEQ, LT, LTE, GT, GTE, - AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, + AND, BITAND, OR, BITOR, NOT, BITNOT, 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 1c04935..3f18f76 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt @@ -45,153 +45,4 @@ fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false override suspend fun execute(context: Context): Obj = f(context) } -class LogicalAndStatement( - override val pos: Pos, - val left: Statement, val right: Statement -) : Statement() { - override suspend fun execute(context: Context): Obj { - - val l = left.execute(context).let { - (it as? ObjBool) ?: raise("left operand is not boolean: $it") - } - val r = right.execute(context).let { - (it as? ObjBool) ?: raise("right operand is not boolean: $it") - } - return ObjBool(l.value && r.value) - } -} - -class LogicalOrStatement( - override val pos: Pos, - val left: Statement, val right: Statement -) : Statement() { - override suspend fun execute(context: Context): Obj { - - val l = left.execute(context).let { - (it as? ObjBool) ?: raise("left operand is not boolean: $it") - } - val r = right.execute(context).let { - (it as? ObjBool) ?: raise("right operand is not boolean: $it") - } - return ObjBool(l.value || r.value) - } -} - -class PlusStatement( - override val pos: Pos, - val left: Statement, val right: Statement -) : Statement() { - override suspend fun execute(context: Context): Obj { - // todo: implement also classes with 'plus' operator - val l = left.execute(context) - - if (l is ObjString) - return ObjString(l.toString() + right.execute(context).asStr) - - if (l !is Numeric) - raise("left operand is not number: $l") - - val r = right.execute(context) - if (r !is Numeric) - raise("right operand is not boolean: $r") - - return if (l is ObjInt && r is ObjInt) - ObjInt(l.longValue + r.longValue) - else - ObjReal(l.doubleValue + r.doubleValue) - } -} - -class MinusStatement( - override val pos: Pos, - val left: Statement, val right: Statement -) : Statement() { - override suspend fun execute(context: Context): Obj { - // todo: implement also classes with 'minus' operator - val l = left.execute(context) - if (l !is Numeric) - raise("left operand is not number: $l") - - val r = right.execute(context) - if (r !is Numeric) - raise("right operand is not number: $r") - - return if (l is ObjInt && r is ObjInt) - ObjInt(l.longValue - r.longValue) - else - ObjReal(l.doubleValue - r.doubleValue) - } -} - -class MulStatement( - override val pos: Pos, - val left: Statement, val right: Statement -) : Statement() { - override suspend fun execute(context: Context): Obj { - val l = left.execute(context) - if (l !is Numeric) - raise("left operand is not number: $l") - - val r = right.execute(context) - if (r !is Numeric) - raise("right operand is not number: $r") - - return if (l is ObjInt && r is ObjInt) - ObjInt(l.longValue * r.longValue) - else - ObjReal(l.doubleValue * r.doubleValue) - } -} - -class DivStatement( - override val pos: Pos, - val left: Statement, val right: Statement -) : Statement() { - override suspend fun execute(context: Context): Obj { - val l = left.execute(context) - if (l !is Numeric) - raise("left operand is not number: $l") - - val r = right.execute(context) - if (r !is Numeric) - raise("right operand is not number: $r") - - return if (l is ObjInt && r is ObjInt) - ObjInt(l.longValue / r.longValue) - else - ObjReal(l.doubleValue / r.doubleValue) - } -} - -class ModStatement( - override val pos: Pos, - val left: Statement, val right: Statement -) : Statement() { - override suspend fun execute(context: Context): Obj { - val l = left.execute(context) - if (l !is Numeric) - raise("left operand is not number: $l") - - val r = right.execute(context) - if (r !is Numeric) - raise("right operand is not number: $r") - - return if (l is ObjInt && r is ObjInt) - ObjInt(l.longValue % r.longValue) - else - ObjReal(l.doubleValue % r.doubleValue) - } -} - - - -class AssignStatement(override val pos: Pos, val name: String, val value: Statement) : Statement() { - override suspend fun execute(context: Context): Obj { - val variable = context[name] ?: raise("can't assign: variable does not exist: $name") - if (!variable.isMutable) - throw ScriptError(pos, "can't reassign val $name") - variable.value = value.execute(context) - return ObjVoid - } -} diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 8e416d5..7876f3e 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -223,8 +223,29 @@ class ScriptTest { } @Test - fun arithmeticParenthesisTest() = runTest { + fun arithmetics() = runTest { + // integer assertEquals(17, eval("2 + 3 * 5").toInt()) + assertEquals(4, eval("5-1").toInt()) + assertEquals(2, eval("8/4").toInt()) + assertEquals(2, eval("8 % 3").toInt()) + + // int-real + assertEquals(9.5, eval("2 + 3 * 2.5").toDouble()) + assertEquals(4.5, eval("5 - 0.5").toDouble()) + assertEquals(2.5, eval("5 / 2.0").toDouble()) + assertEquals(2.5, eval("5.0 / 2.0").toDouble()) + + // real + assertEquals(7.5, eval("2.5 + 5.0").toDouble()) + assertEquals(4.5, eval("5.0 - 0.5").toDouble()) + assertEquals(12.5, eval("5.0 * 2.5").toDouble()) + assertEquals(2.5, eval("5.0 / 2.0").toDouble()) + } + + @Test + fun arithmeticParenthesisTest() = runTest { + assertEquals(17, eval("2.0 + 3 * 5").toInt()) assertEquals(17, eval("2 + (3 * 5)").toInt()) assertEquals(25, eval("(2 + 3) * 5").toInt()) assertEquals(24, eval("(2 + 3) * 5 -1").toInt()) @@ -252,6 +273,22 @@ class ScriptTest { assertTrue { eval("2 == 2 && 3 != 4").toBool() } } + @Test + fun logicTest() = runTest { + assertEquals(ObjBool(false), eval("true && false")) + assertEquals(ObjBool(false), eval("false && false")) + assertEquals(ObjBool(false), eval("false && true")) + assertEquals(ObjBool(true), eval("true && true")) + + assertEquals(ObjBool(true), eval("true || false")) + assertEquals(ObjBool(false), eval("false || false")) + assertEquals(ObjBool(true), eval("false || true")) + assertEquals(ObjBool(true), eval("true || true")) + + assertEquals(ObjBool(false), eval("!true")) + assertEquals(ObjBool(true), eval("!false")) + } + @Test fun gtLtTest() = runTest { assertTrue { eval("3 > 2").toBool() } @@ -423,6 +460,7 @@ class ScriptTest { var t2 = 10 while( t2 > 0 ) { t2 = t2 - 1 + println("t2 " + t2 + " t1 " + t1) if( t2 == 3 && t1 == 7) { break@outer "ok2:"+t2+":"+t1 } @@ -564,5 +602,35 @@ class ScriptTest { eval(src) } + @Test + fun testAssign1() = runTest { + assertEquals(10, eval("var x = 5; x=10; x").toInt()) + val ctx = Context() + ctx.eval(""" + var a = 1 + """.trimIndent()) + assertEquals(3, ctx.eval("a + a + 1").toInt()) + assertEquals(12, ctx.eval("a + (a = 10) + 1").toInt()) + assertEquals(10, ctx.eval("a").toInt()) + } + + @Test + fun testAssign2() = runTest { + val ctx = Context() + ctx.eval("var x = 10") + assertEquals(14, ctx.eval("x += 4").toInt()) + assertEquals(14, ctx.eval("x").toInt()) + assertEquals(12, ctx.eval("x -= 2").toInt()) + assertEquals(12, ctx.eval("x").toInt()) + + assertEquals(24, ctx.eval("x *= 2").toInt()) + assertEquals(24, ctx.eval("x").toInt()) + + assertEquals(12, ctx.eval("x /= 2").toInt()) + assertEquals(12, ctx.eval("x").toInt()) + + assertEquals(2, ctx.eval("x %= 5").toInt()) + } + } \ No newline at end of file