v0.10.1-SNAPSHOT optimization: Accessor refactored now use more effective ObjRef and slots for locals
This commit is contained in:
parent
6ca3e4589e
commit
0b9e94c6e9
@ -36,19 +36,19 @@ Launch has the only argument which should be a callable (lambda usually) that is
|
||||
|
||||
## Synchronization: Mutex
|
||||
|
||||
Suppose we have a resource, that could be used concurrently, a coutner in our case. If we won'r protect it, concurrent usage cause RC, Race Condition, providing wrong result:
|
||||
Suppose we have a resource, that could be used concurrently, a counter in our case. If we won't protect it, concurrent usage cause RC, Race Condition, providing wrong result:
|
||||
|
||||
var counter = 0
|
||||
|
||||
(1..4).map {
|
||||
(1..50).map {
|
||||
launch {
|
||||
// slow increment:
|
||||
val c = counter
|
||||
delay(10)
|
||||
delay(100)
|
||||
counter = c + 1
|
||||
}
|
||||
}.forEach { it.await() }
|
||||
assert(counter < 4)
|
||||
assert(counter < 50) { "counter is "+counter }
|
||||
>>> void
|
||||
|
||||
The obviously wrong result is not 4, as all coroutines capture the counter value, which is 1, then sleep for 5ms, then save 1 + 1 as result. May some coroutines will pass, so it will be 1 or 2, most likely.
|
||||
|
||||
@ -1218,7 +1218,7 @@ same as:
|
||||
Are the same as in string literals with little difference:
|
||||
|
||||
| escape | ASCII value |
|
||||
|--------|-------------------|
|
||||
|--------|-----------------------|
|
||||
| \n | 0x10, newline |
|
||||
| \r | 0x13, carriage return |
|
||||
| \t | 0x07, tabulation |
|
||||
@ -1290,7 +1290,6 @@ Open-ended ranges could be used to get start and end too:
|
||||
assertEquals( "pult", "catapult"[ 4.. ])
|
||||
>>> void
|
||||
|
||||
|
||||
### String operations
|
||||
|
||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
||||
@ -1338,7 +1337,6 @@ Typical set of String functions includes:
|
||||
| matches(re) | matches the regular expression (2) |
|
||||
| | |
|
||||
|
||||
|
||||
(1)
|
||||
: List is mutable therefore a new copy is created on each call.
|
||||
|
||||
@ -1372,8 +1370,8 @@ if blank, will be removed too, for example:
|
||||
See [math functions](math.md). Other general purpose functions are:
|
||||
|
||||
| name | description |
|
||||
|----------------------------------------------|------------------------------------------------------------|
|
||||
| assert(condition,message="assertion failed") | runtime code check. There will be an option to skip them |
|
||||
|---------------------------------------|------------------------------------------------------------|
|
||||
| assert(condition, fn) | (1) runtime code check with generic or custom nessage `fn` |
|
||||
| assertEquals(a,b) | |
|
||||
| assertNotEquals(a,b) | |
|
||||
| assertTrows { /* block */ } | |
|
||||
@ -1386,6 +1384,12 @@ See [math functions](math.md). Other general purpose functions are:
|
||||
| cached(builder) | remembers builder() on first invocation and return it then |
|
||||
| let, also, apply, run | see above, flow controls |
|
||||
|
||||
(1)
|
||||
: `fn` is optional lambda returning string message to add to exception string.
|
||||
Lambda avoid unnecessary execution if assertion is not failed. for example:
|
||||
|
||||
assert( x < 10 ) { "x=%s should be < 10"(x) }
|
||||
|
||||
# Built-in constants
|
||||
|
||||
| name | description |
|
||||
|
||||
@ -86,6 +86,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
||||
override val printHelpOnEmptyArgs = true
|
||||
|
||||
val version by option("-v", "--version", help = "Print version and exit").flag()
|
||||
val benchmark by option("--benchmark", help = "Run JVM microbenchmarks and exit").flag()
|
||||
val script by argument(help = "one or more scripts to execute").optional()
|
||||
val execute: String? by option(
|
||||
"-x", "--execute", help = """
|
||||
|
||||
58
lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Benchmark.kt
Normal file
58
lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Benchmark.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Script
|
||||
|
||||
object BenchmarkRunner {
|
||||
private fun format(nanos: Long, iters: Int): String {
|
||||
val secs = nanos / 1_000_000_000.0
|
||||
val ips = iters / secs
|
||||
return "%.3f s, %.0f ops/s".format(secs, ips)
|
||||
}
|
||||
|
||||
private suspend fun runCase(name: String, code: String, iters: Int): Pair<String, String> {
|
||||
val script = Compiler.compile(code)
|
||||
// warmup
|
||||
repeat(2) { script.execute(Script.newScope()) }
|
||||
val start = System.nanoTime()
|
||||
repeat(iters) { script.execute(Script.newScope()) }
|
||||
val end = System.nanoTime()
|
||||
return name to format(end - start, iters)
|
||||
}
|
||||
|
||||
fun runAll() = runBlocking {
|
||||
val iterations = 2000
|
||||
val cases = listOf(
|
||||
// Field get/set in a loop
|
||||
"field_inc" to """
|
||||
class C { var x = 0 }
|
||||
var c = C()
|
||||
var i = 0
|
||||
while( i < 1000 ) { c.x = c.x + 1; i = i + 1 }
|
||||
c.x
|
||||
""".trimIndent(),
|
||||
// Pure arithmetic with literals
|
||||
"arith_literals" to """
|
||||
var s = 0
|
||||
var i = 0
|
||||
while( i < 1000 ) { s = s + 1 + 2 + 3 + 4 + 5; i = i + 1 }
|
||||
s
|
||||
""".trimIndent(),
|
||||
// Method call overhead via instance method
|
||||
"method_call" to """
|
||||
class C { fun inc() { this.x = this.x + 1 } var x = 0 }
|
||||
var c = C()
|
||||
var i = 0
|
||||
while( i < 1000 ) { c.inc(); i = i + 1 }
|
||||
c.x
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
println("[BENCHMARK] iterations per case: $iterations")
|
||||
for ((name, code) in cases) {
|
||||
val (n, res) = runCase(name, code, iterations)
|
||||
println("[BENCHMARK] $n: $res")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,5 +20,9 @@ package net.sergeych.lyng_cli
|
||||
import net.sergeych.runMain
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
if (args.contains("--benchmark")) {
|
||||
BenchmarkRunner.runAll()
|
||||
return
|
||||
}
|
||||
runMain(args)
|
||||
}
|
||||
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.9.3-SNAPSHOT"
|
||||
version = "0.10.1-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
|
||||
@ -183,13 +183,13 @@ class Compiler(
|
||||
|
||||
private suspend fun parseExpression(): Statement? {
|
||||
val pos = cc.currentPos()
|
||||
return parseExpressionLevel()?.let { a -> statement(pos) { a.getter(it).value } }
|
||||
return parseExpressionLevel()?.let { a -> statement(pos) { a.get(it).value } }
|
||||
}
|
||||
|
||||
private suspend fun parseExpressionLevel(level: Int = 0): Accessor? {
|
||||
private suspend fun parseExpressionLevel(level: Int = 0): ObjRef? {
|
||||
if (level == lastLevel)
|
||||
return parseTerm()
|
||||
var lvalue: Accessor? = parseExpressionLevel(level + 1) ?: return null
|
||||
var lvalue: ObjRef? = parseExpressionLevel(level + 1) ?: return null
|
||||
|
||||
while (true) {
|
||||
|
||||
@ -208,8 +208,8 @@ class Compiler(
|
||||
return lvalue
|
||||
}
|
||||
|
||||
private suspend fun parseTerm(): Accessor? {
|
||||
var operand: Accessor? = null
|
||||
private suspend fun parseTerm(): ObjRef? {
|
||||
var operand: ObjRef? = null
|
||||
|
||||
// newlines _before_
|
||||
cc.skipWsTokens()
|
||||
@ -242,9 +242,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 = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
|
||||
operand = Accessor { op.getter(it).value.logicalNot(it).asReadonly }
|
||||
operand = UnaryOpRef(UnaryOp.NOT, op)
|
||||
}
|
||||
|
||||
Token.Type.DOT, Token.Type.NULL_COALESCE -> {
|
||||
@ -260,58 +260,29 @@ class Compiler(
|
||||
Token.Type.LPAREN -> {
|
||||
cc.next()
|
||||
// instance method call
|
||||
val args = parseArgs().first
|
||||
val parsed = parseArgs()
|
||||
val args = parsed.first
|
||||
val tailBlock = parsed.second
|
||||
isCall = true
|
||||
operand = Accessor { context ->
|
||||
context.pos = next.pos
|
||||
val v = left.getter(context).value
|
||||
if (v == ObjNull && isOptional)
|
||||
ObjNull.asReadonly
|
||||
else
|
||||
ObjRecord(
|
||||
v.invokeInstanceMethod(
|
||||
context,
|
||||
next.value,
|
||||
args.toArguments(context, false)
|
||||
), isMutable = false
|
||||
)
|
||||
}
|
||||
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||
}
|
||||
|
||||
|
||||
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
||||
// single lambda arg, like assertTrows { ... }
|
||||
// single lambda arg, like assertThrows { ... }
|
||||
cc.next()
|
||||
isCall = true
|
||||
val lambda =
|
||||
parseLambdaExpression()
|
||||
operand = Accessor { context ->
|
||||
context.pos = next.pos
|
||||
val v = left.getter(context).value
|
||||
if (v == ObjNull && isOptional)
|
||||
ObjNull.asReadonly
|
||||
else
|
||||
ObjRecord(
|
||||
v.invokeInstanceMethod(
|
||||
context,
|
||||
next.value,
|
||||
Arguments(listOf(lambda.getter(context).value), true)
|
||||
), isMutable = false
|
||||
)
|
||||
}
|
||||
val lambda = parseLambdaExpression()
|
||||
val argStmt = statement { lambda.get(this).value }
|
||||
val args = listOf(ParsedArgument(argStmt, next.pos))
|
||||
operand = MethodCallRef(left, next.value, args, true, isOptional)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (!isCall) {
|
||||
operand = Accessor({ context ->
|
||||
val x = left.getter(context).value
|
||||
if (x == ObjNull && isOptional) ObjNull.asReadonly
|
||||
else x.readField(context, next.value)
|
||||
}) { cxt, newValue ->
|
||||
left.getter(cxt).value.writeField(cxt, next.value, newValue)
|
||||
}
|
||||
operand = FieldRef(left, next.value, isOptional)
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,9 +304,7 @@ class Compiler(
|
||||
} ?: run {
|
||||
// Expression in parentheses
|
||||
val statement = parseStatement() ?: throw ScriptError(t.pos, "Expecting expression")
|
||||
operand = Accessor {
|
||||
statement.execute(it).asReadonly
|
||||
}
|
||||
operand = StatementRef(statement)
|
||||
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||
cc.skipTokenOfType(Token.Type.RPAREN, "missing ')'")
|
||||
}
|
||||
@ -343,41 +312,16 @@ class Compiler(
|
||||
|
||||
Token.Type.LBRACKET, Token.Type.NULL_COALESCE_INDEX -> {
|
||||
operand?.let { left ->
|
||||
// array access
|
||||
// array access via ObjRef
|
||||
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
|
||||
val index = parseStatement() ?: throw ScriptError(t.pos, "Expecting index expression")
|
||||
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
|
||||
operand = Accessor({ cxt ->
|
||||
val i = index.execute(cxt)
|
||||
val x = left.getter(cxt).value
|
||||
if (x == ObjNull && isOptional) ObjNull.asReadonly
|
||||
else x.getAt(cxt, i).asMutable
|
||||
}) { cxt, newValue ->
|
||||
left.getter(cxt).value.putAt(cxt, index.execute(cxt), newValue)
|
||||
}
|
||||
operand = IndexRef(left, StatementRef(index), isOptional)
|
||||
} ?: run {
|
||||
// array literal
|
||||
val entries = parseArrayLiteral()
|
||||
// if it didn't throw, ot parsed ot and consumed it all
|
||||
operand = Accessor { cxt ->
|
||||
val list = mutableListOf<Obj>()
|
||||
for (e in entries) {
|
||||
when (e) {
|
||||
is ListEntry.Element -> {
|
||||
list += e.accessor.getter(cxt).value
|
||||
}
|
||||
|
||||
is ListEntry.Spread -> {
|
||||
val elements = e.accessor.getter(cxt).value
|
||||
when {
|
||||
elements is ObjList -> list.addAll(elements.list)
|
||||
else -> cxt.raiseError("Spread element must be list")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ObjList(list).asReadonly
|
||||
}
|
||||
// build list literal via ObjRef node (no per-access lambdas)
|
||||
operand = ListLiteralRef(entries)
|
||||
}
|
||||
}
|
||||
|
||||
@ -388,7 +332,7 @@ class Compiler(
|
||||
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
|
||||
cc.previous()
|
||||
val s = parseStatement() ?: throw ScriptError(t.pos, "Expecting valid statement")
|
||||
operand = Accessor { s.execute(it).asReadonly }
|
||||
operand = StatementRef(s)
|
||||
}
|
||||
|
||||
"else", "break", "continue" -> {
|
||||
@ -399,22 +343,14 @@ class Compiler(
|
||||
|
||||
"throw" -> {
|
||||
val s = parseThrowStatement()
|
||||
operand = Accessor {
|
||||
s.execute(it).asReadonly
|
||||
}
|
||||
operand = StatementRef(s)
|
||||
}
|
||||
|
||||
else -> operand?.let { left ->
|
||||
// selector: <lvalue>, '.' , <id>
|
||||
// we replace operand with selector code, that
|
||||
// is RW:
|
||||
operand = Accessor({
|
||||
it.pos = t.pos
|
||||
left.getter(it).value.readField(it, t.value)
|
||||
}) { cxt, newValue ->
|
||||
cxt.pos = t.pos
|
||||
left.getter(cxt).value.writeField(cxt, t.value, newValue)
|
||||
}
|
||||
operand = FieldRef(left, t.value, false)
|
||||
} ?: run {
|
||||
// variable to read or like
|
||||
cc.previous()
|
||||
@ -424,64 +360,22 @@ class Compiler(
|
||||
}
|
||||
|
||||
Token.Type.PLUS2 -> {
|
||||
// note: post-increment result is not assignable (truly lvalue)
|
||||
operand?.let { left ->
|
||||
// post increment
|
||||
left.setter(startPos)
|
||||
operand = Accessor { cxt ->
|
||||
val x = left.getter(cxt)
|
||||
if (x.isMutable) {
|
||||
if (x.value.isConst) {
|
||||
x.value.plus(cxt, ObjInt.One).also {
|
||||
left.setter(startPos)(cxt, it)
|
||||
}.asReadonly
|
||||
} else
|
||||
x.value.getAndIncrement(cxt).asReadonly
|
||||
} else cxt.raiseError("Cannot increment immutable value")
|
||||
}
|
||||
// ++ (post if operand exists, pre otherwise)
|
||||
operand = operand?.let { left ->
|
||||
IncDecRef(left, isIncrement = true, isPost = true, atPos = startPos)
|
||||
} ?: run {
|
||||
// no lvalue means pre-increment, expression to increment follows
|
||||
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
|
||||
operand = Accessor { ctx ->
|
||||
val x = next.getter(ctx).also {
|
||||
if (!it.isMutable) ctx.raiseError("Cannot increment immutable value")
|
||||
}.value
|
||||
if (x.isConst) {
|
||||
next.setter(startPos)(ctx, x.plus(ctx, ObjInt.One))
|
||||
x.asReadonly
|
||||
} else x.incrementAndGet(ctx).asReadonly
|
||||
}
|
||||
IncDecRef(next, isIncrement = true, isPost = false, atPos = startPos)
|
||||
}
|
||||
}
|
||||
|
||||
Token.Type.MINUS2 -> {
|
||||
// note: post-decrement result is not assignable (truly lvalue)
|
||||
operand?.let { left ->
|
||||
// post decrement
|
||||
left.setter(startPos)
|
||||
operand = Accessor { cxt ->
|
||||
val x = left.getter(cxt)
|
||||
if (!x.isMutable) cxt.raiseError("Cannot decrement immutable value")
|
||||
if (x.value.isConst) {
|
||||
x.value.minus(cxt, ObjInt.One).also {
|
||||
left.setter(startPos)(cxt, it)
|
||||
}.asReadonly
|
||||
} else
|
||||
x.value.getAndDecrement(cxt).asReadonly
|
||||
}
|
||||
// -- (post if operand exists, pre otherwise)
|
||||
operand = operand?.let { left ->
|
||||
IncDecRef(left, isIncrement = false, isPost = true, atPos = startPos)
|
||||
} ?: run {
|
||||
// no lvalue means pre-decrement, expression to decrement follows
|
||||
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
|
||||
operand = Accessor { cxt ->
|
||||
val x = next.getter(cxt)
|
||||
if (!x.isMutable) cxt.raiseError("Cannot decrement immutable value")
|
||||
if (x.value.isConst) {
|
||||
x.value.minus(cxt, ObjInt.One).also {
|
||||
next.setter(startPos)(cxt, it)
|
||||
}.asReadonly
|
||||
} else
|
||||
x.value.decrementAndGet(cxt).asReadonly
|
||||
}
|
||||
IncDecRef(next, isIncrement = false, isPost = false, atPos = startPos)
|
||||
}
|
||||
}
|
||||
|
||||
@ -497,13 +391,11 @@ class Compiler(
|
||||
null
|
||||
else
|
||||
parseExpression()
|
||||
operand = Accessor {
|
||||
ObjRange(
|
||||
left?.getter?.invoke(it)?.value ?: ObjNull,
|
||||
right?.execute(it) ?: ObjNull,
|
||||
isEndInclusive = isEndInclusive
|
||||
).asReadonly
|
||||
}
|
||||
operand = RangeRef(
|
||||
left,
|
||||
right?.let { StatementRef(it) },
|
||||
isEndInclusive
|
||||
)
|
||||
}
|
||||
|
||||
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
||||
@ -534,7 +426,7 @@ class Compiler(
|
||||
/**
|
||||
* Parse lambda expression, leading '{' is already consumed
|
||||
*/
|
||||
private suspend fun parseLambdaExpression(): Accessor {
|
||||
private suspend fun parseLambdaExpression(): ObjRef {
|
||||
// lambda args are different:
|
||||
val startPos = cc.currentPos()
|
||||
val argsDeclaration = parseArgsDeclaration()
|
||||
@ -570,7 +462,7 @@ class Compiler(
|
||||
body.execute(context)
|
||||
}
|
||||
|
||||
return Accessor { x ->
|
||||
return ValueFnRef { x ->
|
||||
closure = x
|
||||
callStatement.asReadonly
|
||||
}
|
||||
@ -600,14 +492,14 @@ class Compiler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseScopeOperator(operand: Accessor?): Accessor {
|
||||
private fun parseScopeOperator(operand: ObjRef?): ObjRef {
|
||||
// implement global scope maybe?
|
||||
if (operand == null) throw ScriptError(cc.next().pos, "Expecting expression before ::")
|
||||
val t = cc.next()
|
||||
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "Expecting ID after ::")
|
||||
return when (t.value) {
|
||||
"class" -> Accessor {
|
||||
operand.getter(it).value.objClass.asReadonly
|
||||
"class" -> ValueFnRef { scope ->
|
||||
operand.get(scope).value.objClass.asReadonly
|
||||
}
|
||||
|
||||
else -> throw ScriptError(t.pos, "Unknown scope operation: ${t.value}")
|
||||
@ -760,9 +652,9 @@ class Compiler(
|
||||
// last argument - callable
|
||||
val callableAccessor = parseLambdaExpression()
|
||||
args += ParsedArgument(
|
||||
// transform accessor to the callable:
|
||||
// transform ObjRef to the callable value
|
||||
statement {
|
||||
callableAccessor.getter(this).value
|
||||
callableAccessor.get(this).value
|
||||
},
|
||||
end.pos
|
||||
)
|
||||
@ -774,11 +666,10 @@ class Compiler(
|
||||
|
||||
|
||||
private suspend fun parseFunctionCall(
|
||||
left: Accessor,
|
||||
left: ObjRef,
|
||||
blockArgument: Boolean,
|
||||
isOptional: Boolean
|
||||
): Accessor {
|
||||
// insofar, functions always return lvalue
|
||||
): ObjRef {
|
||||
var detectedBlockArgument = blockArgument
|
||||
val args = if (blockArgument) {
|
||||
val blockArg = ParsedArgument(
|
||||
@ -791,75 +682,44 @@ class Compiler(
|
||||
detectedBlockArgument = r.second
|
||||
r.first
|
||||
}
|
||||
|
||||
return Accessor { context ->
|
||||
val v = left.getter(context)
|
||||
if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly
|
||||
v.value.callOn(
|
||||
context.createChildScope(
|
||||
context.pos,
|
||||
args.toArguments(context, detectedBlockArgument)
|
||||
// Arguments(
|
||||
// args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) }
|
||||
// ),
|
||||
)
|
||||
).asReadonly
|
||||
}
|
||||
return CallRef(left, args, detectedBlockArgument, isOptional)
|
||||
}
|
||||
|
||||
private suspend fun parseAccessor(): Accessor? {
|
||||
private suspend fun parseAccessor(): ObjRef? {
|
||||
// could be: literal
|
||||
val t = cc.next()
|
||||
return when (t.type) {
|
||||
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
|
||||
cc.previous()
|
||||
val n = parseNumber(true)
|
||||
Accessor {
|
||||
n.asReadonly
|
||||
}
|
||||
ConstRef(n.asReadonly)
|
||||
}
|
||||
|
||||
Token.Type.STRING -> Accessor { ObjString(t.value).asReadonly }
|
||||
Token.Type.STRING -> ConstRef(ObjString(t.value).asReadonly)
|
||||
|
||||
Token.Type.CHAR -> Accessor { ObjChar(t.value[0]).asReadonly }
|
||||
Token.Type.CHAR -> ConstRef(ObjChar(t.value[0]).asReadonly)
|
||||
|
||||
Token.Type.PLUS -> {
|
||||
val n = parseNumber(true)
|
||||
Accessor { n.asReadonly }
|
||||
ConstRef(n.asReadonly)
|
||||
}
|
||||
|
||||
Token.Type.MINUS -> {
|
||||
parseNumberOrNull(false)?.let { n ->
|
||||
Accessor { n.asReadonly }
|
||||
ConstRef(n.asReadonly)
|
||||
} ?: run {
|
||||
val n = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression after unary minus")
|
||||
Accessor {
|
||||
n.getter.invoke(it).value.negate(it).asReadonly
|
||||
}
|
||||
UnaryOpRef(UnaryOp.NEGATE, n)
|
||||
}
|
||||
}
|
||||
|
||||
Token.Type.ID -> {
|
||||
when (t.value) {
|
||||
"void" -> Accessor { ObjVoid.asReadonly }
|
||||
"null" -> Accessor { ObjNull.asReadonly }
|
||||
"true" -> Accessor { ObjBool(true).asReadonly }
|
||||
"false" -> Accessor { ObjFalse.asReadonly }
|
||||
else -> {
|
||||
Accessor({
|
||||
it.pos = t.pos
|
||||
it[t.value]
|
||||
?: it.raiseError("symbol not defined: '${t.value}'")
|
||||
}) { ctx, newValue ->
|
||||
ctx[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}'")
|
||||
}
|
||||
}
|
||||
"void" -> ConstRef(ObjVoid.asReadonly)
|
||||
"null" -> ConstRef(ObjNull.asReadonly)
|
||||
"true" -> ConstRef(ObjTrue.asReadonly)
|
||||
"false" -> ConstRef(ObjFalse.asReadonly)
|
||||
else -> LocalVarRef(t.value, t.pos)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1908,16 +1768,11 @@ class Compiler(
|
||||
data class Operator(
|
||||
val tokenType: Token.Type,
|
||||
val priority: Int, val arity: Int = 2,
|
||||
val generate: (Pos, Accessor, Accessor) -> Accessor
|
||||
val generate: (Pos, ObjRef, ObjRef) -> ObjRef
|
||||
) {
|
||||
// fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND
|
||||
|
||||
companion object {
|
||||
fun simple(tokenType: Token.Type, priority: Int, f: suspend (Scope, Obj, Obj) -> Obj): Operator =
|
||||
Operator(tokenType, priority, 2) { _: Pos, a: Accessor, b: Accessor ->
|
||||
Accessor { f(it, a.getter(it).value, b.getter(it).value).asReadonly }
|
||||
}
|
||||
}
|
||||
companion object {}
|
||||
|
||||
}
|
||||
|
||||
@ -1931,118 +1786,104 @@ class Compiler(
|
||||
val allOps = listOf(
|
||||
// assignments, lowest priority
|
||||
Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b ->
|
||||
Accessor {
|
||||
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
|
||||
}
|
||||
AssignRef(a, b, pos)
|
||||
},
|
||||
Operator(Token.Type.PLUSASSIGN, lastPriority) { pos, a, b ->
|
||||
Accessor {
|
||||
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
|
||||
}
|
||||
AssignOpRef(BinOp.PLUS, a, b, pos)
|
||||
},
|
||||
Operator(Token.Type.MINUSASSIGN, lastPriority) { pos, a, b ->
|
||||
Accessor {
|
||||
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
|
||||
}
|
||||
AssignOpRef(BinOp.MINUS, a, b, pos)
|
||||
},
|
||||
Operator(Token.Type.STARASSIGN, lastPriority) { pos, a, b ->
|
||||
Accessor {
|
||||
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
|
||||
}
|
||||
AssignOpRef(BinOp.STAR, a, b, pos)
|
||||
},
|
||||
Operator(Token.Type.SLASHASSIGN, lastPriority) { pos, a, b ->
|
||||
Accessor {
|
||||
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
|
||||
}
|
||||
AssignOpRef(BinOp.SLASH, a, b, pos)
|
||||
},
|
||||
Operator(Token.Type.PERCENTASSIGN, lastPriority) { pos, a, b ->
|
||||
Accessor {
|
||||
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
|
||||
}
|
||||
AssignOpRef(BinOp.PERCENT, a, b, pos)
|
||||
},
|
||||
// logical 1
|
||||
Operator.simple(Token.Type.OR, ++lastPriority) { ctx, a, b -> a.logicalOr(ctx, b) },
|
||||
Operator(Token.Type.OR, ++lastPriority) { _, a, b ->
|
||||
LogicalOrRef(a, b)
|
||||
},
|
||||
// logical 2
|
||||
Operator.simple(Token.Type.AND, ++lastPriority) { ctx, a, b -> a.logicalAnd(ctx, b) },
|
||||
// bitwise or 2
|
||||
// bitwise and 3
|
||||
// equality/not equality 4
|
||||
Operator.simple(Token.Type.EQARROW, ++lastPriority) { _, a, b -> ObjMapEntry(a, b) },
|
||||
//
|
||||
Operator.simple(Token.Type.EQ, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) == 0) },
|
||||
Operator.simple(Token.Type.NEQ, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
|
||||
Operator.simple(Token.Type.REF_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
|
||||
Operator.simple(Token.Type.REF_NEQ, lastPriority) { _, a, b -> ObjBool(a !== b) },
|
||||
Operator.simple(Token.Type.MATCH, lastPriority) { s, a, b -> a.operatorMatch(s,b) },
|
||||
Operator.simple(Token.Type.NOTMATCH, lastPriority) { s, a, b -> a.operatorNotMatch(s,b) },
|
||||
// relational <=,... 5
|
||||
Operator.simple(Token.Type.LTE, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
|
||||
Operator.simple(Token.Type.LT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) },
|
||||
Operator.simple(Token.Type.GTE, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) >= 0) },
|
||||
Operator.simple(Token.Type.GT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) > 0) },
|
||||
Operator(Token.Type.AND, ++lastPriority) { _, a, b ->
|
||||
LogicalAndRef(a, b)
|
||||
},
|
||||
// equality/not equality and related
|
||||
Operator(Token.Type.EQARROW, ++lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.EQARROW, a, b)
|
||||
},
|
||||
Operator(Token.Type.EQ, ++lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.EQ, a, b)
|
||||
},
|
||||
Operator(Token.Type.NEQ, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.NEQ, a, b)
|
||||
},
|
||||
Operator(Token.Type.REF_EQ, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.REF_EQ, a, b)
|
||||
},
|
||||
Operator(Token.Type.REF_NEQ, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.REF_NEQ, a, b)
|
||||
},
|
||||
Operator(Token.Type.MATCH, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.MATCH, a, b)
|
||||
},
|
||||
Operator(Token.Type.NOTMATCH, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.NOTMATCH, a, b)
|
||||
},
|
||||
// relational <=,...
|
||||
Operator(Token.Type.LTE, ++lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.LTE, a, b)
|
||||
},
|
||||
Operator(Token.Type.LT, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.LT, a, b)
|
||||
},
|
||||
Operator(Token.Type.GTE, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.GTE, a, b)
|
||||
},
|
||||
Operator(Token.Type.GT, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.GT, a, b)
|
||||
},
|
||||
// in, is:
|
||||
Operator.simple(Token.Type.IN, lastPriority) { c, a, b -> ObjBool(b.contains(c, a)) },
|
||||
Operator.simple(Token.Type.NOTIN, lastPriority) { c, a, b -> ObjBool(!b.contains(c, a)) },
|
||||
Operator.simple(Token.Type.IS, lastPriority) { _, a, b -> ObjBool(a.isInstanceOf(b)) },
|
||||
Operator.simple(Token.Type.NOTIS, lastPriority) { _, a, b -> ObjBool(!a.isInstanceOf(b)) },
|
||||
|
||||
Operator(Token.Type.ELVIS, ++lastPriority, 2) { _: Pos, a: Accessor, b: Accessor ->
|
||||
Accessor {
|
||||
val aa = a.getter(it).value
|
||||
(
|
||||
if (aa != ObjNull) aa
|
||||
else b.getter(it).value
|
||||
).asReadonly
|
||||
}
|
||||
Operator(Token.Type.IN, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.IN, a, b)
|
||||
},
|
||||
Operator(Token.Type.NOTIN, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.NOTIN, a, b)
|
||||
},
|
||||
Operator(Token.Type.IS, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.IS, a, b)
|
||||
},
|
||||
Operator(Token.Type.NOTIS, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.NOTIS, a, b)
|
||||
},
|
||||
|
||||
// shuttle <=> 6
|
||||
Operator.simple(Token.Type.SHUTTLE, ++lastPriority) { c, a, b ->
|
||||
ObjInt(a.compareTo(c, b).toLong())
|
||||
Operator(Token.Type.ELVIS, ++lastPriority, 2) { _, a, b ->
|
||||
ElvisRef(a, b)
|
||||
},
|
||||
// bit shifts 7
|
||||
Operator.simple(Token.Type.PLUS, ++lastPriority) { ctx, a, b -> a.plus(ctx, b) },
|
||||
Operator.simple(Token.Type.MINUS, lastPriority) { ctx, a, b -> a.minus(ctx, b) },
|
||||
|
||||
Operator.simple(Token.Type.STAR, ++lastPriority) { ctx, a, b -> a.mul(ctx, b) },
|
||||
Operator.simple(Token.Type.SLASH, lastPriority) { ctx, a, b -> a.div(ctx, b) },
|
||||
Operator.simple(Token.Type.PERCENT, lastPriority) { ctx, a, b -> a.mod(ctx, b) },
|
||||
// shuttle <=>
|
||||
Operator(Token.Type.SHUTTLE, ++lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.SHUTTLE, a, b)
|
||||
},
|
||||
// arithmetic
|
||||
Operator(Token.Type.PLUS, ++lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.PLUS, a, b)
|
||||
},
|
||||
Operator(Token.Type.MINUS, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.MINUS, a, b)
|
||||
},
|
||||
Operator(Token.Type.STAR, ++lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.STAR, a, b)
|
||||
},
|
||||
Operator(Token.Type.SLASH, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.SLASH, a, b)
|
||||
},
|
||||
Operator(Token.Type.PERCENT, lastPriority) { _, a, b ->
|
||||
BinaryOpRef(BinOp.PERCENT, a, b)
|
||||
},
|
||||
)
|
||||
|
||||
// private val assigner = allOps.first { it.tokenType == Token.Type.ASSIGN }
|
||||
|
||||
@ -17,10 +17,10 @@
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Accessor
|
||||
import net.sergeych.lyng.obj.ObjRef
|
||||
|
||||
sealed class ListEntry {
|
||||
data class Element(val accessor: Accessor) : ListEntry()
|
||||
data class Element(val ref: ObjRef) : ListEntry()
|
||||
|
||||
data class Spread(val accessor: Accessor) : ListEntry()
|
||||
data class Spread(val ref: ObjRef) : ListEntry()
|
||||
}
|
||||
@ -42,6 +42,10 @@ open class Scope(
|
||||
var thisObj: Obj = ObjVoid,
|
||||
var skipScopeCreation: Boolean = false,
|
||||
) {
|
||||
// Fast-path storage for local variables/arguments accessed by slot index.
|
||||
// Enabled by default for child scopes; module/class scopes can ignore it.
|
||||
private val slots: MutableList<ObjRecord> = mutableListOf()
|
||||
private val nameToSlot: MutableMap<String, Int> = mutableMapOf()
|
||||
open val packageName: String = "<anonymous package>"
|
||||
|
||||
constructor(
|
||||
@ -89,8 +93,8 @@ open class Scope(
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseNotFound(message: String="not found"): Nothing {
|
||||
throw ExecutionError(ObjNotFoundException(this,message))
|
||||
fun raiseNotFound(message: String = "not found"): Nothing {
|
||||
throw ExecutionError(ObjNotFoundException(this, message))
|
||||
}
|
||||
|
||||
inline fun <reified T : Obj> requiredArg(index: Int): T {
|
||||
@ -112,7 +116,7 @@ open class Scope(
|
||||
}
|
||||
|
||||
fun requireNoArgs() {
|
||||
if( args.list.isNotEmpty())
|
||||
if (args.list.isNotEmpty())
|
||||
raiseError("This function does not accept any arguments")
|
||||
}
|
||||
|
||||
@ -122,7 +126,7 @@ open class Scope(
|
||||
val t = s!!.thisObj
|
||||
if (t is T) return t
|
||||
s = s.parent
|
||||
} while(s != null)
|
||||
} while (s != null)
|
||||
raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
|
||||
}
|
||||
|
||||
@ -138,6 +142,20 @@ open class Scope(
|
||||
)
|
||||
}
|
||||
|
||||
// Slot fast-path API
|
||||
fun getSlotRecord(index: Int): ObjRecord = slots[index]
|
||||
fun setSlotValue(index: Int, newValue: Obj) {
|
||||
slots[index].value = newValue
|
||||
}
|
||||
|
||||
fun getSlotIndexOf(name: String): Int? = nameToSlot[name]
|
||||
fun allocateSlotFor(name: String, record: ObjRecord): Int {
|
||||
val idx = slots.size
|
||||
slots.add(record)
|
||||
nameToSlot[name] = idx
|
||||
return idx
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
||||
*/
|
||||
@ -160,6 +178,24 @@ open class Scope(
|
||||
*/
|
||||
fun createChildScope() = Scope(this, args, pos, thisObj)
|
||||
|
||||
/**
|
||||
* Add or update ObjRecord with a given value checking rights. Created [ObjRecord] is mutable.
|
||||
* Throws Lyng [ObjIllegalArgumentException] if yje [name] exists and readonly.
|
||||
* @return ObjRector, new or updated.
|
||||
*/
|
||||
fun addOrUpdateItem(
|
||||
name: String,
|
||||
value: Obj,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||
): ObjRecord =
|
||||
objects[name]?.let {
|
||||
if( !it.isMutable )
|
||||
raiseIllegalAssignment("symbol is readonly: $name")
|
||||
it.value = value
|
||||
it
|
||||
} ?: addItem(name, true, value, visibility, recordType)
|
||||
|
||||
fun addItem(
|
||||
name: String,
|
||||
isMutable: Boolean,
|
||||
@ -167,7 +203,13 @@ open class Scope(
|
||||
visibility: Visibility = Visibility.Public,
|
||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||
): ObjRecord {
|
||||
return ObjRecord(value, isMutable, visibility,type = recordType).also { objects[name] = it }
|
||||
val rec = ObjRecord(value, isMutable, visibility, type = recordType)
|
||||
objects[name] = rec
|
||||
// Map to a slot for fast local access (if not already mapped)
|
||||
if (getSlotIndexOf(name) == null) {
|
||||
allocateSlotFor(name, rec)
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
fun getOrCreateNamespace(name: String): ObjClass {
|
||||
@ -236,15 +278,18 @@ open class Scope(
|
||||
parent?.currentImportProvider ?: throw IllegalStateException("this scope has no manager in the chain")
|
||||
}
|
||||
|
||||
val importManager by lazy { (currentImportProvider as? ImportManager)
|
||||
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") }
|
||||
val importManager by lazy {
|
||||
(currentImportProvider as? ImportManager)
|
||||
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider")
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val contents = objects.entries.joinToString { "${if( it.value.isMutable ) "var" else "val" } ${it.key}=${it.value.value}" }
|
||||
val contents =
|
||||
objects.entries.joinToString { "${if (it.value.isMutable) "var" else "val"} ${it.key}=${it.value.value}" }
|
||||
return "S[this=$thisObj $contents]"
|
||||
}
|
||||
|
||||
fun trace(text: String="") {
|
||||
fun trace(text: String = "") {
|
||||
println("trace Scope: $text ------------------")
|
||||
var p = this.parent
|
||||
var level = 0
|
||||
|
||||
@ -19,6 +19,7 @@ package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.yield
|
||||
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||
@ -158,8 +159,11 @@ class Script(
|
||||
|
||||
addVoidFn("assert") {
|
||||
val cond = requiredArg<ObjBool>(0)
|
||||
val message = if( args.size > 1 )
|
||||
": " + (args[1] as Statement).execute(this).toString(this).value
|
||||
else ""
|
||||
if( !cond.value == true )
|
||||
raiseError(ObjAssertionFailedException(this,"Assertion failed"))
|
||||
raiseError(ObjAssertionFailedException(this, "Assertion failed$message"))
|
||||
}
|
||||
|
||||
addVoidFn("assertEquals") {
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Compiler
|
||||
@ -26,25 +25,36 @@ import net.sergeych.lyng.ScriptError
|
||||
// avoid KDOC bug: keep it
|
||||
@Suppress("unused")
|
||||
typealias DocCompiler = Compiler
|
||||
/**
|
||||
* When we need read-write access to an object in some abstract storage, we need Accessor,
|
||||
* as in-site assigning is not always sufficient, in general case we need to replace the object
|
||||
* in the storage.
|
||||
*
|
||||
* Note that assigning new value is more complex than just replacing the object, see how assignment
|
||||
* operator is implemented in [Compiler.allOps].
|
||||
*/
|
||||
data class Accessor(
|
||||
val getter: suspend (Scope) -> ObjRecord,
|
||||
val setterOrNull: (suspend (Scope, Obj) -> Unit)?
|
||||
) {
|
||||
/**
|
||||
* Simplified constructor for immutable stores.
|
||||
*/
|
||||
constructor(getter: suspend (Scope) -> ObjRecord) : this(getter, null)
|
||||
|
||||
/**
|
||||
* Get the setter or throw.
|
||||
/**
|
||||
* Final migration shim: make `Accessor` an alias to `ObjRef`.
|
||||
* This preserves source compatibility while removing lambda-based indirection.
|
||||
*/
|
||||
fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos, "can't assign value")
|
||||
typealias Accessor = ObjRef
|
||||
|
||||
/** Lambda-based reference for edge cases that still construct access via lambdas. */
|
||||
private class LambdaRef(
|
||||
private val getterFn: suspend (Scope) -> ObjRecord,
|
||||
private val setterFn: (suspend (Pos, Scope, Obj) -> Unit)? = null
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord = getterFn(scope)
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
val s = setterFn ?: throw ScriptError(pos, "can't assign value")
|
||||
s(pos, scope, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Factory functions to preserve current call sites like `Accessor { ... }`
|
||||
fun Accessor(getter: suspend (Scope) -> ObjRecord): Accessor = LambdaRef(getter)
|
||||
fun Accessor(
|
||||
getter: suspend (Scope) -> ObjRecord,
|
||||
setter: suspend (Scope, Obj) -> Unit
|
||||
): Accessor = LambdaRef(getter) { _, scope, value -> setter(scope, value) }
|
||||
|
||||
// Compatibility shims used throughout Compiler: `.getter(...)` and `.setter(pos)`
|
||||
val Accessor.getter: suspend (Scope) -> ObjRecord
|
||||
get() = { scope -> this.get(scope) }
|
||||
|
||||
fun Accessor.setter(pos: Pos): suspend (Scope, Obj) -> Unit = { scope, newValue ->
|
||||
this.setAt(pos, scope, newValue)
|
||||
}
|
||||
370
lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt
Normal file
370
lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt
Normal file
@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.*
|
||||
|
||||
/**
|
||||
* A reference to a value with optional write-back path.
|
||||
* This is a sealed, allocation-light alternative to the lambda-based Accessor.
|
||||
*/
|
||||
sealed interface ObjRef {
|
||||
suspend fun get(scope: Scope): ObjRecord
|
||||
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
throw ScriptError(pos, "can't assign value")
|
||||
}
|
||||
}
|
||||
|
||||
/** Runtime-computed read-only reference backed by a lambda. */
|
||||
class ValueFnRef(private val fn: suspend (Scope) -> ObjRecord) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord = fn(scope)
|
||||
}
|
||||
|
||||
/** Unary operations supported by ObjRef. */
|
||||
enum class UnaryOp { NOT, NEGATE }
|
||||
|
||||
/** Binary operations supported by ObjRef. */
|
||||
enum class BinOp {
|
||||
OR, AND,
|
||||
EQARROW, EQ, NEQ, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
|
||||
LTE, LT, GTE, GT,
|
||||
IN, NOTIN,
|
||||
IS, NOTIS,
|
||||
SHUTTLE,
|
||||
PLUS, MINUS, STAR, SLASH, PERCENT
|
||||
}
|
||||
|
||||
/** R-value reference for unary operations. */
|
||||
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val v = a.get(scope).value
|
||||
val r = when (op) {
|
||||
UnaryOp.NOT -> v.logicalNot(scope)
|
||||
UnaryOp.NEGATE -> v.negate(scope)
|
||||
}
|
||||
return r.asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/** R-value reference for binary operations. */
|
||||
class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.get(scope).value
|
||||
val b = right.get(scope).value
|
||||
val r: Obj = when (op) {
|
||||
BinOp.OR -> a.logicalOr(scope, b)
|
||||
BinOp.AND -> a.logicalAnd(scope, b)
|
||||
BinOp.EQARROW -> ObjMapEntry(a, b)
|
||||
BinOp.EQ -> ObjBool(a.compareTo(scope, b) == 0)
|
||||
BinOp.NEQ -> ObjBool(a.compareTo(scope, b) != 0)
|
||||
BinOp.REF_EQ -> ObjBool(a === b)
|
||||
BinOp.REF_NEQ -> ObjBool(a !== b)
|
||||
BinOp.MATCH -> a.operatorMatch(scope, b)
|
||||
BinOp.NOTMATCH -> a.operatorNotMatch(scope, b)
|
||||
BinOp.LTE -> ObjBool(a.compareTo(scope, b) <= 0)
|
||||
BinOp.LT -> ObjBool(a.compareTo(scope, b) < 0)
|
||||
BinOp.GTE -> ObjBool(a.compareTo(scope, b) >= 0)
|
||||
BinOp.GT -> ObjBool(a.compareTo(scope, b) > 0)
|
||||
BinOp.IN -> ObjBool(b.contains(scope, a))
|
||||
BinOp.NOTIN -> ObjBool(!b.contains(scope, a))
|
||||
BinOp.IS -> ObjBool(a.isInstanceOf(b))
|
||||
BinOp.NOTIS -> ObjBool(!a.isInstanceOf(b))
|
||||
BinOp.SHUTTLE -> ObjInt(a.compareTo(scope, b).toLong())
|
||||
BinOp.PLUS -> a.plus(scope, b)
|
||||
BinOp.MINUS -> a.minus(scope, b)
|
||||
BinOp.STAR -> a.mul(scope, b)
|
||||
BinOp.SLASH -> a.div(scope, b)
|
||||
BinOp.PERCENT -> a.mod(scope, b)
|
||||
}
|
||||
return r.asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/** Assignment compound op: target op= value */
|
||||
class AssignOpRef(
|
||||
private val op: BinOp,
|
||||
private val target: ObjRef,
|
||||
private val value: ObjRef,
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val x = target.get(scope).value
|
||||
val y = value.get(scope).value
|
||||
val inPlace: Obj? = when (op) {
|
||||
BinOp.PLUS -> x.plusAssign(scope, y)
|
||||
BinOp.MINUS -> x.minusAssign(scope, y)
|
||||
BinOp.STAR -> x.mulAssign(scope, y)
|
||||
BinOp.SLASH -> x.divAssign(scope, y)
|
||||
BinOp.PERCENT -> x.modAssign(scope, y)
|
||||
else -> null
|
||||
}
|
||||
if (inPlace != null) return inPlace.asReadonly
|
||||
val result: Obj = when (op) {
|
||||
BinOp.PLUS -> x.plus(scope, y)
|
||||
BinOp.MINUS -> x.minus(scope, y)
|
||||
BinOp.STAR -> x.mul(scope, y)
|
||||
BinOp.SLASH -> x.div(scope, y)
|
||||
BinOp.PERCENT -> x.mod(scope, y)
|
||||
else -> scope.raiseError("unsupported assignment op: $op")
|
||||
}
|
||||
target.setAt(atPos, scope, result)
|
||||
return result.asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/** Pre/post ++/-- on l-values */
|
||||
class IncDecRef(
|
||||
private val target: ObjRef,
|
||||
private val isIncrement: Boolean,
|
||||
private val isPost: Boolean,
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val rec = target.get(scope)
|
||||
if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value")
|
||||
val v = rec.value
|
||||
val one = ObjInt.One
|
||||
return if (v.isConst) {
|
||||
// Mirror existing semantics in Compiler for const values
|
||||
val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one)
|
||||
// write back
|
||||
target.setAt(atPos, scope, result)
|
||||
// For post-inc: previous code returned NEW value; for pre-inc: returned ORIGINAL value
|
||||
if (isPost) result.asReadonly else v.asReadonly
|
||||
} else {
|
||||
val res = when {
|
||||
isIncrement && isPost -> v.getAndIncrement(scope)
|
||||
isIncrement && !isPost -> v.incrementAndGet(scope)
|
||||
!isIncrement && isPost -> v.getAndDecrement(scope)
|
||||
else -> v.decrementAndGet(scope)
|
||||
}
|
||||
res.asReadonly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Elvis operator reference: a ?: b */
|
||||
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.get(scope).value
|
||||
val r = if (a != ObjNull) a else right.get(scope).value
|
||||
return r.asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/** Logical OR with short-circuit: a || b */
|
||||
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.get(scope).value
|
||||
if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly
|
||||
val b = right.get(scope).value
|
||||
return a.logicalOr(scope, b).asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/** Logical AND with short-circuit: a && b */
|
||||
class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.get(scope).value
|
||||
if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly
|
||||
val b = right.get(scope).value
|
||||
return a.logicalAnd(scope, b).asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only reference that always returns the same cached record.
|
||||
*/
|
||||
class ConstRef(private val record: ObjRecord) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord = record
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to an object's field with optional chaining.
|
||||
*/
|
||||
class FieldRef(
|
||||
private val target: ObjRef,
|
||||
private val name: String,
|
||||
private val isOptional: Boolean,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val base = target.get(scope).value
|
||||
return if (base == ObjNull && isOptional) ObjNull.asMutable else base.readField(scope, name)
|
||||
}
|
||||
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
val base = target.get(scope).value
|
||||
if (base == ObjNull && isOptional) {
|
||||
// no-op on null receiver for optional chaining assignment
|
||||
return
|
||||
}
|
||||
base.writeField(scope, name, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to index access (a[i]) with optional chaining.
|
||||
*/
|
||||
class IndexRef(
|
||||
private val target: ObjRef,
|
||||
private val index: ObjRef,
|
||||
private val isOptional: Boolean,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val base = target.get(scope).value
|
||||
if (base == ObjNull && isOptional) return ObjNull.asMutable
|
||||
val idx = index.get(scope).value
|
||||
return base.getAt(scope, idx).asMutable
|
||||
}
|
||||
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
val base = target.get(scope).value
|
||||
if (base == ObjNull && isOptional) {
|
||||
// no-op on null receiver for optional chaining assignment
|
||||
return
|
||||
}
|
||||
val idx = index.get(scope).value
|
||||
base.putAt(scope, idx, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* R-value reference that wraps a Statement (used during migration for expressions parsed as Statement).
|
||||
*/
|
||||
class StatementRef(private val statement: Statement) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord = statement.execute(scope).asReadonly
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct function call reference: f(args) and optional f?(args).
|
||||
*/
|
||||
class CallRef(
|
||||
private val target: ObjRef,
|
||||
private val args: List<ParsedArgument>,
|
||||
private val tailBlock: Boolean,
|
||||
private val isOptionalInvoke: Boolean,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val callee = target.get(scope).value
|
||||
if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly
|
||||
val callArgs = args.toArguments(scope, tailBlock)
|
||||
val result = callee.callOn(scope.createChildScope(scope.pos, callArgs))
|
||||
return result.asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance method call reference: obj.method(args) and optional obj?.method(args).
|
||||
*/
|
||||
class MethodCallRef(
|
||||
private val receiver: ObjRef,
|
||||
private val name: String,
|
||||
private val args: List<ParsedArgument>,
|
||||
private val tailBlock: Boolean,
|
||||
private val isOptional: Boolean,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val base = receiver.get(scope).value
|
||||
if (base == ObjNull && isOptional) return ObjNull.asReadonly
|
||||
val callArgs = args.toArguments(scope, tailBlock)
|
||||
val result = base.invokeInstanceMethod(scope, name, callArgs)
|
||||
return result.asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to a local/visible variable by name (Phase A: scope lookup).
|
||||
*/
|
||||
class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
scope.pos = atPos
|
||||
// Fast-path: slot lookup
|
||||
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it) }
|
||||
return scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
||||
}
|
||||
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
scope.pos = atPos
|
||||
// Fast-path: slot lookup
|
||||
scope.getSlotIndexOf(name)?.let {
|
||||
val rec = scope.getSlotRecord(it)
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
return
|
||||
}
|
||||
val stored = scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Array/list literal construction without per-access lambdas.
|
||||
*/
|
||||
class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val list = mutableListOf<Obj>()
|
||||
for (e in entries) {
|
||||
when (e) {
|
||||
is ListEntry.Element -> {
|
||||
list += e.ref.get(scope).value
|
||||
}
|
||||
is ListEntry.Spread -> {
|
||||
val elements = e.ref.get(scope).value
|
||||
when (elements) {
|
||||
is ObjList -> list.addAll(elements.list)
|
||||
else -> scope.raiseError("Spread element must be list")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ObjList(list).asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Range literal: left .. right or left ..< right. Right may be omitted in certain contexts.
|
||||
*/
|
||||
class RangeRef(
|
||||
private val left: ObjRef?,
|
||||
private val right: ObjRef?,
|
||||
private val isEndInclusive: Boolean
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val l = left?.get(scope)?.value ?: ObjNull
|
||||
val r = right?.get(scope)?.value ?: ObjNull
|
||||
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
/** Simple assignment: target = value */
|
||||
class AssignRef(
|
||||
private val target: ObjRef,
|
||||
private val value: ObjRef,
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val v = value.get(scope).value
|
||||
val rec = target.get(scope)
|
||||
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
|
||||
if (rec.value.assign(scope, v) == null) {
|
||||
target.setAt(atPos, scope, v)
|
||||
}
|
||||
return v.asReadonly
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,7 @@ class ObjRegex(val regex: Regex) : Obj() {
|
||||
|
||||
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
|
||||
return regex.find(other.cast<ObjString>(scope).value)?.let {
|
||||
scope.addConst("$~", ObjRegexMatch(it))
|
||||
scope.addOrUpdateItem("$~", ObjRegexMatch(it))
|
||||
ObjTrue
|
||||
} ?: ObjFalse
|
||||
}
|
||||
@ -60,8 +60,10 @@ class ObjRegexMatch(val match: MatchResult) : Obj() {
|
||||
override val objClass = type
|
||||
|
||||
val objGroups: ObjList by lazy {
|
||||
// Use groupValues so that index 0 is the whole match and subsequent indices are capturing groups,
|
||||
// which matches the language/tests expectation for `$~[i]`.
|
||||
ObjList(
|
||||
match.groups.map { it?.let { ObjString(it.value) } ?: ObjNull }.toMutableList()
|
||||
match.groupValues.map { ObjString(it) as Obj }.toMutableList()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -181,7 +181,10 @@ data class ObjString(val value: String) : Obj() {
|
||||
is ObjRegex -> self.matches(s.regex)
|
||||
is ObjString -> {
|
||||
if (s.value == ".*") true
|
||||
else self.matches(s.value.toRegex())
|
||||
else {
|
||||
val re = s.value.toRegex()
|
||||
self.matches(re)
|
||||
}
|
||||
}
|
||||
|
||||
else ->
|
||||
|
||||
9
lynglib/src/commonMain/kotlin/net/sergeych/tools/bm.kt
Normal file
9
lynglib/src/commonMain/kotlin/net/sergeych/tools/bm.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package net.sergeych.tools
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
inline fun bm(text: String="", f: ()->Unit) {
|
||||
val start = Clock.System.now()
|
||||
f()
|
||||
println("$text: ${Clock.System.now() - start}")
|
||||
}
|
||||
@ -22,6 +22,7 @@ import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
||||
import net.sergeych.tools.bm
|
||||
import kotlin.test.*
|
||||
|
||||
class ScriptTest {
|
||||
@ -812,7 +813,8 @@ class ScriptTest {
|
||||
assertEquals(6, c.eval("x").toInt())
|
||||
assertEquals(6, c.eval("x++").toInt())
|
||||
assertEquals(7, c.eval("x++").toInt())
|
||||
assertEquals(8, c.eval("x")
|
||||
assertEquals(
|
||||
8, c.eval("x")
|
||||
.also {
|
||||
println("${it.toDouble()} ${it.toInt()} ${it.toLong()} ${it.toInt()}")
|
||||
}
|
||||
@ -2252,19 +2254,35 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testMatchOperator() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
assert( "abc123".matches(".*\d{3}") )
|
||||
assert( ".*\d{3}".re =~ "abc123" )
|
||||
assert( "abc123" =~ ".*\d{3}".re )
|
||||
assert( "abc123" !~ ".*\d{4}".re )
|
||||
|
||||
|
||||
println($~)
|
||||
|
||||
"abc123" =~ ".*(\d)(\d)(\d)$".re
|
||||
println($~)
|
||||
assertEquals("1", $~[1])
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchingOperator2() = runTest {
|
||||
eval(
|
||||
"""
|
||||
"abc123" =~ ".*(\d)(\d)(\d)$".re
|
||||
println($~)
|
||||
assertEquals("1", $~[1])
|
||||
assertEquals("2", $~[2])
|
||||
assertEquals("3", $~[3])
|
||||
assertEquals("abc123", $~[0])
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
// @Test
|
||||
@ -3314,26 +3332,31 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
|
||||
// @Test
|
||||
// fun testMinimumOptimization() = runTest {
|
||||
// val x = Scope().eval(
|
||||
// """
|
||||
// fun naiveCountHappyNumbers() {
|
||||
// var count = 0
|
||||
// for( n1 in 0..9 )
|
||||
// for( n2 in 0..9 )
|
||||
// for( n3 in 0..9 )
|
||||
// for( n4 in 0..9 )
|
||||
// for( n5 in 0..9 )
|
||||
// for( n6 in 0..9 )
|
||||
// if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
|
||||
// count
|
||||
// }
|
||||
// naiveCountHappyNumbers()
|
||||
// """.trimIndent()
|
||||
// ).toInt()
|
||||
// assertEquals(55252, x)
|
||||
// }
|
||||
// @Test
|
||||
fun testMinimumOptimization() = runTest {
|
||||
for (i in 1..200) {
|
||||
bm {
|
||||
val x = Scope().eval(
|
||||
"""
|
||||
fun naiveCountHappyNumbers() {
|
||||
var count = 0
|
||||
for( n1 in 0..9 )
|
||||
for( n2 in 0..9 )
|
||||
for( n3 in 0..9 )
|
||||
for( n4 in 0..9 )
|
||||
for( n5 in 0..9 )
|
||||
for( n6 in 0..9 )
|
||||
if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
|
||||
count
|
||||
}
|
||||
naiveCountHappyNumbers()
|
||||
""".trimIndent()
|
||||
).toInt()
|
||||
assertEquals(55252, x)
|
||||
}
|
||||
delay(10)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRegex1() = runTest {
|
||||
|
||||
43
lynglib/src/commonTest/kotlin/ScriptTest_OptionalAssign.kt
Normal file
43
lynglib/src/commonTest/kotlin/ScriptTest_OptionalAssign.kt
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Tests for optional chaining assignment semantics (no-op on null receiver)
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.eval
|
||||
import kotlin.test.Test
|
||||
|
||||
class ScriptTest_OptionalAssign {
|
||||
|
||||
@Test
|
||||
fun optionalFieldAssignIsNoOp() = runTest {
|
||||
eval(
|
||||
"""
|
||||
class C { var x = 1 }
|
||||
var c = null
|
||||
// should be no-op and not throw
|
||||
c?.x = 5
|
||||
assertEquals(null, c?.x)
|
||||
// non-null receiver should work as usual
|
||||
c = C()
|
||||
c?.x = 7
|
||||
assertEquals(7, c.x)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun optionalIndexAssignIsNoOp() = runTest {
|
||||
eval(
|
||||
"""
|
||||
var a = null
|
||||
// should be no-op and not throw
|
||||
a?[0] = 42
|
||||
assertEquals(null, a?[0])
|
||||
// non-null receiver should work as usual
|
||||
a = [1,2,3]
|
||||
a?[1] = 99
|
||||
assertEquals(99, a[1])
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user