Compare commits

..

5 Commits

19 changed files with 1134 additions and 390 deletions

View File

@ -2,6 +2,23 @@
### Unreleased ### Unreleased
- Language: Named arguments and named splats
- New call-site syntax for named arguments using colon: `name: value`.
- Positional arguments must come before named; positionals after a named argument inside parentheses are rejected.
- Trailing-lambda interaction: if the last parameter is already assigned by name (or via a named splat), a trailing `{ ... }` block is illegal.
- Named splats: `...` can now expand a Map into named arguments.
- Only string keys are allowed; non-string keys raise a clear error.
- Duplicate assignment across named args and named splats is an error.
- Ellipsis (variadic) parameters remain positional-only and cannot be named.
- Rationale: `=` is assignment and an expression in Lyng; `:` at call sites avoids ambiguity. Declarations keep `name: Type`; call-site casts continue to use `as` / `as?`.
- Documentation updated: proposals and declaring-arguments sections now cover named args/splats and error cases.
- Tests added covering success cases and errors for named args/splats and trailing-lambda interactions.
- Tooling: Highlighters and TextMate bundle updated for named args
- Website/editor highlighter (lyngweb + site) works with `name: value` and `...Map("k" => v)`; added JS tests covering punctuation/operator spans for `:` and `...`.
- TextMate grammar updated to recognize named call arguments: `name: value` after `(` or `,` with `name` highlighted as `variable.parameter.named.lyng` and `:` as punctuation; excludes `::`.
- TextMate bundle version bumped to 0.0.3; README updated with details and guidance.
- Multiple Inheritance (MI) completed and enabled by default: - Multiple Inheritance (MI) completed and enabled by default:
- Active C3 Method Resolution Order (MRO) for deterministic, monotonic lookup across complex hierarchies and diamonds. - Active C3 Method Resolution Order (MRO) for deterministic, monotonic lookup across complex hierarchies and diamonds.
- Qualified dispatch: - Qualified dispatch:

View File

@ -5,7 +5,7 @@ lambdas and class declarations.
## Regular ## Regular
## default values ## Default values
Default parameters should not be mixed with mandatory ones: Default parameters should not be mixed with mandatory ones:
@ -96,5 +96,52 @@ There could be any number of splats at any positions. You can splat any other [I
>>> [start,1,2,3,end] >>> [start,1,2,3,end]
>>> void >>> void
## Named arguments in calls
Lyng supports named arguments at call sites using colon syntax `name: value`:
```lyng
fun test(a="foo", b="bar", c="bazz") { [a, b, c] }
assertEquals(["foo", "b", "bazz"], test(b: "b"))
assertEquals(["a", "bar", "c"], test("a", c: "c"))
```
Rules:
- Named arguments must follow positional arguments. After the first named argument, no positional arguments may appear inside the parentheses.
- The only exception is the syntactic trailing block after the call: `f(args) { ... }`. This block is outside the parentheses and is handled specially (see below).
- A named argument cannot reassign a parameter already set positionally.
- If the last parameter has already been assigned by a named argument (or named splat), a trailing block is not allowed and results in an error.
Why `:` and not `=` at call sites? In Lyng, `=` is an expression (assignment), so we use `:` to avoid ambiguity. Declarations continue to use `:` for types, while call sites use `as` / `as?` for type operations.
## Named splats (map splats)
Splat (`...`) of a Map provides named arguments to the call. Only string keys are allowed:
```lyng
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
val r = test("A?", ...Map("d" => "D!", "b" => "B!"))
assertEquals(["A?","B!","c","D!"], r)
```
Constraints:
- Map splat keys must be strings; otherwise, a clean error is thrown.
- Named splats cannot duplicate parameters already assigned (by positional or named arguments).
- Named splats must follow all positional arguments and positional splats.
- Ellipsis parameters (variadic) remain positional-only and cannot be assigned by name.
## Trailing-lambda rule interaction
If a call is immediately followed by a block `{ ... }`, it is treated as an extra last argument and bound to the last parameter. However, if the last parameter is already assigned by a named argument or a named splat, using a trailing block is an error:
```lyng
fun f(x, onDone) { onDone(x) }
f(x: 1) { 42 } // ERROR
f(1) { 42 } // OK
```
[tutorial]: tutorial.md [tutorial]: tutorial.md

View File

@ -0,0 +1,82 @@
Note for AI. please ignore this file until this remark is removed.
# Map literals proposal
Note to the AI: please ignore this file until this note is removed.
Implement javascript-like literals for maps. It uses the similar syntax already adopted for named arguments in function calls so it should be organic and familiar, as the purpose is similar.
String literal keys
```lyng
val m = { a: "foo", b: "bar" }
assertEqual(m.a, "foo")
assertEqual(m.b, "bar")
```
The map literal is a map with keys that are string literals only. This is important. In a relatively rare case when keys are calculated, or extravagant but still possible case when keys are of different types, literal could be combined with "=>":
```lyng
val k1 = "bar"
val m = { "foo": 123 } + k1 => "buzz"
// this is same as Map("foo" => 123) + Map("bar" => k2) but can be optimized by compiler
assertEqual(m["foo"], 123)
assertEqual(m["bar"], "buzz")
```
The lambda syntax is different, it can start with the `map_lteral_start` above, it should produce compile time error, so we can add map literals of this sort.
Also, we will allow splats in map literals:
```
val m = { foo: "bar", ...{bar: "buzz"} }
assertEquals("bar",m["foo"])
assertEquals("buzz", m["bar"])
```
When the literal argument and splats are used together, they must be evaluated left-to-right with allowed overwriting
between named elements and splats, allowing any combination and multiple splats:
```
val m = { foo: "bar", ...{bar: "buzz"}, ...{foo: "foobar"}, bar: "bar" }
assertEquals("foobar",m["foo"])
assertEquals("bar", m["bar"])
```
Still we disallow duplicating _string literals_:
```
// this is an compile-time exception:
{ foo: 1, bar: 2, foo: 3 }
```
Special syntax allows to insert key-value pair from the variable which name should be the key, and content is value:
```
val foo = "bar"
val bar = "buzz"
assertEquals( {foo: "bar", bar: "buzz"}, { *foo, *bar } )
```
Question to the AI: maybe better syntax than asterisk for that case?
So, summarizing, overwriting/duplication rules are:
- string literals can't duplicate
- splats add or update content, effectively overwrite preceding content,
- string literals overwrite content received from preceding splats (as no duplication string literal keys allowed)
- the priority and order is left-to-right, rightmost wins.
- var inclusion is treated as form of the literal
This approach resolves the ambiguity from lambda syntax, as
```ebnf
ws = zero or more whitespace characters including newline
map_literal start = "{", ws, (s1 | s2 | s3)
s1 = string_literal, ws, ":", ws, expression
s2 = "...", string_literal
s3 = "*", string_literal
```
as we can see, `map_literal_start` is not a valid lambda beginning so it is not create ambiguity.

View File

@ -0,0 +1,67 @@
# Named arguments proposal
Extend function/method calls to allow setting arguments by name using colon syntax at call sites. This is especially convenient with many parameters and default values.
Examples:
```lyng
fun test(a="foo", b="bar", c="bazz") { [a, b, c] }
assertEquals(test(b: "b"), ["foo", "b", "bazz"])
assertEquals(test("a", c: "c"), ["a", "bar", "c"])
```
Rules:
- Named arguments are optional. If named arguments are present, their order is not important.
- Named arguments must follow positional arguments; positional arguments cannot follow named ones (the only exception is the syntactic trailing block outside parentheses, see below).
- A named argument cannot reassign a parameter already set positionally.
- If the last parameter is already assigned by a named argument (or named splat), the trailing-lambda rule must NOT apply: a following `{ ... }` after the call is an error.
Rationale for using `:` instead of `=` in calls: in Lyng, assignment `=` is an expression; using `:` avoids ambiguity and keeps declarations (`name: Type`) distinct from call sites, where casting uses `as` / `as?`.
Migration note: earlier drafts/examples used `name = value`. The final syntax is `name: value` at call sites.
## Extended call argument splats: named splats
With named arguments, splats (`...`) are extended to support maps as named splats. When a splat evaluates to a Map, its entries provide name→value assignments:
```lyng
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
assertEquals(test("A?", ...Map("d" => "D!", "b" => "B!")), ["A?", "B!", "c", "D!"])
```
Constraints for named splats:
- Only string keys are allowed in map splats; otherwise, a clean error is thrown.
- Named splats cannot reassign parameters already set (positionally or by earlier named arguments/splats).
- Named splats follow the same ordering as named arguments: they must appear after all positional arguments and positional splats.
## Trailing-lambda interaction
Lyng supports a syntactic trailing block after a call: `f(args) { ... }`. With named args/splats, if the last parameter is already assigned by name, the trailing block must not apply and the call is an error:
```lyng
fun f(x, onDone) { onDone(x) }
f(x: 1) { 42 } // ERROR: last parameter already assigned by name
f(1) { 42 } // OK
```
## Errors (non-exhaustive)
- Positional argument after any named argument inside parentheses: error.
- Positional splat after any named argument: error.
- Duplicate named assignment (directly or via map splats): error.
- Unknown parameter name in a named argument/splat: error.
- Map splat with non-string keys: error.
- Attempt to target the ellipsis parameter by name: error.
## Notes
- Declarations continue to use `:` for types, while call sites use `:` for named arguments and `as` / `as?` for type casts/checks.

2
docs/samples/sum.lyng Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/bin/env lyng
/* /*
Calculate the limit of Sum( f(n) ) Calculate the limit of Sum( f(n) )
until it reaches asymptotic limit 0.00001% change until it reaches asymptotic limit 0.00001% change
return null or found limit return null or found limit
*/ */
fun findSumLimit(f) { fun findSumLimit(f) {

View File

@ -25,6 +25,7 @@ Files
- Operators including ranges (`..`, `..<`, `...`), null-safe (`?.`, `?[`, `?(`, `?{`, `?:`, `??`), arrows (`->`, `=>`, `::`), match operators (`=~`, `!~`), bitwise, arithmetic, etc. - Operators including ranges (`..`, `..<`, `...`), null-safe (`?.`, `?[`, `?(`, `?{`, `?:`, `??`), arrows (`->`, `=>`, `::`), match operators (`=~`, `!~`), bitwise, arithmetic, etc.
- Shuttle operator `<=>` - Shuttle operator `<=>`
- Division operator `/` (note: Lyng has no regex literal syntax; `/` is always division) - Division operator `/` (note: Lyng has no regex literal syntax; `/` is always division)
- Named arguments at call sites `name: value` (the `name` part is highlighted as `variable.parameter.named.lyng` and the `:` as punctuation). The rule is anchored to `(` or `,` and excludes `::` to avoid conflicts.
Install in IntelliJ IDEA (and other JetBrains IDEs) Install in IntelliJ IDEA (and other JetBrains IDEs)
--------------------------------------------------- ---------------------------------------------------
@ -56,6 +57,7 @@ Notes and limitations
--------------------- ---------------------
- Type highlighting is heuristic (Capitalized identifiers). The IntelliJ plugin will use language semantics and avoid false positives. - Type highlighting is heuristic (Capitalized identifiers). The IntelliJ plugin will use language semantics and avoid false positives.
- If your language adds or changes tokens, please update patterns in `lyng.tmLanguage.json`. The Kotlin sources in `lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/` are a good reference for token kinds. - If your language adds or changes tokens, please update patterns in `lyng.tmLanguage.json`. The Kotlin sources in `lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/` are a good reference for token kinds.
- Labels `name:` at statement level remain supported and are kept distinct from named call arguments by context. The grammar prefers named-argument matching when a `name:` appears right after `(` or `,`.
Lyng specifics Lyng specifics
-------------- --------------

View File

@ -2,7 +2,7 @@
"name": "lyng-textmate", "name": "lyng-textmate",
"displayName": "Lyng", "displayName": "Lyng",
"description": "TextMate grammar for the Lyng language (for JetBrains IDEs via TextMate Bundles and VS Code).", "description": "TextMate grammar for the Lyng language (for JetBrains IDEs via TextMate Bundles and VS Code).",
"version": "0.0.2", "version": "0.0.3",
"publisher": "lyng", "publisher": "lyng",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "vscode": "^1.0.0" }, "engines": { "vscode": "^1.0.0" },

View File

@ -13,6 +13,7 @@
{ "include": "#keywords" }, { "include": "#keywords" },
{ "include": "#constants" }, { "include": "#constants" },
{ "include": "#types" }, { "include": "#types" },
{ "include": "#namedArgs" },
{ "include": "#annotations" }, { "include": "#annotations" },
{ "include": "#labels" }, { "include": "#labels" },
{ "include": "#directives" }, { "include": "#directives" },
@ -41,6 +42,18 @@
] ]
}, },
"annotations": { "patterns": [ { "name": "entity.name.label.at.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*:" }, { "name": "storage.modifier.annotation.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*" } ] }, "annotations": { "patterns": [ { "name": "entity.name.label.at.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*:" }, { "name": "storage.modifier.annotation.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*" } ] },
"namedArgs": {
"patterns": [
{
"name": "meta.argument.named.lyng",
"match": "(?:(?<=\\()|(?<=,))\\s*([\\p{L}_][\\p{L}\\p{N}_]*)\\s*(:)(?!:)",
"captures": {
"1": { "name": "variable.parameter.named.lyng" },
"2": { "name": "punctuation.separator.colon.lyng" }
}
}
]
},
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] }, "labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] },
"directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] }, "directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] },
"declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(?:fun|fn)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(?:val|var)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "variable.other.declaration.lyng" } } } ] }, "declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(?:fun|fn)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(?:val|var)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "variable.other.declaration.lyng" } } } ] },

