Compare commits

...

5 Commits

30 changed files with 1486 additions and 289 deletions

View File

@ -7,6 +7,27 @@ History note:
- Entries below are synchronized and curated for `1.5.x`. - Entries below are synchronized and curated for `1.5.x`.
- Earlier history may be incomplete and should be cross-checked with git tags/commits when needed. - Earlier history may be incomplete and should be cross-checked with git tags/commits when needed.
## 1.5.4 (2026-04-03)
### Runtime and compiler stability
- Stabilized the recent `piSpigot` benchmark/compiler work for release.
- Fixed numeric-mix regressions introduced by overly broad int-coercion in bytecode compilation.
- Restored correct behavior for decimal arithmetic, mixed real/int flows, list literals, list size checks, and national-character script cases.
- Fixed plain-list index fast paths so they no longer bypass subclass behavior such as `ObservableList` hooks and flow notifications.
- Hardened local numeric compare fast paths to correctly handle primitive-coded frame slots.
### Performance and examples
- Added `piSpigot` benchmark/example coverage:
- `examples/pi-test.lyng`
- `examples/pi-bench.lyng`
- JVM benchmark test for release-baseline verification
- Kept the safe list/index/runtime wins that improve the optimized `piSpigot` path without reintroducing type-unsound coercions.
- Changed the default `RVAL_FASTPATH` setting off on JVM/Android and in the benchmark preset after verification that it no longer helps the stabilized `piSpigot` workload.
### Release notes
- Full JVM and wasm test gates pass on the release tree.
- Benchmark findings and remaining post-release optimization targets are documented in `notes/pi_spigot_benchmark_baseline_2026-04-03.md`.
## 1.5.1 (2026-03-25) ## 1.5.1 (2026-03-25)
### Language ### Language

View File

@ -45,6 +45,16 @@ You can concatenate lists or iterable objects:
assert( [4,5] + (1..3) == [4, 5, 1, 2, 3]) assert( [4,5] + (1..3) == [4, 5, 1, 2, 3])
>>> void >>> void
## Constructing lists
Besides literals, you can build a list by size using `List.fill`:
val squares = List.fill(5) { i -> i * i }
assertEquals([0, 1, 4, 9, 16], squares)
>>> void
`List.fill(size) { ... }` calls the block once for each index from `0` to `size - 1` and returns a new mutable list.
## Appending ## Appending
To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will
@ -164,6 +174,7 @@ List could be sorted in place, just like [Collection] provide sorted copies, in
| `[index]` | get or set element at index | Int | | `[index]` | get or set element at index | Int |
| `[Range]` | get slice of the array (copy) | Range | | `[Range]` | get slice of the array (copy) | Range |
| `+=` | append element(s) (2) | List or Obj | | `+=` | append element(s) (2) | List or Obj |
| `List.fill(size, block)` | build a new list from indices `0..<size` | Int, Callable |
| `sort()` | in-place sort, natural order | void | | `sort()` | in-place sort, natural order | void |
| `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | void | | `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | void |
| `sortWith(comparator)` | in-place sort using `comarator` function (4) | void | | `sortWith(comparator)` | in-place sort using `comarator` function (4) | void |

View File

@ -1020,6 +1020,14 @@ For example, we want to create an extension method that would test if a value ca
assert( ! "5.2".isInteger() ) assert( ! "5.2".isInteger() )
>>> void >>> void
Extension methods normally act like instance members. If declared as `static`, they are called on the type object itself:
```lyng
static fun List<T>.fill(size: Int, block: (Int)->T): List<T> { ... }
val tens = List.fill(5) { it * 10 }
```
## Extension properties ## Extension properties
Just like methods, you can extend existing classes with properties. These can be defined using simple initialization (for `val` only) or with custom accessors. Just like methods, you can extend existing classes with properties. These can be defined using simple initialization (for `val` only) or with custom accessors.

View File

@ -25,6 +25,23 @@ Exclusive end ranges are adopted from kotlin either:
assert(4 in r) assert(4 in r)
>>> void >>> void
Descending finite ranges are explicit too:
val r = 5 downTo 1
assert(r.isDescending)
assert(r.toList() == [5,4,3,2,1])
>>> void
Use `downUntil` when the lower bound should be excluded:
val r = 5 downUntil 1
assert(r.toList() == [5,4,3,2])
assert(1 !in r)
>>> void
This is explicit by design: `5..1` is not treated as a reverse range. It is an
ordinary ascending range with no values in it when iterated.
In any case, we can test an object to belong to using `in` and `!in` and In any case, we can test an object to belong to using `in` and `!in` and
access limits: access limits:
@ -73,6 +90,23 @@ but
>>> 2 >>> 2
>>> void >>> void
Descending ranges work in `for` loops exactly the same way:
for( i in 3 downTo 1 )
println(i)
>>> 3
>>> 2
>>> 1
>>> void
And with an exclusive lower bound:
for( i in 3 downUntil 1 )
println(i)
>>> 3
>>> 2
>>> void
### Stepped ranges ### Stepped ranges
Use `step` to change the iteration increment. The range bounds still define membership, Use `step` to change the iteration increment. The range bounds still define membership,
@ -80,9 +114,18 @@ so iteration ends when the next value is no longer in the range.
assert( [1,3,5] == (1..5 step 2).toList() ) assert( [1,3,5] == (1..5 step 2).toList() )
assert( [1,3] == (1..<5 step 2).toList() ) assert( [1,3] == (1..<5 step 2).toList() )
assert( [5,3,1] == (5 downTo 1 step 2).toList() )
assert( ['a','c','e'] == ('a'..'e' step 2).toList() ) assert( ['a','c','e'] == ('a'..'e' step 2).toList() )
>>> void >>> void
Descending ranges still use a positive `step`; the direction comes from
`downTo` / `downUntil`:
assert( ['e','c','a'] == ('e' downTo 'a' step 2).toList() )
>>> void
A negative step with `downTo` / `downUntil` is invalid.
Real ranges require an explicit step: Real ranges require an explicit step:
assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() ) assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() )
@ -119,6 +162,7 @@ Exclusive end char ranges are supported too:
|-----------------|------------------------------|---------------| |-----------------|------------------------------|---------------|
| contains(other) | used in `in` | Range, or Any | | contains(other) | used in `in` | Range, or Any |
| isEndInclusive | true for '..' | Bool | | isEndInclusive | true for '..' | Bool |
| isDescending | true for `downTo`/`downUntil`| Bool |
| isOpen | at any end | Bool | | isOpen | at any end | Bool |
| isIntRange | both start and end are Int | Bool | | isIntRange | both start and end are Int | Bool |
| step | explicit iteration step | Any? | | step | explicit iteration step | Any? |

View File

@ -50,8 +50,10 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
- Range literals: - Range literals:
- inclusive: `a..b` - inclusive: `a..b`
- exclusive end: `a..<b` - exclusive end: `a..<b`
- descending inclusive: `a downTo b`
- descending exclusive end: `a downUntil b`
- open-ended forms are supported (`a..`, `..b`, `..`). - open-ended forms are supported (`a..`, `..b`, `..`).
- optional step: `a..b step 2` - optional step: `a..b step 2`, `a downTo b step 2`
- Lambda literal: - Lambda literal:
- with params: `{ x, y -> x + y }` - with params: `{ x, y -> x + y }`
- implicit `it`: `{ it + 1 }` - implicit `it`: `{ it + 1 }`
@ -114,6 +116,7 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
- shorthand: `fun f(x) = expr`. - shorthand: `fun f(x) = expr`.
- generics: `fun f<T>(x: T): T`. - generics: `fun f<T>(x: T): T`.
- extension functions: `fun Type.name(...) { ... }`. - extension functions: `fun Type.name(...) { ... }`.
- static extension functions are callable on the type object: `static fun List<T>.fill(...)` -> `List.fill(...)`.
- delegated callable: `fun f(...) by delegate`. - delegated callable: `fun f(...) by delegate`.
- Type aliases: - Type aliases:
- `type Name = TypeExpr` - `type Name = TypeExpr`

View File

@ -46,7 +46,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
- Iteration/filtering: `forEach`, `filter`, `filterFlow`, `filterNotNull`, `filterFlowNotNull`, `drop`, `dropLast`, `takeLast`. - Iteration/filtering: `forEach`, `filter`, `filterFlow`, `filterNotNull`, `filterFlowNotNull`, `drop`, `dropLast`, `takeLast`.
- Search/predicates: `findFirst`, `findFirstOrNull`, `any`, `all`, `count`, `first`, `last`. - Search/predicates: `findFirst`, `findFirstOrNull`, `any`, `all`, `count`, `first`, `last`.
- Mapping/aggregation: `map`, `flatMap`, `flatten`, `sum`, `sumOf`, `minOf`, `maxOf`. - Mapping/aggregation: `map`, `flatMap`, `flatten`, `sum`, `sumOf`, `minOf`, `maxOf`.
- Ordering: `sorted`, `sortedBy`, `shuffled`, `List.sort`, `List.sortBy`. - Ordering and list building: `sorted`, `sortedBy`, `shuffled`, `List.sort`, `List.sortBy`, `List.fill`.
- `List.fill(size) { index -> ... }` constructs a new `List<T>` by evaluating the block once per index from `0` to `size - 1`.
- String helper: `joinToString`, `String.re`. - String helper: `joinToString`, `String.re`.
### 4.3 Delegation helpers ### 4.3 Delegation helpers

View File

