Compare commits

..

5 Commits

19 changed files with 1134 additions and 390 deletions

View File

@ -2,6 +2,23 @@
### 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:
- Active C3 Method Resolution Order (MRO) for deterministic, monotonic lookup across complex hierarchies and diamonds.
- Qualified dispatch:

View File

@ -5,7 +5,7 @@ lambdas and class declarations.
## Regular
## default values
## Default values
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]
>>> 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

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) )
until it reaches asymptotic limit 0.00001% change
return null or found limit
*/
fun findSumLimit(f) {

View File

@ -25,6 +25,7 @@ Files
- Operators including ranges (`..`, `..<`, `...`), null-safe (`?.`, `?[`, `?(`, `?{`, `?:`, `??`), arrows (`->`, `=>`, `::`), match operators (`=~`, `!~`), bitwise, arithmetic, etc.
- Shuttle operator `<=>`
- 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)
---------------------------------------------------
@ -56,6 +57,7 @@ Notes and limitations
---------------------
- 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.
- 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
--------------

View File

@ -2,7 +2,7 @@
"name": "lyng-textmate",
"displayName": "Lyng",
"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",
"license": "Apache-2.0",
"engines": { "vscode": "^1.0.0" },

View File

@ -13,6 +13,7 @@
{ "include": "#keywords" },
{ "include": "#constants" },
{ "include": "#types" },
{ "include": "#namedArgs" },
{ "include": "#annotations" },
{ "include": "#labels" },
{ "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}_]*" } ] },
"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}_]*:" } ] },
"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" } } } ] },

View File