View File

@ -31,6 +31,15 @@ repositories {
} }
kotlin { kotlin {
// Suppress Beta warning for expect/actual classes across all targets in this module
targets.configureEach {
compilations.configureEach {
compilerOptions.configure {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}
jvm { jvm {
binaries { binaries {
executable { executable {

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "1.0.0-SNAPSHOT" version = "1.0.1-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
@ -72,6 +72,15 @@ kotlin {
nodejs() nodejs()
} }
// Suppress Beta warning for expect/actual classes across all targets
targets.configureEach {
compilations.configureEach {
compilerOptions.configure {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}
sourceSets { sourceSets {
all { all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
@ -141,6 +150,40 @@ tasks.withType<org.gradle.api.tasks.testing.Test> {
showStandardStreams = true showStandardStreams = true
} }
maxParallelForks = 1 maxParallelForks = 1
// Benchmarks toggle: disabled by default, enable when optimizing locally.
// Enable via any of the following:
// - Gradle property: ./gradlew :lynglib:jvmTest -Pbenchmarks=true
// - JVM system prop: ./gradlew :lynglib:jvmTest -Dbenchmarks=true
// - Environment var: BENCHMARKS=true ./gradlew :lynglib:jvmTest
val benchmarksEnabled: Boolean = run {
val p = (project.findProperty("benchmarks") as String?)?.toBooleanStrictOrNull()
val s = System.getProperty("benchmarks")?.lowercase()?.let { it == "true" || it == "1" || it == "yes" }
val e = System.getenv("BENCHMARKS")?.lowercase()?.let { it == "true" || it == "1" || it == "yes" }
p ?: s ?: e ?: false
}
// Make the flag visible inside tests if they want to branch on it
systemProperty("LYNG_BENCHMARKS", benchmarksEnabled.toString())
if (!benchmarksEnabled) {
// Exclude all JVM tests whose class name ends with or contains BenchmarkTest
// This keeps CI fast and avoids noisy timing logs by default.
filter {
excludeTestsMatching("*BenchmarkTest")
// Also guard against alternative naming
excludeTestsMatching("*Bench*Test")
// Exclude A/B performance tests unless explicitly enabled
excludeTestsMatching("*ABTest")
// Exclude stress/perf soak tests
excludeTestsMatching("*Stress*Test")
// Exclude allocation profiling tests by default
excludeTestsMatching("*AllocationProfileTest")
}
logger.lifecycle("[tests] Benchmarks are DISABLED. To enable: -Pbenchmarks=true or -Dbenchmarks=true or set BENCHMARKS=true")
} else {
logger.lifecycle("[tests] Benchmarks are ENABLED: *BenchmarkTest will run")
}
} }
//mavenPublishing { //mavenPublishing {

View File

@ -57,72 +57,137 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
recordType = ObjRecord.Type.Argument) recordType = ObjRecord.Type.Argument)
} }
// will be used with last lambda arg fix // Prepare positional args and parameter count, handle tail-block binding
val callArgs: List<Obj> val callArgs: List<Obj>
val paramsSize: Int val paramsSize: Int
if (arguments.tailBlockMode) {
if( arguments.tailBlockMode ) { // If last parameter is already assigned by a named argument, it's an error
val lastParam = params.last()
if (arguments.named.containsKey(lastParam.name))
scope.raiseIllegalArgument("trailing block cannot be used when the last parameter is already assigned by a named argument")
paramsSize = params.size - 1 paramsSize = params.size - 1
assign(params.last(), arguments.list.last()) assign(lastParam, arguments.list.last())
callArgs = arguments.list.dropLast(1) callArgs = arguments.list.dropLast(1)
} else { } else {
paramsSize = params.size paramsSize = params.size
callArgs = arguments.list callArgs = arguments.list
} }
suspend fun processHead(index: Int): Int { // Compute which parameter indexes are inevitably covered by positional arguments
// based on the number of supplied positionals, defaults and ellipsis placement.
val coveredByPositional = BooleanArray(paramsSize)
run {
// Count required (non-default, non-ellipsis) params in head and in tail
var headRequired = 0
var tailRequired = 0
val ellipsisIdx = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
if (ellipsisIdx >= 0) {
for (i in 0 until ellipsisIdx) if (!params[i].isEllipsis && params[i].defaultValue == null) headRequired++
for (i in paramsSize - 1 downTo ellipsisIdx + 1) if (params[i].defaultValue == null) tailRequired++
} else {
for (i in 0 until paramsSize) if (params[i].defaultValue == null) headRequired++
}
val P = callArgs.size
if (ellipsisIdx < 0) {
// No ellipsis: all positionals go to head until exhausted
val k = minOf(P, paramsSize)
for (i in 0 until k) coveredByPositional[i] = true
} else {
// With ellipsis: head takes min(P, headRequired) first
val headTake = minOf(P, headRequired)
for (i in 0 until headTake) coveredByPositional[i] = true
val remaining = P - headTake
// tail takes min(remaining, tailRequired) from the end
val tailTake = minOf(remaining, tailRequired)
var j = paramsSize - 1
var taken = 0
while (j > ellipsisIdx && taken < tailTake) {
coveredByPositional[j] = true
j--
taken++
}
}
}
// Prepare arrays for named assignments
val assignedByName = BooleanArray(paramsSize)
val namedValues = arrayOfNulls<Obj>(paramsSize)
if (arguments.named.isNotEmpty()) {
for ((k, v) in arguments.named) {
val idx = params.subList(0, paramsSize).indexOfFirst { it.name == k }
if (idx < 0) scope.raiseIllegalArgument("unknown parameter '$k'")
if (params[idx].isEllipsis) scope.raiseIllegalArgument("ellipsis (variadic) parameter cannot be assigned by name: '$k'")
if (coveredByPositional[idx]) scope.raiseIllegalArgument("argument '$k' is already set by positional argument")
if (assignedByName[idx]) scope.raiseIllegalArgument("argument '$k' is already set")
assignedByName[idx] = true
namedValues[idx] = v
}
}
// Helper: assign head part, consuming from headPos; stop at ellipsis
suspend fun processHead(index: Int, headPos: Int): Pair<Int, Int> {
var i = index var i = index
while (i != paramsSize) { var hp = headPos
while (i < paramsSize) {
val a = params[i] val a = params[i]
if (a.isEllipsis) break if (a.isEllipsis) break
val value = when { if (assignedByName[i]) {
i < callArgs.size -> callArgs[i] assign(a, namedValues[i]!!)
a.defaultValue != null -> a.defaultValue.execute(scope) } else {
else -> { val value = if (hp < callArgs.size) callArgs[hp++]
// println("callArgs: ${callArgs.joinToString()}") else a.defaultValue?.execute(scope)
// println("tailBlockMode: ${arguments.tailBlockMode}") ?: scope.raiseIllegalArgument("too few arguments for the call")
scope.raiseIllegalArgument("too few arguments for the call")
}
}
assign(a, value) assign(a, value)
}
i++ i++
} }
return i return i to hp
} }
suspend fun processTail(index: Int): Int { // Helper: assign tail part from the end, consuming from tailPos; stop before ellipsis index
// Do not consume elements below headPosBound to avoid overlap with head consumption
suspend fun processTail(startExclusive: Int, tailStart: Int, headPosBound: Int): Int {
var i = paramsSize - 1 var i = paramsSize - 1
var j = callArgs.size - 1 var tp = tailStart
while (i > index) { while (i > startExclusive) {
val a = params[i] val a = params[i]
if (a.isEllipsis) break if (a.isEllipsis) break
val value = when { if (i < assignedByName.size && assignedByName[i]) {
j >= index -> { assign(a, namedValues[i]!!)
callArgs[j--] } else {
} val value = if (tp >= headPosBound) callArgs[tp--]
else a.defaultValue?.execute(scope)
a.defaultValue != null -> a.defaultValue.execute(scope) ?: scope.raiseIllegalArgument("too few arguments for the call")
else -> scope.raiseIllegalArgument("too few arguments for the call")
}
assign(a, value) assign(a, value)
}
i-- i--
} }
return j return tp
} }
fun processEllipsis(index: Int, toFromIndex: Int) { fun processEllipsis(index: Int, headPos: Int, tailPos: Int) {
val a = params[index] val a = params[index]
val l = if (index > toFromIndex) ObjList() val from = headPos
else ObjList(callArgs.subList(index, toFromIndex + 1).toMutableList()) val to = tailPos
val l = if (from > to) ObjList()
else ObjList(callArgs.subList(from, to + 1).toMutableList())
assign(a, l) assign(a, l)
} }
val leftIndex = processHead(0) // Locate ellipsis index within considered parameters
if (leftIndex < paramsSize) { val ellipsisIndex = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
val end = processTail(leftIndex)
processEllipsis(leftIndex, end) if (ellipsisIndex >= 0) {
// Assign head first to know how many positionals are consumed from the start
val (afterHead, headConsumedTo) = processHead(0, 0)
// Then assign tail consuming from the end down to headConsumedTo boundary
val tailConsumedFrom = processTail(ellipsisIndex, callArgs.size - 1, headConsumedTo)
// Assign ellipsis list from remaining positionals between headConsumedTo..tailConsumedFrom
processEllipsis(ellipsisIndex, headConsumedTo, tailConsumedFrom)
} else { } else {
if (leftIndex < callArgs.size) // No ellipsis: assign head only; any leftover positionals → error
val (_, headConsumedTo) = processHead(0, 0)
if (headConsumedTo != callArgs.size)
scope.raiseIllegalArgument("too many arguments for the call") scope.raiseIllegalArgument("too many arguments for the call")
} }
} }

View File

@ -17,24 +17,27 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.*
import net.sergeych.lyng.obj.ObjIterable
import net.sergeych.lyng.obj.ObjList
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false) data class ParsedArgument(
val value: Statement,
val pos: Pos,
val isSplat: Boolean = false,
val name: String? = null,
)
suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments { suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
// Small-arity fast path (no splats) to reduce allocations // Detect if we can use the fast path: no splats and no named args
if (PerfFlags.ARG_BUILDER) { if (PerfFlags.ARG_BUILDER) {
val limit = if (PerfFlags.ARG_SMALL_ARITY_12) 12 else 8 val limit = if (PerfFlags.ARG_SMALL_ARITY_12) 12 else 8
var hasSplat = false var hasSplatOrNamed = false
var count = 0 var count = 0
for (pa in this) { for (pa in this) {
if (pa.isSplat) { hasSplat = true; break } if (pa.isSplat || pa.name != null) { hasSplatOrNamed = true; break }
count++ count++
if (count > limit) break if (count > limit) break
} }
if (!hasSplat && count == this.size) { if (!hasSplatOrNamed && count == this.size) {
val quick = when (count) { val quick = when (count) {
0 -> Arguments.EMPTY 0 -> Arguments.EMPTY
1 -> Arguments(listOf(this.elementAt(0).value.execute(scope)), tailBlockMode) 1 -> Arguments(listOf(this.elementAt(0).value.execute(scope)), tailBlockMode)
@ -153,90 +156,86 @@ import net.sergeych.lyng.obj.ObjList
if (quick != null) return quick if (quick != null) return quick
} }
} }
// Single-splat fast path: if there is exactly one splat argument that evaluates to ObjList,
// avoid builder and copies by returning its list directly.
if (PerfFlags.ARG_BUILDER) {
if (this.size == 1) {
val only = this.first()
if (only.isSplat) {
val v = only.value.execute(scope)
if (v is ObjList) {
return Arguments(v.list, tailBlockMode)
} else if (v.isInstanceOf(ObjIterable)) {
// Convert iterable to list once and return directly
val i = (v.invokeInstanceMethod(scope, "toList") as ObjList).list
return Arguments(i, tailBlockMode)
} else {
scope.raiseClassCastError("expected list of objects for splat argument")
}
}
}
}
// General path with builder or simple list fallback // General path: build positional list and named map, enforcing ordering rules
if (PerfFlags.ARG_BUILDER) { val positional: MutableList<Obj> = mutableListOf()
val b = ArgBuilderProvider.acquire() var named: MutableMap<String, Obj>? = null
try { var namedSeen = false
b.reset(this.size) for ((idx, x) in this.withIndex()) {
for (x in this) { if (x.name != null) {
// Named argument
if (named == null) named = linkedMapOf()
if (named.containsKey(x.name)) scope.raiseIllegalArgument("argument '${x.name}' is already set")
val v = x.value.execute(scope)
named[x.name] = v
namedSeen = true
continue
}
val value = x.value.execute(scope) val value = x.value.execute(scope)
if (x.isSplat) { if (x.isSplat) {
when { when {
// IMPORTANT: handle ObjMap BEFORE generic Iterable to ensure map splats
// are treated as named splats, not as positional iteration over entries
value is ObjMap -> {
if (named == null) named = linkedMapOf()
for ((k, v) in value.map) {
if (k !is ObjString) scope.raiseIllegalArgument("named splat expects a Map with string keys")
val key = k.value
if (named.containsKey(key)) scope.raiseIllegalArgument("argument '$key' is already set")
named[key] = v
}
namedSeen = true
}
value is ObjList -> { value is ObjList -> {
b.addAll(value.list) if (namedSeen) {
// allow only if this is the very last positional which will be the trailing block; but
// splat can never be a trailing block, so it's always illegal here
scope.raiseIllegalArgument("positional splat cannot follow named arguments")
}
positional.addAll(value.list)
} }
value.isInstanceOf(ObjIterable) -> { value.isInstanceOf(ObjIterable) -> {
if (namedSeen) scope.raiseIllegalArgument("positional splat cannot follow named arguments")
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
b.addAll(i) positional.addAll(i)
} }
else -> scope.raiseClassCastError("expected list of objects for splat argument") else -> scope.raiseClassCastError("expected list of objects for splat argument")
} }
} else { } else {
b.add(value) if (namedSeen) {
// Allow exactly one positional after named only when it is the very last argument overall
// and tailBlockMode is true (syntactic trailing block). Otherwise, forbid it.
val isLast = idx == this.size - 1
if (!(isLast && tailBlockMode))
scope.raiseIllegalArgument("positional argument cannot follow named arguments")
}
positional.add(value)
} }
} }
return b.build(tailBlockMode) val namedFinal = named ?: emptyMap()
} finally { return Arguments(positional, tailBlockMode, namedFinal)
b.release()
}
} else {
val list: MutableList<Obj> = mutableListOf()
for (x in this) {
val value = x.value.execute(scope)
if (x.isSplat) {
when {
value is ObjList -> list.addAll(value.list)
value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
list.addAll(i)
}
else -> scope.raiseClassCastError("expected list of objects for splat argument")
}
} else {
list.add(value)
}
}
return Arguments(list, tailBlockMode)
}
} }
data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) : List<Obj> by list { data class Arguments(
val list: List<Obj>,
val tailBlockMode: Boolean = false,
val named: Map<String, Obj> = emptyMap(),
) : List<Obj> by list {
constructor(vararg values: Obj) : this(values.toList()) constructor(vararg values: Obj) : this(values.toList())
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
val v = list.first()
// Tiny micro-alloc win: avoid byValueCopy for immutable singletons // Tiny micro-alloc win: avoid byValueCopy for immutable singletons
return when (v) { return when (val v = list.first()) {
net.sergeych.lyng.obj.ObjNull, ObjNull,
net.sergeych.lyng.obj.ObjTrue, ObjTrue,
net.sergeych.lyng.obj.ObjFalse, ObjFalse,
// Immutable scalars: safe to return directly // Immutable scalars: safe to return directly
is net.sergeych.lyng.obj.ObjInt, is ObjInt,
is net.sergeych.lyng.obj.ObjReal, is ObjReal,
is net.sergeych.lyng.obj.ObjChar, is ObjChar,
is net.sergeych.lyng.obj.ObjString -> v is ObjString -> v
else -> v.byValueCopy() else -> v.byValueCopy()
} }
} }

View File

@ -35,8 +35,8 @@ class Compiler(
// Stack of parameter-to-slot plans for current function being parsed (by declaration index) // Stack of parameter-to-slot plans for current function being parsed (by declaration index)
private val paramSlotPlanStack = mutableListOf<Map<String, Int>>() private val paramSlotPlanStack = mutableListOf<Map<String, Int>>()
private val currentParamSlotPlan: Map<String, Int>? // private val currentParamSlotPlan: Map<String, Int>?
get() = paramSlotPlanStack.lastOrNull() // get() = paramSlotPlanStack.lastOrNull()
// Track identifiers known to be locals/parameters in the current function for fast local emission // Track identifiers known to be locals/parameters in the current function for fast local emission
private val localNamesStack = mutableListOf<MutableSet<String>>() private val localNamesStack = mutableListOf<MutableSet<String>>()
@ -50,7 +50,11 @@ class Compiler(
private inline fun <T> withLocalNames(names: Set<String>, block: () -> T): T { private inline fun <T> withLocalNames(names: Set<String>, block: () -> T): T {
localNamesStack.add(names.toMutableSet()) localNamesStack.add(names.toMutableSet())
return try { block() } finally { localNamesStack.removeLast() } return try {
block()
} finally {
localNamesStack.removeLast()
}
} }
private fun declareLocalName(name: String) { private fun declareLocalName(name: String) {
@ -86,6 +90,7 @@ class Compiler(
if (t.startsWith("*")) t.removePrefix("*").trimStart() else line if (t.startsWith("*")) t.removePrefix("*").trimStart() else line
} }
} }
else -> raw else -> raw
} }
} }
@ -158,6 +163,7 @@ class Compiler(
// A standalone newline not immediately following a comment resets doc buffer // A standalone newline not immediately following a comment resets doc buffer
if (!prevWasComment) clearPendingDoc() else prevWasComment = false if (!prevWasComment) clearPendingDoc() else prevWasComment = false
} }
else -> {} else -> {}
} }
cc.next() cc.next()
@ -191,12 +197,15 @@ class Compiler(
val start = Pos(pos.source, pos.line, col) val start = Pos(pos.source, pos.line, col)
val end = Pos(pos.source, pos.line, col + p.length) val end = Pos(pos.source, pos.line, col + p.length)
col += p.length + 1 // account for following '.' between segments col += p.length + 1 // account for following '.' between segments
net.sergeych.lyng.miniast.MiniImport.Segment(p, net.sergeych.lyng.miniast.MiniRange(start, end)) MiniImport.Segment(
p,
MiniRange(start, end)
)
} }
val lastEnd = segs.last().range.end val lastEnd = segs.last().range.end
miniSink?.onImport( miniSink?.onImport(
net.sergeych.lyng.miniast.MiniImport( MiniImport(
net.sergeych.lyng.miniast.MiniRange(pos, lastEnd), MiniRange(pos, lastEnd),
segs segs
) )
) )
@ -241,7 +250,10 @@ class Compiler(
Script(start, statements) Script(start, statements)
}.also { }.also {
// Best-effort script end notification (use current position) // Best-effort script end notification (use current position)
miniSink?.onScriptEnd(cc.currentPos(), net.sergeych.lyng.miniast.MiniScript(MiniRange(start, cc.currentPos()))) miniSink?.onScriptEnd(
cc.currentPos(),
MiniScript(MiniRange(start, cc.currentPos()))
)
} }
} }
@ -327,7 +339,6 @@ class Compiler(
var lvalue: ObjRef? = parseExpressionLevel(level + 1) ?: return null var lvalue: ObjRef? = parseExpressionLevel(level + 1) ?: return null
while (true) { while (true) {
val opToken = cc.next() val opToken = cc.next()
val op = byLevel[level][opToken.type] val op = byLevel[level][opToken.type]
if (op == null) { if (op == null) {
@ -552,11 +563,14 @@ class Compiler(
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
operand = operand?.let { left -> operand = operand?.let { left ->
cc.previous() // Trailing block-argument function call: the leading '{' is already consumed,
// and the lambda must be parsed as a single argument BEFORE any following
// selectors like ".foo" are considered. Do NOT rewind here, otherwise
// the expression parser may capture ".foo" as part of the lambda expression.
parseFunctionCall( parseFunctionCall(
left, left,
blockArgument = true, blockArgument = true,
t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE isOptional = t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
) )
} ?: parseLambdaExpression() } ?: parseLambdaExpression()
} }
@ -778,7 +792,11 @@ class Compiler(
val typeStart = cc.currentPos() val typeStart = cc.currentPos()
var lastEnd = typeStart var lastEnd = typeStart
while (true) { while (true) {
val idTok = if (first) cc.requireToken(Token.Type.ID, "type name or type expression required") else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type") val idTok =
if (first) cc.requireToken(Token.Type.ID, "type name or type expression required") else cc.requireToken(
Token.Type.ID,
"identifier expected after '.' in type"
)
first = false first = false
segments += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos)) segments += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos))
lastEnd = cc.currentPos() lastEnd = cc.currentPos()
@ -796,8 +814,11 @@ class Compiler(
// Helper to build MiniTypeRef (base or generic) // Helper to build MiniTypeRef (base or generic)
fun buildBaseRef(rangeEnd: Pos, args: List<MiniTypeRef>?, nullable: Boolean): MiniTypeRef { fun buildBaseRef(rangeEnd: Pos, args: List<MiniTypeRef>?, nullable: Boolean): MiniTypeRef {
val base = MiniTypeName(MiniRange(typeStart, rangeEnd), segments.toList(), nullable = false) val base = MiniTypeName(MiniRange(typeStart, rangeEnd), segments.toList(), nullable = false)
return if (args == null || args.isEmpty()) base.copy(range = MiniRange(typeStart, rangeEnd), nullable = nullable) return if (args == null || args.isEmpty()) base.copy(
else net.sergeych.lyng.miniast.MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable) range = MiniRange(typeStart, rangeEnd),
nullable = nullable
)
else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable)
} }
// Optional generic arguments: '<' Type (',' Type)* '>' — single-level only (no nested generics for now) // Optional generic arguments: '<' Type (',' Type)* '>' — single-level only (no nested generics for now)
@ -811,12 +832,17 @@ class Compiler(
var argFirst = true var argFirst = true
val argStart = cc.currentPos() val argStart = cc.currentPos()
while (true) { while (true) {
val idTok = if (argFirst) cc.requireToken(Token.Type.ID, "type argument name expected") else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type argument") val idTok = if (argFirst) cc.requireToken(
Token.Type.ID,
"type argument name expected"
) else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type argument")
argFirst = false argFirst = false
argSegs += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos)) argSegs += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos))
val p = cc.savePos() val p = cc.savePos()
val tt = cc.next() val tt = cc.next()
if (tt.type == Token.Type.DOT) continue else { cc.restorePos(p); break } if (tt.type == Token.Type.DOT) continue else {
cc.restorePos(p); break
}
} }
val argNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true) val argNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
val argEnd = cc.currentPos() val argEnd = cc.currentPos()
@ -825,7 +851,9 @@ class Compiler(
val sep = cc.next() val sep = cc.next()
when (sep.type) { when (sep.type) {
Token.Type.COMMA -> { /* continue */ } Token.Type.COMMA -> { /* continue */
}
Token.Type.GT -> break Token.Type.GT -> break
else -> sep.raiseSyntax("expected ',' or '>' in generic arguments") else -> sep.raiseSyntax("expected ',' or '>' in generic arguments")
} }
@ -853,6 +881,21 @@ class Compiler(
private suspend fun parseArgs(): Pair<List<ParsedArgument>, Boolean> { private suspend fun parseArgs(): Pair<List<ParsedArgument>, Boolean> {
val args = mutableListOf<ParsedArgument>() val args = mutableListOf<ParsedArgument>()
suspend fun tryParseNamedArg(): ParsedArgument? {
val save = cc.savePos()
val t1 = cc.next()
if (t1.type == Token.Type.ID) {
val t2 = cc.next()
if (t2.type == Token.Type.COLON) {
// name: expr
val name = t1.value
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
}
}
cc.restorePos(save)
return null
}
do { do {
val t = cc.next() val t = cc.next()
when (t.type) { when (t.type) {
@ -867,10 +910,14 @@ class Compiler(
else -> { else -> {
cc.previous() cc.previous()
val named = tryParseNamedArg()
if (named != null) {
args += named
} else {
parseExpression()?.let { args += ParsedArgument(it, t.pos) } parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list") ?: throw ScriptError(t.pos, "Expecting arguments list")
if (cc.current().type == Token.Type.COLON) // In call-site arguments, ':' is reserved for named args. Do not parse type declarations here.
parseTypeDeclaration() }
// Here should be a valid termination: // Here should be a valid termination:
} }
} }
@ -901,6 +948,20 @@ class Compiler(
*/ */
private suspend fun parseArgsNoTailBlock(): List<ParsedArgument> { private suspend fun parseArgsNoTailBlock(): List<ParsedArgument> {
val args = mutableListOf<ParsedArgument>() val args = mutableListOf<ParsedArgument>()
suspend fun tryParseNamedArg(): ParsedArgument? {
val save = cc.savePos()
val t1 = cc.next()
if (t1.type == Token.Type.ID) {
val t2 = cc.next()
if (t2.type == Token.Type.COLON) {
val name = t1.value
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
}
}
cc.restorePos(save)
return null
}
do { do {
val t = cc.next() val t = cc.next()
when (t.type) { when (t.type) {
@ -915,10 +976,14 @@ class Compiler(
else -> { else -> {
cc.previous() cc.previous()
val named = tryParseNamedArg()
if (named != null) {
args += named
} else {
parseExpression()?.let { args += ParsedArgument(it, t.pos) } parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list") ?: throw ScriptError(t.pos, "Expecting arguments list")
if (cc.current().type == Token.Type.COLON) // Do not parse type declarations in call args
parseTypeDeclaration() }
} }
} }
} while (t.type != Token.Type.RPAREN) } while (t.type != Token.Type.RPAREN)
@ -934,11 +999,14 @@ class Compiler(
): ObjRef { ): ObjRef {
var detectedBlockArgument = blockArgument var detectedBlockArgument = blockArgument
val args = if (blockArgument) { val args = if (blockArgument) {
val blockArg = ParsedArgument( // Leading '{' has already been consumed by the caller token branch.
parseExpression() // Parse only the lambda expression as the last argument and DO NOT
?: throw ScriptError(cc.currentPos(), "lambda body expected"), cc.currentPos() // allow any subsequent selectors (like ".last()") to be absorbed
) // into the lambda body. This ensures expected order:
listOf(blockArg) // foo { ... }.bar() == (foo { ... }).bar()
val callableAccessor = parseLambdaExpression()
val argStmt = statement { callableAccessor.get(this).value }
listOf(ParsedArgument(argStmt, cc.currentPos()))
} else { } else {
val r = parseArgs() val r = parseArgs()
detectedBlockArgument = r.second detectedBlockArgument = r.second
@ -1058,6 +1126,7 @@ class Compiler(
pendingDeclDoc = consumePendingDoc() pendingDeclDoc = consumePendingDoc()
parseVarDeclaration(false, Visibility.Public) parseVarDeclaration(false, Visibility.Public)
} }
"var" -> { "var" -> {
pendingDeclStart = id.pos pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc() pendingDeclDoc = consumePendingDoc()
@ -1069,6 +1138,7 @@ class Compiler(
pendingDeclDoc = consumePendingDoc() pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false) parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false)
} }
"fn" -> { "fn" -> {
pendingDeclStart = id.pos pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc() pendingDeclDoc = consumePendingDoc()
@ -1085,11 +1155,24 @@ class Compiler(
when (k.value) { when (k.value) {
"val" -> parseVarDeclaration(false, Visibility.Private, isStatic = isStatic) "val" -> parseVarDeclaration(false, Visibility.Private, isStatic = isStatic)
"var" -> parseVarDeclaration(true, Visibility.Private, isStatic = isStatic) "var" -> parseVarDeclaration(true, Visibility.Private, isStatic = isStatic)
"fun" -> parseFunctionDeclaration(visibility = Visibility.Private, isOpen = false, isExtern = false, isStatic = isStatic) "fun" -> parseFunctionDeclaration(
"fn" -> parseFunctionDeclaration(visibility = Visibility.Private, isOpen = false, isExtern = false, isStatic = isStatic) visibility = Visibility.Private,
isOpen = false,
isExtern = false,
isStatic = isStatic
)
"fn" -> parseFunctionDeclaration(
visibility = Visibility.Private,
isOpen = false,
isExtern = false,
isStatic = isStatic
)
else -> k.raiseSyntax("unsupported private declaration kind: ${k.value}") else -> k.raiseSyntax("unsupported private declaration kind: ${k.value}")
} }
} }
"protected" -> { "protected" -> {
var k = cc.requireToken(Token.Type.ID, "declaration expected after 'protected'") var k = cc.requireToken(Token.Type.ID, "declaration expected after 'protected'")
var isStatic = false var isStatic = false
@ -1100,11 +1183,24 @@ class Compiler(
when (k.value) { when (k.value) {
"val" -> parseVarDeclaration(false, Visibility.Protected, isStatic = isStatic) "val" -> parseVarDeclaration(false, Visibility.Protected, isStatic = isStatic)
"var" -> parseVarDeclaration(true, Visibility.Protected, isStatic = isStatic) "var" -> parseVarDeclaration(true, Visibility.Protected, isStatic = isStatic)
"fun" -> parseFunctionDeclaration(visibility = Visibility.Protected, isOpen = false, isExtern = false, isStatic = isStatic) "fun" -> parseFunctionDeclaration(
"fn" -> parseFunctionDeclaration(visibility = Visibility.Protected, isOpen = false, isExtern = false, isStatic = isStatic) visibility = Visibility.Protected,
isOpen = false,
isExtern = false,
isStatic = isStatic
)
"fn" -> parseFunctionDeclaration(
visibility = Visibility.Protected,
isOpen = false,
isExtern = false,
isStatic = isStatic
)
else -> k.raiseSyntax("unsupported protected declaration kind: ${k.value}") else -> k.raiseSyntax("unsupported protected declaration kind: ${k.value}")
} }
} }
"while" -> parseWhileStatement() "while" -> parseWhileStatement()
"do" -> parseDoWhileStatement() "do" -> parseDoWhileStatement()
"for" -> parseForStatement() "for" -> parseForStatement()
@ -1116,11 +1212,13 @@ class Compiler(
pendingDeclDoc = consumePendingDoc() pendingDeclDoc = consumePendingDoc()
parseClassDeclaration() parseClassDeclaration()
} }
"enum" -> { "enum" -> {
pendingDeclStart = id.pos pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc() pendingDeclDoc = consumePendingDoc()
parseEnumDeclaration() parseEnumDeclaration()
} }
"try" -> parseTryStatement() "try" -> parseTryStatement()
"throw" -> parseThrowStatement(id.pos) "throw" -> parseThrowStatement(id.pos)
"when" -> parseWhenStatement() "when" -> parseWhenStatement()
@ -1130,9 +1228,10 @@ class Compiler(
val isExtern = cc.skipId("extern") val isExtern = cc.skipId("extern")
when { when {
cc.matchQualifiers("fun", "private") -> { cc.matchQualifiers("fun", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(Visibility.Private, isExtern) parseFunctionDeclaration(Visibility.Private, isExtern)
} }
cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration( cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration(
Visibility.Private, Visibility.Private,
isExtern, isExtern,
@ -1149,27 +1248,78 @@ class Compiler(
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fun") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern) } cc.matchQualifiers("fun") -> {
cc.matchQualifiers("fn") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern) } pendingDeclStart = id.pos; pendingDeclDoc =
consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
}
cc.matchQualifiers("val", "private", "static") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( cc.matchQualifiers("fn") -> {
pendingDeclStart = id.pos; pendingDeclDoc =
consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
}
cc.matchQualifiers("val", "private", "static") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
false, false,
Visibility.Private, Visibility.Private,
isStatic = true isStatic = true
) } )
}
cc.matchQualifiers("val", "static") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(false, Visibility.Public, isStatic = true) } cc.matchQualifiers("val", "static") -> {
cc.matchQualifiers("val", "private") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(false, Visibility.Private) } pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
cc.matchQualifiers("var", "static") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(true, Visibility.Public, isStatic = true) } false,
cc.matchQualifiers("var", "static", "private") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( Visibility.Public,
isStatic = true
)
}
cc.matchQualifiers("val", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
false,
Visibility.Private
)
}
cc.matchQualifiers("var", "static") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
true,
Visibility.Public,
isStatic = true
)
}
cc.matchQualifiers("var", "static", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
true, true,
Visibility.Private, Visibility.Private,
isStatic = true isStatic = true
) } )
}
cc.matchQualifiers("var", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
true,
Visibility.Private
)
}
cc.matchQualifiers("val", "open") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
false,
Visibility.Private,
true
)
}
cc.matchQualifiers("var", "open") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
true,
Visibility.Private,
true
)
}
cc.matchQualifiers("var", "private") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(true, Visibility.Private) }
cc.matchQualifiers("val", "open") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(false, Visibility.Private, true) }
cc.matchQualifiers("var", "open") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(true, Visibility.Private, true) }
else -> { else -> {
cc.next() cc.next()
null null
@ -1306,9 +1456,10 @@ class Compiler(
errorObject.extraData, errorObject.extraData,
errorObject.useStackTrace errorObject.useStackTrace
) )
else -> throwScope.raiseError("this is not an exception object: $errorObject") else -> throwScope.raiseError("this is not an exception object: $errorObject")
} }
throwScope.raiseError(errorObject as ObjException) throwScope.raiseError(errorObject)
} }
} }
@ -1473,6 +1624,7 @@ class Compiler(
// Optional base list: ":" Base ("," Base)* where Base := ID ( "(" args? ")" )? // Optional base list: ":" Base ("," Base)* where Base := ID ( "(" args? ")" )?
data class BaseSpec(val name: String, val args: List<ParsedArgument>?) data class BaseSpec(val name: String, val args: List<ParsedArgument>?)
val baseSpecs = mutableListOf<BaseSpec>() val baseSpecs = mutableListOf<BaseSpec>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) { if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
do { do {
@ -1516,13 +1668,13 @@ class Compiler(
val declRange = MiniRange(pendingDeclStart ?: nameToken.pos, cc.currentPos()) val declRange = MiniRange(pendingDeclStart ?: nameToken.pos, cc.currentPos())
val bases = baseSpecs.map { it.name } val bases = baseSpecs.map { it.name }
// Collect constructor fields declared as val/var in primary constructor // Collect constructor fields declared as val/var in primary constructor
val ctorFields = mutableListOf<net.sergeych.lyng.miniast.MiniCtorField>() val ctorFields = mutableListOf<MiniCtorField>()
constructorArgsDeclaration?.let { ad -> constructorArgsDeclaration?.let { ad ->
for (p in ad.params) { for (p in ad.params) {
val at = p.accessType val at = p.accessType
if (at != null) { if (at != null) {
val mutable = at == AccessType.Var val mutable = at == AccessType.Var
ctorFields += net.sergeych.lyng.miniast.MiniCtorField( ctorFields += MiniCtorField(
name = p.name, name = p.name,
mutable = mutable, mutable = mutable,
type = p.miniType, type = p.miniType,
@ -1571,7 +1723,8 @@ class Compiler(
// accessors, constructor registration, etc. // accessors, constructor registration, etc.
// Resolve parent classes by name at execution time // Resolve parent classes by name at execution time
val parentClasses = baseSpecs.map { baseSpec -> val parentClasses = baseSpecs.map { baseSpec ->
val rec = this[baseSpec.name] ?: throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}") val rec =
this[baseSpec.name] ?: throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}")
(rec.value as? ObjClass) ?: throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class") (rec.value as? ObjClass) ?: throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class")
} }
@ -2082,7 +2235,7 @@ class Compiler(
val paramNames: Set<String> = argsDeclaration.params.map { it.name }.toSet() val paramNames: Set<String> = argsDeclaration.params.map { it.name }.toSet()
// Parse function body while tracking declared locals to compute precise capacity hints // Parse function body while tracking declared locals to compute precise capacity hints
val fnLocalDeclStart = currentLocalDeclCount currentLocalDeclCount
localDeclCountStack.add(0) localDeclCountStack.add(0)
val fnStatements = if (isExtern) val fnStatements = if (isExtern)
statement { raiseError("extern function not provided: $name") } statement { raiseError("extern function not provided: $name") }
@ -2113,7 +2266,7 @@ class Compiler(
} }
fnStatements.execute(context) fnStatements.execute(context)
} }
val enclosingCtx = parentContext parentContext
val fnCreateStatement = statement(start) { context -> val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure // we added fn in the context. now we must save closure
// for the function, unless we're in the class scope: // for the function, unless we're in the class scope:
@ -2363,7 +2516,7 @@ class Compiler(
) { ) {
// fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND // fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND
companion object {} companion object
} }
@ -2377,15 +2530,24 @@ class Compiler(
* Compile [source] while streaming a Mini-AST into the provided [sink]. * Compile [source] while streaming a Mini-AST into the provided [sink].
* When [sink] is null, behaves like [compile]. * When [sink] is null, behaves like [compile].
*/ */
suspend fun compileWithMini(source: Source, importManager: ImportProvider, sink: net.sergeych.lyng.miniast.MiniAstSink?): Script { suspend fun compileWithMini(
return Compiler(CompilerContext(parseLyng(source)), importManager, Settings(miniAstSink = sink)).parseScript() source: Source,
importManager: ImportProvider,
sink: MiniAstSink?
): Script {
return Compiler(
CompilerContext(parseLyng(source)),
importManager,
Settings(miniAstSink = sink)
).parseScript()
} }
/** Convenience overload to compile raw [code] with a Mini-AST [sink]. */ /** Convenience overload to compile raw [code] with a Mini-AST [sink]. */
suspend fun compileWithMini(code: String, sink: net.sergeych.lyng.miniast.MiniAstSink?): Script = suspend fun compileWithMini(code: String, sink: MiniAstSink?): Script =
compileWithMini(Source("<eval>", code), Script.defaultImportManager, sink) compileWithMini(Source("<eval>", code), Script.defaultImportManager, sink)
private var lastPriority = 0 private var lastPriority = 0
// Helpers for conservative constant folding (literal-only). Only pure, side-effect-free ops. // Helpers for conservative constant folding (literal-only). Only pure, side-effect-free ops.
private fun constOf(r: ObjRef): Obj? = (r as? ConstRef)?.constValue private fun constOf(r: ObjRef): Obj? = (r as? ConstRef)?.constValue
@ -2404,30 +2566,35 @@ class Compiler(
a is ObjChar && b is ObjChar -> if (a.value == b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value == b.value) ObjTrue else ObjFalse
else -> null else -> null
} }
BinOp.NEQ -> when { BinOp.NEQ -> when {
a is ObjInt && b is ObjInt -> if (a.value != b.value) ObjTrue else ObjFalse a is ObjInt && b is ObjInt -> if (a.value != b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value != b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value != b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value != b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value != b.value) ObjTrue else ObjFalse
else -> null else -> null
} }
BinOp.LT -> when { BinOp.LT -> when {
a is ObjInt && b is ObjInt -> if (a.value < b.value) ObjTrue else ObjFalse a is ObjInt && b is ObjInt -> if (a.value < b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value < b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value < b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value < b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value < b.value) ObjTrue else ObjFalse
else -> null else -> null
} }
BinOp.LTE -> when { BinOp.LTE -> when {
a is ObjInt && b is ObjInt -> if (a.value <= b.value) ObjTrue else ObjFalse a is ObjInt && b is ObjInt -> if (a.value <= b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value <= b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value <= b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value <= b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value <= b.value) ObjTrue else ObjFalse
else -> null else -> null
} }
BinOp.GT -> when { BinOp.GT -> when {
a is ObjInt && b is ObjInt -> if (a.value > b.value) ObjTrue else ObjFalse a is ObjInt && b is ObjInt -> if (a.value > b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value > b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value > b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value > b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value > b.value) ObjTrue else ObjFalse
else -> null else -> null
} }
BinOp.GTE -> when { BinOp.GTE -> when {
a is ObjInt && b is ObjInt -> if (a.value >= b.value) ObjTrue else ObjFalse a is ObjInt && b is ObjInt -> if (a.value >= b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value >= b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value >= b.value) ObjTrue else ObjFalse
@ -2441,6 +2608,7 @@ class Compiler(
a is ObjString && b is ObjString -> ObjString(a.value + b.value) a is ObjString && b is ObjString -> ObjString(a.value + b.value)
else -> null else -> null
} }
BinOp.MINUS -> if (a is ObjInt && b is ObjInt) ObjInt(a.value - b.value) else null BinOp.MINUS -> if (a is ObjInt && b is ObjInt) ObjInt(a.value - b.value) else null
BinOp.STAR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value * b.value) else null BinOp.STAR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value * b.value) else null
BinOp.SLASH -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt(a.value / b.value) else null BinOp.SLASH -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt(a.value / b.value) else null
@ -2468,6 +2636,7 @@ class Compiler(
is ObjReal -> ObjReal(-a.value) is ObjReal -> ObjReal(-a.value)
else -> null else -> null
} }
UnaryOp.BITNOT -> if (a is ObjInt) ObjInt(a.value.inv()) else null UnaryOp.BITNOT -> if (a is ObjInt) ObjInt(a.value.inv()) else null
} }
} }
@ -2638,5 +2807,5 @@ class Compiler(
} }
} }
suspend fun eval(code: String) = Compiler.compile(code).execute() suspend fun eval(code: String) = compile(code).execute()