@ -811,6 +811,12 @@ Lyng has built-in mutable array class `List` with simple literals:
many collection based methods are implemented there. many collection based methods are implemented there.
For immutable list values, use `list.toImmutable()` and [ImmutableList]. For immutable list values, use `list.toImmutable()` and [ImmutableList].
To construct a list programmatically, use the static helper `List.fill`:
val tens = List.fill(5) { index -> index * 10 }
assertEquals([0, 10, 20, 30, 40], tens)
>>> void
Lists can contain any type of objects, lists too: Lists can contain any type of objects, lists too:
val list = [1, [2, 3], 4] val list = [1, [2, 3], 4]
@ -1359,6 +1365,41 @@ size and index access, like lists:
"total letters: "+letters "total letters: "+letters
>>> "total letters: 10" >>> "total letters: 10"
When you need a counting loop that goes backwards, use an explicit descending
range:
var sum = 0
for( i in 5 downTo 1 ) {
sum += i
}
sum
>>> 15
If the lower bound should be excluded, use `downUntil`:
val xs = []
for( i in 5 downUntil 1 ) {
xs.add(i)
}
xs
>>> [5,4,3,2]
This is intentionally explicit: `5..1` is an empty ascending range, not an
implicit reverse loop.
Descending loops also support `step`:
val xs = []
for( i in 10 downTo 1 step 3 ) {
xs.add(i)
}
xs
>>> [10,7,4,1]
For descending ranges, `step` stays positive. The direction comes from
`downTo` / `downUntil`, so `10 downTo 1 step 3` is valid, while
`10 downTo 1 step -3` is an error.
For loop support breaks the same as while loops above: For loop support breaks the same as while loops above:
fun search(haystack, needle) { fun search(haystack, needle) {
@ -1488,6 +1529,14 @@ It could be open and closed:
assert( 5 !in (1..<5) ) assert( 5 !in (1..<5) )
>>> void >>> void
Descending ranges are explicit too:
(5 downTo 1).toList()
>>> [5,4,3,2,1]
(5 downUntil 1).toList()
>>> [5,4,3,2]
Ranges could be inside other ranges: Ranges could be inside other ranges:
assert( (2..3) in (1..10) ) assert( (2..3) in (1..10) )
@ -1505,6 +1554,14 @@ and you can use ranges in for-loops:
>>> b >>> b
>>> void >>> void
Descending character ranges work the same way:
for( ch in 'e' downTo 'a' step 2 ) println(ch)
>>> e
>>> c
>>> a
>>> void
See [Ranges](Range.md) for detailed documentation on it. See [Ranges](Range.md) for detailed documentation on it.
# Time routines # Time routines

67
examples/pi-bench.lyng Normal file
View File

@ -0,0 +1,67 @@
import lyng.time
val WORK_SIZE = 200
val TASK_COUNT = 10
fn piSpigot(iThread: Int, n: Int) {
var pi = []
val boxes = n * 10 / 3
var reminders = List.fill(boxes) { 2 }
var heldDigits = 0
for (i in 0..n) {
var carriedOver = 0
var sum = 0
for (k in 1..boxes) {
val j = boxes - k
val denom = j * 2 + 1
reminders[j] *= 10
sum = reminders[j] + carriedOver
val quotient = sum / denom
reminders[j] = sum % denom
carriedOver = quotient * j
}
reminders[0] = sum % 10
var q = sum / 10
if (q == 9) {
++heldDigits
} else if (q == 10) {
q = 0
for (k in 1..heldDigits) {
var replaced = pi[i - k]
if (replaced == 9) {
replaced = 0
} else {
++replaced
}
pi[i - k] = replaced
}
heldDigits = 1
} else {
heldDigits = 1
}
pi.add(q)
}
var s = ""
for (i in (n - 8)..<n) {
s += pi[i]
}
println(iThread, " - done: ", s)
}
var counter = 0
val t0 = Instant()
(1..TASK_COUNT).map { n ->
val counterState = counter
val t = launch {
piSpigot(counterState, WORK_SIZE)
}
++counter
t
}.forEach { (it as Deferred).await() }
val dt = Instant() - t0
println("all done, dt = ", dt)

49
examples/pi-test.lyng Normal file
View File

@ -0,0 +1,49 @@
fn piSpigot(n) {
var pi = []
val boxes = n * 10 / 3
var reminders = []
for (i in 0..<boxes) {
reminders.add(2)
}
var heldDigits = 0
for (i in 0..n) {
var carriedOver = 0
var sum = 0
for (k in 1..boxes) {
val j = boxes - k
val denom = j * 2 + 1
reminders[j] *= 10
sum = reminders[j] + carriedOver
// Keep this integer-only. Real coercion here is much slower in the hot loop.
val quotient = sum / denom
reminders[j] = sum % denom
carriedOver = quotient * j
}
reminders[0] = sum % 10
var q = sum / 10
if (q == 9) {
++heldDigits
} else if (q == 10) {
q = 0
for (k in 1..heldDigits) {
var replaced = pi[i - k]
if (replaced == 9) {
replaced = 0
} else {
++replaced
}
pi[i - k] = replaced
}
heldDigits = 1
} else {
heldDigits = 1
}
pi.add(q)
}
var suffix = ""
for (i in (n - 8)..<n) {
suffix += pi[i]
}
suffix
}

View File

@ -26,7 +26,9 @@ import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.arguments.optional import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import net.sergeych.lyng.EvalSession import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.LyngVersion import net.sergeych.lyng.LyngVersion
import net.sergeych.lyng.Script import net.sergeych.lyng.Script
@ -245,12 +247,17 @@ fun executeFileWithArgs(fileName: String, args: List<String>) {
suspend fun executeSource(source: Source) { suspend fun executeSource(source: Source) {
val session = EvalSession(baseScopeDefer.await()) val session = EvalSession(baseScopeDefer.await())
try { try {
session.eval(source) evalOnCliDispatcher(session, source)
} finally { } finally {
session.cancelAndJoin() session.cancelAndJoin()
} }
} }
internal suspend fun evalOnCliDispatcher(session: EvalSession, source: Source): Obj =
withContext(Dispatchers.Default) {
session.eval(source)
}
suspend fun executeFile(fileName: String) { suspend fun executeFile(fileName: String) {
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource -> var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
fileSource.buffer().use { bs -> fileSource.buffer().use { bs ->

View File

@ -0,0 +1,72 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* 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
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.Script
import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjString
import org.junit.Test
import kotlin.test.assertNotEquals
class CliDispatcherJvmTest {
@Test
fun executeSourceRunsOnDefaultDispatcher() = runBlocking {
val callerThread = Thread.currentThread()
val callerThreadKey = "${System.identityHashCode(callerThread)}:${callerThread.name}"
val scope = Script.newScope().apply {
addFn("threadKey") { ObjString("${System.identityHashCode(Thread.currentThread())}:${Thread.currentThread().name}") }
addFn("threadName") { ObjString(Thread.currentThread().name) }
}
val session = EvalSession(scope)
try {
val result = evalOnCliDispatcher(
session,
Source(
"<test>",
"""
val task = launch { [threadKey(), threadName()] }
val child = task.await()
[threadKey(), threadName(), child]
""".trimIndent()
)
) as ObjList
val topLevelThreadKey = (result.list[0] as ObjString).value
val topLevelThreadName = (result.list[1] as ObjString).value
val child = result.list[2] as ObjList
val childThreadKey = (child.list[0] as ObjString).value
val childThreadName = (child.list[1] as ObjString).value
assertNotEquals(
callerThreadKey,
topLevelThreadKey,
"CLI top-level script body should not run on the runBlocking caller thread: $topLevelThreadName"
)
assertNotEquals(
callerThreadKey,
childThreadKey,
"CLI launch child should not inherit the runBlocking caller thread: $childThreadName"
)
} finally {
session.cancelAndJoin()
}
}
}

View File

@ -36,7 +36,7 @@ actual object PerfDefaults {
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
actual val PRIMITIVE_FASTOPS: Boolean = true actual val PRIMITIVE_FASTOPS: Boolean = true
actual val RVAL_FASTPATH: Boolean = true actual val RVAL_FASTPATH: Boolean = false
// Regex caching aligns with JVM behavior on Android (Dalvik/ART) // Regex caching aligns with JVM behavior on Android (Dalvik/ART)
actual val REGEX_CACHE: Boolean = true actual val REGEX_CACHE: Boolean = true
actual val ARG_SMALL_ARITY_12: Boolean = false actual val ARG_SMALL_ARITY_12: Boolean = false

View File

@ -3058,9 +3058,10 @@ class Compiler(
} }
} }
Token.Type.DOTDOT, Token.Type.DOTDOTLT -> { Token.Type.DOTDOT, Token.Type.DOTDOTLT, Token.Type.DOWNTO, Token.Type.DOWNUNTIL -> {
// range operator // range operator
val isEndInclusive = t.type == Token.Type.DOTDOT val isEndInclusive = t.type == Token.Type.DOTDOT || t.type == Token.Type.DOWNTO
val isDescending = t.type == Token.Type.DOWNTO || t.type == Token.Type.DOWNUNTIL
val left = operand val left = operand
// if it is an open end range, then the end of line could be here that we do not want // if it is an open end range, then the end of line could be here that we do not want
// to skip in parseExpression: // to skip in parseExpression:
@ -3078,12 +3079,19 @@ class Compiler(
val lConst = constIntValueOrNull(left) val lConst = constIntValueOrNull(left)
val rConst = constIntValueOrNull(rightRef) val rConst = constIntValueOrNull(rightRef)
if (lConst != null && rConst != null) { if (lConst != null && rConst != null) {
operand = ConstRef(ObjRange(ObjInt.of(lConst), ObjInt.of(rConst), isEndInclusive).asReadonly) operand = ConstRef(
ObjRange(
ObjInt.of(lConst),
ObjInt.of(rConst),
isEndInclusive,
isDescending = isDescending
).asReadonly
)
} else { } else {
operand = RangeRef(left, rightRef, isEndInclusive) operand = RangeRef(left, rightRef, isEndInclusive, isDescending = isDescending)
} }
} else { } else {
operand = RangeRef(left, rightRef, isEndInclusive) operand = RangeRef(left, rightRef, isEndInclusive, isDescending = isDescending)
} }
} }
@ -3098,7 +3106,7 @@ class Compiler(
} }
val leftRef = range.start?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) } val leftRef = range.start?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
val rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) } val rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
RangeRef(leftRef, rightRef, range.isEndInclusive) RangeRef(leftRef, rightRef, range.isEndInclusive, isDescending = range.isDescending)
} }
else -> { else -> {
cc.previous() cc.previous()
@ -3108,7 +3116,13 @@ class Compiler(
if (rangeRef.step != null) throw ScriptError(t.pos, "step is already specified for this range") if (rangeRef.step != null) throw ScriptError(t.pos, "step is already specified for this range")
val stepExpr = parseExpression() ?: throw ScriptError(t.pos, "Expected step expression") val stepExpr = parseExpression() ?: throw ScriptError(t.pos, "Expected step expression")
val stepRef = StatementRef(stepExpr) val stepRef = StatementRef(stepExpr)
operand = RangeRef(rangeRef.left, rangeRef.right, rangeRef.isEndInclusive, stepRef) operand = RangeRef(
rangeRef.left,
rangeRef.right,
rangeRef.isEndInclusive,
isDescending = rangeRef.isDescending,
step = stepRef
)
} }
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
@ -4345,6 +4359,7 @@ class Compiler(
is ListLiteralRef -> inferListLiteralTypeDecl(ref) is ListLiteralRef -> inferListLiteralTypeDecl(ref)
is MapLiteralRef -> inferMapLiteralTypeDecl(ref) is MapLiteralRef -> inferMapLiteralTypeDecl(ref)
is ConstRef -> inferTypeDeclFromConst(ref.constValue) is ConstRef -> inferTypeDeclFromConst(ref.constValue)
is RangeRef -> TypeDecl.Simple("Range", false)
is CallRef -> { is CallRef -> {
val targetDecl = resolveReceiverTypeDecl(ref.target) ?: seedTypeDeclFromRef(ref.target) val targetDecl = resolveReceiverTypeDecl(ref.target) ?: seedTypeDeclFromRef(ref.target)
val targetName = when (val target = ref.target) { val targetName = when (val target = ref.target) {
@ -4380,6 +4395,7 @@ class Compiler(
is ObjString -> TypeDecl.Simple("String", false) is ObjString -> TypeDecl.Simple("String", false)
is ObjBool -> TypeDecl.Simple("Bool", false) is ObjBool -> TypeDecl.Simple("Bool", false)
is ObjChar -> TypeDecl.Simple("Char", false) is ObjChar -> TypeDecl.Simple("Char", false)
is ObjRange -> TypeDecl.Simple("Range", false)
is ObjNull -> TypeDecl.TypeNullableAny is ObjNull -> TypeDecl.TypeNullableAny
is ObjList -> TypeDecl.Generic("List", listOf(TypeDecl.TypeAny), false) is ObjList -> TypeDecl.Generic("List", listOf(TypeDecl.TypeAny), false)
is ObjMap -> TypeDecl.Generic("Map", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false) is ObjMap -> TypeDecl.Generic("Map", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
@ -7828,15 +7844,23 @@ class Compiler(
if (range.step != null && !range.step.isNull) return null if (range.step != null && !range.step.isNull) return null
val start = range.start?.toLong() ?: return null val start = range.start?.toLong() ?: return null
val end = range.end?.toLong() ?: return null val end = range.end?.toLong() ?: return null
val endExclusive = if (range.isEndInclusive) end + 1 else end val stopBoundary = if (range.isDescending) {
return ConstIntRange(start, endExclusive) if (range.isEndInclusive) end - 1 else end
} else {
if (range.isEndInclusive) end + 1 else end
}
return ConstIntRange(start, stopBoundary, range.isDescending)
} }
is RangeRef -> { is RangeRef -> {
if (ref.step != null) return null if (ref.step != null) return null
val start = constIntValueOrNull(ref.left) ?: return null val start = constIntValueOrNull(ref.left) ?: return null
val end = constIntValueOrNull(ref.right) ?: return null val end = constIntValueOrNull(ref.right) ?: return null
val endExclusive = if (ref.isEndInclusive) end + 1 else end val stopBoundary = if (ref.isDescending) {
return ConstIntRange(start, endExclusive) if (ref.isEndInclusive) end - 1 else end
} else {
if (ref.isEndInclusive) end + 1 else end
}
return ConstIntRange(start, stopBoundary, ref.isDescending)
} }
else -> return null else -> return null
} }
@ -8692,7 +8716,7 @@ class Compiler(
startPos = start startPos = start
) )
val declaredFn = FunctionDeclStatement(spec) val declaredFn = FunctionDeclStatement(spec)
if (isStatic) { if (isStatic && parentIsClassBody) {
currentInitScope += declaredFn currentInitScope += declaredFn
NopStatement NopStatement
} else } else

View File

@ -169,18 +169,29 @@ internal suspend fun executeFunctionDecl(
spec.extTypeName?.let { typeName -> spec.extTypeName?.let { typeName ->
val type = scope[typeName]?.value ?: scope.raiseSymbolNotFound("class $typeName not found") val type = scope[typeName]?.value ?: scope.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) scope.raiseClassCastError("$typeName is not the class instance") if (type !is ObjClass) scope.raiseClassCastError("$typeName is not the class instance")
scope.addExtension( if (spec.isStatic) {
type, type.createClassField(
spec.name, spec.name,
ObjRecord(
compiledFnBody, compiledFnBody,
isMutable = false, isMutable = false,
visibility = spec.visibility, visibility = spec.visibility,
declaringClass = null, pos = spec.startPos,
type = ObjRecord.Type.Fun, type = ObjRecord.Type.Fun,
typeDecl = spec.typeDecl
) )
) } else {
scope.addExtension(
type,
spec.name,
ObjRecord(
compiledFnBody,
isMutable = false,
visibility = spec.visibility,
declaringClass = null,
type = ObjRecord.Type.Fun,
typeDecl = spec.typeDecl
)
)
}
val wrapperName = spec.extensionWrapperName ?: extensionCallableName(typeName, spec.name) val wrapperName = spec.extensionWrapperName ?: extensionCallableName(typeName, spec.name)
val wrapper = ObjExtensionMethodCallable(spec.name, compiledFnBody) val wrapper = ObjExtensionMethodCallable(spec.name, compiledFnBody)
scope.addItem( scope.addItem(

View File

@ -448,6 +448,8 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
"is" -> Token("is", from, Token.Type.IS) "is" -> Token("is", from, Token.Type.IS)
"by" -> Token("by", from, Token.Type.BY) "by" -> Token("by", from, Token.Type.BY)
"step" -> Token("step", from, Token.Type.STEP) "step" -> Token("step", from, Token.Type.STEP)
"downTo" -> Token("downTo", from, Token.Type.DOWNTO)
"downUntil" -> Token("downUntil", from, Token.Type.DOWNUNTIL)
"object" -> Token("object", from, Token.Type.OBJECT) "object" -> Token("object", from, Token.Type.OBJECT)
"as" -> { "as" -> {
// support both `as` and tight `as?` without spaces // support both `as` and tight `as?` without spaces

View File

@ -180,7 +180,7 @@ object PerfProfiles {
PerfFlags.PIC_DEBUG_COUNTERS = false PerfFlags.PIC_DEBUG_COUNTERS = false
PerfFlags.PRIMITIVE_FASTOPS = true PerfFlags.PRIMITIVE_FASTOPS = true
PerfFlags.RVAL_FASTPATH = true PerfFlags.RVAL_FASTPATH = false
// Keep regex cache/platform setting; enable on JVM typically // Keep regex cache/platform setting; enable on JVM typically
PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE

View File

@ -36,7 +36,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
PLUS, MINUS, STAR, SLASH, PERCENT, PLUS, MINUS, STAR, SLASH, PERCENT,
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, IFNULLASSIGN, ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, IFNULLASSIGN,
PLUS2, MINUS2, PLUS2, MINUS2,
IN, NOTIN, IS, NOTIS, BY, STEP, IN, NOTIN, IS, NOTIS, BY, STEP, DOWNTO, DOWNUNTIL,
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH, EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
SHUTTLE, SHUTTLE,
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON, AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,

View File

@ -1783,7 +1783,11 @@ class BytecodeCompiler(
return when (slotTypes[slot]) { return when (slotTypes[slot]) {
SlotType.INT -> NumericKind.INT SlotType.INT -> NumericKind.INT
SlotType.REAL -> NumericKind.REAL SlotType.REAL -> NumericKind.REAL
else -> NumericKind.UNKNOWN else -> when (slotObjClass[slot]) {
ObjInt.type -> NumericKind.INT
ObjReal.type -> NumericKind.REAL
else -> NumericKind.UNKNOWN
}
} }
} }
@ -1800,7 +1804,21 @@ class BytecodeCompiler(
is ConstRef -> numericKindFromConst(ref.constValue) is ConstRef -> numericKindFromConst(ref.constValue)
is LocalVarRef -> resolveDirectNameSlot(ref.name)?.let { numericKindFromSlot(it.slot) } ?: NumericKind.UNKNOWN is LocalVarRef -> resolveDirectNameSlot(ref.name)?.let { numericKindFromSlot(it.slot) } ?: NumericKind.UNKNOWN
is FastLocalVarRef -> resolveDirectNameSlot(ref.name)?.let { numericKindFromSlot(it.slot) } ?: NumericKind.UNKNOWN is FastLocalVarRef -> resolveDirectNameSlot(ref.name)?.let { numericKindFromSlot(it.slot) } ?: NumericKind.UNKNOWN
is LocalSlotRef -> resolveSlot(ref)?.let { numericKindFromSlot(it) } ?: NumericKind.UNKNOWN is LocalSlotRef -> resolveLocalSlotByRefOrName(ref)?.let { numericKindFromSlot(it) } ?: NumericKind.UNKNOWN
is IndexRef -> {
val receiver = when (val target = ref.targetRef) {
is LocalSlotRef -> resolveLocalSlotByRefOrName(target)
is LocalVarRef -> resolveDirectNameSlot(target.name)?.slot
is FastLocalVarRef -> resolveDirectNameSlot(target.name)?.slot
else -> null
}
val elementClass = receiver?.let { listElementClassBySlot[it] } ?: listElementClassFromReceiverRef(ref.targetRef)
when (elementClass) {
ObjInt.type -> NumericKind.INT
ObjReal.type -> NumericKind.REAL
else -> NumericKind.UNKNOWN
}
}
is UnaryOpRef -> inferNumericKind(ref.a) is UnaryOpRef -> inferNumericKind(ref.a)
is BinaryOpRef -> { is BinaryOpRef -> {
val op = ref.op val op = ref.op
@ -2431,7 +2449,7 @@ class BytecodeCompiler(
updateSlotType(slot, SlotType.OBJ) updateSlotType(slot, SlotType.OBJ)
return value return value
} }
val value = compileRef(assignValue(ref)) ?: return null var value = compileRef(assignValue(ref)) ?: return null
if (isLoopVarRef(localTarget)) { if (isLoopVarRef(localTarget)) {
emitLoopVarReassignError(localTarget.name, localTarget.pos()) emitLoopVarReassignError(localTarget.name, localTarget.pos())
return value return value
@ -2473,7 +2491,7 @@ class BytecodeCompiler(
else -> null else -> null
} }
if (nameTarget != null) { if (nameTarget != null) {
val value = compileRef(assignValue(ref)) ?: return null var value = compileRef(assignValue(ref)) ?: return null
val resolved = resolveAssignableSlotByName(nameTarget) ?: return null val resolved = resolveAssignableSlotByName(nameTarget) ?: return null
val slot = resolved.first val slot = resolved.first
val isMutable = resolved.second val isMutable = resolved.second
@ -2698,6 +2716,7 @@ class BytecodeCompiler(
if (!target.optionalRef) { if (!target.optionalRef) {
val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot) builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot)
noteListElementClassMutation(receiver.slot, value)
} else { } else {
val nullSlot = allocSlot() val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot) builder.emit(Opcode.CONST_NULL, nullSlot)
@ -2710,6 +2729,7 @@ class BytecodeCompiler(
) )
val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot) builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot)
noteListElementClassMutation(receiver.slot, value)
builder.mark(endLabel) builder.mark(endLabel)
} }
return value return value
@ -3026,9 +3046,32 @@ class BytecodeCompiler(
val receiver = compileRefWithFallback(indexTarget.targetRef, null, Pos.builtIn) ?: return null val receiver = compileRefWithFallback(indexTarget.targetRef, null, Pos.builtIn) ?: return null
val current = allocSlot() val current = allocSlot()
val result = allocSlot() val result = allocSlot()
val rhs = compileRef(ref.value) ?: return compileEvalRef(ref) var rhs = compileRef(ref.value) ?: return compileEvalRef(ref)
val elementClass = listElementClassBySlot[receiver.slot] ?: listElementClassFromReceiverRef(indexTarget.targetRef)
if (!indexTarget.optionalRef) { if (!indexTarget.optionalRef) {
val index = compileRefWithFallback(indexTarget.indexRef, null, Pos.builtIn) ?: return null val index = compileRefWithFallback(indexTarget.indexRef, null, Pos.builtIn) ?: return null
if (elementClass == ObjInt.type) {
builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current)
val currentInt = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, current, currentInt)
updateSlotType(currentInt, SlotType.INT)
if (rhs.type != SlotType.INT) {
coerceToArithmeticInt(ref.value, rhs)?.let { rhs = it }
}
val typed = when (ref.op) {
BinOp.PLUS -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.ADD_INT, Opcode.ADD_REAL, Opcode.ADD_OBJ)
BinOp.MINUS -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.SUB_INT, Opcode.SUB_REAL, Opcode.SUB_OBJ)
BinOp.STAR -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.MUL_INT, Opcode.MUL_REAL, Opcode.MUL_OBJ)
BinOp.SLASH -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.DIV_INT, Opcode.DIV_REAL, Opcode.DIV_OBJ)
BinOp.PERCENT -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.MOD_INT, null, Opcode.MOD_OBJ)
else -> null
}
if (typed != null && typed.type == SlotType.INT) {
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, currentInt)
noteListElementClassMutation(receiver.slot, typed)
return CompiledValue(currentInt, SlotType.INT)
}
}
builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current) builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current)
builder.emit(objOp, current, rhs.slot, result) builder.emit(objOp, current, rhs.slot, result)
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, result) builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, result)
@ -3586,7 +3629,7 @@ class BytecodeCompiler(
val elementClass = listElementClassBySlot[receiver.slot] ?: listElementClassFromReceiverRef(ref.targetRef) val elementClass = listElementClassBySlot[receiver.slot] ?: listElementClassFromReceiverRef(ref.targetRef)
if (elementClass != null) { if (elementClass != null) {
slotObjClass[dst] = elementClass slotObjClass[dst] = elementClass
if (elementClass == ObjString.type && elementClass.isClosed) { if (elementClass.isClosed) {
stableObjSlots.add(dst) stableObjSlots.add(dst)
} else { } else {
stableObjSlots.remove(dst) stableObjSlots.remove(dst)
@ -3617,6 +3660,9 @@ class BytecodeCompiler(
val inclusiveSlot = allocSlot() val inclusiveSlot = allocSlot()
val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive)) val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive))
builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot) builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot)
val descendingSlot = allocSlot()
val descendingId = builder.addConst(BytecodeConst.Bool(ref.isDescending))
builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
val stepSlot = if (ref.step != null) { val stepSlot = if (ref.step != null) {
val step = compileRefWithFallback(ref.step, null, Pos.builtIn) ?: return null val step = compileRefWithFallback(ref.step, null, Pos.builtIn) ?: return null
ensureObjSlot(step).slot ensureObjSlot(step).slot
@ -3627,7 +3673,7 @@ class BytecodeCompiler(
slot slot
} }
val dst = allocSlot() val dst = allocSlot()
builder.emit(Opcode.MAKE_RANGE, startSlot, endSlot, inclusiveSlot, stepSlot, dst) builder.emit(Opcode.MAKE_RANGE, startSlot, endSlot, inclusiveSlot, descendingSlot, stepSlot, dst)
updateSlotType(dst, SlotType.OBJ) updateSlotType(dst, SlotType.OBJ)
slotObjClass[dst] = ObjRange.type slotObjClass[dst] = ObjRange.type
return CompiledValue(dst, SlotType.OBJ) return CompiledValue(dst, SlotType.OBJ)
@ -4646,6 +4692,9 @@ class BytecodeCompiler(
val encodedCount = encodeCallArgCount(args) ?: return null val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos) setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, encodedMethodId, args.base, encodedCount, dst) builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, encodedMethodId, args.base, encodedCount, dst)
if (receiverClass == ObjList.type && ref.name == "add" && ref.args.size == 1 && !ref.args.first().isSplat) {
noteListElementClassMutation(receiver.slot, CompiledValue(args.base, SlotType.OBJ))
}
return CompiledValue(dst, SlotType.OBJ) return CompiledValue(dst, SlotType.OBJ)
} }
val nullSlot = allocSlot() val nullSlot = allocSlot()
@ -4812,7 +4861,7 @@ class BytecodeCompiler(
" receiver=$kind(${ref.name}) slot=$slot slotClass=$slotCls nameClass=$nameCls" " receiver=$kind(${ref.name}) slot=$slot slotClass=$slotCls nameClass=$nameCls"
} }
is LocalSlotRef -> { is LocalSlotRef -> {
val slot = resolveSlot(ref) val slot = resolveLocalSlotByRefOrName(ref)
val slotCls = slot?.let { slotObjClass[it]?.className } val slotCls = slot?.let { slotObjClass[it]?.className }
val nameCls = nameObjClass[ref.name]?.className val nameCls = nameObjClass[ref.name]?.className
val scopeId = refScopeId(ref) val scopeId = refScopeId(ref)
@ -4968,9 +5017,10 @@ class BytecodeCompiler(
val specs = if (needPlan) ArrayList<BytecodeConst.CallArgSpec>(args.size) else null val specs = if (needPlan) ArrayList<BytecodeConst.CallArgSpec>(args.size) else null
for ((index, arg) in args.withIndex()) { for ((index, arg) in args.withIndex()) {
val compiled = compileArgValue(arg.value) ?: return null val compiled = compileArgValue(arg.value) ?: return null
val objValue = ensureObjSlot(compiled)
val dst = argSlots[index] val dst = argSlots[index]
if (compiled.slot != dst || compiled.type != SlotType.OBJ) { if (objValue.slot != dst) {
builder.emit(Opcode.BOX_OBJ, compiled.slot, dst) emitMove(objValue, dst)
} }
updateSlotType(dst, SlotType.OBJ) updateSlotType(dst, SlotType.OBJ)
specs?.add(BytecodeConst.CallArgSpec(arg.name, arg.isSplat)) specs?.add(BytecodeConst.CallArgSpec(arg.name, arg.isSplat))
@ -5830,7 +5880,8 @@ class BytecodeCompiler(
emitMove(value, localSlot) emitMove(value, localSlot)
} }
updateSlotType(localSlot, value.type) updateSlotType(localSlot, value.type)
updateSlotObjClass(localSlot, stmt.initializer, stmt.initializerObjClass) slotObjClass[value.slot]?.let { slotObjClass[localSlot] = it }
?: updateSlotObjClass(localSlot, stmt.initializer, stmt.initializerObjClass)
updateListElementClassFromDecl(localSlot, scopeId, stmt.slotIndex) updateListElementClassFromDecl(localSlot, scopeId, stmt.slotIndex)
updateListElementClassFromInitializer(localSlot, stmt.initializer) updateListElementClassFromInitializer(localSlot, stmt.initializer)
updateNameObjClassFromSlot(stmt.name, localSlot) updateNameObjClassFromSlot(stmt.name, localSlot)
@ -5862,7 +5913,8 @@ class BytecodeCompiler(
} }
updateSlotType(scopeSlot, value.type) updateSlotType(scopeSlot, value.type)
updateNameObjClassFromSlot(stmt.name, scopeSlot) updateNameObjClassFromSlot(stmt.name, scopeSlot)
updateSlotObjClass(scopeSlot, stmt.initializer, stmt.initializerObjClass) slotObjClass[value.slot]?.let { slotObjClass[scopeSlot] = it }
?: updateSlotObjClass(scopeSlot, stmt.initializer, stmt.initializerObjClass)
updateListElementClassFromDecl(scopeSlot, scopeId, stmt.slotIndex) updateListElementClassFromDecl(scopeSlot, scopeId, stmt.slotIndex)
updateListElementClassFromInitializer(scopeSlot, stmt.initializer) updateListElementClassFromInitializer(scopeSlot, stmt.initializer)
val declId = builder.addConst( val declId = builder.addConst(
@ -5898,7 +5950,9 @@ class BytecodeCompiler(
updateSlotTypeByName(stmt.name, value.type) updateSlotTypeByName(stmt.name, value.type)
} }
updateNameObjClassFromSlot(stmt.name, value.slot) updateNameObjClassFromSlot(stmt.name, value.slot)
updateSlotObjClass(value.slot, stmt.initializer, stmt.initializerObjClass) if (slotObjClass[value.slot] == null) {
updateSlotObjClass(value.slot, stmt.initializer, stmt.initializerObjClass)
}
updateListElementClassFromDecl(value.slot, scopeId, stmt.slotIndex) updateListElementClassFromDecl(value.slot, scopeId, stmt.slotIndex)
updateListElementClassFromInitializer(value.slot, stmt.initializer) updateListElementClassFromInitializer(value.slot, stmt.initializer)
return value return value
@ -5988,6 +6042,16 @@ class BytecodeCompiler(
listElementClassBySlot[slot] = elementClass listElementClassBySlot[slot] = elementClass
} }
private fun noteListElementClassMutation(receiverSlot: Int, value: CompiledValue) {
val newClass = elementClassFromValue(value) ?: return
val current = listElementClassBySlot[receiverSlot]
if (current == null || current == newClass) {
listElementClassBySlot[receiverSlot] = newClass
} else {
listElementClassBySlot.remove(receiverSlot)
}
}
private fun updateNameObjClassFromSlot(name: String, slot: Int) { private fun updateNameObjClassFromSlot(name: String, slot: Int) {
val cls = slotObjClass[slot] ?: return val cls = slotObjClass[slot] ?: return
nameObjClass[name] = cls nameObjClass[name] = cls
@ -6083,9 +6147,6 @@ class BytecodeCompiler(
if (range == null && rangeRef == null) { if (range == null && rangeRef == null) {
rangeRef = extractRangeFromLocal(stmt.source) rangeRef = extractRangeFromLocal(stmt.source)
} }
if (rangeRef != null && !isConstIntRange(rangeRef)) {
rangeRef = null
}
val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null
val loopSlotPlan = stmt.loopSlotPlan val loopSlotPlan = stmt.loopSlotPlan
val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName]
@ -6126,141 +6187,60 @@ class BytecodeCompiler(
val breakFlagSlot = allocSlot() val breakFlagSlot = allocSlot()
if (range == null && rangeRef == null && typedRangeLocal == null) { if (range == null && rangeRef == null && typedRangeLocal == null) {
val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null
val sourceObj = ensureObjSlot(sourceValue) return emitIterableForIn(
val typeId = builder.addConst(BytecodeConst.ObjRef(ObjIterable)) stmt = stmt,
val typeSlot = allocSlot() sourceValue = sourceValue,
builder.emit(Opcode.CONST_OBJ, typeId, typeSlot) wantResult = wantResult,
builder.emit(Opcode.ASSERT_IS, sourceObj.slot, typeSlot) loopSlotId = loopSlotId,
breakFlagSlot = breakFlagSlot,
val iterableMethods = ObjIterable.instanceMethodIdMap(includeAbstract = true) needsBreakFlag = needsBreakFlag,
val iteratorMethodId = iterableMethods["iterator"] hasRealWiden = hasRealWiden,
if (iteratorMethodId == null) { realWidenSlots = realWidenSlots,
throw BytecodeCompileException("Missing member id for Iterable.iterator", stmt.pos)
}
val iteratorMethods = ObjIterator.instanceMethodIdMap(includeAbstract = true)
val hasNextMethodId = iteratorMethods["hasNext"]
if (hasNextMethodId == null) {
throw BytecodeCompileException("Missing member id for Iterator.hasNext", stmt.pos)
}
val nextMethodId = iteratorMethods["next"]
if (nextMethodId == null) {
throw BytecodeCompileException("Missing member id for Iterator.next", stmt.pos)
}
val iterSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, sourceObj.slot, iteratorMethodId, 0, 0, iterSlot)
builder.emit(Opcode.ITER_PUSH, iterSlot)
if (needsBreakFlag) {
val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
}
val resultSlot = if (wantResult) {
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, slot)
slot
} else {
null
}
val loopLabel = builder.label()
val continueLabel = builder.label()
val endLabel = builder.label()
builder.mark(loopLabel)
val hasNextSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, hasNextMethodId, 0, 0, hasNextSlot)
val condSlot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, hasNextSlot, condSlot)
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condSlot), CmdBuilder.Operand.LabelRef(endLabel))
) )
val nextSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, nextMethodId, 0, 0, nextSlot)
val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN))
emitMove(CompiledValue(nextObj.slot, SlotType.OBJ), loopSlotId)
updateSlotType(loopSlotId, SlotType.OBJ)
updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ)
loopStack.addLast(
LoopContext(
stmt.label,
endLabel,
continueLabel,
breakFlagSlot,
resultSlot,
hasIterator = true
)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
if (hasRealWiden) {
applySlotTypes(realWidenSlots, SlotType.UNKNOWN)
}
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
}
builder.mark(continueLabel)
if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots)
}
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
if (needsBreakFlag) {
val afterPop = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterPop))
)
builder.emit(Opcode.ITER_POP)
builder.mark(afterPop)
} else {
builder.emit(Opcode.ITER_POP)
}
if (stmt.elseStatement != null) {
val afterElse = if (needsBreakFlag) builder.label() else null
if (needsBreakFlag) {
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterElse!!))
)
}
val elseValue = compileStatementValueOrFallback(stmt.elseStatement, wantResult) ?: return null
if (wantResult) {
val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot!!)
}
if (needsBreakFlag) {
builder.mark(afterElse!!)
}
}
return resultSlot ?: breakFlagSlot
} }
val iSlot = loopSlotId val iSlot = loopSlotId
val endSlot = allocSlot() val endSlot = allocSlot()
val descendingSlot = allocSlot()
if (range != null) { if (range != null) {
val startId = builder.addConst(BytecodeConst.IntVal(range.start)) val startId = builder.addConst(BytecodeConst.IntVal(range.start))
val endId = builder.addConst(BytecodeConst.IntVal(range.endExclusive)) val endId = builder.addConst(BytecodeConst.IntVal(range.stopBoundary))
val descendingId = builder.addConst(BytecodeConst.Bool(range.isDescending))
builder.emit(Opcode.CONST_INT, startId, iSlot) builder.emit(Opcode.CONST_INT, startId, iSlot)
builder.emit(Opcode.CONST_INT, endId, endSlot) builder.emit(Opcode.CONST_INT, endId, endSlot)
builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
updateSlotType(iSlot, SlotType.INT) updateSlotType(iSlot, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
} else { } else {
if (rangeRef != null) { if (rangeRef != null) {
val left = rangeRef.left ?: return null val left = rangeRef.left ?: return null
val right = rangeRef.right ?: return null val right = rangeRef.right ?: return null
val startValue = compileRef(left) ?: return null val startCompiled = compileRef(left) ?: return null
val endValue = compileRef(right) ?: return null val endCompiled = compileRef(right) ?: return null
if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null val startValue = coerceToLoopInt(startCompiled)
val endValue = coerceToLoopInt(endCompiled)
if (startValue == null || endValue == null) {
val rangeValue = emitRangeObject(startCompiled, endCompiled, rangeRef)
return emitIterableForIn(
stmt = stmt,
sourceValue = rangeValue,
wantResult = wantResult,
loopSlotId = loopSlotId,
breakFlagSlot = breakFlagSlot,
needsBreakFlag = needsBreakFlag,
hasRealWiden = hasRealWiden,
realWidenSlots = realWidenSlots,
)
}
val descendingId = builder.addConst(BytecodeConst.Bool(rangeRef.isDescending))
emitMove(startValue, iSlot) emitMove(startValue, iSlot)
emitMove(endValue, endSlot) emitMove(endValue, endSlot)
if (rangeRef.isEndInclusive) { builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
if (rangeRef.isDescending) {
if (rangeRef.isEndInclusive) {
builder.emit(Opcode.DEC_INT, endSlot)
}
} else if (rangeRef.isEndInclusive) {
builder.emit(Opcode.INC_INT, endSlot) builder.emit(Opcode.INC_INT, endSlot)
} }
updateSlotType(iSlot, SlotType.INT) updateSlotType(iSlot, SlotType.INT)
@ -6270,7 +6250,7 @@ class BytecodeCompiler(
val rangeValue = compileRef(rangeLocal) ?: return null val rangeValue = compileRef(rangeLocal) ?: return null
val rangeObj = ensureObjSlot(rangeValue) val rangeObj = ensureObjSlot(rangeValue)
val okSlot = allocSlot() val okSlot = allocSlot()
builder.emit(Opcode.RANGE_INT_BOUNDS, rangeObj.slot, iSlot, endSlot, okSlot) builder.emit(Opcode.RANGE_INT_BOUNDS, rangeObj.slot, iSlot, endSlot, descendingSlot, okSlot)
val badRangeLabel = builder.label() val badRangeLabel = builder.label()
builder.emit( builder.emit(
Opcode.JMP_IF_FALSE, Opcode.JMP_IF_FALSE,
@ -6294,14 +6274,7 @@ class BytecodeCompiler(
val endLabel = builder.label() val endLabel = builder.label()
val doneLabel = builder.label() val doneLabel = builder.label()
builder.mark(loopLabel) builder.mark(loopLabel)
builder.emit( emitIntForLoopCheck(iSlot, endSlot, descendingSlot, endLabel)
Opcode.JMP_IF_GTE_INT,
listOf(
CmdBuilder.Operand.IntVal(iSlot),
CmdBuilder.Operand.IntVal(endSlot),
CmdBuilder.Operand.LabelRef(endLabel)
)
)
updateSlotType(iSlot, SlotType.INT) updateSlotType(iSlot, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
loopStack.addLast( loopStack.addLast(
@ -6324,7 +6297,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
} }
builder.mark(continueLabel) builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot) emitIntForLoopStep(iSlot, descendingSlot)
if (hasRealWiden) { if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots) emitLoopRealCoercions(realWidenSlots)
} }
@ -6377,14 +6350,7 @@ class BytecodeCompiler(
val continueLabel = builder.label() val continueLabel = builder.label()
val endLabel = builder.label() val endLabel = builder.label()
builder.mark(loopLabel) builder.mark(loopLabel)
builder.emit( emitIntForLoopCheck(iSlot, endSlot, descendingSlot, endLabel)
Opcode.JMP_IF_GTE_INT,
listOf(
CmdBuilder.Operand.IntVal(iSlot),
CmdBuilder.Operand.IntVal(endSlot),
CmdBuilder.Operand.LabelRef(endLabel)
)
)
updateSlotType(iSlot, SlotType.INT) updateSlotType(iSlot, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
loopStack.addLast( loopStack.addLast(
@ -6407,7 +6373,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
} }
builder.mark(continueLabel) builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot) emitIntForLoopStep(iSlot, descendingSlot)
if (hasRealWiden) { if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots) emitLoopRealCoercions(realWidenSlots)
} }
@ -7310,7 +7276,8 @@ class BytecodeCompiler(
is LocalSlotRef -> { is LocalSlotRef -> {
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
val ownerSlot = ref.captureOwnerSlot ?: ref.slot val ownerSlot = ref.captureOwnerSlot ?: ref.slot
slotTypeByScopeId[ownerScopeId]?.get(ownerSlot) resolveLocalSlotByRefOrName(ref)?.let { slotObjClass[it] }
?: slotTypeByScopeId[ownerScopeId]?.get(ownerSlot)
?: slotInitClassByKey[ScopeSlotKey(ownerScopeId, ownerSlot)] ?: slotInitClassByKey[ScopeSlotKey(ownerScopeId, ownerSlot)]
?: nameObjClass[ref.name] ?: nameObjClass[ref.name]
?: resolveTypeNameClass(ref.name) ?: resolveTypeNameClass(ref.name)
@ -7723,6 +7690,11 @@ class BytecodeCompiler(
return resolved return resolved
} }
private fun resolveLocalSlotByRefOrName(ref: LocalSlotRef): Int? {
return resolveSlot(ref)
?: ref.name.takeIf { it.isNotEmpty() }?.let { name -> resolveDirectNameSlot(name)?.slot }
}
private fun resolveCapturedOwnerScopeSlot(ref: LocalSlotRef): Int? { private fun resolveCapturedOwnerScopeSlot(ref: LocalSlotRef): Int? {
val ownerScopeId = ref.captureOwnerScopeId ?: return null val ownerScopeId = ref.captureOwnerScopeId ?: return null
val ownerSlot = ref.captureOwnerSlot ?: return null val ownerSlot = ref.captureOwnerSlot ?: return null
@ -8700,10 +8672,237 @@ class BytecodeCompiler(
return if (ref.step != null) null else ref return if (ref.step != null) null else ref
} }
private fun isConstIntRange(ref: RangeRef): Boolean { private fun emitIterableForIn(
val left = ref.left as? ConstRef ?: return false stmt: net.sergeych.lyng.ForInStatement,
val right = ref.right as? ConstRef ?: return false sourceValue: CompiledValue,
return left.constValue is ObjInt && right.constValue is ObjInt wantResult: Boolean,
loopSlotId: Int,
breakFlagSlot: Int,
needsBreakFlag: Boolean,
hasRealWiden: Boolean,
realWidenSlots: Set<Int>,
): Int? {
val sourceObj = ensureObjSlot(sourceValue)
val typeId = builder.addConst(BytecodeConst.ObjRef(ObjIterable))
val typeSlot = allocSlot()
builder.emit(Opcode.CONST_OBJ, typeId, typeSlot)
builder.emit(Opcode.ASSERT_IS, sourceObj.slot, typeSlot)
val iterableMethods = ObjIterable.instanceMethodIdMap(includeAbstract = true)
val iteratorMethodId = iterableMethods["iterator"]
?: throw BytecodeCompileException("Missing member id for Iterable.iterator", stmt.pos)
val iteratorMethods = ObjIterator.instanceMethodIdMap(includeAbstract = true)
val hasNextMethodId = iteratorMethods["hasNext"]
?: throw BytecodeCompileException("Missing member id for Iterator.hasNext", stmt.pos)
val nextMethodId = iteratorMethods["next"]
?: throw BytecodeCompileException("Missing member id for Iterator.next", stmt.pos)
val iterSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, sourceObj.slot, iteratorMethodId, 0, 0, iterSlot)
builder.emit(Opcode.ITER_PUSH, iterSlot)
if (needsBreakFlag) {
val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
}
val resultSlot = if (wantResult) {
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, slot)
slot
} else {
null
}
val loopLabel = builder.label()
val continueLabel = builder.label()
val endLabel = builder.label()
builder.mark(loopLabel)
val hasNextSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, hasNextMethodId, 0, 0, hasNextSlot)
val condSlot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, hasNextSlot, condSlot)
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condSlot), CmdBuilder.Operand.LabelRef(endLabel))
)
val nextSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, nextMethodId, 0, 0, nextSlot)
val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN))
emitMove(CompiledValue(nextObj.slot, SlotType.OBJ), loopSlotId)
updateSlotType(loopSlotId, SlotType.OBJ)
updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ)
loopStack.addLast(
LoopContext(
stmt.label,
endLabel,
continueLabel,
breakFlagSlot,
resultSlot,
hasIterator = true
)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
if (hasRealWiden) {
applySlotTypes(realWidenSlots, SlotType.UNKNOWN)
}
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
}
builder.mark(continueLabel)
if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots)
}
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
if (needsBreakFlag) {
val afterPop = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterPop))
)
builder.emit(Opcode.ITER_POP)
builder.mark(afterPop)
} else {
builder.emit(Opcode.ITER_POP)
}
if (stmt.elseStatement != null) {
val afterElse = if (needsBreakFlag) builder.label() else null
if (needsBreakFlag) {
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterElse!!))
)
}
val elseValue = compileStatementValueOrFallback(stmt.elseStatement, wantResult) ?: return null
if (wantResult) {
val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot!!)
}
if (needsBreakFlag) {
builder.mark(afterElse!!)
}
}
return resultSlot ?: breakFlagSlot
}
private fun emitRangeObject(startValue: CompiledValue, endValue: CompiledValue, ref: RangeRef): CompiledValue {
val startObj = ensureObjSlot(startValue)
val endObj = ensureObjSlot(endValue)
val inclusiveSlot = allocSlot()
val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive))
builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot)
val descendingSlot = allocSlot()
val descendingId = builder.addConst(BytecodeConst.Bool(ref.isDescending))
builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
val stepSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, stepSlot)
updateSlotType(stepSlot, SlotType.OBJ)
val dst = allocSlot()
builder.emit(Opcode.MAKE_RANGE, startObj.slot, endObj.slot, inclusiveSlot, descendingSlot, stepSlot, dst)
updateSlotType(dst, SlotType.OBJ)
slotObjClass[dst] = ObjRange.type
return CompiledValue(dst, SlotType.OBJ)
}
private fun isDynamicIntRangeCandidate(ref: RangeRef): Boolean {
val left = ref.left ?: return false
val right = ref.right ?: return false
return isIntLikeRef(left) && isIntLikeRef(right)
}
private fun isIntLikeRef(ref: ObjRef): Boolean {
if (inferNumericKind(ref) == NumericKind.INT) {
return true
}
return when (ref) {
is ConstRef -> ref.constValue is ObjInt
is LocalSlotRef,
is LocalVarRef,
is FastLocalVarRef,
is BoundLocalVarRef,
is CallRef,
is MethodCallRef,
is FieldRef,
is CastRef,
is StatementRef -> resolveReceiverClass(ref) == ObjInt.type
is ThisMethodSlotCallRef,
is ImplicitThisMethodCallRef,
is ThisFieldSlotRef,
is ImplicitThisMemberRef -> resolveReceiverClassForScopeCollection(ref) == ObjInt.type
is UnaryOpRef -> ref.op == UnaryOp.NEGATE && isIntLikeRef(unaryOperand(ref))
is BinaryOpRef -> when (binaryOp(ref)) {
BinOp.PLUS,
BinOp.MINUS,
BinOp.STAR,
BinOp.SLASH,
BinOp.PERCENT,
BinOp.BAND,
BinOp.BXOR,
BinOp.BOR,
BinOp.SHL,
BinOp.SHR -> isIntLikeRef(binaryLeft(ref)) && isIntLikeRef(binaryRight(ref))
else -> false
}
else -> false
}
}
private fun coerceToLoopInt(value: CompiledValue): CompiledValue? {
return when (value.type) {
SlotType.INT -> value
SlotType.OBJ -> {
val isExactInt = isExactNonNullSlotClassOrTemp(value.slot, ObjInt.type)
val isStableIntObj = slotObjClass[value.slot] == ObjInt.type && isStablePrimitiveSourceSlot(value.slot)
if (!isExactInt && !isStableIntObj && !isStablePrimitiveSourceSlot(value.slot)) return null
val objSlot = if (isExactInt || isStableIntObj) {
value.slot
} else {
val boxed = allocSlot()
builder.emit(Opcode.BOX_OBJ, value.slot, boxed)
updateSlotType(boxed, SlotType.OBJ)
emitAssertObjSlotIsInt(boxed)
}
val intSlot = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, objSlot, intSlot)
updateSlotType(intSlot, SlotType.INT)
CompiledValue(intSlot, SlotType.INT)
}
SlotType.UNKNOWN -> {
if (!isStablePrimitiveSourceSlot(value.slot)) return null
val boxed = allocSlot()
builder.emit(Opcode.BOX_OBJ, value.slot, boxed)
updateSlotType(boxed, SlotType.OBJ)
val checked = emitAssertObjSlotIsInt(boxed)
val intSlot = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, checked, intSlot)
updateSlotType(intSlot, SlotType.INT)
CompiledValue(intSlot, SlotType.INT)
}
else -> null
}
}
private fun coerceToArithmeticInt(ref: ObjRef, value: CompiledValue): CompiledValue? {
if (value.type == SlotType.INT) return value
val refSuggestsInt = inferNumericKind(ref) == NumericKind.INT
val stableNonTemp = !isTempSlot(value.slot) && isStablePrimitiveSourceSlot(value.slot)
if (!refSuggestsInt && !stableNonTemp) return null
return coerceToLoopInt(value)
}
private fun emitAssertObjSlotIsInt(slot: Int): Int {
val typeId = builder.addConst(BytecodeConst.ObjRef(ObjInt.type))
val typeSlot = allocSlot()
builder.emit(Opcode.CONST_OBJ, typeId, typeSlot)
builder.emit(Opcode.ASSERT_IS, slot, typeSlot)
return slot
} }
private fun extractDeclaredRange(stmt: Statement?): RangeRef? { private fun extractDeclaredRange(stmt: Statement?): RangeRef? {
@ -8719,11 +8918,59 @@ class BytecodeCompiler(
val end = range.end as? ObjInt ?: return null val end = range.end as? ObjInt ?: return null
val left = ConstRef(start.asReadonly) val left = ConstRef(start.asReadonly)
val right = ConstRef(end.asReadonly) val right = ConstRef(end.asReadonly)
return RangeRef(left, right, range.isEndInclusive) return RangeRef(left, right, range.isEndInclusive, isDescending = range.isDescending)
} }
return null return null
} }
private fun emitIntForLoopCheck(iSlot: Int, stopSlot: Int, descendingSlot: Int, endLabel: CmdBuilder.Label) {
val descendingLabel = builder.label()
val afterCheckLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(
CmdBuilder.Operand.IntVal(descendingSlot),
CmdBuilder.Operand.LabelRef(descendingLabel)
)
)
builder.emit(
Opcode.JMP_IF_GTE_INT,
listOf(
CmdBuilder.Operand.IntVal(iSlot),
CmdBuilder.Operand.IntVal(stopSlot),
CmdBuilder.Operand.LabelRef(endLabel)
)
)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(afterCheckLabel)))
builder.mark(descendingLabel)
builder.emit(
Opcode.JMP_IF_LTE_INT,
listOf(
CmdBuilder.Operand.IntVal(iSlot),
CmdBuilder.Operand.IntVal(stopSlot),
CmdBuilder.Operand.LabelRef(endLabel)
)
)
builder.mark(afterCheckLabel)
}
private fun emitIntForLoopStep(iSlot: Int, descendingSlot: Int) {
val descendingLabel = builder.label()
val afterStepLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(
CmdBuilder.Operand.IntVal(descendingSlot),
CmdBuilder.Operand.LabelRef(descendingLabel)
)
)
builder.emit(Opcode.INC_INT, iSlot)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(afterStepLabel)))
builder.mark(descendingLabel)
builder.emit(Opcode.DEC_INT, iSlot)
builder.mark(afterStepLabel)
}
private fun extractRangeFromLocal(source: Statement): RangeRef? { private fun extractRangeFromLocal(source: Statement): RangeRef? {
val target = if (source is BytecodeStatement) source.original else source val target = if (source is BytecodeStatement) source.original else source
val expr = target as? ExpressionStatement ?: return null val expr = target as? ExpressionStatement ?: return null

View File

@ -147,7 +147,7 @@ class CmdBuilder {
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW -> Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RANGE_INT_BOUNDS -> Opcode.RANGE_INT_BOUNDS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RET_LABEL, Opcode.THROW -> Opcode.RET_LABEL, Opcode.THROW ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT -> Opcode.RESOLVE_SCOPE_SLOT ->
@ -228,7 +228,7 @@ class CmdBuilder {
Opcode.SET_INDEX -> Opcode.SET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.MAKE_RANGE -> Opcode.MAKE_RANGE ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_LITERAL -> Opcode.LIST_LITERAL ->
listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_MEMBER_SLOT -> Opcode.GET_MEMBER_SLOT ->
@ -311,10 +311,10 @@ class CmdBuilder {
} }
Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1]) Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1])
Opcode.GET_OBJ_CLASS -> CmdGetObjClass(operands[0], operands[1]) Opcode.GET_OBJ_CLASS -> CmdGetObjClass(operands[0], operands[1])
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3]) Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3], operands[4])
Opcode.LOAD_THIS -> CmdLoadThis(operands[0]) Opcode.LOAD_THIS -> CmdLoadThis(operands[0])
Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1]) Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1])
Opcode.MAKE_RANGE -> CmdMakeRange(operands[0], operands[1], operands[2], operands[3], operands[4]) Opcode.MAKE_RANGE -> CmdMakeRange(operands[0], operands[1], operands[2], operands[3], operands[4], operands[5])
Opcode.CHECK_IS -> CmdCheckIs(operands[0], operands[1], operands[2]) Opcode.CHECK_IS -> CmdCheckIs(operands[0], operands[1], operands[2])
Opcode.ASSERT_IS -> CmdAssertIs(operands[0], operands[1]) Opcode.ASSERT_IS -> CmdAssertIs(operands[0], operands[1])
Opcode.MAKE_QUALIFIED_VIEW -> CmdMakeQualifiedView(operands[0], operands[1], operands[2]) Opcode.MAKE_QUALIFIED_VIEW -> CmdMakeQualifiedView(operands[0], operands[1], operands[2])

