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
|
## 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
|
var counter = 0
|
||||||
|
|
||||||
(1..4).map {
|
(1..50).map {
|
||||||
launch {
|
launch {
|
||||||
// slow increment:
|
// slow increment:
|
||||||
val c = counter
|
val c = counter
|
||||||
delay(10)
|
delay(100)
|
||||||
counter = c + 1
|
counter = c + 1
|
||||||
}
|
}
|
||||||
}.forEach { it.await() }
|
}.forEach { it.await() }
|
||||||
assert(counter < 4)
|
assert(counter < 50) { "counter is "+counter }
|
||||||
>>> void
|
>>> 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.
|
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.
|
||||||
|
|||||||
@ -1217,13 +1217,13 @@ same as:
|
|||||||
|
|
||||||
Are the same as in string literals with little difference:
|
Are the same as in string literals with little difference:
|
||||||
|
|
||||||
| escape | ASCII value |
|
| escape | ASCII value |
|
||||||
|--------|-------------------|
|
|--------|-----------------------|
|
||||||
| \n | 0x10, newline |
|
| \n | 0x10, newline |
|
||||||
| \r | 0x13, carriage return |
|
| \r | 0x13, carriage return |
|
||||||
| \t | 0x07, tabulation |
|
| \t | 0x07, tabulation |
|
||||||
| \\ | \ slash character |
|
| \\ | \ slash character |
|
||||||
| \' | ' apostrophe |
|
| \' | ' apostrophe |
|
||||||
|
|
||||||
### Char instance members
|
### Char instance members
|
||||||
|
|
||||||
@ -1290,7 +1290,6 @@ Open-ended ranges could be used to get start and end too:
|
|||||||
assertEquals( "pult", "catapult"[ 4.. ])
|
assertEquals( "pult", "catapult"[ 4.. ])
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
|
||||||
### String operations
|
### String operations
|
||||||
|
|
||||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
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) |
|
| matches(re) | matches the regular expression (2) |
|
||||||
| | |
|
| | |
|
||||||
|
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
: List is mutable therefore a new copy is created on each call.
|
: List is mutable therefore a new copy is created on each call.
|
||||||
|
|
||||||
@ -1371,20 +1369,26 @@ if blank, will be removed too, for example:
|
|||||||
|
|
||||||
See [math functions](math.md). Other general purpose functions are:
|
See [math functions](math.md). Other general purpose functions are:
|
||||||
|
|
||||||
| name | description |
|
| 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) | |
|
| assertEquals(a,b) | |
|
||||||
| assertNotEquals(a,b) | |
|
| assertNotEquals(a,b) | |
|
||||||
| assertTrows { /* block */ } | |
|
| assertTrows { /* block */ } | |
|
||||||
| check(condition, message=<default>) | throws IllegalStateException" of condition isn't met |
|
| check(condition, message=<default>) | throws IllegalStateException" of condition isn't met |
|
||||||
| require(condition, message=<default>) | throws IllegalArgumentException" of condition isn't met |
|
| require(condition, message=<default>) | throws IllegalArgumentException" of condition isn't met |
|
||||||
| println(args...) | Open for overriding, it prints to stdout with newline. |
|
| println(args...) | Open for overriding, it prints to stdout with newline. |
|
||||||
| print(args...) | Open for overriding, it prints to stdout without newline. |
|
| print(args...) | Open for overriding, it prints to stdout without newline. |
|
||||||
| flow {} | create flow sequence, see [parallelism] |
|
| flow {} | create flow sequence, see [parallelism] |
|
||||||
| delay, launch, yield | see [parallelism] |
|
| delay, launch, yield | see [parallelism] |
|
||||||
| cached(builder) | remembers builder() on first invocation and return it then |
|
| cached(builder) | remembers builder() on first invocation and return it then |
|
||||||
| let, also, apply, run | see above, flow controls |
|
| 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
|
# Built-in constants
|
||||||
|
|
||||||
|
|||||||
@ -86,6 +86,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
override val printHelpOnEmptyArgs = true
|
override val printHelpOnEmptyArgs = true
|
||||||
|
|
||||||
val version by option("-v", "--version", help = "Print version and exit").flag()
|
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 script by argument(help = "one or more scripts to execute").optional()
|
||||||
val execute: String? by option(
|
val execute: String? by option(
|
||||||
"-x", "--execute", help = """
|
"-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
|
import net.sergeych.runMain
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
if (args.contains("--benchmark")) {
|
||||||
|
BenchmarkRunner.runAll()
|
||||||
|
return
|
||||||
|
}
|
||||||
runMain(args)
|
runMain(args)
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.9.3-SNAPSHOT"
|
version = "0.10.1-SNAPSHOT"
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
|
|||||||
@ -183,13 +183,13 @@ class Compiler(
|
|||||||
|
|
||||||
private suspend fun parseExpression(): Statement? {
|
private suspend fun parseExpression(): Statement? {
|
||||||
val pos = cc.currentPos()
|
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)
|
if (level == lastLevel)
|
||||||
return parseTerm()
|
return parseTerm()
|
||||||
var lvalue: Accessor? = parseExpressionLevel(level + 1) ?: return null
|
var lvalue: ObjRef? = parseExpressionLevel(level + 1) ?: return null
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
@ -208,8 +208,8 @@ class Compiler(
|
|||||||
return lvalue
|
return lvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun parseTerm(): Accessor? {
|
private suspend fun parseTerm(): ObjRef? {
|
||||||
var operand: Accessor? = null
|
var operand: ObjRef? = null
|
||||||
|
|
||||||
// newlines _before_
|
// newlines _before_
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
@ -242,9 +242,9 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.NOT -> {
|
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")
|
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 -> {
|
Token.Type.DOT, Token.Type.NULL_COALESCE -> {
|
||||||
@ -260,58 +260,29 @@ class Compiler(
|
|||||||
Token.Type.LPAREN -> {
|
Token.Type.LPAREN -> {
|
||||||
cc.next()
|
cc.next()
|
||||||
// instance method call
|
// instance method call
|
||||||
val args = parseArgs().first
|
val parsed = parseArgs()
|
||||||
|
val args = parsed.first
|
||||||
|
val tailBlock = parsed.second
|
||||||
isCall = true
|
isCall = true
|
||||||
operand = Accessor { context ->
|
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
||||||
// single lambda arg, like assertTrows { ... }
|
// single lambda arg, like assertThrows { ... }
|
||||||
cc.next()
|
cc.next()
|
||||||
isCall = true
|
isCall = true
|
||||||
val lambda =
|
val lambda = parseLambdaExpression()
|
||||||
parseLambdaExpression()
|
val argStmt = statement { lambda.get(this).value }
|
||||||
operand = Accessor { context ->
|
val args = listOf(ParsedArgument(argStmt, next.pos))
|
||||||
context.pos = next.pos
|
operand = MethodCallRef(left, next.value, args, true, isOptional)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isCall) {
|
if (!isCall) {
|
||||||
operand = Accessor({ context ->
|
operand = FieldRef(left, next.value, isOptional)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,9 +304,7 @@ class Compiler(
|
|||||||
} ?: run {
|
} ?: run {
|
||||||
// Expression in parentheses
|
// Expression in parentheses
|
||||||
val statement = parseStatement() ?: throw ScriptError(t.pos, "Expecting expression")
|
val statement = parseStatement() ?: throw ScriptError(t.pos, "Expecting expression")
|
||||||
operand = Accessor {
|
operand = StatementRef(statement)
|
||||||
statement.execute(it).asReadonly
|
|
||||||
}
|
|
||||||
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||||
cc.skipTokenOfType(Token.Type.RPAREN, "missing ')'")
|
cc.skipTokenOfType(Token.Type.RPAREN, "missing ')'")
|
||||||
}
|
}
|
||||||
@ -343,41 +312,16 @@ class Compiler(
|
|||||||
|
|
||||||
Token.Type.LBRACKET, Token.Type.NULL_COALESCE_INDEX -> {
|
Token.Type.LBRACKET, Token.Type.NULL_COALESCE_INDEX -> {
|
||||||
operand?.let { left ->
|
operand?.let { left ->
|
||||||
// array access
|
// array access via ObjRef
|
||||||
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
|
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
|
||||||
val index = parseStatement() ?: throw ScriptError(t.pos, "Expecting index expression")
|
val index = parseStatement() ?: throw ScriptError(t.pos, "Expecting index expression")
|
||||||
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
|
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
|
||||||
operand = Accessor({ cxt ->
|
operand = IndexRef(left, StatementRef(index), isOptional)
|
||||||
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)
|
|
||||||
}
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// array literal
|
// array literal
|
||||||
val entries = parseArrayLiteral()
|
val entries = parseArrayLiteral()
|
||||||
// if it didn't throw, ot parsed ot and consumed it all
|
// build list literal via ObjRef node (no per-access lambdas)
|
||||||
operand = Accessor { cxt ->
|
operand = ListLiteralRef(entries)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,7 +332,7 @@ class Compiler(
|
|||||||
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
|
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
|
||||||
cc.previous()
|
cc.previous()
|
||||||
val s = parseStatement() ?: throw ScriptError(t.pos, "Expecting valid statement")
|
val s = parseStatement() ?: throw ScriptError(t.pos, "Expecting valid statement")
|
||||||
operand = Accessor { s.execute(it).asReadonly }
|
operand = StatementRef(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
"else", "break", "continue" -> {
|
"else", "break", "continue" -> {
|
||||||
@ -399,22 +343,14 @@ class Compiler(
|
|||||||
|
|
||||||
"throw" -> {
|
"throw" -> {
|
||||||
val s = parseThrowStatement()
|
val s = parseThrowStatement()
|
||||||
operand = Accessor {
|
operand = StatementRef(s)
|
||||||
s.execute(it).asReadonly
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> operand?.let { left ->
|
else -> operand?.let { left ->
|
||||||
// selector: <lvalue>, '.' , <id>
|
// selector: <lvalue>, '.' , <id>
|
||||||
// we replace operand with selector code, that
|
// we replace operand with selector code, that
|
||||||
// is RW:
|
// is RW:
|
||||||
operand = Accessor({
|
operand = FieldRef(left, t.value, false)
|
||||||
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)
|
|
||||||
}
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// variable to read or like
|
// variable to read or like
|
||||||
cc.previous()
|
cc.previous()
|
||||||
@ -424,64 +360,22 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.PLUS2 -> {
|
Token.Type.PLUS2 -> {
|
||||||
// note: post-increment result is not assignable (truly lvalue)
|
// ++ (post if operand exists, pre otherwise)
|
||||||
operand?.let { left ->
|
operand = operand?.let { left ->
|
||||||
// post increment
|
IncDecRef(left, isIncrement = true, isPost = true, atPos = startPos)
|
||||||
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")
|
|
||||||
}
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// no lvalue means pre-increment, expression to increment follows
|
|
||||||
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
|
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
|
||||||
operand = Accessor { ctx ->
|
IncDecRef(next, isIncrement = true, isPost = false, atPos = startPos)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.MINUS2 -> {
|
Token.Type.MINUS2 -> {
|
||||||
// note: post-decrement result is not assignable (truly lvalue)
|
// -- (post if operand exists, pre otherwise)
|
||||||
operand?.let { left ->
|
operand = operand?.let { left ->
|
||||||
// post decrement
|
IncDecRef(left, isIncrement = false, isPost = true, atPos = startPos)
|
||||||
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
|
|
||||||
}
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// no lvalue means pre-decrement, expression to decrement follows
|
|
||||||
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
|
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
|
||||||
operand = Accessor { cxt ->
|
IncDecRef(next, isIncrement = false, isPost = false, atPos = startPos)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,13 +391,11 @@ class Compiler(
|
|||||||
null
|
null
|
||||||
else
|
else
|
||||||
parseExpression()
|
parseExpression()
|
||||||
operand = Accessor {
|
operand = RangeRef(
|
||||||
ObjRange(
|
left,
|
||||||
left?.getter?.invoke(it)?.value ?: ObjNull,
|
right?.let { StatementRef(it) },
|
||||||
right?.execute(it) ?: ObjNull,
|
isEndInclusive
|
||||||
isEndInclusive = isEndInclusive
|
)
|
||||||
).asReadonly
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
||||||
@ -534,7 +426,7 @@ class Compiler(
|
|||||||
/**
|
/**
|
||||||
* Parse lambda expression, leading '{' is already consumed
|
* Parse lambda expression, leading '{' is already consumed
|
||||||
*/
|
*/
|
||||||
private suspend fun parseLambdaExpression(): Accessor {
|
private suspend fun parseLambdaExpression(): ObjRef {
|
||||||
// lambda args are different:
|
// lambda args are different:
|
||||||
val startPos = cc.currentPos()
|
val startPos = cc.currentPos()
|
||||||
val argsDeclaration = parseArgsDeclaration()
|
val argsDeclaration = parseArgsDeclaration()
|
||||||
@ -570,7 +462,7 @@ class Compiler(
|
|||||||
body.execute(context)
|
body.execute(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Accessor { x ->
|
return ValueFnRef { x ->
|
||||||
closure = x
|
closure = x
|
||||||
callStatement.asReadonly
|
callStatement.asReadonly
|
||||||
}
|
}
|
||||||
@ -600,14 +492,14 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseScopeOperator(operand: Accessor?): Accessor {
|
private fun parseScopeOperator(operand: ObjRef?): ObjRef {
|
||||||
// implement global scope maybe?
|
// implement global scope maybe?
|
||||||
if (operand == null) throw ScriptError(cc.next().pos, "Expecting expression before ::")
|
if (operand == null) throw ScriptError(cc.next().pos, "Expecting expression before ::")
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "Expecting ID after ::")
|
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "Expecting ID after ::")
|
||||||
return when (t.value) {
|
return when (t.value) {
|
||||||
"class" -> Accessor {
|
"class" -> ValueFnRef { scope ->
|
||||||
operand.getter(it).value.objClass.asReadonly
|
operand.get(scope).value.objClass.asReadonly
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw ScriptError(t.pos, "Unknown scope operation: ${t.value}")
|
else -> throw ScriptError(t.pos, "Unknown scope operation: ${t.value}")
|
||||||
@ -760,9 +652,9 @@ class Compiler(
|
|||||||
// last argument - callable
|
// last argument - callable
|
||||||
val callableAccessor = parseLambdaExpression()
|
val callableAccessor = parseLambdaExpression()
|
||||||
args += ParsedArgument(
|
args += ParsedArgument(
|
||||||
// transform accessor to the callable:
|
// transform ObjRef to the callable value
|
||||||
statement {
|
statement {
|
||||||
callableAccessor.getter(this).value
|
callableAccessor.get(this).value
|
||||||
},
|
},
|
||||||
end.pos
|
end.pos
|
||||||
)
|
)
|
||||||
@ -774,11 +666,10 @@ class Compiler(
|
|||||||
|
|
||||||
|
|
||||||
private suspend fun parseFunctionCall(
|
private suspend fun parseFunctionCall(
|
||||||
left: Accessor,
|
left: ObjRef,
|
||||||
blockArgument: Boolean,
|
blockArgument: Boolean,
|
||||||
isOptional: Boolean
|
isOptional: Boolean
|
||||||
): Accessor {
|
): ObjRef {
|
||||||
// insofar, functions always return lvalue
|
|
||||||
var detectedBlockArgument = blockArgument
|
var detectedBlockArgument = blockArgument
|
||||||
val args = if (blockArgument) {
|
val args = if (blockArgument) {
|
||||||
val blockArg = ParsedArgument(
|
val blockArg = ParsedArgument(
|
||||||
@ -791,75 +682,44 @@ class Compiler(
|
|||||||
detectedBlockArgument = r.second
|
detectedBlockArgument = r.second
|
||||||
r.first
|
r.first
|
||||||
}
|
}
|
||||||
|
return CallRef(left, args, detectedBlockArgument, isOptional)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun parseAccessor(): Accessor? {
|
private suspend fun parseAccessor(): ObjRef? {
|
||||||
// could be: literal
|
// could be: literal
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
return when (t.type) {
|
return when (t.type) {
|
||||||
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
|
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
|
||||||
cc.previous()
|
cc.previous()
|
||||||
val n = parseNumber(true)
|
val n = parseNumber(true)
|
||||||
Accessor {
|
ConstRef(n.asReadonly)
|
||||||
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 -> {
|
Token.Type.PLUS -> {
|
||||||
val n = parseNumber(true)
|
val n = parseNumber(true)
|
||||||
Accessor { n.asReadonly }
|
ConstRef(n.asReadonly)
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.MINUS -> {
|
Token.Type.MINUS -> {
|
||||||
parseNumberOrNull(false)?.let { n ->
|
parseNumberOrNull(false)?.let { n ->
|
||||||
Accessor { n.asReadonly }
|
ConstRef(n.asReadonly)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
val n = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression after unary minus")
|
val n = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression after unary minus")
|
||||||
Accessor {
|
UnaryOpRef(UnaryOp.NEGATE, n)
|
||||||
n.getter.invoke(it).value.negate(it).asReadonly
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.ID -> {
|
Token.Type.ID -> {
|
||||||
when (t.value) {
|
when (t.value) {
|
||||||
"void" -> Accessor { ObjVoid.asReadonly }
|
"void" -> ConstRef(ObjVoid.asReadonly)
|
||||||
"null" -> Accessor { ObjNull.asReadonly }
|
"null" -> ConstRef(ObjNull.asReadonly)
|
||||||
"true" -> Accessor { ObjBool(true).asReadonly }
|
"true" -> ConstRef(ObjTrue.asReadonly)
|
||||||
"false" -> Accessor { ObjFalse.asReadonly }
|
"false" -> ConstRef(ObjFalse.asReadonly)
|
||||||
else -> {
|
else -> LocalVarRef(t.value, t.pos)
|
||||||
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}'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1908,16 +1768,11 @@ class Compiler(
|
|||||||
data class Operator(
|
data class Operator(
|
||||||
val tokenType: Token.Type,
|
val tokenType: Token.Type,
|
||||||
val priority: Int, val arity: Int = 2,
|
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
|
// fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND
|
||||||
|
|
||||||
companion object {
|
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1931,118 +1786,104 @@ class Compiler(
|
|||||||
val allOps = listOf(
|
val allOps = listOf(
|
||||||
// assignments, lowest priority
|
// assignments, lowest priority
|
||||||
Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b ->
|
Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b ->
|
||||||
Accessor {
|
AssignRef(a, b, pos)
|
||||||
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, lastPriority) { pos, a, b ->
|
Operator(Token.Type.PLUSASSIGN, lastPriority) { pos, a, b ->
|
||||||
Accessor {
|
AssignOpRef(BinOp.PLUS, a, b, pos)
|
||||||
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, lastPriority) { pos, a, b ->
|
Operator(Token.Type.MINUSASSIGN, lastPriority) { pos, a, b ->
|
||||||
Accessor {
|
AssignOpRef(BinOp.MINUS, a, b, pos)
|
||||||
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, lastPriority) { pos, a, b ->
|
Operator(Token.Type.STARASSIGN, lastPriority) { pos, a, b ->
|
||||||
Accessor {
|
AssignOpRef(BinOp.STAR, a, b, pos)
|
||||||
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, lastPriority) { pos, a, b ->
|
Operator(Token.Type.SLASHASSIGN, lastPriority) { pos, a, b ->
|
||||||
Accessor {
|
AssignOpRef(BinOp.SLASH, a, b, pos)
|
||||||
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, lastPriority) { pos, a, b ->
|
Operator(Token.Type.PERCENTASSIGN, lastPriority) { pos, a, b ->
|
||||||
Accessor {
|
AssignOpRef(BinOp.PERCENT, a, b, pos)
|
||||||
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
|
// 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
|
// logical 2
|
||||||
Operator.simple(Token.Type.AND, ++lastPriority) { ctx, a, b -> a.logicalAnd(ctx, b) },
|
Operator(Token.Type.AND, ++lastPriority) { _, a, b ->
|
||||||
// bitwise or 2
|
LogicalAndRef(a, b)
|
||||||
// bitwise and 3
|
},
|
||||||
// equality/not equality 4
|
// equality/not equality and related
|
||||||
Operator.simple(Token.Type.EQARROW, ++lastPriority) { _, a, b -> ObjMapEntry(a, b) },
|
Operator(Token.Type.EQARROW, ++lastPriority) { _, a, b ->
|
||||||
//
|
BinaryOpRef(BinOp.EQARROW, 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(Token.Type.EQ, ++lastPriority) { _, a, b ->
|
||||||
Operator.simple(Token.Type.REF_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
|
BinaryOpRef(BinOp.EQ, 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(Token.Type.NEQ, lastPriority) { _, a, b ->
|
||||||
Operator.simple(Token.Type.NOTMATCH, lastPriority) { s, a, b -> a.operatorNotMatch(s,b) },
|
BinaryOpRef(BinOp.NEQ, a, b)
|
||||||
// relational <=,... 5
|
},
|
||||||
Operator.simple(Token.Type.LTE, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
|
Operator(Token.Type.REF_EQ, lastPriority) { _, a, b ->
|
||||||
Operator.simple(Token.Type.LT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) },
|
BinaryOpRef(BinOp.REF_EQ, a, b)
|
||||||
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.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:
|
// in, is:
|
||||||
Operator.simple(Token.Type.IN, lastPriority) { c, a, b -> ObjBool(b.contains(c, a)) },
|
Operator(Token.Type.IN, lastPriority) { _, a, b ->
|
||||||
Operator.simple(Token.Type.NOTIN, lastPriority) { c, a, b -> ObjBool(!b.contains(c, a)) },
|
BinaryOpRef(BinOp.IN, a, b)
|
||||||
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.NOTIN, lastPriority) { _, a, b ->
|
||||||
|
BinaryOpRef(BinOp.NOTIN, a, b)
|
||||||
Operator(Token.Type.ELVIS, ++lastPriority, 2) { _: Pos, a: Accessor, b: Accessor ->
|
},
|
||||||
Accessor {
|
Operator(Token.Type.IS, lastPriority) { _, a, b ->
|
||||||
val aa = a.getter(it).value
|
BinaryOpRef(BinOp.IS, a, b)
|
||||||
(
|
},
|
||||||
if (aa != ObjNull) aa
|
Operator(Token.Type.NOTIS, lastPriority) { _, a, b ->
|
||||||
else b.getter(it).value
|
BinaryOpRef(BinOp.NOTIS, a, b)
|
||||||
).asReadonly
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// shuttle <=> 6
|
Operator(Token.Type.ELVIS, ++lastPriority, 2) { _, a, b ->
|
||||||
Operator.simple(Token.Type.SHUTTLE, ++lastPriority) { c, a, b ->
|
ElvisRef(a, b)
|
||||||
ObjInt(a.compareTo(c, b).toLong())
|
|
||||||
},
|
},
|
||||||
// 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) },
|
// shuttle <=>
|
||||||
Operator.simple(Token.Type.SLASH, lastPriority) { ctx, a, b -> a.div(ctx, b) },
|
Operator(Token.Type.SHUTTLE, ++lastPriority) { _, a, b ->
|
||||||
Operator.simple(Token.Type.PERCENT, lastPriority) { ctx, a, b -> a.mod(ctx, 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 }
|
// private val assigner = allOps.first { it.tokenType == Token.Type.ASSIGN }
|
||||||
|
|||||||
@ -17,10 +17,10 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
|
|
||||||
import net.sergeych.lyng.obj.Accessor
|
import net.sergeych.lyng.obj.ObjRef
|
||||||
|
|
||||||
sealed class ListEntry {
|
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 thisObj: Obj = ObjVoid,
|
||||||
var skipScopeCreation: Boolean = false,
|
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>"
|
open val packageName: String = "<anonymous package>"
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -89,8 +93,8 @@ open class Scope(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun raiseNotFound(message: String="not found"): Nothing {
|
fun raiseNotFound(message: String = "not found"): Nothing {
|
||||||
throw ExecutionError(ObjNotFoundException(this,message))
|
throw ExecutionError(ObjNotFoundException(this, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Obj> requiredArg(index: Int): T {
|
inline fun <reified T : Obj> requiredArg(index: Int): T {
|
||||||
@ -112,7 +116,7 @@ open class Scope(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun requireNoArgs() {
|
fun requireNoArgs() {
|
||||||
if( args.list.isNotEmpty())
|
if (args.list.isNotEmpty())
|
||||||
raiseError("This function does not accept any arguments")
|
raiseError("This function does not accept any arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +126,7 @@ open class Scope(
|
|||||||
val t = s!!.thisObj
|
val t = s!!.thisObj
|
||||||
if (t is T) return t
|
if (t is T) return t
|
||||||
s = s.parent
|
s = s.parent
|
||||||
} while(s != null)
|
} while (s != null)
|
||||||
raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
|
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`.
|
* 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)
|
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(
|
fun addItem(
|
||||||
name: String,
|
name: String,
|
||||||
isMutable: Boolean,
|
isMutable: Boolean,
|
||||||
@ -167,7 +203,13 @@ open class Scope(
|
|||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||||
): ObjRecord {
|
): 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 {
|
fun getOrCreateNamespace(name: String): ObjClass {
|
||||||
@ -236,15 +278,18 @@ open class Scope(
|
|||||||
parent?.currentImportProvider ?: throw IllegalStateException("this scope has no manager in the chain")
|
parent?.currentImportProvider ?: throw IllegalStateException("this scope has no manager in the chain")
|
||||||
}
|
}
|
||||||
|
|
||||||
val importManager by lazy { (currentImportProvider as? ImportManager)
|
val importManager by lazy {
|
||||||
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") }
|
(currentImportProvider as? ImportManager)
|
||||||
|
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider")
|
||||||
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
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]"
|
return "S[this=$thisObj $contents]"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trace(text: String="") {
|
fun trace(text: String = "") {
|
||||||
println("trace Scope: $text ------------------")
|
println("trace Scope: $text ------------------")
|
||||||
var p = this.parent
|
var p = this.parent
|
||||||
var level = 0
|
var level = 0
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package net.sergeych.lyng
|
|||||||
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
|
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||||
@ -158,8 +159,11 @@ class Script(
|
|||||||
|
|
||||||
addVoidFn("assert") {
|
addVoidFn("assert") {
|
||||||
val cond = requiredArg<ObjBool>(0)
|
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 )
|
if( !cond.value == true )
|
||||||
raiseError(ObjAssertionFailedException(this,"Assertion failed"))
|
raiseError(ObjAssertionFailedException(this, "Assertion failed$message"))
|
||||||
}
|
}
|
||||||
|
|
||||||
addVoidFn("assertEquals") {
|
addVoidFn("assertEquals") {
|
||||||
|
|||||||
@ -15,7 +15,6 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
import net.sergeych.lyng.Compiler
|
import net.sergeych.lyng.Compiler
|
||||||
@ -26,25 +25,36 @@ import net.sergeych.lyng.ScriptError
|
|||||||
// avoid KDOC bug: keep it
|
// avoid KDOC bug: keep it
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
typealias DocCompiler = Compiler
|
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 {
|
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
|
||||||
return regex.find(other.cast<ObjString>(scope).value)?.let {
|
return regex.find(other.cast<ObjString>(scope).value)?.let {
|
||||||
scope.addConst("$~", ObjRegexMatch(it))
|
scope.addOrUpdateItem("$~", ObjRegexMatch(it))
|
||||||
ObjTrue
|
ObjTrue
|
||||||
} ?: ObjFalse
|
} ?: ObjFalse
|
||||||
}
|
}
|
||||||
@ -60,8 +60,10 @@ class ObjRegexMatch(val match: MatchResult) : Obj() {
|
|||||||
override val objClass = type
|
override val objClass = type
|
||||||
|
|
||||||
val objGroups: ObjList by lazy {
|
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(
|
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 ObjRegex -> self.matches(s.regex)
|
||||||
is ObjString -> {
|
is ObjString -> {
|
||||||
if (s.value == ".*") true
|
if (s.value == ".*") true
|
||||||
else self.matches(s.value.toRegex())
|
else {
|
||||||
|
val re = s.value.toRegex()
|
||||||
|
self.matches(re)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else ->
|
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.*
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
||||||
|
import net.sergeych.tools.bm
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
|
|
||||||
class ScriptTest {
|
class ScriptTest {
|
||||||
@ -812,7 +813,8 @@ class ScriptTest {
|
|||||||
assertEquals(6, c.eval("x").toInt())
|
assertEquals(6, c.eval("x").toInt())
|
||||||
assertEquals(6, c.eval("x++").toInt())
|
assertEquals(6, c.eval("x++").toInt())
|
||||||
assertEquals(7, c.eval("x++").toInt())
|
assertEquals(7, c.eval("x++").toInt())
|
||||||
assertEquals(8, c.eval("x")
|
assertEquals(
|
||||||
|
8, c.eval("x")
|
||||||
.also {
|
.also {
|
||||||
println("${it.toDouble()} ${it.toInt()} ${it.toLong()} ${it.toInt()}")
|
println("${it.toDouble()} ${it.toInt()} ${it.toLong()} ${it.toInt()}")
|
||||||
}
|
}
|
||||||
@ -2252,19 +2254,35 @@ class ScriptTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMatchOperator() = runTest {
|
fun testMatchOperator() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
assert( "abc123".matches(".*\d{3}") )
|
assert( "abc123".matches(".*\d{3}") )
|
||||||
assert( ".*\d{3}".re =~ "abc123" )
|
assert( ".*\d{3}".re =~ "abc123" )
|
||||||
assert( "abc123" =~ ".*\d{3}".re )
|
assert( "abc123" =~ ".*\d{3}".re )
|
||||||
assert( "abc123" !~ ".*\d{4}".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
|
"abc123" =~ ".*(\d)(\d)(\d)$".re
|
||||||
println($~)
|
println($~)
|
||||||
assertEquals("1", $~[1])
|
assertEquals("1", $~[1])
|
||||||
assertEquals("2", $~[2])
|
assertEquals("2", $~[2])
|
||||||
assertEquals("3", $~[3])
|
assertEquals("3", $~[3])
|
||||||
assertEquals("abc123", $~[0])
|
assertEquals("abc123", $~[0])
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
@ -3314,26 +3332,31 @@ class ScriptTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
// fun testMinimumOptimization() = runTest {
|
fun testMinimumOptimization() = runTest {
|
||||||
// val x = Scope().eval(
|
for (i in 1..200) {
|
||||||
// """
|
bm {
|
||||||
// fun naiveCountHappyNumbers() {
|
val x = Scope().eval(
|
||||||
// var count = 0
|
"""
|
||||||
// for( n1 in 0..9 )
|
fun naiveCountHappyNumbers() {
|
||||||
// for( n2 in 0..9 )
|
var count = 0
|
||||||
// for( n3 in 0..9 )
|
for( n1 in 0..9 )
|
||||||
// for( n4 in 0..9 )
|
for( n2 in 0..9 )
|
||||||
// for( n5 in 0..9 )
|
for( n3 in 0..9 )
|
||||||
// for( n6 in 0..9 )
|
for( n4 in 0..9 )
|
||||||
// if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
|
for( n5 in 0..9 )
|
||||||
// count
|
for( n6 in 0..9 )
|
||||||
// }
|
if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
|
||||||
// naiveCountHappyNumbers()
|
count
|
||||||
// """.trimIndent()
|
}
|
||||||
// ).toInt()
|
naiveCountHappyNumbers()
|
||||||
// assertEquals(55252, x)
|
""".trimIndent()
|
||||||
// }
|
).toInt()
|
||||||
|
assertEquals(55252, x)
|
||||||
|
}
|
||||||
|
delay(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRegex1() = runTest {
|
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