View File

@ -15,6 +15,8 @@
* *
*/ */
@file:Suppress("INLINE_NOT_NEEDED", "REDUNDANT_INLINE")
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.* import net.sergeych.lyng.*
@ -62,9 +64,9 @@ enum class BinOp {
/** R-value reference for unary operations. */ /** R-value reference for unary operations. */
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef { class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val v = if (fastRval) a.evalValue(scope) else a.get(scope).value val v = if (fastRval) a.evalValue(scope) else a.get(scope).value
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { if (PerfFlags.PRIMITIVE_FASTOPS) {
val rFast: Obj? = when (op) { val rFast: Obj? = when (op) {
UnaryOp.NOT -> if (v is ObjBool) if (!v.value) ObjTrue else ObjFalse else null UnaryOp.NOT -> if (v is ObjBool) if (!v.value) ObjTrue else ObjFalse else null
UnaryOp.NEGATE -> when (v) { UnaryOp.NEGATE -> when (v) {
@ -75,7 +77,7 @@ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
UnaryOp.BITNOT -> if (v is ObjInt) ObjInt(v.value.inv()) else null UnaryOp.BITNOT -> if (v is ObjInt) ObjInt(v.value.inv()) else null
} }
if (rFast != null) { if (rFast != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return rFast.asReadonly return rFast.asReadonly
} }
} }
@ -91,11 +93,11 @@ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
/** R-value reference for binary operations. */ /** R-value reference for binary operations. */
class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef { class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value val a = if (PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value
val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value val b = if (PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value
// Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS) // Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS)
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { if (PerfFlags.PRIMITIVE_FASTOPS) {
// Fast boolean ops when both operands are ObjBool // Fast boolean ops when both operands are ObjBool
if (a is ObjBool && b is ObjBool) { if (a is ObjBool && b is ObjBool) {
val r: Obj? = when (op) { val r: Obj? = when (op) {
@ -106,7 +108,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> null else -> null
} }
if (r != null) { if (r != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return r.asReadonly return r.asReadonly
} }
} }
@ -134,7 +136,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> null else -> null
} }
if (r != null) { if (r != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return r.asReadonly return r.asReadonly
} }
} }
@ -151,7 +153,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> null else -> null
} }
if (r != null) { if (r != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return r.asReadonly return r.asReadonly
} }
} }
@ -169,7 +171,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> null else -> null
} }
if (r != null) { if (r != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return r.asReadonly return r.asReadonly
} }
} }
@ -177,19 +179,19 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
if (op == BinOp.PLUS) { if (op == BinOp.PLUS) {
when { when {
a is ObjString && b is ObjInt -> { a is ObjString && b is ObjInt -> {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return ObjString(a.value + b.value.toString()).asReadonly return ObjString(a.value + b.value.toString()).asReadonly
} }
a is ObjString && b is ObjChar -> { a is ObjString && b is ObjChar -> {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return ObjString(a.value + b.value).asReadonly return ObjString(a.value + b.value).asReadonly
} }
b is ObjString && a is ObjInt -> { b is ObjString && a is ObjInt -> {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return ObjString(a.value.toString() + b.value).asReadonly return ObjString(a.value.toString() + b.value).asReadonly
} }
b is ObjString && a is ObjChar -> { b is ObjString && a is ObjChar -> {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return ObjString(a.value.toString() + b.value).asReadonly return ObjString(a.value.toString() + b.value).asReadonly
} }
} }
@ -213,7 +215,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> null else -> null
} }
if (rNum != null) { if (rNum != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++
return rNum.asReadonly return rNum.asReadonly
} }
} }
@ -260,7 +262,7 @@ class ConditionalRef(
private val ifFalse: ObjRef private val ifFalse: ObjRef
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val condVal = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) condition.evalValue(scope) else condition.get(scope).value val condVal = if (PerfFlags.RVAL_FASTPATH) condition.evalValue(scope) else condition.get(scope).value
val condTrue = when (condVal) { val condTrue = when (condVal) {
is ObjBool -> condVal.value is ObjBool -> condVal.value
is ObjInt -> condVal.value != 0L is ObjInt -> condVal.value != 0L
@ -279,8 +281,8 @@ class CastRef(
private val atPos: Pos, private val atPos: Pos,
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val v0 = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) valueRef.evalValue(scope) else valueRef.get(scope).value val v0 = if (PerfFlags.RVAL_FASTPATH) valueRef.evalValue(scope) else valueRef.get(scope).value
val t = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) typeRef.evalValue(scope) else typeRef.get(scope).value val t = if (PerfFlags.RVAL_FASTPATH) typeRef.evalValue(scope) else typeRef.get(scope).value
val target = (t as? ObjClass) ?: scope.raiseClassCastError("${'$'}t is not the class instance") val target = (t as? ObjClass) ?: scope.raiseClassCastError("${'$'}t is not the class instance")
// unwrap qualified views // unwrap qualified views
val v = when (v0) { val v = when (v0) {
@ -323,7 +325,7 @@ class AssignOpRef(
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val x = target.get(scope).value val x = target.get(scope).value
val y = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value val y = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
val inPlace: Obj? = when (op) { val inPlace: Obj? = when (op) {
BinOp.PLUS -> x.plusAssign(scope, y) BinOp.PLUS -> x.plusAssign(scope, y)
BinOp.MINUS -> x.minusAssign(scope, y) BinOp.MINUS -> x.minusAssign(scope, y)
@ -380,7 +382,7 @@ class IncDecRef(
/** Elvis operator reference: a ?: b */ /** Elvis operator reference: a ?: b */
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
val r = if (a != ObjNull) a else if (fastRval) right.evalValue(scope) else right.get(scope).value val r = if (a != ObjNull) a else if (fastRval) right.evalValue(scope) else right.get(scope).value
return r.asReadonly return r.asReadonly
@ -390,8 +392,8 @@ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
/** Logical OR with short-circuit: a || b */ /** Logical OR with short-circuit: a || b */
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val fastPrim = net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS val fastPrim = PerfFlags.PRIMITIVE_FASTOPS
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly
val b = if (fastRval) right.evalValue(scope) else right.get(scope).value val b = if (fastRval) right.evalValue(scope) else right.get(scope).value
@ -408,8 +410,8 @@ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef
class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
// Hoist flags to locals for JIT friendliness // Hoist flags to locals for JIT friendliness
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val fastPrim = net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS val fastPrim = PerfFlags.PRIMITIVE_FASTOPS
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly
val b = if (fastRval) right.evalValue(scope) else right.get(scope).value val b = if (fastRval) right.evalValue(scope) else right.get(scope).value
@ -459,18 +461,18 @@ class FieldRef(
private var rAccesses: Int = 0; private var rMisses: Int = 0; private var rPromotedTo4: Boolean = false private var rAccesses: Int = 0; private var rMisses: Int = 0; private var rPromotedTo4: Boolean = false
private var wAccesses: Int = 0; private var wMisses: Int = 0; private var wPromotedTo4: Boolean = false private var wAccesses: Int = 0; private var wMisses: Int = 0; private var wPromotedTo4: Boolean = false
private inline fun size4ReadsEnabled(): Boolean = private inline fun size4ReadsEnabled(): Boolean =
net.sergeych.lyng.PerfFlags.FIELD_PIC_SIZE_4 || PerfFlags.FIELD_PIC_SIZE_4 ||
(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 && rPromotedTo4) (PerfFlags.PIC_ADAPTIVE_2_TO_4 && rPromotedTo4)
private inline fun size4WritesEnabled(): Boolean = private inline fun size4WritesEnabled(): Boolean =
net.sergeych.lyng.PerfFlags.FIELD_PIC_SIZE_4 || PerfFlags.FIELD_PIC_SIZE_4 ||
(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 && wPromotedTo4) (PerfFlags.PIC_ADAPTIVE_2_TO_4 && wPromotedTo4)
private fun noteReadHit() { private fun noteReadHit() {
if (!net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4) return if (!PerfFlags.PIC_ADAPTIVE_2_TO_4) return
val a = (rAccesses + 1).coerceAtMost(1_000_000) val a = (rAccesses + 1).coerceAtMost(1_000_000)
rAccesses = a rAccesses = a
} }
private fun noteReadMiss() { private fun noteReadMiss() {
if (!net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4) return if (!PerfFlags.PIC_ADAPTIVE_2_TO_4) return
val a = (rAccesses + 1).coerceAtMost(1_000_000) val a = (rAccesses + 1).coerceAtMost(1_000_000)
rAccesses = a rAccesses = a
rMisses = (rMisses + 1).coerceAtMost(1_000_000) rMisses = (rMisses + 1).coerceAtMost(1_000_000)
@ -482,12 +484,12 @@ class FieldRef(
} }
} }
private fun noteWriteHit() { private fun noteWriteHit() {
if (!net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4) return if (!PerfFlags.PIC_ADAPTIVE_2_TO_4) return
val a = (wAccesses + 1).coerceAtMost(1_000_000) val a = (wAccesses + 1).coerceAtMost(1_000_000)
wAccesses = a wAccesses = a
} }
private fun noteWriteMiss() { private fun noteWriteMiss() {
if (!net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4) return if (!PerfFlags.PIC_ADAPTIVE_2_TO_4) return
val a = (wAccesses + 1).coerceAtMost(1_000_000) val a = (wAccesses + 1).coerceAtMost(1_000_000)
wAccesses = a wAccesses = a
wMisses = (wMisses + 1).coerceAtMost(1_000_000) wMisses = (wMisses + 1).coerceAtMost(1_000_000)
@ -498,15 +500,15 @@ class FieldRef(
} }
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC val fieldPic = PerfFlags.FIELD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) return ObjNull.asMutable if (base == ObjNull && isOptional) return ObjNull.asMutable
if (fieldPic) { if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base) val (key, ver) = receiverKeyAndVersion(base)
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
noteReadHit() noteReadHit()
val rec0 = g(base, scope) val rec0 = g(base, scope)
if (base is ObjClass) { if (base is ObjClass) {
@ -516,7 +518,7 @@ class FieldRef(
return rec0 return rec0
} } } }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
noteReadHit() noteReadHit()
// move-to-front: promote 2→1 // move-to-front: promote 2→1
val tK = rKey2; val tV = rVer2; val tG = rGetter2 val tK = rKey2; val tV = rVer2; val tG = rGetter2
@ -530,7 +532,7 @@ class FieldRef(
return rec0 return rec0
} } } }
if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
noteReadHit() noteReadHit()
// move-to-front: promote 3→1 // move-to-front: promote 3→1
val tK = rKey3; val tV = rVer3; val tG = rGetter3 val tK = rKey3; val tV = rVer3; val tG = rGetter3
@ -545,7 +547,7 @@ class FieldRef(
return rec0 return rec0
} } } }
if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
noteReadHit() noteReadHit()
// move-to-front: promote 4→1 // move-to-front: promote 4→1
val tK = rKey4; val tV = rVer4; val tG = rGetter4 val tK = rKey4; val tV = rVer4; val tG = rGetter4
@ -561,7 +563,7 @@ class FieldRef(
return rec0 return rec0
} } } }
// Slow path // Slow path
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicMiss++ if (picCounters) PerfStats.fieldPicMiss++
noteReadMiss() noteReadMiss()
val rec = try { val rec = try {
base.readField(scope, name) base.readField(scope, name)
@ -606,8 +608,8 @@ class FieldRef(
} }
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC val fieldPic = PerfFlags.FIELD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = target.get(scope).value val base = target.get(scope).value
if (base == ObjNull && isOptional) { if (base == ObjNull && isOptional) {
// no-op on null receiver for optional chaining assignment // no-op on null receiver for optional chaining assignment
@ -629,12 +631,12 @@ class FieldRef(
if (fieldPic) { if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base) val (key, ver) = receiverKeyAndVersion(base)
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit() noteWriteHit()
return s(base, scope, newValue) return s(base, scope, newValue)
} } } }
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) { wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit() noteWriteHit()
// move-to-front: promote 2→1 // move-to-front: promote 2→1
val tK = wKey2; val tV = wVer2; val tS = wSetter2 val tK = wKey2; val tV = wVer2; val tS = wSetter2
@ -643,7 +645,7 @@ class FieldRef(
return s(base, scope, newValue) return s(base, scope, newValue)
} } } }
if (size4WritesEnabled()) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) { if (size4WritesEnabled()) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit() noteWriteHit()
// move-to-front: promote 3→1 // move-to-front: promote 3→1
val tK = wKey3; val tV = wVer3; val tS = wSetter3 val tK = wKey3; val tV = wVer3; val tS = wSetter3
@ -653,7 +655,7 @@ class FieldRef(
return s(base, scope, newValue) return s(base, scope, newValue)
} } } }
if (size4WritesEnabled()) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) { if (size4WritesEnabled()) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++ if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit() noteWriteHit()
// move-to-front: promote 4→1 // move-to-front: promote 4→1
val tK = wKey4; val tV = wVer4; val tS = wSetter4 val tK = wKey4; val tV = wVer4; val tS = wSetter4
@ -664,7 +666,7 @@ class FieldRef(
return s(base, scope, newValue) return s(base, scope, newValue)
} } } }
// Slow path // Slow path
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetMiss++ if (picCounters) PerfStats.fieldPicSetMiss++
noteWriteMiss() noteWriteMiss()
base.writeField(scope, name, newValue) base.writeField(scope, name, newValue)
// Install move-to-front with a handle-aware setter; honor PIC size flag // Install move-to-front with a handle-aware setter; honor PIC size flag
@ -707,26 +709,26 @@ class FieldRef(
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
// Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths // Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC val fieldPic = PerfFlags.FIELD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) return ObjNull if (base == ObjNull && isOptional) return ObjNull
if (fieldPic) { if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base) val (key, ver) = receiverKeyAndVersion(base)
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
return g(base, scope).value return g(base, scope).value
} } } }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
val tK = rKey2; val tV = rVer2; val tG = rGetter2 val tK = rKey2; val tV = rVer2; val tG = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value return g(base, scope).value
} } } }
if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
val tK = rKey3; val tV = rVer3; val tG = rGetter3 val tK = rKey3; val tV = rVer3; val tG = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
@ -734,7 +736,7 @@ class FieldRef(
return g(base, scope).value return g(base, scope).value
} } } }
if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
val tK = rKey4; val tV = rVer4; val tG = rGetter4 val tK = rKey4; val tV = rVer4; val tG = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
@ -742,7 +744,7 @@ class FieldRef(
rKey1 = tK; rVer1 = tV; rGetter1 = tG rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value return g(base, scope).value
} } } }
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicMiss++ if (picCounters) PerfStats.fieldPicMiss++
val rec = base.readField(scope, name) val rec = base.readField(scope, name)
// install primary generic getter for this shape // install primary generic getter for this shape
when (base) { when (base) {
@ -789,7 +791,7 @@ class IndexRef(
else -> 0L to -1 else -> 0L to -1
} }
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) return ObjNull.asMutable if (base == ObjNull && isOptional) return ObjNull.asMutable
val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value
@ -810,7 +812,7 @@ class IndexRef(
val v = base.map[idx] ?: ObjNull val v = base.map[idx] ?: ObjNull
return v.asMutable return v.asMutable
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC) { if (PerfFlags.INDEX_PIC) {
// Polymorphic inline cache for other common shapes // Polymorphic inline cache for other common shapes
val (key, ver) = when (base) { val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
@ -819,26 +821,26 @@ class IndexRef(
} }
if (key != 0L) { if (key != 0L) {
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
return g(base, scope, idx).asMutable return g(base, scope, idx).asMutable
} } } }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey2; val tv = rVer2; val tg = rGetter2 val tk = rKey2; val tv = rVer2; val tg = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable return g(base, scope, idx).asMutable
} } } }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { if (PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey3; val tv = rVer3; val tg = rGetter3 val tk = rKey3; val tv = rVer3; val tg = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable return g(base, scope, idx).asMutable
} } } }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { if (PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey4; val tv = rVer4; val tg = rGetter4 val tk = rKey4; val tv = rVer4; val tg = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
@ -847,9 +849,9 @@ class IndexRef(
return g(base, scope, idx).asMutable return g(base, scope, idx).asMutable
} } } }
// Miss: resolve and install generic handler // Miss: resolve and install generic handler
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicMiss++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicMiss++
val v = base.getAt(scope, idx) val v = base.getAt(scope, idx)
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) { if (PerfFlags.INDEX_PIC_SIZE_4) {
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
} }
@ -863,7 +865,7 @@ class IndexRef(
} }
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) return ObjNull if (base == ObjNull && isOptional) return ObjNull
val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value
@ -882,7 +884,7 @@ class IndexRef(
if (base is ObjMap && idx is ObjString) { if (base is ObjMap && idx is ObjString) {
return base.map[idx] ?: ObjNull return base.map[idx] ?: ObjNull
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC) { if (PerfFlags.INDEX_PIC) {
// PIC path analogous to get(), but returning raw Obj // PIC path analogous to get(), but returning raw Obj
val (key, ver) = when (base) { val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
@ -891,26 +893,26 @@ class IndexRef(
} }
if (key != 0L) { if (key != 0L) {
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
return g(base, scope, idx) return g(base, scope, idx)
} } } }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey2; val tv = rVer2; val tg = rGetter2 val tk = rKey2; val tv = rVer2; val tg = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx) return g(base, scope, idx)
} } } }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { if (PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey3; val tv = rVer3; val tg = rGetter3 val tk = rKey3; val tv = rVer3; val tg = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx) return g(base, scope, idx)
} } } }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { if (PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey4; val tv = rVer4; val tg = rGetter4 val tk = rKey4; val tv = rVer4; val tg = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
@ -918,9 +920,9 @@ class IndexRef(
rKey1 = tk; rVer1 = tv; rGetter1 = tg rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx) return g(base, scope, idx)
} } } }
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.indexPicMiss++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicMiss++
val v = base.getAt(scope, idx) val v = base.getAt(scope, idx)
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) { if (PerfFlags.INDEX_PIC_SIZE_4) {
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
} }
@ -934,7 +936,7 @@ class IndexRef(
} }
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) { if (base == ObjNull && isOptional) {
// no-op on null receiver for optional chaining assignment // no-op on null receiver for optional chaining assignment
@ -953,7 +955,7 @@ class IndexRef(
base.map[idx] = newValue base.map[idx] = newValue
return return
} }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC) { if (PerfFlags.INDEX_PIC) {
// Polymorphic inline cache for index write // Polymorphic inline cache for index write
val (key, ver) = when (base) { val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
@ -968,14 +970,14 @@ class IndexRef(
wKey1 = tk; wVer1 = tv; wSetter1 = ts wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return s(base, scope, idx, newValue); return
} } } }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) { if (PerfFlags.INDEX_PIC_SIZE_4) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
val tk = wKey3; val tv = wVer3; val ts = wSetter3 val tk = wKey3; val tv = wVer3; val ts = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return s(base, scope, idx, newValue); return
} } } }
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) { if (PerfFlags.INDEX_PIC_SIZE_4) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
val tk = wKey4; val tv = wVer4; val ts = wSetter4 val tk = wKey4; val tv = wVer4; val ts = wSetter4
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
@ -985,7 +987,7 @@ class IndexRef(
} } } }
// Miss: perform write and install generic handler // Miss: perform write and install generic handler
base.putAt(scope, idx, newValue) base.putAt(scope, idx, newValue)
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) { if (PerfFlags.INDEX_PIC_SIZE_4) {
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
} }
@ -1016,8 +1018,8 @@ class CallRef(
private val isOptionalInvoke: Boolean, private val isOptionalInvoke: Boolean,
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val usePool = net.sergeych.lyng.PerfFlags.SCOPE_POOL val usePool = PerfFlags.SCOPE_POOL
val callee = if (fastRval) target.evalValue(scope) else target.get(scope).value val callee = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly
val callArgs = args.toArguments(scope, tailBlock) val callArgs = args.toArguments(scope, tailBlock)
@ -1055,20 +1057,20 @@ class MethodCallRef(
private var mWindowAccesses: Int = 0 private var mWindowAccesses: Int = 0
private var mWindowMisses: Int = 0 private var mWindowMisses: Int = 0
private inline fun size4MethodsEnabled(): Boolean = private inline fun size4MethodsEnabled(): Boolean =
net.sergeych.lyng.PerfFlags.METHOD_PIC_SIZE_4 || PerfFlags.METHOD_PIC_SIZE_4 ||
((net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 || net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_METHODS_ONLY) && mPromotedTo4 && mFreezeWindowsLeft == 0) ((PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY) && mPromotedTo4 && mFreezeWindowsLeft == 0)
private fun noteMethodHit() { private fun noteMethodHit() {
if (!(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 || net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return if (!(PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return
val a = (mAccesses + 1).coerceAtMost(1_000_000) val a = (mAccesses + 1).coerceAtMost(1_000_000)
mAccesses = a mAccesses = a
if (net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_HEURISTIC) { if (PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
// Windowed tracking // Windowed tracking
mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000) mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000)
if (mWindowAccesses >= 256) endHeuristicWindow() if (mWindowAccesses >= 256) endHeuristicWindow()
} }
} }
private fun noteMethodMiss() { private fun noteMethodMiss() {
if (!(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 || net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return if (!(PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return
val a = (mAccesses + 1).coerceAtMost(1_000_000) val a = (mAccesses + 1).coerceAtMost(1_000_000)
mAccesses = a mAccesses = a
mMisses = (mMisses + 1).coerceAtMost(1_000_000) mMisses = (mMisses + 1).coerceAtMost(1_000_000)
@ -1076,7 +1078,7 @@ class MethodCallRef(
if (mMisses * 100 / a > 20) mPromotedTo4 = true if (mMisses * 100 / a > 20) mPromotedTo4 = true
mAccesses = 0; mMisses = 0 mAccesses = 0; mMisses = 0
} }
if (net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_HEURISTIC) { if (PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000) mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000)
mWindowMisses = (mWindowMisses + 1).coerceAtMost(1_000_000) mWindowMisses = (mWindowMisses + 1).coerceAtMost(1_000_000)
if (mWindowAccesses >= 256) endHeuristicWindow() if (mWindowAccesses >= 256) endHeuristicWindow()
@ -1106,26 +1108,26 @@ class MethodCallRef(
} }
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH val fastRval = PerfFlags.RVAL_FASTPATH
val methodPic = net.sergeych.lyng.PerfFlags.METHOD_PIC val methodPic = PerfFlags.METHOD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = if (fastRval) receiver.evalValue(scope) else receiver.get(scope).value val base = if (fastRval) receiver.evalValue(scope) else receiver.get(scope).value
if (base == ObjNull && isOptional) return ObjNull.asReadonly if (base == ObjNull && isOptional) return ObjNull.asReadonly
val callArgs = args.toArguments(scope, tailBlock) val callArgs = args.toArguments(scope, tailBlock)
if (methodPic) { if (methodPic) {
val (key, ver) = receiverKeyAndVersion(base) val (key, ver) = receiverKeyAndVersion(base)
mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) { mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ if (picCounters) PerfStats.methodPicHit++
noteMethodHit() noteMethodHit()
return inv(base, scope, callArgs).asReadonly return inv(base, scope, callArgs).asReadonly
} } } }
mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) { mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ if (picCounters) PerfStats.methodPicHit++
noteMethodHit() noteMethodHit()
return inv(base, scope, callArgs).asReadonly return inv(base, scope, callArgs).asReadonly
} } } }
if (size4MethodsEnabled()) mInvoker3?.let { inv -> if (key == mKey3 && ver == mVer3) { if (size4MethodsEnabled()) mInvoker3?.let { inv -> if (key == mKey3 && ver == mVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ if (picCounters) PerfStats.methodPicHit++
noteMethodHit() noteMethodHit()
// move-to-front: promote 3→1 // move-to-front: promote 3→1
val tK = mKey3; val tV = mVer3; val tI = mInvoker3 val tK = mKey3; val tV = mVer3; val tI = mInvoker3
@ -1135,7 +1137,7 @@ class MethodCallRef(
return inv(base, scope, callArgs).asReadonly return inv(base, scope, callArgs).asReadonly
} } } }
if (size4MethodsEnabled()) mInvoker4?.let { inv -> if (key == mKey4 && ver == mVer4) { if (size4MethodsEnabled()) mInvoker4?.let { inv -> if (key == mKey4 && ver == mVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++ if (picCounters) PerfStats.methodPicHit++
noteMethodHit() noteMethodHit()
// move-to-front: promote 4→1 // move-to-front: promote 4→1
val tK = mKey4; val tV = mVer4; val tI = mInvoker4 val tK = mKey4; val tV = mVer4; val tI = mInvoker4
@ -1146,7 +1148,7 @@ class MethodCallRef(
return inv(base, scope, callArgs).asReadonly return inv(base, scope, callArgs).asReadonly
} } } }
// Slow path // Slow path
if (picCounters) net.sergeych.lyng.PerfStats.methodPicMiss++ if (picCounters) PerfStats.methodPicMiss++
noteMethodMiss() noteMethodMiss()
val result = try { val result = try {
base.invokeInstanceMethod(scope, name, callArgs) base.invokeInstanceMethod(scope, name, callArgs)
@ -1232,34 +1234,26 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
// 1) Try fast slot/local // 1) Try fast slot/local
if (!PerfFlags.LOCAL_SLOT_PIC) { if (!PerfFlags.LOCAL_SLOT_PIC) {
scope.getSlotIndexOf(name)?.let { scope.getSlotIndexOf(name)?.let {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicHit++
return scope.getSlotRecord(it) return scope.getSlotRecord(it)
} }
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
// 2) Fallback to current-scope object or field on `this` // 2) Fallback to current-scope object or field on `this`
scope[name]?.let { return it } scope[name]?.let { return it }
val th = scope.thisObj return scope.thisObj.readField(scope, name)
return when (th) {
is Obj -> th.readField(scope, name)
else -> scope.raiseError("symbol not defined: '$name'")
}
} }
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope) val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) { if (slot >= 0) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) { if (PerfFlags.PIC_DEBUG_COUNTERS) {
if (hit) net.sergeych.lyng.PerfStats.localVarPicHit++ else net.sergeych.lyng.PerfStats.localVarPicMiss++ if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++
} }
return scope.getSlotRecord(slot) return scope.getSlotRecord(slot)
} }
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
// 2) Fallback name in scope or field on `this` // 2) Fallback name in scope or field on `this`
scope[name]?.let { return it } scope[name]?.let { return it }
val th = scope.thisObj return scope.thisObj.readField(scope, name)
return when (th) {
is Obj -> th.readField(scope, name)
else -> scope.raiseError("symbol not defined: '$name'")
}
} }
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
@ -1268,22 +1262,14 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value } scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
// fallback to current-scope object or field on `this` // fallback to current-scope object or field on `this`
scope[name]?.let { return it.value } scope[name]?.let { return it.value }
val th = scope.thisObj return scope.thisObj.readField(scope, name).value
return when (th) {
is Obj -> th.readField(scope, name).value
else -> scope.raiseError("symbol not defined: '$name'")
}
} }
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope) val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) return scope.getSlotRecord(slot).value if (slot >= 0) return scope.getSlotRecord(slot).value
// Fallback name in scope or field on `this` // Fallback name in scope or field on `this`
scope[name]?.let { return it.value } scope[name]?.let { return it.value }
val th = scope.thisObj return scope.thisObj.readField(scope, name).value
return when (th) {
is Obj -> th.readField(scope, name).value
else -> scope.raiseError("symbol not defined: '$name'")
}
} }
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
@ -1301,13 +1287,9 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
return return
} }
// Fallback: write to field on `this` // Fallback: write to field on `this`
val th = scope.thisObj scope.thisObj.writeField(scope, name, newValue)
if (th is Obj) {
th.writeField(scope, name, newValue)
return return
} }
scope.raiseError("symbol not defined: '$name'")
}
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope) val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
if (slot >= 0) { if (slot >= 0) {
val rec = scope.getSlotRecord(slot) val rec = scope.getSlotRecord(slot)
@ -1320,13 +1302,9 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
else scope.raiseError("Cannot assign to immutable value") else scope.raiseError("Cannot assign to immutable value")
return return
} }
val th = scope.thisObj scope.thisObj.writeField(scope, name, newValue)
if (th is Obj) {
th.writeField(scope, name, newValue)
return return
} }
scope.raiseError("symbol not defined: '$name'")
}
} }
@ -1401,8 +1379,8 @@ class FastLocalVarRef(
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope) val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
val actualOwner = cachedOwnerScope val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) { if (slot >= 0 && actualOwner != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) { if (PerfFlags.PIC_DEBUG_COUNTERS) {
if (ownerValid) net.sergeych.lyng.PerfStats.fastLocalHit++ else net.sergeych.lyng.PerfStats.fastLocalMiss++ if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
} }
return actualOwner.getSlotRecord(slot) return actualOwner.getSlotRecord(slot)
} }
@ -1423,9 +1401,7 @@ class FastLocalVarRef(
// Fallback to standard name lookup (locals or closure chain) if the slot owner changed across suspension // Fallback to standard name lookup (locals or closure chain) if the slot owner changed across suspension
scope[name]?.let { return it } scope[name]?.let { return it }
// As a last resort, treat as field on `this` // As a last resort, treat as field on `this`
val th = scope.thisObj return scope.thisObj.readField(scope, name)
if (th is Obj) return th.readField(scope, name)
scope.raiseError("local '$name' is not available in this scope")
} }
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
@ -1450,9 +1426,7 @@ class FastLocalVarRef(
} }
// Fallback to standard name lookup (locals or closure chain) // Fallback to standard name lookup (locals or closure chain)
scope[name]?.let { return it.value } scope[name]?.let { return it.value }
val th = scope.thisObj return scope.thisObj.readField(scope, name).value
if (th is Obj) return th.readField(scope, name).value
scope.raiseError("local '$name' is not available in this scope")
} }
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
@ -1485,13 +1459,9 @@ class FastLocalVarRef(
else scope.raiseError("Cannot assign to immutable value") else scope.raiseError("Cannot assign to immutable value")
return return
} }
val th = scope.thisObj scope.thisObj.writeField(scope, name, newValue)
if (th is Obj) {
th.writeField(scope, name, newValue)
return return
} }
scope.raiseError("local '$name' is not available in this scope")
}
} }
class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef { class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
@ -1502,11 +1472,11 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
for (e in entries) { for (e in entries) {
when (e) { when (e) {
is ListEntry.Element -> { is ListEntry.Element -> {
val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value val v = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
list += v list += v
} }
is ListEntry.Spread -> { is ListEntry.Spread -> {
val elements = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value val elements = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
when (elements) { when (elements) {
is ObjList -> { is ObjList -> {
// Grow underlying array once when possible // Grow underlying array once when possible
@ -1531,8 +1501,8 @@ class RangeRef(
private val isEndInclusive: Boolean private val isEndInclusive: Boolean
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val l = left?.let { if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull val l = left?.let { if (PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
val r = right?.let { if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull val r = right?.let { if (PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
} }
} }
@ -1544,7 +1514,7 @@ class AssignRef(
private val atPos: Pos, private val atPos: Pos,
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value val v = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
val rec = target.get(scope) val rec = target.get(scope)
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable") if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
if (rec.value.assign(scope, v) == null) { if (rec.value.assign(scope, v) == null) {

View File

@ -0,0 +1,154 @@
/*
* Copyright 2025 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.
*
*/
/*
* Named arguments and named splats test suite
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.eval
import kotlin.test.Test
import kotlin.test.assertFailsWith
class NamedArgsTest {
@Test
fun basicNamedArgsAndDefaults() = runTest {
eval(
"""
fun test(a="foo", b="bar", c="bazz") { [a, b, c] }
assertEquals( ["foo", "b", "bazz"], test(b: "b") )
assertEquals( ["a", "bar", "c"], test("a", c: "c") )
""".trimIndent()
)
}
@Test
fun positionalAfterNamedIsError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a, b) { [a,b] }
f(a: 1, 2)
""".trimIndent()
)
}
}
@Test
fun namedSplatsBasic() = runTest {
eval(
"""
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
val r = test("A?", ...Map("d" => "D!", "b" => "B!"))
assertEquals(["A?","B!","c","D!"], r)
""".trimIndent()
)
}
@Test
fun namedSplatsNonStringKeysError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun test(a,b) {}
test(1, ...Map(1 => "x"))
""".trimIndent()
)
}
}
@Test
fun trailingBlockConflictWhenLastNamed() = runTest {
// Error: last parameter already assigned by a named argument; trailing block must be rejected
assertFailsWith<ExecutionError> {
eval(
"""
fun f(x, onDone) { onDone(x) }
// Name the last parameter inside parentheses, then try to pass a trailing block
f(1, onDone: { it }) { 42 }
""".trimIndent()
)
}
// Normal case still works when last parameter is not assigned by name
eval(
"""
fun f(x, onDone) { onDone(x) }
var res = 0
f(1) { it -> res = it }
assertEquals(1, res)
""".trimIndent()
)
}
@Test
fun duplicateNamedIsError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a,b,c) {}
f(a: 1, a: 2)
""".trimIndent()
)
}
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a,b,c) {}
f(a: 1, ...Map("a" => 2))
""".trimIndent()
)
}
}
@Test
fun unknownParameterIsError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a,b) {}
f(z: 1)
""".trimIndent()
)
}
}
@Test
fun ellipsisCannotBeNamed() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun g(args..., tail) {}
g(args: [1], tail: 2)
""".trimIndent()
)
}
}
@Test
fun positionalSplatAfterNamedIsError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a,b,c) {}
f(a: 1, ...[2,3])
""".trimIndent()
)
}
}
}

View File

@ -2569,7 +2569,6 @@ class ScriptTest {
x += i x += i
} }
delay(100) delay(100)
println("-> "+x)
assert(x == 5050) assert(x == 5050)
} }
""".trimIndent()) """.trimIndent())
@ -3544,6 +3543,40 @@ class ScriptTest {
""".trimIndent()) """.trimIndent())
} }
@Test
fun testCallAndResultOrder() = runTest {
eval("""
import lyng.stdlib
fun test(a="a", b="b", c="c") { [a, b, c] }
// the parentheses here are in fact unnecessary:
val ok1 = (test { void }).last()
assert( ok1 is Callable)
// it should work without them, as the call test() {} must be executed
// first, then the result should be used to call methods on it:
// the parentheses here are in fact unnecessary:
val ok2 = test { void }.last()
assert( ok2 is Callable)
""".trimIndent())
}
// @Test
// fun namedArgsProposal() = runTest {
// eval("""
// import lyng.stdlib
//
// fun test(a="a", b="b", c="c") { [a, b, c] }
//
// val l = (test{ void }).last()
// println(l)
//
// """.trimIndent())
// }
// @Ignore // @Ignore
// @Test // @Test

View File

@ -67,6 +67,7 @@ fun HomePage() {
I({ classes("bi", "bi-braces", "me-1") }) I({ classes("bi", "bi-braces", "me-1") })
Text("Try Lyng") Text("Try Lyng")
} }
// (Telegram button moved to the bottom of the page)
} }
} }
} }
@ -105,4 +106,17 @@ assertEquals([4, 16], evens2)
} }
} }
} }
// Bottom section with a small Telegram button
Div({ classes("text-center", "mt-5", "pb-4") }) {
A(attrs = {
classes("btn", "btn-outline-primary", "btn-sm")
attr("href", "https://t.me/lynglang")
attr("target", "_blank")
attr("rel", "noopener noreferrer")
}) {
I({ classes("bi", "bi-telegram", "me-1") })
Text("Join our Telegram channel")
}
}
} }

View File

@ -146,9 +146,44 @@
[data-bs-theme="dark"] .hl-lbl { color: #ffa657; } [data-bs-theme="dark"] .hl-lbl { color: #ffa657; }
[data-bs-theme="dark"] .hl-dir { color: #d2a8ff; } [data-bs-theme="dark"] .hl-dir { color: #d2a8ff; }
[data-bs-theme="dark"] .hl-err { color: #ffa198; text-decoration-color: #ffa198; } [data-bs-theme="dark"] .hl-err { color: #ffa198; text-decoration-color: #ffa198; }
/* Top-left corner ribbon for version label */
.corner-ribbon {
position: fixed;
/* Place below the fixed navbar (Bootstrap fixed-top ~ z-index: 1030) */
z-index: 1020; /* above content, below navbar */
top: var(--navbar-offset, 56px);
left: 0;
width: 162px; /* 10% narrower than 180px */
text-align: center;
/* Slightly asymmetric padding to visually center text within rotated band (desktop) */
/* Nudge text a bit lower on large screens */
padding: .42rem 0 .28rem;
font-weight: 600;
font-size: .75rem; /* make text smaller by default */
line-height: 1.1; /* stabilize vertical centering */
transform: translate(-52px, 10px) rotate(-45deg);
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,.2);
border: 1px solid rgba(0,0,0,.15);
pointer-events: none; /* decorative; don't block clicks underneath */
}
@media (max-width: 576px) {
.corner-ribbon {
width: 135px; /* 10% narrower than 150px */
transform: translate(-50px, 8px) rotate(-45deg);
font-size: .7rem; /* even smaller on phones */
padding: .32rem 0 .28rem; /* keep mobile text centered too */
}
}
</style> </style>
</head> </head>
<body> <body>
<!-- Top-left version ribbon -->
<div class="corner-ribbon bg-danger text-white">
<span class="me-3">
v1.0.1
</span>
</div>
<!-- Fixed top navbar for the whole site --> <!-- Fixed top navbar for the whole site -->
<a href="#root" class="visually-hidden-focusable position-absolute top-0 start-0 m-2 px-2 py-1 bg-body border rounded">Skip to content</a> <a href="#root" class="visually-hidden-focusable position-absolute top-0 start-0 m-2 px-2 py-1 bg-body border rounded">Skip to content</a>
<nav class="navbar navbar-expand-lg bg-body-tertiary fixed-top border-bottom" role="navigation" aria-label="Primary"> <nav class="navbar navbar-expand-lg bg-body-tertiary fixed-top border-bottom" role="navigation" aria-label="Primary">

View File

@ -63,4 +63,29 @@ class HighlightSmokeTest {
assertTrue(html.contains("hl-op")) assertTrue(html.contains("hl-op"))
} }
@Test
fun highlightNamedArgsAndMapSplat() {
val text = """
fun test(a,b,c,d) { [a,b,c,d] }
val r = test("A?", c: "C!", ...Map("d" => "D!", "b" => "B!"))
""".trimIndent()
val spans = SimpleLyngHighlighter().highlight(text)
val labeled = spansToLabeled(text, spans)
// Ensure identifier for function name appears
assertTrue(labeled.any { it.first == "test" && it.second == HighlightKind.Identifier })
// Ensure colon for named argument is tokenized as punctuation
assertTrue(labeled.any { it.first == ":" && it.second == HighlightKind.Punctuation })
// Ensure ellipsis operator present
assertTrue(labeled.any { it.first == "..." && it.second == HighlightKind.Operator })
// Ensure Map identifier is present
assertTrue(labeled.any { it.first == "Map" && it.second == HighlightKind.Identifier })
}
@Test
fun highlightNamedArgsHtml() {
val text = "val res = test( a: 1, b: 2 )"
val html = SiteHighlight.renderHtml(text)
// Expect a colon wrapped as punctuation span
assertTrue(html.contains("<span class=\"hl-punc\">:</span>"))
}
} }