View File

@ -96,11 +96,12 @@ object CmdDisassembler {
is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst) is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot) is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot)
is CmdMakeQualifiedView -> Opcode.MAKE_QUALIFIED_VIEW to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst) is CmdMakeQualifiedView -> Opcode.MAKE_QUALIFIED_VIEW to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
is CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.okSlot) is CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.descendingSlot, cmd.okSlot)
is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf( is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf(
cmd.startSlot, cmd.startSlot,
cmd.endSlot, cmd.endSlot,
cmd.inclusiveSlot, cmd.inclusiveSlot,
cmd.descendingSlot,
cmd.stepSlot, cmd.stepSlot,
cmd.dst cmd.dst
) )

View File

@ -273,6 +273,7 @@ class CmdMakeRange(
internal val startSlot: Int, internal val startSlot: Int,
internal val endSlot: Int, internal val endSlot: Int,
internal val inclusiveSlot: Int, internal val inclusiveSlot: Int,
internal val descendingSlot: Int,
internal val stepSlot: Int, internal val stepSlot: Int,
internal val dst: Int, internal val dst: Int,
) : Cmd() { ) : Cmd() {
@ -280,9 +281,10 @@ class CmdMakeRange(
val start = frame.slotToObj(startSlot) val start = frame.slotToObj(startSlot)
val end = frame.slotToObj(endSlot) val end = frame.slotToObj(endSlot)
val inclusive = frame.slotToObj(inclusiveSlot).toBool() val inclusive = frame.slotToObj(inclusiveSlot).toBool()
val descending = frame.slotToObj(descendingSlot).toBool()
val stepObj = frame.slotToObj(stepSlot) val stepObj = frame.slotToObj(stepSlot)
val step = if (stepObj.isNull) null else stepObj val step = if (stepObj.isNull) null else stepObj
frame.storeObjResult(dst, ObjRange(start, end, isEndInclusive = inclusive, step = step)) frame.storeObjResult(dst, ObjRange(start, end, isEndInclusive = inclusive, isDescending = descending, step = step))
return return
} }
} }
@ -312,8 +314,13 @@ class CmdUnboxIntObj(internal val src: Int, internal val dst: Int) : Cmd() {
class CmdUnboxIntObjLocal(internal val src: Int, internal val dst: Int) : Cmd() { class CmdUnboxIntObjLocal(internal val src: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val value = frame.frame.getRawObj(src) as ObjInt when (frame.frame.getSlotTypeCode(src)) {
frame.setLocalInt(dst, value.value) SlotType.INT.code -> frame.setLocalInt(dst, frame.frame.getInt(src))
else -> {
val value = frame.frame.getRawObj(src) as ObjInt
frame.setLocalInt(dst, value.value)
}
}
return return
} }
} }
@ -329,8 +336,13 @@ class CmdUnboxRealObj(internal val src: Int, internal val dst: Int) : Cmd() {
class CmdUnboxRealObjLocal(internal val src: Int, internal val dst: Int) : Cmd() { class CmdUnboxRealObjLocal(internal val src: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val value = frame.frame.getRawObj(src) as ObjReal when (frame.frame.getSlotTypeCode(src)) {
frame.setLocalReal(dst, value.value) SlotType.REAL.code -> frame.setLocalReal(dst, frame.frame.getReal(src))
else -> {
val value = frame.frame.getRawObj(src) as ObjReal
frame.setLocalReal(dst, value.value)
}
}
return return
} }
} }
@ -430,6 +442,7 @@ class CmdRangeIntBounds(
internal val src: Int, internal val src: Int,
internal val startSlot: Int, internal val startSlot: Int,
internal val endSlot: Int, internal val endSlot: Int,
internal val descendingSlot: Int,
internal val okSlot: Int, internal val okSlot: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
@ -439,10 +452,19 @@ class CmdRangeIntBounds(
frame.setBool(okSlot, false) frame.setBool(okSlot, false)
return return
} }
if (range.isDescending) {
frame.setBool(okSlot, false)
return
}
val start = (range.start as ObjInt).value val start = (range.start as ObjInt).value
val end = (range.end as ObjInt).value val end = (range.end as ObjInt).value
frame.setInt(startSlot, start) frame.setInt(startSlot, start)
frame.setInt(endSlot, if (range.isEndInclusive) end + 1 else end) frame.setInt(endSlot, if (range.isDescending) {
if (range.isEndInclusive) end - 1 else end
} else {
if (range.isEndInclusive) end + 1 else end
})
frame.setBool(descendingSlot, range.isDescending)
frame.setBool(okSlot, true) frame.setBool(okSlot, true)
return return
} }
@ -1528,9 +1550,15 @@ class CmdCmpEqIntObj(internal val a: Int, internal val b: Int, internal val dst:
class CmdCmpEqIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpEqIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value == right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left == right)
return return
} }
} }
@ -1551,9 +1579,15 @@ class CmdCmpNeqIntObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpNeqIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpNeqIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value != right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left != right)
return return
} }
} }
@ -1574,9 +1608,15 @@ class CmdCmpLtIntObj(internal val a: Int, internal val b: Int, internal val dst:
class CmdCmpLtIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpLtIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value < right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left < right)
return return
} }
} }
@ -1597,9 +1637,15 @@ class CmdCmpLteIntObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpLteIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpLteIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value <= right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left <= right)
return return
} }
} }
@ -1620,9 +1666,15 @@ class CmdCmpGtIntObj(internal val a: Int, internal val b: Int, internal val dst:
class CmdCmpGtIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpGtIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value > right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left > right)
return return
} }
} }
@ -1643,9 +1695,15 @@ class CmdCmpGteIntObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpGteIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpGteIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value >= right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left >= right)
return return
} }
} }
@ -1666,9 +1724,15 @@ class CmdCmpEqRealObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpEqRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpEqRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value == right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left == right)
return return
} }
} }
@ -1689,9 +1753,15 @@ class CmdCmpNeqRealObj(internal val a: Int, internal val b: Int, internal val ds
class CmdCmpNeqRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpNeqRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value != right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left != right)
return return
} }
} }
@ -1712,9 +1782,15 @@ class CmdCmpLtRealObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpLtRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpLtRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value < right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left < right)
return return
} }
} }
@ -1735,9 +1811,15 @@ class CmdCmpLteRealObj(internal val a: Int, internal val b: Int, internal val ds
class CmdCmpLteRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpLteRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value <= right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left <= right)
return return
} }
} }
@ -1758,9 +1840,15 @@ class CmdCmpGtRealObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpGtRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpGtRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value > right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left > right)
return return
} }
} }
@ -1781,9 +1869,15 @@ class CmdCmpGteRealObj(internal val a: Int, internal val b: Int, internal val ds
class CmdCmpGteRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpGteRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value >= right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left >= right)
return return
} }
} }
@ -3612,7 +3706,13 @@ class CmdGetIndex(
internal val dst: Int, internal val dst: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val result = frame.slotToObj(targetSlot).getAt(frame.ensureScope(), frame.slotToObj(indexSlot)) val target = frame.storedSlotObj(targetSlot)
val index = frame.storedSlotObj(indexSlot)
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
frame.storeObjResult(dst, target.list[index.toInt()])
return
}
val result = target.getAt(frame.ensureScope(), index)
frame.storeObjResult(dst, result) frame.storeObjResult(dst, result)
return return
} }
@ -3624,7 +3724,14 @@ class CmdSetIndex(
internal val valueSlot: Int, internal val valueSlot: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
frame.slotToObj(targetSlot).putAt(frame.ensureScope(), frame.slotToObj(indexSlot), frame.slotToObj(valueSlot)) val target = frame.storedSlotObj(targetSlot)
val index = frame.storedSlotObj(indexSlot)
val value = frame.slotToObj(valueSlot)
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
target.list[index.toInt()] = value
return
}
target.putAt(frame.ensureScope(), index, value)
return return
} }
} }

