better checks for mutability, sipport for inplace mutation with assign()

This commit is contained in:
Sergey Chernov 2025-05-29 01:35:02 +04:00
parent ff03f3066d
commit ca93d73b9c
6 changed files with 149 additions and 82 deletions

View File

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

View File

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

View File

@ -12,10 +12,10 @@ import kotlin.math.roundToLong
data class WithAccess<T>(var value: T, val isMutable: Boolean)
data class Accessor(
val getter: suspend (Context) -> Obj,
val getter: suspend (Context) -> WithAccess<Obj>,
val setterOrNull: (suspend (Context, Obj) -> Unit)?
) {
constructor(getter: suspend (Context) -> Obj) : this(getter, null)
constructor(getter: suspend (Context) -> WithAccess<Obj>) : 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<Obj>? {
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<Obj> =
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 <T> 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<Obj> = 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<Obj> { 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<Obj> by lazy { WithAccess(this, false) }
val asMutable: WithAccess<Obj> by lazy { WithAccess(this, true) }
companion object {
inline fun <reified T> from(obj: T): Obj {

View File

@ -6,4 +6,6 @@ package net.sergeych.ling
data class StoredObj(
var value: Obj?,
val isMutable: Boolean = false
)
) {
val asAccess: WithAccess<Obj>? get() = value?.let { WithAccess(it, isMutable) }
}

View File

@ -149,8 +149,9 @@ class ScriptTest {
assertFailsWith<ScriptError> {
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()
// )
// }
//
}

View File

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