@ -31,6 +31,15 @@ repositories {
}
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 {
binaries {
executable {

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
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
@ -72,6 +72,15 @@ kotlin {
nodejs()
}
// Suppress Beta warning for expect/actual classes across all targets
targets.configureEach {
compilations.configureEach {
compilerOptions.configure {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
@ -141,6 +150,40 @@ tasks.withType<org.gradle.api.tasks.testing.Test> {
showStandardStreams = true
}
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 {

View File

@ -57,72 +57,137 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
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 paramsSize: Int
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
assign(params.last(), arguments.list.last())
assign(lastParam, arguments.list.last())
callArgs = arguments.list.dropLast(1)
} else {
paramsSize = params.size
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
while (i != paramsSize) {
var hp = headPos
while (i < paramsSize) {
val a = params[i]
if (a.isEllipsis) break
val value = when {
i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(scope)
else -> {
// println("callArgs: ${callArgs.joinToString()}")
// println("tailBlockMode: ${arguments.tailBlockMode}")
scope.raiseIllegalArgument("too few arguments for the call")
}
}
if (assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (hp < callArgs.size) callArgs[hp++]
else a.defaultValue?.execute(scope)
?: scope.raiseIllegalArgument("too few arguments for the call")
assign(a, value)
}
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 j = callArgs.size - 1
while (i > index) {
var tp = tailStart
while (i > startExclusive) {
val a = params[i]
if (a.isEllipsis) break
val value = when {
j >= index -> {
callArgs[j--]
}
a.defaultValue != null -> a.defaultValue.execute(scope)
else -> scope.raiseIllegalArgument("too few arguments for the call")
}
if (i < assignedByName.size && assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (tp >= headPosBound) callArgs[tp--]
else a.defaultValue?.execute(scope)
?: scope.raiseIllegalArgument("too few arguments for the call")
assign(a, value)
}
i--
}
return j
return tp
}
fun processEllipsis(index: Int, toFromIndex: Int) {
fun processEllipsis(index: Int, headPos: Int, tailPos: Int) {
val a = params[index]
val l = if (index > toFromIndex) ObjList()
else ObjList(callArgs.subList(index, toFromIndex + 1).toMutableList())
val from = headPos
val to = tailPos
val l = if (from > to) ObjList()
else ObjList(callArgs.subList(from, to + 1).toMutableList())
assign(a, l)
}
val leftIndex = processHead(0)
if (leftIndex < paramsSize) {
val end = processTail(leftIndex)
processEllipsis(leftIndex, end)
// Locate ellipsis index within considered parameters
val ellipsisIndex = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
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 {
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")
}
}

View File

@ -17,24 +17,27 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjIterable
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.*
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 {
// 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) {
val limit = if (PerfFlags.ARG_SMALL_ARITY_12) 12 else 8
var hasSplat = false
var hasSplatOrNamed = false
var count = 0
for (pa in this) {
if (pa.isSplat) { hasSplat = true; break }
if (pa.isSplat || pa.name != null) { hasSplatOrNamed = true; break }
count++
if (count > limit) break
}
if (!hasSplat && count == this.size) {
if (!hasSplatOrNamed && count == this.size) {
val quick = when (count) {
0 -> Arguments.EMPTY
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
}
}
// 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
if (PerfFlags.ARG_BUILDER) {
val b = ArgBuilderProvider.acquire()
try {
b.reset(this.size)
for (x in this) {
// General path: build positional list and named map, enforcing ordering rules
val positional: MutableList<Obj> = mutableListOf()
var named: MutableMap<String, Obj>? = null
var namedSeen = false
for ((idx, x) in this.withIndex()) {
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)
if (x.isSplat) {
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 -> {
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) -> {
if (namedSeen) scope.raiseIllegalArgument("positional splat cannot follow named arguments")
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 {
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)
} finally {
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)
}
val namedFinal = named ?: emptyMap()
return Arguments(positional, tailBlockMode, namedFinal)
}
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())
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
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
return when (v) {
net.sergeych.lyng.obj.ObjNull,
net.sergeych.lyng.obj.ObjTrue,
net.sergeych.lyng.obj.ObjFalse,
return when (val v = list.first()) {
ObjNull,
ObjTrue,
ObjFalse,
// Immutable scalars: safe to return directly
is net.sergeych.lyng.obj.ObjInt,
is net.sergeych.lyng.obj.ObjReal,
is net.sergeych.lyng.obj.ObjChar,
is net.sergeych.lyng.obj.ObjString -> v
is ObjInt,
is ObjReal,
is ObjChar,
is ObjString -> v
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)
private val paramSlotPlanStack = mutableListOf<Map<String, Int>>()
private val currentParamSlotPlan: Map<String, Int>?
get() = paramSlotPlanStack.lastOrNull()
// private val currentParamSlotPlan: Map<String, Int>?
// get() = paramSlotPlanStack.lastOrNull()
// Track identifiers known to be locals/parameters in the current function for fast local emission
private val localNamesStack = mutableListOf<MutableSet<String>>()
@ -50,7 +50,11 @@ class Compiler(
private inline fun <T> withLocalNames(names: Set<String>, block: () -> T): T {
localNamesStack.add(names.toMutableSet())
return try { block() } finally { localNamesStack.removeLast() }
return try {
block()
} finally {
localNamesStack.removeLast()
}
}
private fun declareLocalName(name: String) {
@ -86,6 +90,7 @@ class Compiler(
if (t.startsWith("*")) t.removePrefix("*").trimStart() else line
}
}
else -> raw
}
}
@ -158,6 +163,7 @@ class Compiler(
// A standalone newline not immediately following a comment resets doc buffer
if (!prevWasComment) clearPendingDoc() else prevWasComment = false
}
else -> {}
}
cc.next()
@ -191,12 +197,15 @@ class Compiler(
val start = Pos(pos.source, pos.line, col)
val end = Pos(pos.source, pos.line, col + p.length)
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
miniSink?.onImport(
net.sergeych.lyng.miniast.MiniImport(
net.sergeych.lyng.miniast.MiniRange(pos, lastEnd),
MiniImport(
MiniRange(pos, lastEnd),
segs
)
)
@ -241,7 +250,10 @@ class Compiler(
Script(start, statements)
}.also {
// 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
while (true) {
val opToken = cc.next()
val op = byLevel[level][opToken.type]
if (op == null) {
@ -552,11 +563,14 @@ class Compiler(
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
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(
left,
blockArgument = true,
t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
isOptional = t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
)
} ?: parseLambdaExpression()
}
@ -778,7 +792,11 @@ class Compiler(
val typeStart = cc.currentPos()
var lastEnd = typeStart
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
segments += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos))
lastEnd = cc.currentPos()
@ -796,8 +814,11 @@ class Compiler(
// Helper to build MiniTypeRef (base or generic)
fun buildBaseRef(rangeEnd: Pos, args: List<MiniTypeRef>?, nullable: Boolean): MiniTypeRef {
val base = MiniTypeName(MiniRange(typeStart, rangeEnd), segments.toList(), nullable = false)
return if (args == null || args.isEmpty()) base.copy(range = MiniRange(typeStart, rangeEnd), nullable = nullable)
else net.sergeych.lyng.miniast.MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable)
return if (args == null || args.isEmpty()) base.copy(
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)
@ -811,12 +832,17 @@ class Compiler(
var argFirst = true
val argStart = cc.currentPos()
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
argSegs += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos))
val p = cc.savePos()
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 argEnd = cc.currentPos()
@ -825,7 +851,9 @@ class Compiler(
val sep = cc.next()
when (sep.type) {
Token.Type.COMMA -> { /* continue */ }
Token.Type.COMMA -> { /* continue */
}
Token.Type.GT -> break
else -> sep.raiseSyntax("expected ',' or '>' in generic arguments")
}
@ -853,6 +881,21 @@ class Compiler(
private suspend fun parseArgs(): Pair<List<ParsedArgument>, Boolean> {
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 {
val t = cc.next()
when (t.type) {
@ -867,10 +910,14 @@ class Compiler(
else -> {
cc.previous()
val named = tryParseNamedArg()
if (named != null) {
args += named
} else {
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
if (cc.current().type == Token.Type.COLON)
parseTypeDeclaration()
// In call-site arguments, ':' is reserved for named args. Do not parse type declarations here.
}
// Here should be a valid termination:
}
}
@ -901,6 +948,20 @@ class Compiler(
*/
private suspend fun parseArgsNoTailBlock(): List<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 {
val t = cc.next()
when (t.type) {
@ -915,10 +976,14 @@ class Compiler(
else -> {
cc.previous()
val named = tryParseNamedArg()
if (named != null) {
args += named
} else {
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
if (cc.current().type == Token.Type.COLON)
parseTypeDeclaration()
// Do not parse type declarations in call args
}
}
}
} while (t.type != Token.Type.RPAREN)
@ -934,11 +999,14 @@ class Compiler(
): ObjRef {
var detectedBlockArgument = blockArgument
val args = if (blockArgument) {
val blockArg = ParsedArgument(
parseExpression()
?: throw ScriptError(cc.currentPos(), "lambda body expected"), cc.currentPos()
)
listOf(blockArg)
// Leading '{' has already been consumed by the caller token branch.
// Parse only the lambda expression as the last argument and DO NOT
// allow any subsequent selectors (like ".last()") to be absorbed
// into the lambda body. This ensures expected order:
// foo { ... }.bar() == (foo { ... }).bar()
val callableAccessor = parseLambdaExpression()
val argStmt = statement { callableAccessor.get(this).value }
listOf(ParsedArgument(argStmt, cc.currentPos()))
} else {
val r = parseArgs()
detectedBlockArgument = r.second
@ -1058,6 +1126,7 @@ class Compiler(
pendingDeclDoc = consumePendingDoc()
parseVarDeclaration(false, Visibility.Public)
}
"var" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
@ -1069,6 +1138,7 @@ class Compiler(
pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false)
}
"fn" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
@ -1085,11 +1155,24 @@ class Compiler(
when (k.value) {
"val" -> parseVarDeclaration(false, Visibility.Private, isStatic = isStatic)
"var" -> parseVarDeclaration(true, Visibility.Private, isStatic = isStatic)
"fun" -> parseFunctionDeclaration(visibility = Visibility.Private, isOpen = false, isExtern = false, isStatic = isStatic)
"fn" -> parseFunctionDeclaration(visibility = Visibility.Private, isOpen = false, isExtern = false, isStatic = isStatic)
"fun" -> parseFunctionDeclaration(
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}")
}
}
"protected" -> {
var k = cc.requireToken(Token.Type.ID, "declaration expected after 'protected'")
var isStatic = false
@ -1100,11 +1183,24 @@ class Compiler(
when (k.value) {
"val" -> parseVarDeclaration(false, Visibility.Protected, isStatic = isStatic)
"var" -> parseVarDeclaration(true, Visibility.Protected, isStatic = isStatic)
"fun" -> parseFunctionDeclaration(visibility = Visibility.Protected, isOpen = false, isExtern = false, isStatic = isStatic)
"fn" -> parseFunctionDeclaration(visibility = Visibility.Protected, isOpen = false, isExtern = false, isStatic = isStatic)
"fun" -> parseFunctionDeclaration(
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}")
}
}
"while" -> parseWhileStatement()
"do" -> parseDoWhileStatement()
"for" -> parseForStatement()
@ -1116,11 +1212,13 @@ class Compiler(
pendingDeclDoc = consumePendingDoc()
parseClassDeclaration()
}
"enum" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseEnumDeclaration()
}
"try" -> parseTryStatement()
"throw" -> parseThrowStatement(id.pos)
"when" -> parseWhenStatement()
@ -1130,9 +1228,10 @@ class Compiler(
val isExtern = cc.skipId("extern")
when {
cc.matchQualifiers("fun", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc();
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(Visibility.Private, isExtern)
}
cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration(
Visibility.Private,
isExtern,
@ -1149,27 +1248,78 @@ class Compiler(
cc.matchQualifiers("fun", "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("fn") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern) }
cc.matchQualifiers("fun") -> {
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,
Visibility.Private,
isStatic = true
) }
)
}
cc.matchQualifiers("val", "static") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(false, 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(
cc.matchQualifiers("val", "static") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
false,
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,
Visibility.Private,
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 -> {
cc.next()
null
@ -1306,9 +1456,10 @@ class Compiler(
errorObject.extraData,
errorObject.useStackTrace
)
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? ")" )?
data class BaseSpec(val name: String, val args: List<ParsedArgument>?)
val baseSpecs = mutableListOf<BaseSpec>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
do {
@ -1516,13 +1668,13 @@ class Compiler(
val declRange = MiniRange(pendingDeclStart ?: nameToken.pos, cc.currentPos())
val bases = baseSpecs.map { it.name }
// 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 ->
for (p in ad.params) {
val at = p.accessType
if (at != null) {
val mutable = at == AccessType.Var
ctorFields += net.sergeych.lyng.miniast.MiniCtorField(
ctorFields += MiniCtorField(
name = p.name,
mutable = mutable,
type = p.miniType,
@ -1571,7 +1723,8 @@ class Compiler(
// accessors, constructor registration, etc.
// Resolve parent classes by name at execution time
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")
}
@ -2082,7 +2235,7 @@ class Compiler(
val paramNames: Set<String> = argsDeclaration.params.map { it.name }.toSet()
// Parse function body while tracking declared locals to compute precise capacity hints
val fnLocalDeclStart = currentLocalDeclCount
currentLocalDeclCount
localDeclCountStack.add(0)
val fnStatements = if (isExtern)
statement { raiseError("extern function not provided: $name") }
@ -2113,7 +2266,7 @@ class Compiler(
}
fnStatements.execute(context)
}
val enclosingCtx = parentContext
parentContext
val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure
// 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
companion object {}
companion object
}
@ -2377,15 +2530,24 @@ class Compiler(
* Compile [source] while streaming a Mini-AST into the provided [sink].
* When [sink] is null, behaves like [compile].
*/
suspend fun compileWithMini(source: Source, importManager: ImportProvider, sink: net.sergeych.lyng.miniast.MiniAstSink?): Script {
return Compiler(CompilerContext(parseLyng(source)), importManager, Settings(miniAstSink = sink)).parseScript()
suspend fun compileWithMini(
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]. */
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)
private var lastPriority = 0
// Helpers for conservative constant folding (literal-only). Only pure, side-effect-free ops.
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
else -> null
}
BinOp.NEQ -> when {
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 ObjChar && b is ObjChar -> if (a.value != b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.LT -> when {
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 ObjChar && b is ObjChar -> if (a.value < b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.LTE -> when {
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 ObjChar && b is ObjChar -> if (a.value <= b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.GT -> when {
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 ObjChar && b is ObjChar -> if (a.value > b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.GTE -> when {
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
@ -2441,6 +2608,7 @@ class Compiler(
a is ObjString && b is ObjString -> ObjString(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.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)
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
import net.sergeych.lyng.*
@ -62,9 +64,9 @@ enum class BinOp {
/** R-value reference for unary operations. */
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val fastRval = PerfFlags.RVAL_FASTPATH
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) {
UnaryOp.NOT -> if (v is ObjBool) if (!v.value) ObjTrue else ObjFalse else null
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
}
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
}
}
@ -91,11 +93,11 @@ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
/** R-value reference for binary operations. */
class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
val a = if (net.sergeych.lyng.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 a = if (PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.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)
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
if (PerfFlags.PRIMITIVE_FASTOPS) {
// Fast boolean ops when both operands are ObjBool
if (a is ObjBool && b is ObjBool) {
val r: Obj? = when (op) {
@ -106,7 +108,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> 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
}
}
@ -134,7 +136,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> 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
}
}
@ -151,7 +153,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> 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
}
}
@ -169,7 +171,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> 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
}
}
@ -177,19 +179,19 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
if (op == BinOp.PLUS) {
when {
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
}
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
}
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
}
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
}
}
@ -213,7 +215,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
else -> 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
}
}
@ -260,7 +262,7 @@ class ConditionalRef(
private val ifFalse: ObjRef
) : ObjRef {
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) {
is ObjBool -> condVal.value
is ObjInt -> condVal.value != 0L
@ -279,8 +281,8 @@ class CastRef(
private val atPos: Pos,
) : ObjRef {
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 t = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) typeRef.evalValue(scope) else typeRef.get(scope).value
val v0 = if (PerfFlags.RVAL_FASTPATH) valueRef.evalValue(scope) else valueRef.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")
// unwrap qualified views
val v = when (v0) {
@ -323,7 +325,7 @@ class AssignOpRef(
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
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) {
BinOp.PLUS -> x.plusAssign(scope, y)
BinOp.MINUS -> x.minusAssign(scope, y)
@ -380,7 +382,7 @@ class IncDecRef(
/** Elvis operator reference: a ?: b */
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
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 r = if (a != ObjNull) a else if (fastRval) right.evalValue(scope) else right.get(scope).value
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 */
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val fastPrim = net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS
val fastRval = PerfFlags.RVAL_FASTPATH
val fastPrim = PerfFlags.PRIMITIVE_FASTOPS
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly
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 {
override suspend fun get(scope: Scope): ObjRecord {
// Hoist flags to locals for JIT friendliness
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val fastPrim = net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS
val fastRval = PerfFlags.RVAL_FASTPATH
val fastPrim = PerfFlags.PRIMITIVE_FASTOPS
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly
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 wAccesses: Int = 0; private var wMisses: Int = 0; private var wPromotedTo4: Boolean = false
private inline fun size4ReadsEnabled(): Boolean =
net.sergeych.lyng.PerfFlags.FIELD_PIC_SIZE_4 ||
(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 && rPromotedTo4)
PerfFlags.FIELD_PIC_SIZE_4 ||
(PerfFlags.PIC_ADAPTIVE_2_TO_4 && rPromotedTo4)
private inline fun size4WritesEnabled(): Boolean =
net.sergeych.lyng.PerfFlags.FIELD_PIC_SIZE_4 ||
(net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_2_TO_4 && wPromotedTo4)
PerfFlags.FIELD_PIC_SIZE_4 ||
(PerfFlags.PIC_ADAPTIVE_2_TO_4 && wPromotedTo4)
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)
rAccesses = a
}
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)
rAccesses = a
rMisses = (rMisses + 1).coerceAtMost(1_000_000)
@ -482,12 +484,12 @@ class FieldRef(
}
}
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)
wAccesses = a
}
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)
wAccesses = a
wMisses = (wMisses + 1).coerceAtMost(1_000_000)
@ -498,15 +500,15 @@ class FieldRef(
}
override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS
val fastRval = PerfFlags.RVAL_FASTPATH
val fieldPic = PerfFlags.FIELD_PIC
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) return ObjNull.asMutable
if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base)
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
if (picCounters) PerfStats.fieldPicHit++
noteReadHit()
val rec0 = g(base, scope)
if (base is ObjClass) {
@ -516,7 +518,7 @@ class FieldRef(
return rec0
} }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
if (picCounters) PerfStats.fieldPicHit++
noteReadHit()
// move-to-front: promote 2→1
val tK = rKey2; val tV = rVer2; val tG = rGetter2
@ -530,7 +532,7 @@ class FieldRef(
return rec0
} }
if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
if (picCounters) PerfStats.fieldPicHit++
noteReadHit()
// move-to-front: promote 3→1
val tK = rKey3; val tV = rVer3; val tG = rGetter3
@ -545,7 +547,7 @@ class FieldRef(
return rec0
} }
if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
if (picCounters) PerfStats.fieldPicHit++
noteReadHit()
// move-to-front: promote 4→1
val tK = rKey4; val tV = rVer4; val tG = rGetter4
@ -561,7 +563,7 @@ class FieldRef(
return rec0
} }
// Slow path
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicMiss++
if (picCounters) PerfStats.fieldPicMiss++
noteReadMiss()
val rec = try {
base.readField(scope, name)
@ -606,8 +608,8 @@ class FieldRef(
}
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS
val fieldPic = PerfFlags.FIELD_PIC
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = target.get(scope).value
if (base == ObjNull && isOptional) {
// no-op on null receiver for optional chaining assignment
@ -629,12 +631,12 @@ class FieldRef(
if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base)
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++
if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit()
return s(base, scope, newValue)
} }
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++
if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit()
// move-to-front: promote 2→1
val tK = wKey2; val tV = wVer2; val tS = wSetter2
@ -643,7 +645,7 @@ class FieldRef(
return s(base, scope, newValue)
} }
if (size4WritesEnabled()) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++
if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit()
// move-to-front: promote 3→1
val tK = wKey3; val tV = wVer3; val tS = wSetter3
@ -653,7 +655,7 @@ class FieldRef(
return s(base, scope, newValue)
} }
if (size4WritesEnabled()) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetHit++
if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit()
// move-to-front: promote 4→1
val tK = wKey4; val tV = wVer4; val tS = wSetter4
@ -664,7 +666,7 @@ class FieldRef(
return s(base, scope, newValue)
} }
// Slow path
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicSetMiss++
if (picCounters) PerfStats.fieldPicSetMiss++
noteWriteMiss()
base.writeField(scope, name, newValue)
// 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 {
// Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val fieldPic = net.sergeych.lyng.PerfFlags.FIELD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS
val fastRval = PerfFlags.RVAL_FASTPATH
val fieldPic = PerfFlags.FIELD_PIC
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) return ObjNull
if (fieldPic) {
val (key, ver) = receiverKeyAndVersion(base)
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicHit++
if (picCounters) PerfStats.fieldPicHit++
return g(base, scope).value
} }
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
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value
} }
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
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
@ -734,7 +736,7 @@ class FieldRef(
return g(base, scope).value
} }
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
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
@ -742,7 +744,7 @@ class FieldRef(
rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value
} }
if (picCounters) net.sergeych.lyng.PerfStats.fieldPicMiss++
if (picCounters) PerfStats.fieldPicMiss++
val rec = base.readField(scope, name)
// install primary generic getter for this shape
when (base) {
@ -789,7 +791,7 @@ class IndexRef(
else -> 0L to -1
}
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
if (base == ObjNull && isOptional) return ObjNull.asMutable
val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value
@ -810,7 +812,7 @@ class IndexRef(
val v = base.map[idx] ?: ObjNull
return v.asMutable
}
if (net.sergeych.lyng.PerfFlags.INDEX_PIC) {
if (PerfFlags.INDEX_PIC) {
// Polymorphic inline cache for other common shapes
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
@ -819,26 +821,26 @@ class IndexRef(
}
if (key != 0L) {
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
} }
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
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
} }
if (net.sergeych.lyng.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.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey3; val tv = rVer3; val tg = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
} }
if (net.sergeych.lyng.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.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey4; val tv = rVer4; val tg = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
@ -847,9 +849,9 @@ class IndexRef(
return g(base, scope, idx).asMutable
} }
// 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)
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) {
if (PerfFlags.INDEX_PIC_SIZE_4) {
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
}
@ -863,7 +865,7 @@ class IndexRef(
}
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
if (base == ObjNull && isOptional) return ObjNull
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) {
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
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
@ -891,26 +893,26 @@ class IndexRef(
}
if (key != 0L) {
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)
} }
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
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx)
} }
if (net.sergeych.lyng.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.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey3; val tv = rVer3; val tg = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx)
} }
if (net.sergeych.lyng.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.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++
val tk = rKey4; val tv = rVer4; val tg = rGetter4
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
@ -918,9 +920,9 @@ class IndexRef(
rKey1 = tk; rVer1 = tv; rGetter1 = tg
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)
if (net.sergeych.lyng.PerfFlags.INDEX_PIC_SIZE_4) {
if (PerfFlags.INDEX_PIC_SIZE_4) {
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
}
@ -934,7 +936,7 @@ class IndexRef(
}
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
if (base == ObjNull && isOptional) {
// no-op on null receiver for optional chaining assignment
@ -953,7 +955,7 @@ class IndexRef(
base.map[idx] = newValue
return
}
if (net.sergeych.lyng.PerfFlags.INDEX_PIC) {
if (PerfFlags.INDEX_PIC) {
// Polymorphic inline cache for index write
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
@ -968,14 +970,14 @@ class IndexRef(
wKey1 = tk; wVer1 = tv; wSetter1 = ts
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
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
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
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
@ -985,7 +987,7 @@ class IndexRef(
} }
// Miss: perform write and install generic handler
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
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
}
@ -1016,8 +1018,8 @@ class CallRef(
private val isOptionalInvoke: Boolean,
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val usePool = net.sergeych.lyng.PerfFlags.SCOPE_POOL
val fastRval = PerfFlags.RVAL_FASTPATH
val usePool = PerfFlags.SCOPE_POOL
val callee = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly
val callArgs = args.toArguments(scope, tailBlock)
@ -1055,20 +1057,20 @@ class MethodCallRef(
private var mWindowAccesses: Int = 0
private var mWindowMisses: Int = 0
private inline fun size4MethodsEnabled(): Boolean =
net.sergeych.lyng.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.METHOD_PIC_SIZE_4 ||
((PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY) && mPromotedTo4 && mFreezeWindowsLeft == 0)
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)
mAccesses = a
if (net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
if (PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
// Windowed tracking
mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000)
if (mWindowAccesses >= 256) endHeuristicWindow()
}
}
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)
mAccesses = a
mMisses = (mMisses + 1).coerceAtMost(1_000_000)
@ -1076,7 +1078,7 @@ class MethodCallRef(
if (mMisses * 100 / a > 20) mPromotedTo4 = true
mAccesses = 0; mMisses = 0
}
if (net.sergeych.lyng.PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
if (PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000)
mWindowMisses = (mWindowMisses + 1).coerceAtMost(1_000_000)
if (mWindowAccesses >= 256) endHeuristicWindow()
@ -1106,26 +1108,26 @@ class MethodCallRef(
}
override suspend fun get(scope: Scope): ObjRecord {
val fastRval = net.sergeych.lyng.PerfFlags.RVAL_FASTPATH
val methodPic = net.sergeych.lyng.PerfFlags.METHOD_PIC
val picCounters = net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS
val fastRval = PerfFlags.RVAL_FASTPATH
val methodPic = PerfFlags.METHOD_PIC
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
val base = if (fastRval) receiver.evalValue(scope) else receiver.get(scope).value
if (base == ObjNull && isOptional) return ObjNull.asReadonly
val callArgs = args.toArguments(scope, tailBlock)
if (methodPic) {
val (key, ver) = receiverKeyAndVersion(base)
mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++
if (picCounters) PerfStats.methodPicHit++
noteMethodHit()
return inv(base, scope, callArgs).asReadonly
} }
mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++
if (picCounters) PerfStats.methodPicHit++
noteMethodHit()
return inv(base, scope, callArgs).asReadonly
} }
if (size4MethodsEnabled()) mInvoker3?.let { inv -> if (key == mKey3 && ver == mVer3) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++
if (picCounters) PerfStats.methodPicHit++
noteMethodHit()
// move-to-front: promote 3→1
val tK = mKey3; val tV = mVer3; val tI = mInvoker3
@ -1135,7 +1137,7 @@ class MethodCallRef(
return inv(base, scope, callArgs).asReadonly
} }
if (size4MethodsEnabled()) mInvoker4?.let { inv -> if (key == mKey4 && ver == mVer4) {
if (picCounters) net.sergeych.lyng.PerfStats.methodPicHit++
if (picCounters) PerfStats.methodPicHit++
noteMethodHit()
// move-to-front: promote 4→1
val tK = mKey4; val tV = mVer4; val tI = mInvoker4
@ -1146,7 +1148,7 @@ class MethodCallRef(
return inv(base, scope, callArgs).asReadonly
} }
// Slow path
if (picCounters) net.sergeych.lyng.PerfStats.methodPicMiss++
if (picCounters) PerfStats.methodPicMiss++
noteMethodMiss()
val result = try {
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
if (!PerfFlags.LOCAL_SLOT_PIC) {
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)
}
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`
scope[name]?.let { return it }
val th = scope.thisObj
return when (th) {
is Obj -> th.readField(scope, name)
else -> scope.raiseError("symbol not defined: '$name'")
}
return scope.thisObj.readField(scope, name)
}
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) {
if (hit) net.sergeych.lyng.PerfStats.localVarPicHit++ else net.sergeych.lyng.PerfStats.localVarPicMiss++
if (PerfFlags.PIC_DEBUG_COUNTERS) {
if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++
}
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`
scope[name]?.let { return it }
val th = scope.thisObj
return when (th) {
is Obj -> th.readField(scope, name)
else -> scope.raiseError("symbol not defined: '$name'")
}
return scope.thisObj.readField(scope, name)
}
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 }
// fallback to current-scope object or field on `this`
scope[name]?.let { return it.value }
val th = scope.thisObj
return when (th) {
is Obj -> th.readField(scope, name).value
else -> scope.raiseError("symbol not defined: '$name'")
}
return scope.thisObj.readField(scope, name).value
}
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) return scope.getSlotRecord(slot).value
// Fallback name in scope or field on `this`
scope[name]?.let { return it.value }
val th = scope.thisObj
return when (th) {
is Obj -> th.readField(scope, name).value
else -> scope.raiseError("symbol not defined: '$name'")
}
return scope.thisObj.readField(scope, name).value
}
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
}
// Fallback: write to field on `this`
val th = scope.thisObj
if (th is Obj) {
th.writeField(scope, name, newValue)
scope.thisObj.writeField(scope, name, newValue)
return
}
scope.raiseError("symbol not defined: '$name'")
}
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
if (slot >= 0) {
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")
return
}
val th = scope.thisObj
if (th is Obj) {
th.writeField(scope, name, newValue)
scope.thisObj.writeField(scope, name, newValue)
return
}
scope.raiseError("symbol not defined: '$name'")
}
}
@ -1401,8 +1379,8 @@ class FastLocalVarRef(
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) {
if (ownerValid) net.sergeych.lyng.PerfStats.fastLocalHit++ else net.sergeych.lyng.PerfStats.fastLocalMiss++
if (PerfFlags.PIC_DEBUG_COUNTERS) {
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
}
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
scope[name]?.let { return it }
// As a last resort, treat as field on `this`
val th = scope.thisObj
if (th is Obj) return th.readField(scope, name)
scope.raiseError("local '$name' is not available in this scope")
return scope.thisObj.readField(scope, name)
}
override suspend fun evalValue(scope: Scope): Obj {
@ -1450,9 +1426,7 @@ class FastLocalVarRef(
}
// Fallback to standard name lookup (locals or closure chain)
scope[name]?.let { return it.value }
val th = scope.thisObj
if (th is Obj) return th.readField(scope, name).value
scope.raiseError("local '$name' is not available in this scope")
return scope.thisObj.readField(scope, name).value
}
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
@ -1485,13 +1459,9 @@ class FastLocalVarRef(
else scope.raiseError("Cannot assign to immutable value")
return
}
val th = scope.thisObj
if (th is Obj) {
th.writeField(scope, name, newValue)
scope.thisObj.writeField(scope, name, newValue)
return
}
scope.raiseError("local '$name' is not available in this scope")
}
}
class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
@ -1502,11 +1472,11 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
for (e in entries) {
when (e) {
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
}
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) {
is ObjList -> {
// Grow underlying array once when possible
@ -1531,8 +1501,8 @@ class RangeRef(
private val isEndInclusive: Boolean
) : ObjRef {
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 r = right?.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 (PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
}
}
@ -1544,7 +1514,7 @@ class AssignRef(
private val atPos: Pos,
) : ObjRef {
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)
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
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
}
delay(100)
println("-> "+x)
assert(x == 5050)
}
""".trimIndent())
@ -3544,6 +3543,40 @@ class ScriptTest {
""".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
// @Test

View File

@ -67,6 +67,7 @@ fun HomePage() {
I({ classes("bi", "bi-braces", "me-1") })
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-dir { color: #d2a8ff; }
[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>
</head>
<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 -->
<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">

View File

@ -63,4 +63,29 @@ class HighlightSmokeTest {
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>"))
}
}