View File

@ -43,7 +43,7 @@ private val fallbackKeywordIds = setOf(
// declarations & modifiers // declarations & modifiers
"fun", "fn", "class", "interface", "enum", "val", "var", "type", "import", "package", "fun", "fn", "class", "interface", "enum", "val", "var", "type", "import", "package",
"abstract", "closed", "override", "public", "lazy", "dynamic", "abstract", "closed", "override", "public", "lazy", "dynamic",
"private", "protected", "static", "open", "extern", "init", "get", "set", "by", "step", "private", "protected", "static", "open", "extern", "init", "get", "set", "by", "step", "downTo", "downUntil",
// control flow and misc // control flow and misc
"if", "else", "when", "while", "do", "for", "try", "catch", "finally", "if", "else", "when", "while", "do", "for", "try", "catch", "finally",
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset", "void" "throw", "return", "break", "continue", "this", "null", "true", "false", "unset", "void"
@ -74,7 +74,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) {
Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation
// textual control keywords // textual control keywords
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.STEP, Type.OBJECT, Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.STEP, Type.DOWNTO, Type.DOWNUNTIL, Type.OBJECT,
Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword
// labels / annotations // labels / annotations

View File

@ -28,6 +28,7 @@ class ObjRange(
val start: Obj?, val start: Obj?,
val end: Obj?, val end: Obj?,
val isEndInclusive: Boolean, val isEndInclusive: Boolean,
val isDescending: Boolean = false,
val step: Obj? = null val step: Obj? = null
) : Obj() { ) : Obj() {
@ -39,15 +40,38 @@ class ObjRange(
override suspend fun defaultToString(scope: Scope): ObjString { override suspend fun defaultToString(scope: Scope): ObjString {
val result = StringBuilder() val result = StringBuilder()
result.append("${start?.inspect(scope) ?: '∞'} ..") result.append(start?.inspect(scope) ?: "")
if (!isEndInclusive) result.append('<') when {
result.append(" ${end?.inspect(scope) ?: '∞'}") isDescending && isEndInclusive -> result.append(" downTo ")
isDescending && !isEndInclusive -> result.append(" downUntil ")
else -> {
result.append(" ..")
if (!isEndInclusive) result.append('<')
result.append(' ')
}
}
result.append(end?.inspect(scope) ?: "")
if (hasExplicitStep) { if (hasExplicitStep) {
result.append(" step ${step?.inspect(scope)}") result.append(" step ${step?.inspect(scope)}")
} }
return ObjString(result.toString()) return ObjString(result.toString())
} }
private data class NormalizedLowerBound(val value: Obj, val inclusive: Boolean)
private data class NormalizedUpperBound(val value: Obj, val inclusive: Boolean)
private fun normalizedLowerBound(): NormalizedLowerBound? =
when {
isDescending -> end?.takeUnless { it.isNull }?.let { NormalizedLowerBound(it, isEndInclusive) }
else -> start?.takeUnless { it.isNull }?.let { NormalizedLowerBound(it, true) }
}
private fun normalizedUpperBound(): NormalizedUpperBound? =
when {
isDescending -> start?.takeUnless { it.isNull }?.let { NormalizedUpperBound(it, true) }
else -> end?.takeUnless { it.isNull }?.let { NormalizedUpperBound(it, isEndInclusive) }
}
/** /**
* IF end is open (null/ObjNull), returns null * IF end is open (null/ObjNull), returns null
* Otherwise, return correct value for the exclusive end * Otherwise, return correct value for the exclusive end
@ -74,29 +98,21 @@ class ObjRange(
} }
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean { suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {
if (!isOpenStart) { val ourLower = normalizedLowerBound()
// our start is not -∞ so other start should be GTE or is not contained: val otherLower = other.normalizedLowerBound()
if (!other.isOpenStart && start!!.compareTo(scope, other.start!!) > 0) return false if (ourLower != null) {
if (otherLower == null) return false
val cmp = ourLower.value.compareTo(scope, otherLower.value)
if (cmp == -2 || cmp > 0) return false
if (cmp == 0 && otherLower.inclusive && !ourLower.inclusive) return false
} }
if (!isOpenEnd) { val ourUpper = normalizedUpperBound()
// same with the end: if it is open, it can't be contained in ours: val otherUpper = other.normalizedUpperBound()
if (other.isOpenEnd) return false if (ourUpper != null) {
// both exists, now there could be 4 cases: if (otherUpper == null) return false
return when { val cmp = ourUpper.value.compareTo(scope, otherUpper.value)
other.isEndInclusive && isEndInclusive -> if (cmp == -2 || cmp < 0) return false
end!!.compareTo(scope, other.end!!) >= 0 if (cmp == 0 && otherUpper.inclusive && !ourUpper.inclusive) return false
!other.isEndInclusive && !isEndInclusive ->
end!!.compareTo(scope, other.end!!) >= 0
other.isEndInclusive && !isEndInclusive ->
end!!.compareTo(scope, other.end!!) > 0
!other.isEndInclusive && isEndInclusive ->
end!!.compareTo(scope, other.end!!) >= 0
else -> throw IllegalStateException("unknown comparison")
}
} }
return true return true
} }
@ -108,35 +124,38 @@ class ObjRange(
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
if (start is ObjInt && end is ObjInt && other is ObjInt) { if (start is ObjInt && end is ObjInt && other is ObjInt) {
val s = start.value val lower = if (isDescending) end.value else start.value
val e = end.value val upper = if (isDescending) start.value else end.value
val v = other.value val v = other.value
if (v < s) return false if (v < lower || v > upper) return false
return if (isEndInclusive) v <= e else v < e return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
} }
if (start is ObjChar && end is ObjChar && other is ObjChar) { if (start is ObjChar && end is ObjChar && other is ObjChar) {
val s = start.value val lower = if (isDescending) end.value else start.value
val e = end.value val upper = if (isDescending) start.value else end.value
val v = other.value val v = other.value
if (v < s) return false if (v < lower || v > upper) return false
return if (isEndInclusive) v <= e else v < e return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
} }
if (start is ObjString && end is ObjString && other is ObjString) { if (start is ObjString && end is ObjString && other is ObjString) {
val s = start.value val lower = if (isDescending) end.value else start.value
val e = end.value val upper = if (isDescending) start.value else end.value
val v = other.value val v = other.value
if (v < s) return false if (v < lower || v > upper) return false
return if (isEndInclusive) v <= e else v < e return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
} }
} }
if (isOpenStart && isOpenEnd) return true val lower = normalizedLowerBound()
if (!isOpenStart) { val upper = normalizedUpperBound()
if (start!!.compareTo(scope, other) > 0) return false if (lower == null && upper == null) return true
if (lower != null) {
val cmp = lower.value.compareTo(scope, other)
if (cmp == -2 || cmp > 0 || (!lower.inclusive && cmp == 0)) return false
} }
if (!isOpenEnd) { if (upper != null) {
val cmp = end!!.compareTo(scope, other) val cmp = upper.value.compareTo(scope, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false if (cmp == -2 || cmp < 0 || (!upper.inclusive && cmp == 0)) return false
} }
return true return true
} }
@ -153,7 +172,12 @@ class ObjRange(
if (!hasExplicitStep && start is ObjInt && end is ObjInt) { if (!hasExplicitStep && start is ObjInt && end is ObjInt) {
val s = start.value val s = start.value
val e = end.value val e = end.value
if (isEndInclusive) { if (isDescending) {
val last = if (isEndInclusive) e else e + 1
for (i in s downTo last) {
if (!callback(ObjInt.of(i))) break
}
} else if (isEndInclusive) {
for (i in s..e) { for (i in s..e) {
if (!callback(ObjInt.of(i))) break if (!callback(ObjInt.of(i))) break
} }
@ -165,7 +189,14 @@ class ObjRange(
} else if (!hasExplicitStep && start is ObjChar && end is ObjChar) { } else if (!hasExplicitStep && start is ObjChar && end is ObjChar) {
val s = start.value val s = start.value
val e = end.value val e = end.value
if (isEndInclusive) { if (isDescending) {
var c = s.code
val last = if (isEndInclusive) e.code else e.code + 1
while (c >= last) {
if (!callback(ObjChar(c.toChar()))) break
c--
}
} else if (isEndInclusive) {
for (c in s..e) { for (c in s..e) {
if (!callback(ObjChar(c))) break if (!callback(ObjChar(c))) break
} }
@ -184,6 +215,7 @@ class ObjRange(
if (start == other.start && if (start == other.start &&
end == other.end && end == other.end &&
isEndInclusive == other.isEndInclusive && isEndInclusive == other.isEndInclusive &&
isDescending == other.isDescending &&
step == other.step step == other.step
) 0 else -1 ) 0 else -1
} }
@ -194,6 +226,7 @@ class ObjRange(
var result = start?.hashCode() ?: 0 var result = start?.hashCode() ?: 0
result = 31 * result + (end?.hashCode() ?: 0) result = 31 * result + (end?.hashCode() ?: 0)
result = 31 * result + isEndInclusive.hashCode() result = 31 * result + isEndInclusive.hashCode()
result = 31 * result + isDescending.hashCode()
result = 31 * result + (step?.hashCode() ?: 0) result = 31 * result + (step?.hashCode() ?: 0)
return result return result
} }
@ -207,6 +240,7 @@ class ObjRange(
if (start != other.start) return false if (start != other.start) return false
if (end != other.end) return false if (end != other.end) return false
if (isEndInclusive != other.isEndInclusive) return false if (isEndInclusive != other.isEndInclusive) return false
if (isDescending != other.isDescending) return false
if (step != other.step) return false if (step != other.step) return false
return true return true
@ -264,6 +298,13 @@ class ObjRange(
moduleName = "lyng.stdlib", moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().isEndInclusive.toObj() } getter = { thisAs<ObjRange>().isEndInclusive.toObj() }
) )
addPropertyDoc(
name = "isDescending",
doc = "Whether the range iterates from the start bound down toward the end bound.",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().isDescending.toObj() }
)
addFnDoc( addFnDoc(
name = "iterator", name = "iterator",
doc = "Iterator over elements in this range (optimized for Int ranges).", doc = "Iterator over elements in this range (optimized for Int ranges).",
@ -290,18 +331,22 @@ class ObjRange(
if (startObj is Numeric && explicitStep !is Numeric) { if (startObj is Numeric && explicitStep !is Numeric) {
scope.raiseIllegalState("Numeric range step must be numeric") scope.raiseIllegalState("Numeric range step must be numeric")
} }
if (isDescending) {
val sign = when (explicitStep) {
is ObjInt -> explicitStep.value.compareTo(0)
is Numeric -> explicitStep.doubleValue.compareTo(0.0)
else -> 1
}
if (sign < 0) scope.raiseIllegalState("Descending range step must be positive")
return explicitStep.negate(scope)
}
return explicitStep return explicitStep
} }
if (startObj is ObjInt) { if (startObj is ObjInt) {
val cmp = if (end == null || end.isNull) 0 else startObj.compareTo(scope, end) return ObjInt.of(if (isDescending) -1 else 1)
val dir = if (cmp >= 0) -1 else 1
return ObjInt.of(dir.toLong())
} }
if (startObj is ObjChar) { if (startObj is ObjChar) {
val endChar = end as? ObjChar return ObjInt.of(if (isDescending) -1 else 1)
?: scope.raiseIllegalState("Char range requires Char end to infer step")
val dir = if (startObj.value >= endChar.value) -1 else 1
return ObjInt.of(dir.toLong())
} }
if (startObj is ObjReal) { if (startObj is ObjReal) {
scope.raiseIllegalState("Real range requires explicit step") scope.raiseIllegalState("Real range requires explicit step")

View File

@ -1028,6 +1028,7 @@ class RangeRef(
internal val left: ObjRef?, internal val left: ObjRef?,
internal val right: ObjRef?, internal val right: ObjRef?,
internal val isEndInclusive: Boolean, internal val isEndInclusive: Boolean,
internal val isDescending: Boolean = false,
internal val step: ObjRef? = null internal val step: ObjRef? = null
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord = scope.raiseObjRefEvalDisabled() override suspend fun get(scope: Scope): ObjRecord = scope.raiseObjRefEvalDisabled()

View File

@ -77,7 +77,11 @@ class IfStatement(
} }
} }
data class ConstIntRange(val start: Long, val endExclusive: Long) data class ConstIntRange(
val start: Long,
val stopBoundary: Long,
val isDescending: Boolean,
)
class ForInStatement( class ForInStatement(
val loopVarName: String, val loopVarName: String,

View File

@ -294,6 +294,18 @@ class ScriptTest {
assertEquals(Token.Type.INT, tt[0].type) assertEquals(Token.Type.INT, tt[0].type)
assertEquals(Token.Type.DOTDOTLT, tt[1].type) assertEquals(Token.Type.DOTDOTLT, tt[1].type)
assertEquals(Token.Type.INT, tt[2].type) assertEquals(Token.Type.INT, tt[2].type)
tt = parseLyng("5 downTo 4".toSource())
assertEquals(Token.Type.INT, tt[0].type)
assertEquals(Token.Type.DOWNTO, tt[1].type)
assertEquals(Token.Type.INT, tt[2].type)
tt = parseLyng("5 downUntil 4".toSource())
assertEquals(Token.Type.INT, tt[0].type)
assertEquals(Token.Type.DOWNUNTIL, tt[1].type)
assertEquals(Token.Type.INT, tt[2].type)
} }
@Test @Test
@ -1280,6 +1292,36 @@ class ScriptTest {
assertTrue(convIndex > incIndex, "INT_TO_REAL should appear after INC_INT") assertTrue(convIndex > incIndex, "INT_TO_REAL should appear after INC_INT")
} }
@Test
fun testDescendingForLoopDisasm() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun countDown() {
var acc = 0
for (i in 5 downTo 1) {
acc += i
}
}
fun countDownVar() {
var acc = 0
val r = 5 downTo 1
for (i in r) {
acc += i
}
}
""".trimIndent()
)
val constDisasm = scope.disassembleSymbol("countDown")
val varDisasm = scope.disassembleSymbol("countDownVar")
assertTrue("DEC_INT" in constDisasm, "expected DEC_INT in descending for-loop disasm")
assertTrue("JMP_IF_LTE_INT" in constDisasm, "expected JMP_IF_LTE_INT in descending for-loop disasm")
assertTrue("CALL_MEMBER_SLOT" !in constDisasm, "descending literal range should avoid iterator fallback")
assertTrue("DEC_INT" in varDisasm, "expected DEC_INT in descending range-variable for-loop disasm")
assertTrue("JMP_IF_LTE_INT" in varDisasm, "expected descending comparison in range-variable for-loop disasm")
assertTrue("CALL_MEMBER_SLOT" !in varDisasm, "descending range-variable loop should avoid iterator fallback")
}
@Test @Test
fun testIntClosedRangeInclusive() = runTest { fun testIntClosedRangeInclusive() = runTest {
eval( eval(
@ -3485,17 +3527,58 @@ class ScriptTest {
fun testRangeStepIteration() = runTest { fun testRangeStepIteration() = runTest {
val ints = eval("""(1..5 step 2).toList()""") as ObjList val ints = eval("""(1..5 step 2).toList()""") as ObjList
assertEquals(listOf(1, 3, 5), ints.list.map { it.toInt() }) assertEquals(listOf(1, 3, 5), ints.list.map { it.toInt() })
val descending = eval("""(5 downTo 1).toList()""") as ObjList
assertEquals(listOf(5, 4, 3, 2, 1), descending.list.map { it.toInt() })
val descendingExclusive = eval("""(5 downUntil 1).toList()""") as ObjList
assertEquals(listOf(5, 4, 3, 2), descendingExclusive.list.map { it.toInt() })
val descendingStep = eval("""(10 downTo 1 step 3).toList()""") as ObjList
assertEquals(listOf(10, 7, 4, 1), descendingStep.list.map { it.toInt() })
val descendingChars = eval("""('e' downTo 'a' step 2).toList()""") as ObjList
assertEquals(listOf('e', 'c', 'a'), descendingChars.list.map { it.toString().single() })
val chars = eval("""('a'..'e' step 2).toList()""") as ObjList val chars = eval("""('a'..'e' step 2).toList()""") as ObjList
assertEquals(listOf('a', 'c', 'e'), chars.list.map { it.toString().single() }) assertEquals(listOf('a', 'c', 'e'), chars.list.map { it.toString().single() })
val reals = eval("""(0.0..1.0 step 0.25).toList()""") as ObjList val reals = eval("""(0.0..1.0 step 0.25).toList()""") as ObjList
assertEquals(listOf(0.0, 0.25, 0.5, 0.75, 1.0), reals.list.map { it.toDouble() }) assertEquals(listOf(0.0, 0.25, 0.5, 0.75, 1.0), reals.list.map { it.toDouble() })
val empty = eval("""(5..1 step 1).toList()""") as ObjList val empty = eval("""(5..1 step 1).toList()""") as ObjList
assertEquals(0, empty.list.size) assertEquals(0, empty.list.size)
val plainDescending = eval("""(5..1).toList()""") as ObjList
assertEquals(0, plainDescending.list.size)
val openEnd = eval("""(0.. step 1).take(3).toList()""") as ObjList val openEnd = eval("""(0.. step 1).take(3).toList()""") as ObjList
assertEquals(listOf(0, 1, 2), openEnd.list.map { it.toInt() }) assertEquals(listOf(0, 1, 2), openEnd.list.map { it.toInt() })
assertEquals(
true,
eval(
"""
val r = 10 downTo 1
r.isDescending && r.isEndInclusive && (10 in r) && (1 in r) && (0 !in r)
""".trimIndent()
).toBool()
)
assertEquals(
true,
eval(
"""
val r = 10 downUntil 1
r.isDescending && !r.isEndInclusive && (10 in r) && (1 !in r) && ((8 downTo 3) in r)
""".trimIndent()
).toBool()
)
assertEquals(
15,
(eval(
"""
var s = 0
for (i in 5 downTo 1) s += i
s
""".trimIndent()
) as ObjInt).toInt()
)
assertFailsWith<ExecutionError> { assertFailsWith<ExecutionError> {
eval("""(0.0..1.0).toList()""") eval("""(0.0..1.0).toList()""")
} }
assertFailsWith<ExecutionError> {
eval("""(5 downTo 1 step -1).toList()""")
}
assertFailsWith<ExecutionError> { assertFailsWith<ExecutionError> {
eval("""(0..).toList()""") eval("""(0..).toList()""")
} }
@ -3938,6 +4021,14 @@ class ScriptTest {
) )
} }
@Test
fun testListFill() = runTest {
eval("""
val x = List.fill(5) { it*10 }
assertEquals( [0,10,20,30,40], x)
""".trimIndent())
}
@Test @Test
fun binarySearchTest() = runTest { fun binarySearchTest() = runTest {
eval( eval(

View File

@ -36,7 +36,7 @@ actual object PerfDefaults {
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
actual val PRIMITIVE_FASTOPS: Boolean = true actual val PRIMITIVE_FASTOPS: Boolean = true
actual val RVAL_FASTPATH: Boolean = true actual val RVAL_FASTPATH: Boolean = false
// Regex caching (JVM-first): enabled by default on JVM // Regex caching (JVM-first): enabled by default on JVM
actual val REGEX_CACHE: Boolean = true actual val REGEX_CACHE: Boolean = true

View File

@ -0,0 +1,145 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* 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.
*
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Benchmarks
import net.sergeych.lyng.BytecodeBodyProvider
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.PerfProfiles
import net.sergeych.lyng.Script
import net.sergeych.lyng.Statement
import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.bytecode.CmdCallMemberSlot
import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.bytecode.CmdGetIndex
import net.sergeych.lyng.bytecode.CmdIterPush
import net.sergeych.lyng.bytecode.CmdMakeRange
import net.sergeych.lyng.bytecode.CmdSetIndex
import net.sergeych.lyng.obj.ObjString
import java.nio.file.Files
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.TimeSource
class PiSpigotBenchmarkTest {
@Test
fun benchmarkPiSpigot() = runTest {
if (!Benchmarks.enabled) return@runTest
val source = Files.readString(resolveExample("pi-test.lyng"))
val legacySource = source.replace(
"val quotient = sum / denom",
"var quotient = floor((sum / (denom * 1.0))).toInt()"
)
assertTrue(legacySource != source, "failed to build legacy piSpigot benchmark case")
val digits = 200
val expectedSuffix = "49303819"
val legacyElapsed = runCase("legacy-real-division", legacySource, digits, expectedSuffix, dumpBytecode = true)
val saved = PerfProfiles.snapshot()
PerfFlags.RVAL_FASTPATH = false
val optimizedRvalOffElapsed = runCase(
"optimized-int-division-rval-off",
source,
digits,
expectedSuffix,
dumpBytecode = false
)
PerfProfiles.restore(saved)
val optimizedElapsed = runCase("optimized-int-division-rval-on", source, digits, expectedSuffix, dumpBytecode = true)
val sourceSpeedup = legacyElapsed.toDouble() / optimizedRvalOffElapsed.toDouble()
val runtimeSpeedup = optimizedRvalOffElapsed.toDouble() / optimizedElapsed.toDouble()
val totalSpeedup = legacyElapsed.toDouble() / optimizedElapsed.toDouble()
println(
"[DEBUG_LOG] [BENCH] pi-spigot compare n=$digits legacy=${legacyElapsed} ms " +
"intDiv=${optimizedRvalOffElapsed} ms rvalOn=${optimizedElapsed} ms " +
"intDivSpeedup=${"%.2f".format(sourceSpeedup)}x " +
"rvalSpeedup=${"%.2f".format(runtimeSpeedup)}x " +
"total=${"%.2f".format(totalSpeedup)}x"
)
}
private suspend fun runCase(
name: String,
source: String,
digits: Int,
expectedSuffix: String,
dumpBytecode: Boolean,
): Long {
val scope = Script.newScope()
scope.eval(source)
if (dumpBytecode) {
println("[DEBUG_LOG] [BENCH] pi-spigot cmd:\n${scope.disassembleSymbol("piSpigot")}")
dumpHotOps(scope, "piSpigot")
}
val first = scope.eval("piSpigot($digits)") as ObjString
assertEquals(expectedSuffix, first.value)
repeat(2) {
val warm = scope.eval("piSpigot($digits)") as ObjString
assertEquals(expectedSuffix, warm.value)
}
val iterations = 3
val start = TimeSource.Monotonic.markNow()
repeat(iterations) {
val result = scope.eval("piSpigot($digits)") as ObjString
assertEquals(expectedSuffix, result.value)
}
val elapsedMs = start.elapsedNow().inWholeMilliseconds
val avgMs = elapsedMs.toDouble() / iterations.toDouble()
println(
"[DEBUG_LOG] [BENCH] pi-spigot $name n=$digits iterations=$iterations " +
"elapsed=${elapsedMs} ms avg=${"%.2f".format(avgMs)} ms"
)
return elapsedMs
}
private fun dumpHotOps(scope: net.sergeych.lyng.Scope, name: String) {
val fn = resolveBytecodeFunction(scope, name) ?: return
val makeRange = fn.cmds.count { it is CmdMakeRange }
val callMemberSlot = fn.cmds.count { it is CmdCallMemberSlot }
val iterPush = fn.cmds.count { it is CmdIterPush }
val getIndex = fn.cmds.count { it is CmdGetIndex }
val setIndex = fn.cmds.count { it is CmdSetIndex }
println(
"[DEBUG_LOG] [BENCH] pi-spigot hot-ops " +
"makeRange=$makeRange callMemberSlot=$callMemberSlot iterPush=$iterPush " +
"getIndex=$getIndex setIndex=$setIndex total=${fn.cmds.size}"
)
}
private fun resolveBytecodeFunction(scope: net.sergeych.lyng.Scope, name: String): CmdFunction? {
val record = scope.get(name) ?: return null
val stmt = record.value as? Statement ?: return null
return (stmt as? BytecodeStatement)?.bytecodeFunction()
?: (stmt as? BytecodeBodyProvider)?.bytecodeBody()?.bytecodeFunction()
}
private fun resolveExample(name: String): Path {
val direct = Path.of("examples", name)
if (Files.exists(direct)) return direct
val parent = Path.of("..", "examples", name)
if (Files.exists(parent)) return parent
error("example not found: $name")
}
}

View File

@ -422,6 +422,13 @@ fun List<T>.sort(): void {
sortWith { a, b -> a <=> b } sortWith { a, b -> a <=> b }
} }
/* Build a new list of `size` elements by calling `block(index)` for each index. */
static fun List<T>.fill(size: Int, block: (Int)->T): List<T> {
val result = List<T>()
for( i in 0..<size ) result += block(i)
result
}
/* Print this exception and its stack trace to standard output. */ /* Print this exception and its stack trace to standard output. */
fun Exception.printStackTrace(): void { fun Exception.printStackTrace(): void {
println(this) println(this)

View File

@ -0,0 +1,172 @@
# Pi Spigot Benchmark Baseline
Date: 2026-04-03
Command:
`./gradlew :lynglib:jvmTest -Pbenchmarks=true --tests 'PiSpigotBenchmarkTest' --rerun-tasks`
Results for `n=200`:
- legacy-real-division: 1108 ms (3 iters, avg 369.33 ms)
- optimized-int-division-rval-off: 756 ms (3 iters, avg 252.00 ms)
- optimized-int-division-rval-on: 674 ms (3 iters, avg 224.67 ms)
Derived speedups:
- intDivSpeedup: 1.47x
- rvalSpeedup: 1.12x
- total: 1.64x
Notes:
- Bytecode still shows generic range iteration (`MAKE_RANGE`, `CALL_MEMBER_SLOT`, `ITER_PUSH`) for loop constructs in the legacy benchmark case.
- This baseline is captured before enabling counted-loop lowering for dynamic inline int ranges.
Optimization #1 follow-up:
- Attempt: broaden compiler loop lowering for dynamic int ranges and validate with `PiSpigotBenchmarkTest` bytecode dumps.
- Final result: success after switching loop-bound coercion to a runtime-checked int path for stable slots with missing metadata.
- Latest measured run after the working compiler change:
- legacy-real-division: 783 ms (3 iters, avg 261.00 ms)
- optimized-int-division-rval-off: 729 ms (3 iters, avg 243.00 ms)
- optimized-int-division-rval-on: 593 ms (3 iters, avg 197.67 ms)
- Hot-op counts for optimized bytecode now show the generic range iterator path is gone from the main loops:
- `makeRange=0`
- `callMemberSlot=2`
- `iterPush=0`
- `getIndex=4`
- `setIndex=4`
- The remaining member calls are non-loop overhead; the main improvement came from lowering `for` ranges to counted int loops.
Optimization #2 follow-up:
- Attempt: coerce stable integer operands into `INT` arithmetic during binary-op lowering so hot expressions stop falling back to `OBJ` math.
- Latest measured run after the arithmetic change:
- legacy-real-division: 593 ms (3 iters, avg 197.67 ms)
- optimized-int-division-rval-off: 542 ms (3 iters, avg 180.67 ms)
- optimized-int-division-rval-on: 516 ms (3 iters, avg 172.00 ms)
- Compiled-code impact in the optimized case:
- `boxes = n * 10 / 3` is now `UNBOX_INT_OBJ` + `MUL_INT` + `DIV_INT`
- `j = boxes - k` is now `SUB_INT`
- `denom = j * 2 + 1` is now `MUL_INT` + `ADD_INT`
- `carriedOver = quotient * j` is now `MUL_INT`
- Remaining hot object arithmetic is centered on list-backed reminder values and derived sums:
- `reminders[j] * 10`
- `reminders[j] + carriedOver`
- `sum / denom`, `sum % denom`, `sum / 10`
- Conclusion: loop lowering is fixed; the next likely win is preserving `List<Int>` element typing for `reminders` so indexed loads stay in int space.
Optimization #3 follow-up:
- Attempt: teach numeric-kind inference that `IndexRef` can be `INT`/`REAL` when the receiver list has a known element class.
- Compiler change:
- `inferNumericKind()` now handles `IndexRef` and resolves the receiver slot or receiver-declared list element class before choosing `INT`/`REAL`.
- Latest measured run after the indexed-load inference change:
- legacy-real-division: 656 ms (3 iters, avg 218.67 ms)
- optimized-int-division-rval-off: 509 ms (3 iters, avg 169.67 ms)
- optimized-int-division-rval-on: 403 ms (3 iters, avg 134.33 ms)
- Derived speedups vs legacy in this run:
- intDivSpeedup: 1.29x
- rvalSpeedup: 1.26x
- total: 1.63x
- Compiled-code impact in the optimized case:
- `carriedOver = quotient * j` stays in `INT` space (`ASSERT_IS` + `UNBOX_INT_OBJ` + `MUL_INT`) instead of plain object multiply.
- Counted int loops remain intact (`MAKE_RANGE=0`, `ITER_PUSH=0`).
- Remaining bottlenecks in the optimized bytecode:
- `GET_INDEX reminders[j]` still feeds `MUL_OBJ` / `ADD_OBJ`
- `sum / denom`, `sum % denom`, and `sum / 10` still compile to object arithmetic
- `suffix += pi[i]` remains `ADD_OBJ`, which is expected because it is string/object concatenation
- Conclusion:
- The new inference produced a real VM-speed gain, especially with `RVAL_FASTPATH` enabled.
- The next compiler win is stronger propagation from `List<Int>` indexed loads into the produced temporary slot so `sum` can stay typed as `INT` across the inner loop.
Optimization #4 follow-up:
- Attempt: preserve boxed-argument metadata through `compileCallArgs()` so `list.add(x)` retains `ObjInt` / `ObjReal` element typing.
- Compiler/runtime fixes:
- `compileCallArgs()` now routes arguments through `ensureObjSlot()` + `emitMove()` instead of raw `BOX_OBJ`, preserving `slotObjClass` and `stableObjSlots`.
- `CmdSetIndex` now reads `valueSlot` via `slotToObj()` so `SET_INDEX` can safely accept primitive slots.
- Fast local unbox ops (`CmdUnboxIntObjLocal`, `CmdUnboxRealObjLocal`) now handle already-primitive source slots directly instead of assuming a raw object payload.
- Plain assignment now coerces object-int RHS back into `INT` when the destination slot is currently compiled as `INT`, keeping loop-carried locals type-consistent.
- Latest measured run after the propagation + VM fixes:
- legacy-real-division: 438 ms (3 iters, avg 146.00 ms)
- optimized-int-division-rval-off: 238 ms (3 iters, avg 79.33 ms)
- optimized-int-division-rval-on: 201 ms (3 iters, avg 67.00 ms)
- Derived speedups vs legacy in this run:
- intDivSpeedup: 1.84x
- rvalSpeedup: 1.18x
- total: 2.18x
- Compiled-code impact in the optimized case:
- `sum = reminders[j] + carriedOver` is now `GET_INDEX` + `UNBOX_INT_OBJ` + `ADD_INT`
- `reminders[j] = sum % denom` is now `MOD_INT` + `SET_INDEX`
- `q = sum / 10` is now `DIV_INT`
- `carriedOver = quotient * j` is now `MUL_INT`
- Remaining hot object arithmetic in the optimized case:
- `reminders[j] *= 10` still compiles as `GET_INDEX` + `MUL_OBJ` + `SET_INDEX`
- `suffix += pi[i]` remains `ADD_OBJ`, which is expected string/object concatenation
- Conclusion:
- The main remaining arithmetic bottleneck is the compound index assignment path for `reminders[j] *= 10`.
- The next direct win is to specialize `AssignOpRef` on typed list elements so indexed compound assignment can lower to `UNBOX_INT_OBJ` + `MUL_INT` + boxed `SET_INDEX`.
Optimization #5 follow-up:
- Attempt: specialize typed `IndexRef` compound assignment so `List<Int>` element updates avoid object arithmetic.
- Compiler change:
- `compileAssignOp()` now detects non-optional typed `List<Int>` index targets and lowers arithmetic assign-ops through `UNBOX_INT_OBJ` + `*_INT` + `SET_INDEX`.
- Latest measured run after the indexed compound-assignment change:
- legacy-real-division: 394 ms (3 iters, avg 131.33 ms)
- optimized-int-division-rval-off: 216 ms (3 iters, avg 72.00 ms)
- optimized-int-division-rval-on: 184 ms (3 iters, avg 61.33 ms)
- Derived speedups vs legacy in this run:
- intDivSpeedup: 1.82x
- rvalSpeedup: 1.17x
- total: 2.14x
- Compiled-code impact in the optimized case:
- `reminders[j] *= 10` is now:
- `GET_INDEX`
- `UNBOX_INT_OBJ`
- `MUL_INT`
- `SET_INDEX`
- The optimized inner loop no longer contains object arithmetic for the `reminders` state update path.
- Remaining hot object work in the optimized case:
- `suffix += pi[i]` remains `ADD_OBJ` and is expected string/object concatenation
- The legacy benchmark case still carries real/object work because it intentionally keeps the original `floor(sum / (denom * 1.0))` path
- Conclusion:
- The inner arithmetic hot loop is now effectively int-lowered end-to-end in the optimized benchmark path.
- Further wins will likely require reducing list access overhead itself (`GET_INDEX` / `SET_INDEX`) or changing the source algorithm/data layout, not more basic arithmetic lowering.
Optimization #6 follow-up:
- Attempt: move the direct `ObjList` index fast path out from behind `RVAL_FASTPATH` so the common plain-list case is fast by default.
- Runtime change:
- `CmdGetIndex` and `CmdSetIndex` now always use direct `target.list[index]` / `target.list[index] = value` for exact `ObjList` receivers with `ObjInt` indices.
- Subclasses such as `ObjObservableList` still use their overridden `getAt` / `putAt` logic, so semantics stay intact.
- Latest measured run after the default plain-list path:
- legacy-real-division: 397 ms (3 iters, avg 132.33 ms)
- optimized-int-division-rval-off: 138 ms (3 iters, avg 46.00 ms)
- optimized-int-division-rval-on: 164 ms (3 iters, avg 54.67 ms)
- Derived speedups vs legacy in this run:
- intDivSpeedup: 2.88x
- rvalSpeedup: 0.84x
- total: 2.42x
- Interpretation:
- The stable fast baseline is now the `rval-off` case, because the direct plain-`ObjList` path no longer depends on `RVAL_FASTPATH`.
- `RVAL_FASTPATH` no longer improves this benchmark and only reflects remaining unrelated runtime variance.
- Conclusion:
- For `piSpigot`, the main VM list-access bottleneck is addressed in the default runtime path.
- Further work on this benchmark should target algorithm/data-layout changes or string-result construction, not the old `RVAL_FASTPATH` gate.
Remaining optimization candidates:
- `suffix += pi[i]` still compiles as repeated `ADD_OBJ` string/object concatenation.
- Best next option: build the suffix through a dedicated buffer/list-join path instead of per-iteration concatenation.
- The benchmark still performs many `GET_INDEX` / `SET_INDEX` operations even after the direct plain-`ObjList` fast path.
- Best next option: reduce indexed access count at the source level or introduce a more specialized typed-list storage layout if this benchmark matters enough.
- The legacy benchmark variant intentionally keeps the real-number `floor(sum / (denom * 1.0))` path.
- No release optimization needed there; it remains only as a regression/control case.
- `RVAL_FASTPATH` is no longer a useful tuning knob for this workload after the plain-list VM fast path.
- Best next option: profile other workloads before changing or removing it globally.
Release stabilization note:
- The broad assignment-side `INT` coercion and subclass-bypassing list fast path were rolled back/narrowed to restore correctness across numeric-mix, decimal, list, observable-list, and wasm tests.
- Full release gates now pass:
- `./gradlew test`
- `./gradlew :lynglib:wasmJsNodeTest`
- Current release-safe benchmark on the stabilized tree:
- legacy-real-division: 732 ms (3 iters, avg 244.00 ms)
- optimized-int-division-rval-off: 545 ms (3 iters, avg 181.67 ms)
- optimized-int-division-rval-on: 697 ms (3 iters, avg 232.33 ms)
- Interpretation:
- The release baseline is now `optimized-int-division-rval-off` at 545 ms for the current correct/stable tree.
- The removed coercion had been masking a real compiler typing gap; reintroducing it broadly is not release-safe.
- Highest-value remaining compiler optimization after release:
- Recover typed int lowering for `j = boxes - k`, `denom = j * 2 + 1`, `sum = reminders[j] + carriedOver`, and `carriedOver = quotient * j` using a narrower proof than the removed generic arithmetic coercion.