Compare commits
5 Commits
d8c53c500e
...
161f3f74e2
| Author | SHA1 | Date | |
|---|---|---|---|
| 161f3f74e2 | |||
| 1cead7822a | |||
| 064b927b1a | |||
| a7ab0d3905 | |||
| f1003f5b95 |
21
CHANGELOG.md
21
CHANGELOG.md
@ -7,6 +7,27 @@ History note:
|
||||
- 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.
|
||||
|
||||
## 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)
|
||||
|
||||
### Language
|
||||
|
||||
11
docs/List.md
11
docs/List.md
@ -45,6 +45,16 @@ You can concatenate lists or iterable objects:
|
||||
assert( [4,5] + (1..3) == [4, 5, 1, 2, 3])
|
||||
>>> 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
|
||||
|
||||
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 |
|
||||
| `[Range]` | get slice of the array (copy) | Range |
|
||||
| `+=` | 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 |
|
||||
| `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | void |
|
||||
| `sortWith(comparator)` | in-place sort using `comarator` function (4) | void |
|
||||
|
||||
@ -1020,6 +1020,14 @@ For example, we want to create an extension method that would test if a value ca
|
||||
assert( ! "5.2".isInteger() )
|
||||
>>> 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
|
||||
|
||||
Just like methods, you can extend existing classes with properties. These can be defined using simple initialization (for `val` only) or with custom accessors.
|
||||
|
||||
@ -25,6 +25,23 @@ Exclusive end ranges are adopted from kotlin either:
|
||||
assert(4 in r)
|
||||
>>> 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
|
||||
access limits:
|
||||
|
||||
@ -73,6 +90,23 @@ but
|
||||
>>> 2
|
||||
>>> 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
|
||||
|
||||
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] == (1..<5 step 2).toList() )
|
||||
assert( [5,3,1] == (5 downTo 1 step 2).toList() )
|
||||
assert( ['a','c','e'] == ('a'..'e' step 2).toList() )
|
||||
>>> 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:
|
||||
|
||||
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 |
|
||||
| isEndInclusive | true for '..' | Bool |
|
||||
| isDescending | true for `downTo`/`downUntil`| Bool |
|
||||
| isOpen | at any end | Bool |
|
||||
| isIntRange | both start and end are Int | Bool |
|
||||
| step | explicit iteration step | Any? |
|
||||
|
||||
@ -50,8 +50,10 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
- Range literals:
|
||||
- inclusive: `a..b`
|
||||
- exclusive end: `a..<b`
|
||||
- descending inclusive: `a downTo b`
|
||||
- descending exclusive end: `a downUntil 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:
|
||||
- with params: `{ x, y -> x + y }`
|
||||
- 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`.
|
||||
- generics: `fun f<T>(x: T): T`.
|
||||
- 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`.
|
||||
- Type aliases:
|
||||
- `type Name = TypeExpr`
|
||||
|
||||
@ -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`.
|
||||
- Search/predicates: `findFirst`, `findFirstOrNull`, `any`, `all`, `count`, `first`, `last`.
|
||||
- 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`.
|
||||
|
||||
### 4.3 Delegation helpers
|
||||
|
||||
@ -811,6 +811,12 @@ Lyng has built-in mutable array class `List` with simple literals:
|
||||
many collection based methods are implemented there.
|
||||
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:
|
||||
|
||||
val list = [1, [2, 3], 4]
|
||||
@ -1359,6 +1365,41 @@ size and index access, like lists:
|
||||
"total letters: "+letters
|
||||
>>> "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:
|
||||
|
||||
fun search(haystack, needle) {
|
||||
@ -1488,6 +1529,14 @@ It could be open and closed:
|
||||
assert( 5 !in (1..<5) )
|
||||
>>> 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:
|
||||
|
||||
assert( (2..3) in (1..10) )
|
||||
@ -1505,6 +1554,14 @@ and you can use ranges in for-loops:
|
||||
>>> b
|
||||
>>> 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.
|
||||
|
||||
# Time routines
|
||||
|
||||
67
examples/pi-bench.lyng
Normal file
67
examples/pi-bench.lyng
Normal 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
49
examples/pi-test.lyng
Normal 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
|
||||
}
|
||||
@ -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.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.LyngVersion
|
||||
import net.sergeych.lyng.Script
|
||||
@ -245,12 +247,17 @@ fun executeFileWithArgs(fileName: String, args: List<String>) {
|
||||
suspend fun executeSource(source: Source) {
|
||||
val session = EvalSession(baseScopeDefer.await())
|
||||
try {
|
||||
session.eval(source)
|
||||
evalOnCliDispatcher(session, source)
|
||||
} finally {
|
||||
session.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun evalOnCliDispatcher(session: EvalSession, source: Source): Obj =
|
||||
withContext(Dispatchers.Default) {
|
||||
session.eval(source)
|
||||
}
|
||||
|
||||
suspend fun executeFile(fileName: String) {
|
||||
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
|
||||
fileSource.buffer().use { bs ->
|
||||
|
||||
72
lyng/src/jvmTest/kotlin/net/sergeych/CliDispatcherJvmTest.kt
Normal file
72
lyng/src/jvmTest/kotlin/net/sergeych/CliDispatcherJvmTest.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -36,7 +36,7 @@ actual object PerfDefaults {
|
||||
actual val PIC_DEBUG_COUNTERS: Boolean = false
|
||||
|
||||
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)
|
||||
actual val REGEX_CACHE: Boolean = true
|
||||
actual val ARG_SMALL_ARITY_12: Boolean = false
|
||||
|
||||
@ -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
|
||||
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
|
||||
// if it is an open end range, then the end of line could be here that we do not want
|
||||
// to skip in parseExpression:
|
||||
@ -3078,12 +3079,19 @@ class Compiler(
|
||||
val lConst = constIntValueOrNull(left)
|
||||
val rConst = constIntValueOrNull(rightRef)
|
||||
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 {
|
||||
operand = RangeRef(left, rightRef, isEndInclusive)
|
||||
operand = RangeRef(left, rightRef, isEndInclusive, isDescending = isDescending)
|
||||
}
|
||||
} 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 rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
|
||||
RangeRef(leftRef, rightRef, range.isEndInclusive)
|
||||
RangeRef(leftRef, rightRef, range.isEndInclusive, isDescending = range.isDescending)
|
||||
}
|
||||
else -> {
|
||||
cc.previous()
|
||||
@ -3108,7 +3116,13 @@ class Compiler(
|
||||
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 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 -> {
|
||||
@ -4345,6 +4359,7 @@ class Compiler(
|
||||
is ListLiteralRef -> inferListLiteralTypeDecl(ref)
|
||||
is MapLiteralRef -> inferMapLiteralTypeDecl(ref)
|
||||
is ConstRef -> inferTypeDeclFromConst(ref.constValue)
|
||||
is RangeRef -> TypeDecl.Simple("Range", false)
|
||||
is CallRef -> {
|
||||
val targetDecl = resolveReceiverTypeDecl(ref.target) ?: seedTypeDeclFromRef(ref.target)
|
||||
val targetName = when (val target = ref.target) {
|
||||
@ -4380,6 +4395,7 @@ class Compiler(
|
||||
is ObjString -> TypeDecl.Simple("String", false)
|
||||
is ObjBool -> TypeDecl.Simple("Bool", false)
|
||||
is ObjChar -> TypeDecl.Simple("Char", false)
|
||||
is ObjRange -> TypeDecl.Simple("Range", false)
|
||||
is ObjNull -> TypeDecl.TypeNullableAny
|
||||
is ObjList -> TypeDecl.Generic("List", listOf(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
|
||||
val start = range.start?.toLong() ?: return null
|
||||
val end = range.end?.toLong() ?: return null
|
||||
val endExclusive = if (range.isEndInclusive) end + 1 else end
|
||||
return ConstIntRange(start, endExclusive)
|
||||
val stopBoundary = if (range.isDescending) {
|
||||
if (range.isEndInclusive) end - 1 else end
|
||||
} else {
|
||||
if (range.isEndInclusive) end + 1 else end
|
||||
}
|
||||
return ConstIntRange(start, stopBoundary, range.isDescending)
|
||||
}
|
||||
is RangeRef -> {
|
||||
if (ref.step != null) return null
|
||||
val start = constIntValueOrNull(ref.left) ?: return null
|
||||
val end = constIntValueOrNull(ref.right) ?: return null
|
||||
val endExclusive = if (ref.isEndInclusive) end + 1 else end
|
||||
return ConstIntRange(start, endExclusive)
|
||||
val stopBoundary = if (ref.isDescending) {
|
||||
if (ref.isEndInclusive) end - 1 else end
|
||||
} else {
|
||||
if (ref.isEndInclusive) end + 1 else end
|
||||
}
|
||||
return ConstIntRange(start, stopBoundary, ref.isDescending)
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
@ -8692,7 +8716,7 @@ class Compiler(
|
||||
startPos = start
|
||||
)
|
||||
val declaredFn = FunctionDeclStatement(spec)
|
||||
if (isStatic) {
|
||||
if (isStatic && parentIsClassBody) {
|
||||
currentInitScope += declaredFn
|
||||
NopStatement
|
||||
} else
|
||||
|
||||
@ -169,18 +169,29 @@ internal suspend fun executeFunctionDecl(
|
||||
spec.extTypeName?.let { typeName ->
|
||||
val type = scope[typeName]?.value ?: scope.raiseSymbolNotFound("class $typeName not found")
|
||||
if (type !is ObjClass) scope.raiseClassCastError("$typeName is not the class instance")
|
||||
scope.addExtension(
|
||||
type,
|
||||
spec.name,
|
||||
ObjRecord(
|
||||
if (spec.isStatic) {
|
||||
type.createClassField(
|
||||
spec.name,
|
||||
compiledFnBody,
|
||||
isMutable = false,
|
||||
visibility = spec.visibility,
|
||||
declaringClass = null,
|
||||
pos = spec.startPos,
|
||||
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 wrapper = ObjExtensionMethodCallable(spec.name, compiledFnBody)
|
||||
scope.addItem(
|
||||
|
||||
@ -448,6 +448,8 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
|
||||
"is" -> Token("is", from, Token.Type.IS)
|
||||
"by" -> Token("by", from, Token.Type.BY)
|
||||
"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)
|
||||
"as" -> {
|
||||
// support both `as` and tight `as?` without spaces
|
||||
|
||||
@ -180,7 +180,7 @@ object PerfProfiles {
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
|
||||
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||
PerfFlags.RVAL_FASTPATH = true
|
||||
PerfFlags.RVAL_FASTPATH = false
|
||||
|
||||
// Keep regex cache/platform setting; enable on JVM typically
|
||||
PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE
|
||||
|
||||
@ -36,7 +36,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
||||
PLUS, MINUS, STAR, SLASH, PERCENT,
|
||||
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, IFNULLASSIGN,
|
||||
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,
|
||||
SHUTTLE,
|
||||
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
||||
|
||||
@ -1783,7 +1783,11 @@ class BytecodeCompiler(
|
||||
return when (slotTypes[slot]) {
|
||||
SlotType.INT -> NumericKind.INT
|
||||
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 LocalVarRef -> 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 BinaryOpRef -> {
|
||||
val op = ref.op
|
||||
@ -2431,7 +2449,7 @@ class BytecodeCompiler(
|
||||
updateSlotType(slot, SlotType.OBJ)
|
||||
return value
|
||||
}
|
||||
val value = compileRef(assignValue(ref)) ?: return null
|
||||
var value = compileRef(assignValue(ref)) ?: return null
|
||||
if (isLoopVarRef(localTarget)) {
|
||||
emitLoopVarReassignError(localTarget.name, localTarget.pos())
|
||||
return value
|
||||
@ -2473,7 +2491,7 @@ class BytecodeCompiler(
|
||||
else -> 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 slot = resolved.first
|
||||
val isMutable = resolved.second
|
||||
@ -2698,6 +2716,7 @@ class BytecodeCompiler(
|
||||
if (!target.optionalRef) {
|
||||
val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null
|
||||
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot)
|
||||
noteListElementClassMutation(receiver.slot, value)
|
||||
} else {
|
||||
val nullSlot = allocSlot()
|
||||
builder.emit(Opcode.CONST_NULL, nullSlot)
|
||||
@ -2710,6 +2729,7 @@ class BytecodeCompiler(
|
||||
)
|
||||
val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null
|
||||
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot)
|
||||
noteListElementClassMutation(receiver.slot, value)
|
||||
builder.mark(endLabel)
|
||||
}
|
||||
return value
|
||||
@ -3026,9 +3046,32 @@ class BytecodeCompiler(
|
||||
val receiver = compileRefWithFallback(indexTarget.targetRef, null, Pos.builtIn) ?: return null
|
||||
val current = 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) {
|
||||
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(objOp, current, rhs.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)
|
||||
if (elementClass != null) {
|
||||
slotObjClass[dst] = elementClass
|
||||
if (elementClass == ObjString.type && elementClass.isClosed) {
|
||||
if (elementClass.isClosed) {
|
||||
stableObjSlots.add(dst)
|
||||
} else {
|
||||
stableObjSlots.remove(dst)
|
||||
@ -3617,6 +3660,9 @@ class BytecodeCompiler(
|
||||
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 = if (ref.step != null) {
|
||||
val step = compileRefWithFallback(ref.step, null, Pos.builtIn) ?: return null
|
||||
ensureObjSlot(step).slot
|
||||
@ -3627,7 +3673,7 @@ class BytecodeCompiler(
|
||||
slot
|
||||
}
|
||||
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)
|
||||
slotObjClass[dst] = ObjRange.type
|
||||
return CompiledValue(dst, SlotType.OBJ)
|
||||
@ -4646,6 +4692,9 @@ class BytecodeCompiler(
|
||||
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||
setPos(callPos)
|
||||
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)
|
||||
}
|
||||
val nullSlot = allocSlot()
|
||||
@ -4812,7 +4861,7 @@ class BytecodeCompiler(
|
||||
" receiver=$kind(${ref.name}) slot=$slot slotClass=$slotCls nameClass=$nameCls"
|
||||
}
|
||||
is LocalSlotRef -> {
|
||||
val slot = resolveSlot(ref)
|
||||
val slot = resolveLocalSlotByRefOrName(ref)
|
||||
val slotCls = slot?.let { slotObjClass[it]?.className }
|
||||
val nameCls = nameObjClass[ref.name]?.className
|
||||
val scopeId = refScopeId(ref)
|
||||
@ -4968,9 +5017,10 @@ class BytecodeCompiler(
|
||||
val specs = if (needPlan) ArrayList<BytecodeConst.CallArgSpec>(args.size) else null
|
||||
for ((index, arg) in args.withIndex()) {
|
||||
val compiled = compileArgValue(arg.value) ?: return null
|
||||
val objValue = ensureObjSlot(compiled)
|
||||
val dst = argSlots[index]
|
||||
if (compiled.slot != dst || compiled.type != SlotType.OBJ) {
|
||||
builder.emit(Opcode.BOX_OBJ, compiled.slot, dst)
|
||||
if (objValue.slot != dst) {
|
||||
emitMove(objValue, dst)
|
||||
}
|
||||
updateSlotType(dst, SlotType.OBJ)
|
||||
specs?.add(BytecodeConst.CallArgSpec(arg.name, arg.isSplat))
|
||||
@ -5830,7 +5880,8 @@ class BytecodeCompiler(
|
||||
emitMove(value, localSlot)
|
||||
}
|
||||
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)
|
||||
updateListElementClassFromInitializer(localSlot, stmt.initializer)
|
||||
updateNameObjClassFromSlot(stmt.name, localSlot)
|
||||
@ -5862,7 +5913,8 @@ class BytecodeCompiler(
|
||||
}
|
||||
updateSlotType(scopeSlot, value.type)
|
||||
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)
|
||||
updateListElementClassFromInitializer(scopeSlot, stmt.initializer)
|
||||
val declId = builder.addConst(
|
||||
@ -5898,7 +5950,9 @@ class BytecodeCompiler(
|
||||
updateSlotTypeByName(stmt.name, value.type)
|
||||
}
|
||||
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)
|
||||
updateListElementClassFromInitializer(value.slot, stmt.initializer)
|
||||
return value
|
||||
@ -5988,6 +6042,16 @@ class BytecodeCompiler(
|
||||
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) {
|
||||
val cls = slotObjClass[slot] ?: return
|
||||
nameObjClass[name] = cls
|
||||
@ -6083,9 +6147,6 @@ class BytecodeCompiler(
|
||||
if (range == null && rangeRef == null) {
|
||||
rangeRef = extractRangeFromLocal(stmt.source)
|
||||
}
|
||||
if (rangeRef != null && !isConstIntRange(rangeRef)) {
|
||||
rangeRef = null
|
||||
}
|
||||
val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null
|
||||
val loopSlotPlan = stmt.loopSlotPlan
|
||||
val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName]
|
||||
@ -6126,141 +6187,60 @@ class BytecodeCompiler(
|
||||
val breakFlagSlot = allocSlot()
|
||||
if (range == null && rangeRef == null && typedRangeLocal == null) {
|
||||
val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null
|
||||
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"]
|
||||
if (iteratorMethodId == null) {
|
||||
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))
|
||||
return emitIterableForIn(
|
||||
stmt = stmt,
|
||||
sourceValue = sourceValue,
|
||||
wantResult = wantResult,
|
||||
loopSlotId = loopSlotId,
|
||||
breakFlagSlot = breakFlagSlot,
|
||||
needsBreakFlag = needsBreakFlag,
|
||||
hasRealWiden = hasRealWiden,
|
||||
realWidenSlots = realWidenSlots,
|
||||
)
|
||||
|
||||
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 endSlot = allocSlot()
|
||||
val descendingSlot = allocSlot()
|
||||
if (range != null) {
|
||||
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, endId, endSlot)
|
||||
builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
|
||||
updateSlotType(iSlot, SlotType.INT)
|
||||
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
|
||||
} else {
|
||||
if (rangeRef != null) {
|
||||
val left = rangeRef.left ?: return null
|
||||
val right = rangeRef.right ?: return null
|
||||
val startValue = compileRef(left) ?: return null
|
||||
val endValue = compileRef(right) ?: return null
|
||||
if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null
|
||||
val startCompiled = compileRef(left) ?: return null
|
||||
val endCompiled = compileRef(right) ?: 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(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)
|
||||
}
|
||||
updateSlotType(iSlot, SlotType.INT)
|
||||
@ -6270,7 +6250,7 @@ class BytecodeCompiler(
|
||||
val rangeValue = compileRef(rangeLocal) ?: return null
|
||||
val rangeObj = ensureObjSlot(rangeValue)
|
||||
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()
|
||||
builder.emit(
|
||||
Opcode.JMP_IF_FALSE,
|
||||
@ -6294,14 +6274,7 @@ class BytecodeCompiler(
|
||||
val endLabel = builder.label()
|
||||
val doneLabel = builder.label()
|
||||
builder.mark(loopLabel)
|
||||
builder.emit(
|
||||
Opcode.JMP_IF_GTE_INT,
|
||||
listOf(
|
||||
CmdBuilder.Operand.IntVal(iSlot),
|
||||
CmdBuilder.Operand.IntVal(endSlot),
|
||||
CmdBuilder.Operand.LabelRef(endLabel)
|
||||
)
|
||||
)
|
||||
emitIntForLoopCheck(iSlot, endSlot, descendingSlot, endLabel)
|
||||
updateSlotType(iSlot, SlotType.INT)
|
||||
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
|
||||
loopStack.addLast(
|
||||
@ -6324,7 +6297,7 @@ class BytecodeCompiler(
|
||||
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
|
||||
}
|
||||
builder.mark(continueLabel)
|
||||
builder.emit(Opcode.INC_INT, iSlot)
|
||||
emitIntForLoopStep(iSlot, descendingSlot)
|
||||
if (hasRealWiden) {
|
||||
emitLoopRealCoercions(realWidenSlots)
|
||||
}
|
||||
@ -6377,14 +6350,7 @@ class BytecodeCompiler(
|
||||
val continueLabel = builder.label()
|
||||
val endLabel = builder.label()
|
||||
builder.mark(loopLabel)
|
||||
builder.emit(
|
||||
Opcode.JMP_IF_GTE_INT,
|
||||
listOf(
|
||||
CmdBuilder.Operand.IntVal(iSlot),
|
||||
CmdBuilder.Operand.IntVal(endSlot),
|
||||
CmdBuilder.Operand.LabelRef(endLabel)
|
||||
)
|
||||
)
|
||||
emitIntForLoopCheck(iSlot, endSlot, descendingSlot, endLabel)
|
||||
updateSlotType(iSlot, SlotType.INT)
|
||||
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
|
||||
loopStack.addLast(
|
||||
@ -6407,7 +6373,7 @@ class BytecodeCompiler(
|
||||
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
|
||||
}
|
||||
builder.mark(continueLabel)
|
||||
builder.emit(Opcode.INC_INT, iSlot)
|
||||
emitIntForLoopStep(iSlot, descendingSlot)
|
||||
if (hasRealWiden) {
|
||||
emitLoopRealCoercions(realWidenSlots)
|
||||
}
|
||||
@ -7310,7 +7276,8 @@ class BytecodeCompiler(
|
||||
is LocalSlotRef -> {
|
||||
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
|
||||
val ownerSlot = ref.captureOwnerSlot ?: ref.slot
|
||||
slotTypeByScopeId[ownerScopeId]?.get(ownerSlot)
|
||||
resolveLocalSlotByRefOrName(ref)?.let { slotObjClass[it] }
|
||||
?: slotTypeByScopeId[ownerScopeId]?.get(ownerSlot)
|
||||
?: slotInitClassByKey[ScopeSlotKey(ownerScopeId, ownerSlot)]
|
||||
?: nameObjClass[ref.name]
|
||||
?: resolveTypeNameClass(ref.name)
|
||||
@ -7723,6 +7690,11 @@ class BytecodeCompiler(
|
||||
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? {
|
||||
val ownerScopeId = ref.captureOwnerScopeId ?: return null
|
||||
val ownerSlot = ref.captureOwnerSlot ?: return null
|
||||
@ -8700,10 +8672,237 @@ class BytecodeCompiler(
|
||||
return if (ref.step != null) null else ref
|
||||
}
|
||||
|
||||
private fun isConstIntRange(ref: RangeRef): Boolean {
|
||||
val left = ref.left as? ConstRef ?: return false
|
||||
val right = ref.right as? ConstRef ?: return false
|
||||
return left.constValue is ObjInt && right.constValue is ObjInt
|
||||
private fun emitIterableForIn(
|
||||
stmt: net.sergeych.lyng.ForInStatement,
|
||||
sourceValue: CompiledValue,
|
||||
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? {
|
||||
@ -8719,11 +8918,59 @@ class BytecodeCompiler(
|
||||
val end = range.end as? ObjInt ?: return null
|
||||
val left = ConstRef(start.asReadonly)
|
||||
val right = ConstRef(end.asReadonly)
|
||||
return RangeRef(left, right, range.isEndInclusive)
|
||||
return RangeRef(left, right, range.isEndInclusive, isDescending = range.isDescending)
|
||||
}
|
||||
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? {
|
||||
val target = if (source is BytecodeStatement) source.original else source
|
||||
val expr = target as? ExpressionStatement ?: return null
|
||||
|
||||
@ -147,7 +147,7 @@ class CmdBuilder {
|
||||
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
|
||||
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
|
||||
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 ->
|
||||
listOf(OperandKind.CONST, OperandKind.SLOT)
|
||||
Opcode.RESOLVE_SCOPE_SLOT ->
|
||||
@ -228,7 +228,7 @@ class CmdBuilder {
|
||||
Opcode.SET_INDEX ->
|
||||
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
|
||||
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 ->
|
||||
listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
|
||||
Opcode.GET_MEMBER_SLOT ->
|
||||
@ -311,10 +311,10 @@ class CmdBuilder {
|
||||
}
|
||||
Opcode.OBJ_TO_BOOL -> CmdObjToBool(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_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.ASSERT_IS -> CmdAssertIs(operands[0], operands[1])
|
||||
Opcode.MAKE_QUALIFIED_VIEW -> CmdMakeQualifiedView(operands[0], operands[1], operands[2])
|
||||
|
||||
@ -96,11 +96,12 @@ object CmdDisassembler {
|
||||
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 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(
|
||||
cmd.startSlot,
|
||||
cmd.endSlot,
|
||||
cmd.inclusiveSlot,
|
||||
cmd.descendingSlot,
|
||||
cmd.stepSlot,
|
||||
cmd.dst
|
||||
)
|
||||
|
||||
@ -273,6 +273,7 @@ class CmdMakeRange(
|
||||
internal val startSlot: Int,
|
||||
internal val endSlot: Int,
|
||||
internal val inclusiveSlot: Int,
|
||||
internal val descendingSlot: Int,
|
||||
internal val stepSlot: Int,
|
||||
internal val dst: Int,
|
||||
) : Cmd() {
|
||||
@ -280,9 +281,10 @@ class CmdMakeRange(
|
||||
val start = frame.slotToObj(startSlot)
|
||||
val end = frame.slotToObj(endSlot)
|
||||
val inclusive = frame.slotToObj(inclusiveSlot).toBool()
|
||||
val descending = frame.slotToObj(descendingSlot).toBool()
|
||||
val stepObj = frame.slotToObj(stepSlot)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val value = frame.frame.getRawObj(src) as ObjInt
|
||||
frame.setLocalInt(dst, value.value)
|
||||
when (frame.frame.getSlotTypeCode(src)) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val value = frame.frame.getRawObj(src) as ObjReal
|
||||
frame.setLocalReal(dst, value.value)
|
||||
when (frame.frame.getSlotTypeCode(src)) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -430,6 +442,7 @@ class CmdRangeIntBounds(
|
||||
internal val src: Int,
|
||||
internal val startSlot: Int,
|
||||
internal val endSlot: Int,
|
||||
internal val descendingSlot: Int,
|
||||
internal val okSlot: Int,
|
||||
) : Cmd() {
|
||||
override suspend fun perform(frame: CmdFrame) {
|
||||
@ -439,10 +452,19 @@ class CmdRangeIntBounds(
|
||||
frame.setBool(okSlot, false)
|
||||
return
|
||||
}
|
||||
if (range.isDescending) {
|
||||
frame.setBool(okSlot, false)
|
||||
return
|
||||
}
|
||||
val start = (range.start as ObjInt).value
|
||||
val end = (range.end as ObjInt).value
|
||||
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)
|
||||
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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjInt
|
||||
val right = frame.frame.getRawObj(b) as ObjInt
|
||||
frame.setLocalBool(dst, left.value == right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.INT.code -> frame.frame.getInt(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjInt
|
||||
val right = frame.frame.getRawObj(b) as ObjInt
|
||||
frame.setLocalBool(dst, left.value != right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.INT.code -> frame.frame.getInt(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjInt
|
||||
val right = frame.frame.getRawObj(b) as ObjInt
|
||||
frame.setLocalBool(dst, left.value < right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.INT.code -> frame.frame.getInt(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjInt
|
||||
val right = frame.frame.getRawObj(b) as ObjInt
|
||||
frame.setLocalBool(dst, left.value <= right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.INT.code -> frame.frame.getInt(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjInt
|
||||
val right = frame.frame.getRawObj(b) as ObjInt
|
||||
frame.setLocalBool(dst, left.value > right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.INT.code -> frame.frame.getInt(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjInt
|
||||
val right = frame.frame.getRawObj(b) as ObjInt
|
||||
frame.setLocalBool(dst, left.value >= right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.INT.code -> frame.frame.getInt(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjReal
|
||||
val right = frame.frame.getRawObj(b) as ObjReal
|
||||
frame.setLocalBool(dst, left.value == right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.REAL.code -> frame.frame.getReal(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjReal
|
||||
val right = frame.frame.getRawObj(b) as ObjReal
|
||||
frame.setLocalBool(dst, left.value != right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.REAL.code -> frame.frame.getReal(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjReal
|
||||
val right = frame.frame.getRawObj(b) as ObjReal
|
||||
frame.setLocalBool(dst, left.value < right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.REAL.code -> frame.frame.getReal(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjReal
|
||||
val right = frame.frame.getRawObj(b) as ObjReal
|
||||
frame.setLocalBool(dst, left.value <= right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.REAL.code -> frame.frame.getReal(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjReal
|
||||
val right = frame.frame.getRawObj(b) as ObjReal
|
||||
frame.setLocalBool(dst, left.value > right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.REAL.code -> frame.frame.getReal(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
override val isFast: Boolean = true
|
||||
override fun performFast(frame: CmdFrame) {
|
||||
val left = frame.frame.getRawObj(a) as ObjReal
|
||||
val right = frame.frame.getRawObj(b) as ObjReal
|
||||
frame.setLocalBool(dst, left.value >= right.value)
|
||||
val left = when (frame.frame.getSlotTypeCode(a)) {
|
||||
SlotType.REAL.code -> frame.frame.getReal(a)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -3612,7 +3706,13 @@ class CmdGetIndex(
|
||||
internal val dst: Int,
|
||||
) : Cmd() {
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -3624,7 +3724,14 @@ class CmdSetIndex(
|
||||
internal val valueSlot: Int,
|
||||
) : Cmd() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ private val fallbackKeywordIds = setOf(
|
||||
// declarations & modifiers
|
||||
"fun", "fn", "class", "interface", "enum", "val", "var", "type", "import", "package",
|
||||
"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
|
||||
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
|
||||
"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
|
||||
|
||||
// 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
|
||||
|
||||
// labels / annotations
|
||||
|
||||
@ -28,6 +28,7 @@ class ObjRange(
|
||||
val start: Obj?,
|
||||
val end: Obj?,
|
||||
val isEndInclusive: Boolean,
|
||||
val isDescending: Boolean = false,
|
||||
val step: Obj? = null
|
||||
) : Obj() {
|
||||
|
||||
@ -39,15 +40,38 @@ class ObjRange(
|
||||
|
||||
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||
val result = StringBuilder()
|
||||
result.append("${start?.inspect(scope) ?: '∞'} ..")
|
||||
if (!isEndInclusive) result.append('<')
|
||||
result.append(" ${end?.inspect(scope) ?: '∞'}")
|
||||
result.append(start?.inspect(scope) ?: "∞")
|
||||
when {
|
||||
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) {
|
||||
result.append(" step ${step?.inspect(scope)}")
|
||||
}
|
||||
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
|
||||
* Otherwise, return correct value for the exclusive end
|
||||
@ -74,29 +98,21 @@ class ObjRange(
|
||||
}
|
||||
|
||||
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {
|
||||
if (!isOpenStart) {
|
||||
// our start is not -∞ so other start should be GTE or is not contained:
|
||||
if (!other.isOpenStart && start!!.compareTo(scope, other.start!!) > 0) return false
|
||||
val ourLower = normalizedLowerBound()
|
||||
val otherLower = other.normalizedLowerBound()
|
||||
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) {
|
||||
// same with the end: if it is open, it can't be contained in ours:
|
||||
if (other.isOpenEnd) return false
|
||||
// both exists, now there could be 4 cases:
|
||||
return when {
|
||||
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
|
||||
|
||||
!other.isEndInclusive && isEndInclusive ->
|
||||
end!!.compareTo(scope, other.end!!) >= 0
|
||||
|
||||
else -> throw IllegalStateException("unknown comparison")
|
||||
}
|
||||
val ourUpper = normalizedUpperBound()
|
||||
val otherUpper = other.normalizedUpperBound()
|
||||
if (ourUpper != null) {
|
||||
if (otherUpper == null) return false
|
||||
val cmp = ourUpper.value.compareTo(scope, otherUpper.value)
|
||||
if (cmp == -2 || cmp < 0) return false
|
||||
if (cmp == 0 && otherUpper.inclusive && !ourUpper.inclusive) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -108,35 +124,38 @@ class ObjRange(
|
||||
|
||||
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
|
||||
if (start is ObjInt && end is ObjInt && other is ObjInt) {
|
||||
val s = start.value
|
||||
val e = end.value
|
||||
val lower = if (isDescending) end.value else start.value
|
||||
val upper = if (isDescending) start.value else end.value
|
||||
val v = other.value
|
||||
if (v < s) return false
|
||||
return if (isEndInclusive) v <= e else v < e
|
||||
if (v < lower || v > upper) return false
|
||||
return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
|
||||
}
|
||||
if (start is ObjChar && end is ObjChar && other is ObjChar) {
|
||||
val s = start.value
|
||||
val e = end.value
|
||||
val lower = if (isDescending) end.value else start.value
|
||||
val upper = if (isDescending) start.value else end.value
|
||||
val v = other.value
|
||||
if (v < s) return false
|
||||
return if (isEndInclusive) v <= e else v < e
|
||||
if (v < lower || v > upper) return false
|
||||
return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
|
||||
}
|
||||
if (start is ObjString && end is ObjString && other is ObjString) {
|
||||
val s = start.value
|
||||
val e = end.value
|
||||
val lower = if (isDescending) end.value else start.value
|
||||
val upper = if (isDescending) start.value else end.value
|
||||
val v = other.value
|
||||
if (v < s) return false
|
||||
return if (isEndInclusive) v <= e else v < e
|
||||
if (v < lower || v > upper) return false
|
||||
return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpenStart && isOpenEnd) return true
|
||||
if (!isOpenStart) {
|
||||
if (start!!.compareTo(scope, other) > 0) return false
|
||||
val lower = normalizedLowerBound()
|
||||
val upper = normalizedUpperBound()
|
||||
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) {
|
||||
val cmp = end!!.compareTo(scope, other)
|
||||
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
|
||||
if (upper != null) {
|
||||
val cmp = upper.value.compareTo(scope, other)
|
||||
if (cmp == -2 || cmp < 0 || (!upper.inclusive && cmp == 0)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -153,7 +172,12 @@ class ObjRange(
|
||||
if (!hasExplicitStep && start is ObjInt && end is ObjInt) {
|
||||
val s = start.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) {
|
||||
if (!callback(ObjInt.of(i))) break
|
||||
}
|
||||
@ -165,7 +189,14 @@ class ObjRange(
|
||||
} else if (!hasExplicitStep && start is ObjChar && end is ObjChar) {
|
||||
val s = start.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) {
|
||||
if (!callback(ObjChar(c))) break
|
||||
}
|
||||
@ -184,6 +215,7 @@ class ObjRange(
|
||||
if (start == other.start &&
|
||||
end == other.end &&
|
||||
isEndInclusive == other.isEndInclusive &&
|
||||
isDescending == other.isDescending &&
|
||||
step == other.step
|
||||
) 0 else -1
|
||||
}
|
||||
@ -194,6 +226,7 @@ class ObjRange(
|
||||
var result = start?.hashCode() ?: 0
|
||||
result = 31 * result + (end?.hashCode() ?: 0)
|
||||
result = 31 * result + isEndInclusive.hashCode()
|
||||
result = 31 * result + isDescending.hashCode()
|
||||
result = 31 * result + (step?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
@ -207,6 +240,7 @@ class ObjRange(
|
||||
if (start != other.start) return false
|
||||
if (end != other.end) return false
|
||||
if (isEndInclusive != other.isEndInclusive) return false
|
||||
if (isDescending != other.isDescending) return false
|
||||
if (step != other.step) return false
|
||||
|
||||
return true
|
||||
@ -264,6 +298,13 @@ class ObjRange(
|
||||
moduleName = "lyng.stdlib",
|
||||
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(
|
||||
name = "iterator",
|
||||
doc = "Iterator over elements in this range (optimized for Int ranges).",
|
||||
@ -290,18 +331,22 @@ class ObjRange(
|
||||
if (startObj is Numeric && explicitStep !is 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
|
||||
}
|
||||
if (startObj is ObjInt) {
|
||||
val cmp = if (end == null || end.isNull) 0 else startObj.compareTo(scope, end)
|
||||
val dir = if (cmp >= 0) -1 else 1
|
||||
return ObjInt.of(dir.toLong())
|
||||
return ObjInt.of(if (isDescending) -1 else 1)
|
||||
}
|
||||
if (startObj is ObjChar) {
|
||||
val endChar = end as? ObjChar
|
||||
?: 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())
|
||||
return ObjInt.of(if (isDescending) -1 else 1)
|
||||
}
|
||||
if (startObj is ObjReal) {
|
||||
scope.raiseIllegalState("Real range requires explicit step")
|
||||
|
||||
@ -1028,6 +1028,7 @@ class RangeRef(
|
||||
internal val left: ObjRef?,
|
||||
internal val right: ObjRef?,
|
||||
internal val isEndInclusive: Boolean,
|
||||
internal val isDescending: Boolean = false,
|
||||
internal val step: ObjRef? = null
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord = scope.raiseObjRefEvalDisabled()
|
||||
|
||||
@ -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(
|
||||
val loopVarName: String,
|
||||
|
||||
@ -294,6 +294,18 @@ class ScriptTest {
|
||||
assertEquals(Token.Type.INT, tt[0].type)
|
||||
assertEquals(Token.Type.DOTDOTLT, tt[1].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
|
||||
@ -1280,6 +1292,36 @@ class ScriptTest {
|
||||
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
|
||||
fun testIntClosedRangeInclusive() = runTest {
|
||||
eval(
|
||||
@ -3485,17 +3527,58 @@ class ScriptTest {
|
||||
fun testRangeStepIteration() = runTest {
|
||||
val ints = eval("""(1..5 step 2).toList()""") as ObjList
|
||||
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
|
||||
assertEquals(listOf('a', 'c', 'e'), chars.list.map { it.toString().single() })
|
||||
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() })
|
||||
val empty = eval("""(5..1 step 1).toList()""") as ObjList
|
||||
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
|
||||
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> {
|
||||
eval("""(0.0..1.0).toList()""")
|
||||
}
|
||||
assertFailsWith<ExecutionError> {
|
||||
eval("""(5 downTo 1 step -1).toList()""")
|
||||
}
|
||||
assertFailsWith<ExecutionError> {
|
||||
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
|
||||
fun binarySearchTest() = runTest {
|
||||
eval(
|
||||
|
||||
@ -36,7 +36,7 @@ actual object PerfDefaults {
|
||||
actual val PIC_DEBUG_COUNTERS: Boolean = false
|
||||
|
||||
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
|
||||
actual val REGEX_CACHE: Boolean = true
|
||||
|
||||
145
lynglib/src/jvmTest/kotlin/PiSpigotBenchmarkTest.kt
Normal file
145
lynglib/src/jvmTest/kotlin/PiSpigotBenchmarkTest.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
@ -422,6 +422,13 @@ fun List<T>.sort(): void {
|
||||
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. */
|
||||
fun Exception.printStackTrace(): void {
|
||||
println(this)
|
||||
|
||||
172
notes/pi_spigot_benchmark_baseline_2026-04-03.md
Normal file
172
notes/pi_spigot_benchmark_baseline_2026-04-03.md
Normal 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.
|
||||
Loading…
x
Reference in New Issue
Block a user