diff --git a/docs/tutorial.md b/docs/tutorial.md index 3f35a42..85086d7 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -76,13 +76,16 @@ Assignemnt is an expression that changes its lvalue and return assigned value: 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) +allow chained assignments (we might fix it later). Use parentheses insofar: var x = 0 var y = 0 x = (y = 5) - x + y - >>> 10 + assert(x==5) + assert(y==5) + >>> void + +Note that assignment operator returns rvalue, it can't be assigned. ## Modifying arithmetics @@ -98,6 +101,11 @@ There is a set of assigning operations: `+=`, `-=`, `*=`, `/=` and even `%=`. Notice the parentheses here: the assignment has low priority! +These operators return rvalue, unmodifiable. + +## Assignemnt return r-value! + + ## Math It is rather simple, like everywhere else: @@ -110,7 +118,7 @@ See [math](math.md) for more on it. Notice using Greek as identifier, all langua Logical operation could be used the same - val x = 10 + var x = 10 ++x >= 11 >>> true @@ -154,11 +162,18 @@ Correct pattern is: // now is OK: foo + bar -This is though a rare case when you need uninitialized variables, most often you can use conditional operatorss +This is though a rare case when you need uninitialized variables, most often you can use conditional operators and even loops to assign results (see below). # Constants +Almost the same, using `val`: + + val foo = 1 + foo += 1 // this will throw exception + +# Constants + Same as in kotlin: val HalfPi = π / 2 diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 4c76efa..763061d 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -86,7 +86,7 @@ class Compiler( private fun parseExpression(tokens: CompilerContext): Statement? { val pos = tokens.currentPos() - return parseExpressionLevel(tokens)?.let { a -> statement(pos) { a.getter(it) } } + return parseExpressionLevel(tokens)?.let { a -> statement(pos) { a.getter(it).value } } } private fun parseExpressionLevel(tokens: CompilerContext, level: Int = 0): Accessor? { @@ -185,9 +185,9 @@ class Compiler( } Token.Type.NOT -> { - if( operand != null ) throw ScriptError(t.pos, "unexpected operator 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) } + operand = Accessor { op.getter(it).value.logicalNot(it).asReadonly } } Token.Type.DOT -> { @@ -202,18 +202,20 @@ class Compiler( isCall = true operand = Accessor { context -> context.pos = next.pos - val v = left.getter(context) - v.callInstanceMethod( - context, - next.value, - args.toArguments() + val v = left.getter(context).value + WithAccess( + v.callInstanceMethod( + context, + next.value, + args.toArguments() + ), isMutable = false ) } } } if (!isCall) { operand = Accessor { context -> - left.getter(context).readField(context, next.value) + left.getter(context).value.readField(context, next.value) } } } ?: throw ScriptError(t.pos, "Expecting expression before dot") @@ -234,7 +236,7 @@ class Compiler( // Expression in parentheses val statement = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting expression") operand = Accessor { - statement.execute(it) + statement.execute(it).asReadonly } cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.RPAREN, "missing ')'") @@ -248,7 +250,7 @@ class Compiler( if (operand != null) throw ScriptError(t.pos, "unexpected keyword") cc.previous() val s = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting valid statement") - operand = Accessor { s.execute(it) } + operand = Accessor { s.execute(it).asReadonly } } "else", "break", "continue" -> { @@ -263,10 +265,10 @@ class Compiler( // is RW: operand = Accessor({ it.pos = t.pos - left.getter(it).readField(it, t.value) + left.getter(it).value.readField(it, t.value) }) { cxt, newValue -> cxt.pos = t.pos - left.getter(cxt).writeField(cxt, t.value, newValue) + left.getter(cxt).value.writeField(cxt, t.value, newValue) } } ?: run { // variable to read or like @@ -284,13 +286,20 @@ class Compiler( operand?.let { left -> // post increment left.setter(startPos) - operand = Accessor({ ctx -> - left.getter(ctx).getAndIncrement(ctx) + operand = Accessor({ cxt -> + val x = left.getter(cxt) + if (x.isMutable) + x.value.getAndIncrement(cxt).asReadonly + else cxt.raiseError("Cannot increment immutable value") }) } ?: 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) }) + operand = Accessor({ ctx -> + next.getter(ctx).also { + if (!it.isMutable) ctx.raiseError("Cannot increment immutable value") + }.value.incrementAndGet(ctx).asReadonly + }) } } @@ -300,12 +309,18 @@ class Compiler( // post decrement left.setter(startPos) operand = Accessor { ctx -> - left.getter(ctx).getAndDecrement(ctx) + left.getter(ctx).also { + if (!it.isMutable) ctx.raiseError("Cannot decrement immutable value") + }.value.getAndDecrement(ctx).asReadonly } } ?: 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) } + operand = Accessor { ctx -> + next.getter(ctx).also { + if (!it.isMutable) ctx.raiseError("Cannot decrement immutable value") + }.value.decrementAndGet(ctx).asReadonly + } } } @@ -327,7 +342,7 @@ class Compiler( if (t.type != Token.Type.ID) throw ScriptError(t.pos, "Expecting ID after ::") return when (t.value) { "class" -> Accessor { - operand.getter(it).objClass + operand.getter(it).value.objClass.asReadonly } else -> throw ScriptError(t.pos, "Unknown scope operation: ${t.value}") @@ -357,13 +372,13 @@ class Compiler( return Accessor { context -> val v = left.getter(context) - v.callOn(context.copy( + v.value.callOn(context.copy( context.pos, Arguments( args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) } ), ) - ) + ).asReadonly } } @@ -374,31 +389,31 @@ class Compiler( Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> { cc.previous() val n = parseNumber(true, cc) - Accessor({ n }) + Accessor{ n.asReadonly } } - Token.Type.STRING -> Accessor({ ObjString(t.value) }) + Token.Type.STRING -> Accessor { ObjString(t.value).asReadonly } Token.Type.PLUS -> { val n = parseNumber(true, cc) - Accessor { n } + Accessor { n.asReadonly } } Token.Type.MINUS -> { val n = parseNumber(false, cc) - Accessor { n } + Accessor { n.asReadonly } } Token.Type.ID -> { when (t.value) { - "void" -> Accessor { ObjVoid } - "null" -> Accessor { ObjNull } - "true" -> Accessor { ObjBool(true) } - "false" -> Accessor { ObjBool(false) } + "void" -> Accessor { ObjVoid.asReadonly } + "null" -> Accessor { ObjNull.asReadonly } + "true" -> Accessor { ObjBool(true).asReadonly } + "false" -> Accessor { ObjBool(false).asReadonly } else -> { Accessor({ it.pos = t.pos - it.get(t.value)?.value + it.get(t.value)?.asAccess ?: it.raiseError("symbol not defined: '${t.value}'") }) { ctx, newValue -> ctx.get(t.value)?.let { stored -> @@ -719,7 +734,7 @@ class Compiler( data class Operator( val tokenType: Token.Type, - val priority: Int, val arity: Int=2, + val priority: Int, val arity: Int = 2, val generate: (Pos, Accessor, Accessor) -> Accessor ) { // fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND @@ -727,7 +742,7 @@ class Compiler( 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)) } + Accessor { f(it, a.getter(it).value, b.getter(it).value).asReadonly } }) } @@ -740,73 +755,74 @@ class Compiler( // assignments Operator(Token.Type.ASSIGN, lastPrty) { pos, a, b -> Accessor { - val value = b.getter(it) - a.setter(pos)(it, value) - value + val value = b.getter(it).value + val access = a.getter(it) + if (!access.isMutable) throw ScriptError(pos, "cannot assign to immutable variable") + if (access.value.assign(it, value) == null) + a.setter(pos)(it, value) + value.asReadonly } }, 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 x = a.getter(it).value + val y = b.getter(it).value + (x.plusAssign(it, y) ?: run { val result = x.plus(it, y) a.setter(pos)(it, result) result - } + }).asReadonly } }, 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 x = a.getter(it).value + val y = b.getter(it).value + (x.minusAssign(it, y) ?: run { val result = x.minus(it, y) a.setter(pos)(it, result) result - } + }).asReadonly } }, 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 x = a.getter(it).value + val y = b.getter(it).value + (x.mulAssign(it, y) ?: run { val result = x.mul(it, y) a.setter(pos)(it, result) result - } + }).asReadonly } }, 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 x = a.getter(it).value + val y = b.getter(it).value + (x.divAssign(it, y) ?: run { val result = x.div(it, y) a.setter(pos)(it, result) result - - } + }).asReadonly } }, 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 x = a.getter(it).value + val y = b.getter(it).value + (x.modAssign(it, y) ?: run { val result = x.mod(it, y) a.setter(pos)(it, result) result - - } + }).asReadonly } }, // logical 1 - Operator.simple(Token.Type.OR, ++lastPrty) { ctx, a, b -> a.logicalOr(ctx,b) }, + 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) }, + Operator.simple(Token.Type.AND, ++lastPrty) { ctx, a, b -> a.logicalAnd(ctx, b) }, // bitwise or 2 // bitwise and 3 // equality/ne 4 @@ -819,10 +835,10 @@ class Compiler( Operator.simple(Token.Type.GT, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) > 0) }, // shuttle <=> 6 // 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.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.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) }, ) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index 7c8921c..9fefee8 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -12,10 +12,10 @@ import kotlin.math.roundToLong data class WithAccess(var value: T, val isMutable: Boolean) data class Accessor( - val getter: suspend (Context) -> Obj, + val getter: suspend (Context) -> WithAccess, val setterOrNull: (suspend (Context, Obj) -> Unit)? ) { - constructor(getter: suspend (Context) -> Obj) : this(getter, null) + constructor(getter: suspend (Context) -> WithAccess) : this(getter, null) fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos, "can't assign value") } @@ -35,13 +35,13 @@ sealed class Obj { /** * Get instance member traversing the hierarchy if needed. Its meaning is different for different objects. */ - fun getInstanceMemberOrNull(name: String): Obj? { - members[name]?.let { return it.value } + fun getInstanceMemberOrNull(name: String): WithAccess? { + members[name]?.let { return it } parentInstances.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } } return null } - fun getInstanceMember(atPos: Pos, name: String): Obj = + fun getInstanceMember(atPos: Pos, name: String): WithAccess = getInstanceMemberOrNull(name) ?: throw ScriptError(atPos, "symbol doesn't exist: $name") @@ -52,7 +52,7 @@ sealed class Obj { // note that getInstanceMember traverses the hierarchy // instance _methods_ are our ObjClass instance: // note that getInstanceMember traverses the hierarchy - objClass.getInstanceMember(context.pos, name).invoke(context, this, args) + objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args) // methods that to override @@ -102,9 +102,7 @@ sealed class Obj { context.raiseNotImplemented() } - open suspend fun assign(context: Context, other: Obj): Obj { - context.raiseNotImplemented() - } + open suspend fun assign(context: Context, other: Obj): Obj? = null /** * a += b @@ -143,7 +141,7 @@ sealed class Obj { suspend fun sync(block: () -> T): T = monitor.withLock { block() } - fun readField(context: Context, name: String): Obj = getInstanceMember(context.pos, name) + fun readField(context: Context, name: String): WithAccess = getInstanceMember(context.pos, name) fun writeField(context: Context, name: String, newValue: Obj) { willMutate(context) @@ -152,7 +150,7 @@ sealed class Obj { } fun createField(name: String, initialValue: Obj, isMutable: Boolean = false, pos: Pos = Pos.builtIn) { - if (name in members || parentInstances.any { name in it.members }) + if (name in members || parentInstances.any { name in it.members }) throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes") members[name] = WithAccess(initialValue, isMutable) } @@ -169,6 +167,9 @@ sealed class Obj { suspend fun invoke(context: Context, atPos: Pos, thisObj: Obj, args: Arguments): Obj = callOn(context.copy(atPos, args = args, newThisObj = thisObj)) + val asReadonly: WithAccess by lazy { WithAccess(this, false) } + val asMutable: WithAccess by lazy { WithAccess(this, true) } + companion object { inline fun from(obj: T): Obj { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt index 1f28201..193828f 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt @@ -6,4 +6,6 @@ package net.sergeych.ling data class StoredObj( var value: Obj?, val isMutable: Boolean = false -) \ No newline at end of file +) { + val asAccess: WithAccess? get() = value?.let { WithAccess(it, isMutable) } +} \ No newline at end of file diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 7876f3e..ff15377 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -149,8 +149,9 @@ class ScriptTest { assertFailsWith { context.eval("a = 10") } - assertEquals(10, context.eval("b = a - 3 - 4; b").toInt()) - assertEquals(10, context.eval("b").toInt()) + assertEquals(17, context.eval("a").toInt()) + assertEquals(5, context.eval("b = a - 7 - 5").toInt()) + assertEquals(5, context.eval("b").toInt()) } @Test @@ -632,5 +633,37 @@ class ScriptTest { assertEquals(2, ctx.eval("x %= 5").toInt()) } + @Test + fun testVals() = runTest { + val cxt = Context() + cxt.eval("val x = 11") + assertEquals(11, cxt.eval("x").toInt()) + assertFails { cxt.eval("x = 12") } + assertFails { cxt.eval("x += 12") } + assertFails { cxt.eval("x -= 12") } + assertFails { cxt.eval("x *= 2") } + assertFails { cxt.eval("x /= 2") } + assertFails { cxt.eval("x++") } + assertFails { cxt.eval("++x") } + assertFails { cxt.eval("x--") } + assertFails { cxt.eval("--x") } + assertEquals(11, cxt.eval("x").toInt()) + } + +// @Test +// fun testMultiAssign() = runTest { +// assertEquals( +// 7, +// eval(""" +// var x = 10 +// var y = 2 +// (x = 1) = 5 +// println(x) +// println(y) +// x + y +// """.trimIndent()).toInt() +// ) +// } +// } \ No newline at end of file diff --git a/library/src/jvmTest/kotlin/BookTest.kt b/library/src/jvmTest/kotlin/BookTest.kt index 9c50227..21b6024 100644 --- a/library/src/jvmTest/kotlin/BookTest.kt +++ b/library/src/jvmTest/kotlin/BookTest.kt @@ -38,7 +38,7 @@ data class DocTest( } val detailedString by lazy { - val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line}: $s" }.joinToString("\n") + val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line + 1}: $s" }.joinToString("\n") var result = "$this\n$codeWithLines\n" if (expectedOutput.isNotBlank()) result += "--------expected output--------\n$expectedOutput\n"