From 0b9e94c6e99ccc4a4595441c550c5d9f5016f184 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 9 Nov 2025 23:19:02 +0100 Subject: [PATCH] v0.10.1-SNAPSHOT optimization: Accessor refactored now use more effective ObjRef and slots for locals --- docs/parallelism.md | 8 +- docs/tutorial.md | 48 +- lyng/src/commonMain/kotlin/Common.kt | 1 + .../kotlin/net/sergeych/lyng_cli/Benchmark.kt | 58 +++ .../kotlin/net/sergeych/lyng_cli/Main.kt | 4 + lynglib/build.gradle.kts | 2 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 443 ++++++------------ .../kotlin/net/sergeych/lyng/ListEntry.kt | 6 +- .../kotlin/net/sergeych/lyng/Scope.kt | 63 ++- .../kotlin/net/sergeych/lyng/Script.kt | 6 +- .../kotlin/net/sergeych/lyng/obj/Accessor.kt | 52 +- .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 370 +++++++++++++++ .../kotlin/net/sergeych/lyng/obj/ObjRegex.kt | 6 +- .../kotlin/net/sergeych/lyng/obj/ObjString.kt | 5 +- .../kotlin/net/sergeych/tools/bm.kt | 9 + lynglib/src/commonTest/kotlin/ScriptTest.kt | 71 ++- .../kotlin/ScriptTest_OptionalAssign.kt | 43 ++ 17 files changed, 806 insertions(+), 389 deletions(-) create mode 100644 lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Benchmark.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/tools/bm.kt create mode 100644 lynglib/src/commonTest/kotlin/ScriptTest_OptionalAssign.kt diff --git a/docs/parallelism.md b/docs/parallelism.md index 7a86de2..9b28fbe 100644 --- a/docs/parallelism.md +++ b/docs/parallelism.md @@ -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. diff --git a/docs/tutorial.md b/docs/tutorial.md index 593aa4f..eca8bb6 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1217,13 +1217,13 @@ same as: Are the same as in string literals with little difference: -| escape | ASCII value | -|--------|-------------------| -| \n | 0x10, newline | +| escape | ASCII value | +|--------|-----------------------| +| \n | 0x10, newline | | \r | 0x13, carriage return | -| \t | 0x07, tabulation | -| \\ | \ slash character | -| \' | ' apostrophe | +| \t | 0x07, tabulation | +| \\ | \ slash character | +| \' | ' apostrophe | ### Char instance members @@ -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. @@ -1371,20 +1369,26 @@ 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 | -| assertEquals(a,b) | | -| assertNotEquals(a,b) | | -| assertTrows { /* block */ } | | -| check(condition, message=) | throws IllegalStateException" of condition isn't met | -| require(condition, message=) | throws IllegalArgumentException" of condition isn't met | -| println(args...) | Open for overriding, it prints to stdout with newline. | -| print(args...) | Open for overriding, it prints to stdout without newline. | -| flow {} | create flow sequence, see [parallelism] | -| delay, launch, yield | see [parallelism] | -| cached(builder) | remembers builder() on first invocation and return it then | -| let, also, apply, run | see above, flow controls | +| name | description | +|---------------------------------------|------------------------------------------------------------| +| assert(condition, fn) | (1) runtime code check with generic or custom nessage `fn` | +| assertEquals(a,b) | | +| assertNotEquals(a,b) | | +| assertTrows { /* block */ } | | +| check(condition, message=) | throws IllegalStateException" of condition isn't met | +| require(condition, message=) | throws IllegalArgumentException" of condition isn't met | +| println(args...) | Open for overriding, it prints to stdout with newline. | +| print(args...) | Open for overriding, it prints to stdout without newline. | +| flow {} | create flow sequence, see [parallelism] | +| delay, launch, yield | see [parallelism] | +| 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 diff --git a/lyng/src/commonMain/kotlin/Common.kt b/lyng/src/commonMain/kotlin/Common.kt index ce8e8c9..db6ff0d 100644 --- a/lyng/src/commonMain/kotlin/Common.kt +++ b/lyng/src/commonMain/kotlin/Common.kt @@ -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 = """ diff --git a/lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Benchmark.kt b/lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Benchmark.kt new file mode 100644 index 0000000..dada3f6 --- /dev/null +++ b/lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Benchmark.kt @@ -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 { + 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") + } + } +} diff --git a/lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Main.kt b/lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Main.kt index 422be52..c61a9be 100644 --- a/lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Main.kt +++ b/lyng/src/jvmMain/kotlin/net/sergeych/lyng_cli/Main.kt @@ -20,5 +20,9 @@ package net.sergeych.lyng_cli import net.sergeych.runMain fun main(args: Array) { + if (args.contains("--benchmark")) { + BenchmarkRunner.runAll() + return + } runMain(args) } \ No newline at end of file diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index bbbf46f..5af014c 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index e98f426..a5582fc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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() - 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: , '.' , // 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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ListEntry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ListEntry.kt index e8c1dfb..e4c290f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ListEntry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ListEntry.kt @@ -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() } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 3429432..8f253f5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -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 = mutableListOf() + private val nameToSlot: MutableMap = mutableMapOf() open val packageName: String = "" 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 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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index bdad379..6b8d6c5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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(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") { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Accessor.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Accessor.kt index b58278d..a6d339c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Accessor.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Accessor.kt @@ -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. - */ - fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos, "can't assign value") +/** + * Final migration shim: make `Accessor` an alias to `ObjRef`. + * This preserves source compatibility while removing lambda-based indirection. + */ +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) } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt new file mode 100644 index 0000000..b9c8cf5 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -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, + 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, + 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) : ObjRef { + override suspend fun get(scope: Scope): ObjRecord { + val list = mutableListOf() + 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 + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRegex.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRegex.kt index 2eaa08d..4293e81 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRegex.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRegex.kt @@ -24,7 +24,7 @@ class ObjRegex(val regex: Regex) : Obj() { override suspend fun operatorMatch(scope: Scope, other: Obj): Obj { return regex.find(other.cast(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() ) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt index 122063d..5219090 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt @@ -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 -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/tools/bm.kt b/lynglib/src/commonMain/kotlin/net/sergeych/tools/bm.kt new file mode 100644 index 0000000..d65cbac --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/tools/bm.kt @@ -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}") +} \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 25d0a91..2c9941a 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -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 { diff --git a/lynglib/src/commonTest/kotlin/ScriptTest_OptionalAssign.kt b/lynglib/src/commonTest/kotlin/ScriptTest_OptionalAssign.kt new file mode 100644 index 0000000..51f4eae --- /dev/null +++ b/lynglib/src/commonTest/kotlin/ScriptTest_OptionalAssign.kt @@ -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() + ) + } +}