v0.10.1-SNAPSHOT optimization: Accessor refactored now use more effective ObjRef and slots for locals

This commit is contained in:
Sergey Chernov 2025-11-09 23:19:02 +01:00
parent 6ca3e4589e
commit 0b9e94c6e9
17 changed files with 806 additions and 389 deletions

View File

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

View File

@ -1218,7 +1218,7 @@ 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 |
@ -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.
@ -1372,8 +1370,8 @@ 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 */ } | |
@ -1386,6 +1384,12 @@ See [math functions](math.md). Other general purpose functions are:
| cached(builder) | remembers builder() on first invocation and return it then | | 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
| name | description | | name | description |

View File

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

View 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")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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(
@ -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,11 +278,14 @@ 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]"
} }

View File

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

View File

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

View 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
}
}

View File

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

View File

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

View 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}")
}

View File

@ -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
@ -3315,25 +3333,30 @@ 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 {

View 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()
)
}
}