Added delegation support: Delegate, lazy, "by" keyword for valr/var/fun and object {} singletons
This commit is contained in:
parent
8e766490d9
commit
5f819dc87a
62
docs/OOP.md
62
docs/OOP.md
@ -42,6 +42,35 @@ a _constructor_ that requires two parameters for fields. So when creating it wit
|
|||||||
Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the
|
Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the
|
||||||
example above.
|
example above.
|
||||||
|
|
||||||
|
## Singleton Objects
|
||||||
|
|
||||||
|
Singleton objects are declared using the `object` keyword. An `object` declaration defines both a class and a single instance of that class at the same time. This is perfect for stateless utilities, global configuration, or shared delegates.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object Config {
|
||||||
|
val version = "1.0.0"
|
||||||
|
val debug = true
|
||||||
|
|
||||||
|
fun printInfo() {
|
||||||
|
println("App version: " + version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
println(Config.version)
|
||||||
|
Config.printInfo()
|
||||||
|
```
|
||||||
|
|
||||||
|
Objects can also inherit from classes or interfaces:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object DefaultLogger : Logger("Default") {
|
||||||
|
override fun log(msg) {
|
||||||
|
println("[DEFAULT] " + msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
Properties allow you to define member accessors that look like fields but execute code when read or written. Unlike regular fields, properties in Lyng do **not** have automatic backing fields; they are pure accessors.
|
Properties allow you to define member accessors that look like fields but execute code when read or written. Unlike regular fields, properties in Lyng do **not** have automatic backing fields; they are pure accessors.
|
||||||
@ -122,6 +151,39 @@ println(service.data()) // Returns "Record 42" immediately (no second fetch)
|
|||||||
|
|
||||||
Note that `cached` returns a lambda, so you access the value by calling it like a method: `service.data()`. This is a powerful pattern for lazy-loading resources, caching results of database queries, or delaying expensive computations until they are truly needed.
|
Note that `cached` returns a lambda, so you access the value by calling it like a method: `service.data()`. This is a powerful pattern for lazy-loading resources, caching results of database queries, or delaying expensive computations until they are truly needed.
|
||||||
|
|
||||||
|
## Delegation
|
||||||
|
|
||||||
|
Delegation allows you to hand over the logic of a property or function to another object. This is done using the `by` keyword.
|
||||||
|
|
||||||
|
### Property Delegation
|
||||||
|
|
||||||
|
Instead of providing `get()` and `set()` accessors, you can delegate them to an object that implements the `getValue` and `setValue` methods.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class User {
|
||||||
|
var name by MyDelegate()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Delegation
|
||||||
|
|
||||||
|
You can also delegate a whole function to an object. When the function is called, it will invoke the delegate's `invoke` method.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun remoteAction by RemoteProxy("actionName")
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Unified Delegate Interface
|
||||||
|
|
||||||
|
A delegate is any object that provides the following methods (all optional depending on usage):
|
||||||
|
|
||||||
|
- `getValue(thisRef, name)`: Called when a delegated `val` or `var` is read.
|
||||||
|
- `setValue(thisRef, name, newValue)`: Called when a delegated `var` is written.
|
||||||
|
- `invoke(thisRef, name, args...)`: Called when a delegated `fun` is invoked.
|
||||||
|
- `bind(name, access, thisRef)`: Called once during initialization to configure or validate the delegate.
|
||||||
|
|
||||||
|
For more details and advanced patterns (like `lazy`, `observable`, and shared stateless delegates), see the [Delegation Guide](delegation.md).
|
||||||
|
|
||||||
## Instance initialization: init block
|
## Instance initialization: init block
|
||||||
|
|
||||||
In addition to the primary constructor arguments, you can provide an `init` block that runs on each instance creation. This is useful for more complex initializations, side effects, or setting up fields that depend on multiple constructor parameters.
|
In addition to the primary constructor arguments, you can provide an `init` block that runs on each instance creation. This is useful for more complex initializations, side effects, or setting up fields that depend on multiple constructor parameters.
|
||||||
|
|||||||
175
docs/delegation.md
Normal file
175
docs/delegation.md
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# Delegation in Lyng
|
||||||
|
|
||||||
|
Delegation is a powerful pattern that allows you to outsource the logic of properties (`val`, `var`) and functions (`fun`) to another object. This enables code reuse, separation of concerns, and the implementation of common patterns like lazy initialization, observable properties, and remote procedure calls (RPC) with minimal boilerplate.
|
||||||
|
|
||||||
|
## The `by` Keyword
|
||||||
|
|
||||||
|
Delegation is triggered using the `by` keyword in a declaration. The expression following `by` is evaluated once when the member is initialized, and the resulting object becomes the **delegate**.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
val x by MyDelegate()
|
||||||
|
var y by MyDelegate()
|
||||||
|
fun f by MyDelegate()
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Unified Delegate Model
|
||||||
|
|
||||||
|
A delegate object can implement any of the following methods to intercept member access. All methods receive the `thisRef` (the instance containing the member) and the `name` of the member.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
interface Delegate {
|
||||||
|
// Called when a 'val' or 'var' is read
|
||||||
|
fun getValue(thisRef, name)
|
||||||
|
|
||||||
|
// Called when a 'var' is assigned
|
||||||
|
fun setValue(thisRef, name, newValue)
|
||||||
|
|
||||||
|
// Called when a 'fun' is invoked
|
||||||
|
fun invoke(thisRef, name, args...)
|
||||||
|
|
||||||
|
// Optional: Called once during initialization to "bind" the delegate
|
||||||
|
// Can be used for validation or to return a different delegate instance
|
||||||
|
fun bind(name, access, thisRef) = this
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delegate Access Types
|
||||||
|
|
||||||
|
The `bind` method receives an `access` parameter of type `DelegateAccess`, which can be one of:
|
||||||
|
- `DelegateAccess.Val`
|
||||||
|
- `DelegateAccess.Var`
|
||||||
|
- `DelegateAccess.Callable` (for `fun`)
|
||||||
|
|
||||||
|
## Usage Cases and Examples
|
||||||
|
|
||||||
|
### 1. Lazy Initialization
|
||||||
|
|
||||||
|
The classic `lazy` pattern ensures a value is computed only when first accessed and then cached. In Lyng, `lazy` is implemented as a class that follows this pattern. While classes typically start with an uppercase letter, `lazy` is an exception to make its usage feel like a native language feature.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class lazy(val creator) : Delegate {
|
||||||
|
private var value = Unset
|
||||||
|
|
||||||
|
override fun bind(name, access, thisRef) {
|
||||||
|
if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef, name) {
|
||||||
|
if (value == Unset) {
|
||||||
|
value = creator()
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
val expensiveData by lazy {
|
||||||
|
println("Performing expensive computation...")
|
||||||
|
42
|
||||||
|
}
|
||||||
|
|
||||||
|
println(expensiveData) // Computes and prints 42
|
||||||
|
println(expensiveData) // Returns 42 immediately
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Observable Properties
|
||||||
|
|
||||||
|
Delegates can be used to react to property changes.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Observable(initialValue, val onChange) {
|
||||||
|
private var value = initialValue
|
||||||
|
|
||||||
|
fun getValue(thisRef, name) = value
|
||||||
|
|
||||||
|
fun setValue(thisRef, name, newValue) {
|
||||||
|
val oldValue = value
|
||||||
|
value = newValue
|
||||||
|
onChange(name, oldValue, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
var name by Observable("Guest") { name, old, new ->
|
||||||
|
println("Property %s changed from %s to %s"(name, old, new))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val u = User()
|
||||||
|
u.name = "Alice" // Prints: Property name changed from Guest to Alice
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Function Delegation (Proxies)
|
||||||
|
|
||||||
|
You can delegate an entire function to an object. This is particularly useful for implementing decorators or RPC clients.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object LoggerDelegate {
|
||||||
|
fun invoke(thisRef, name, args...) {
|
||||||
|
println("Calling function: " + name + " with args: " + args)
|
||||||
|
// Logic here...
|
||||||
|
"Result of " + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remoteAction by LoggerDelegate
|
||||||
|
|
||||||
|
println(remoteAction(1, 2, 3))
|
||||||
|
// Prints: Calling function: remoteAction with args: [1, 2, 3]
|
||||||
|
// Prints: Result of remoteAction
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Stateless Delegates (Shared Singletons)
|
||||||
|
|
||||||
|
Because `getValue`, `setValue`, and `invoke` receive `thisRef`, a single object can act as a delegate for multiple properties across many instances without any per-property memory overhead.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object Constant42 {
|
||||||
|
fun getValue(thisRef, name) = 42
|
||||||
|
}
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
val a by Constant42
|
||||||
|
val b by Constant42
|
||||||
|
}
|
||||||
|
|
||||||
|
val f = Foo()
|
||||||
|
assertEquals(42, f.a)
|
||||||
|
assertEquals(42, f.b)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Local Delegation
|
||||||
|
|
||||||
|
Delegation is not limited to class members; you can also use it for local variables inside functions.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun test() {
|
||||||
|
val x by LocalProxy(123)
|
||||||
|
println(x)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `bind` Hook
|
||||||
|
|
||||||
|
The `bind(name, access, thisRef)` method is called exactly once when the member is being initialized. It allows the delegate to:
|
||||||
|
1. **Validate usage**: Throw an error if the delegate is used with the wrong member type (e.g., `lazy` on a `var`).
|
||||||
|
2. **Initialize state**: Set up internal state based on the property name or the containing instance.
|
||||||
|
3. **Substitute itself**: Return a different object that will act as the actual delegate.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class ValidatedDelegate() {
|
||||||
|
fun bind(name, access, thisRef) {
|
||||||
|
if (access == DelegateAccess.Var) {
|
||||||
|
throw "This delegate cannot be used with 'var'"
|
||||||
|
}
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getValue(thisRef, name) = "Validated"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Delegation in Lyng combines the elegance of Kotlin-style properties with the flexibility of dynamic function interception. By unifying `val`, `var`, and `fun` delegation into a single model, Lyng provides a consistent and powerful tool for meta-programming and code reuse.
|
||||||
@ -87,6 +87,27 @@ Lyng supports simple enums for a fixed set of named constants. Declare with `enu
|
|||||||
|
|
||||||
For more details (usage patterns, `when` switching, serialization), see OOP notes: [Enums in detail](OOP.md#enums).
|
For more details (usage patterns, `when` switching, serialization), see OOP notes: [Enums in detail](OOP.md#enums).
|
||||||
|
|
||||||
|
## Singleton Objects
|
||||||
|
|
||||||
|
Singleton objects are declared using the `object` keyword. They define a class and create its single instance immediately.
|
||||||
|
|
||||||
|
object Logger {
|
||||||
|
fun log(msg) { println("[LOG] " + msg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.log("Hello singleton!")
|
||||||
|
|
||||||
|
## Delegation (briefly)
|
||||||
|
|
||||||
|
You can delegate properties and functions to other objects using the `by` keyword. This is perfect for patterns like `lazy` initialization.
|
||||||
|
|
||||||
|
val expensiveData by lazy {
|
||||||
|
// computed only once on demand
|
||||||
|
"computed"
|
||||||
|
}
|
||||||
|
|
||||||
|
For more details on these features, see [Delegation in Lyng](delegation.md) and [OOP notes](OOP.md).
|
||||||
|
|
||||||
When putting multiple statments in the same line it is convenient and recommended to use `;`:
|
When putting multiple statments in the same line it is convenient and recommended to use `;`:
|
||||||
|
|
||||||
var from; var to
|
var from; var to
|
||||||
|
|||||||
@ -93,6 +93,36 @@ let d = Derived()
|
|||||||
println((d as B).foo()) // Disambiguation via cast
|
println((d as B).foo()) // Disambiguation via cast
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Singleton Objects
|
||||||
|
Singleton objects are declared using the `object` keyword. They provide a convenient way to define a class and its single instance in one go.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object Config {
|
||||||
|
val version = "1.2.3"
|
||||||
|
fun show() = println("Config version: " + version)
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unified Delegation Model
|
||||||
|
A powerful new delegation system allows `val`, `var`, and `fun` members to delegate their logic to other objects using the `by` keyword.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
// Property delegation
|
||||||
|
val lazyValue by lazy { "expensive" }
|
||||||
|
|
||||||
|
// Function delegation
|
||||||
|
fun remoteAction by myProxy
|
||||||
|
|
||||||
|
// Observable properties
|
||||||
|
var name by Observable("initial") { n, old, new ->
|
||||||
|
println("Changed!")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The system features a unified interface (`getValue`, `setValue`, `invoke`) and a `bind` hook for initialization-time validation and configuration. See the [Delegation Guide](delegation.md) for more.
|
||||||
|
|
||||||
## Tooling and Infrastructure
|
## Tooling and Infrastructure
|
||||||
|
|
||||||
### CLI: Formatting Command
|
### CLI: Formatting Command
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -138,7 +138,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
} else {
|
} else {
|
||||||
val value = if (hp < callArgs.size) callArgs[hp++]
|
val value = if (hp < callArgs.size) callArgs[hp++]
|
||||||
else a.defaultValue?.execute(scope)
|
else a.defaultValue?.execute(scope)
|
||||||
?: scope.raiseIllegalArgument("too few arguments for the call")
|
?: scope.raiseIllegalArgument("too few arguments for the call (missing ${a.name})")
|
||||||
assign(a, value)
|
assign(a, value)
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
|
|||||||
@ -286,7 +286,7 @@ class Compiler(
|
|||||||
while (true) {
|
while (true) {
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
return when (t.type) {
|
return when (t.type) {
|
||||||
Token.Type.ID -> {
|
Token.Type.ID, Token.Type.OBJECT -> {
|
||||||
parseKeywordStatement(t)
|
parseKeywordStatement(t)
|
||||||
?: run {
|
?: run {
|
||||||
cc.previous()
|
cc.previous()
|
||||||
@ -337,7 +337,7 @@ class Compiler(
|
|||||||
|
|
||||||
private suspend fun parseExpression(): Statement? {
|
private suspend fun parseExpression(): Statement? {
|
||||||
val pos = cc.currentPos()
|
val pos = cc.currentPos()
|
||||||
return parseExpressionLevel()?.let { a -> statement(pos) { a.get(it).value } }
|
return parseExpressionLevel()?.let { a -> statement(pos) { a.evalValue(it) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun parseExpressionLevel(level: Int = 0): ObjRef? {
|
private suspend fun parseExpressionLevel(level: Int = 0): ObjRef? {
|
||||||
@ -1063,7 +1063,7 @@ class Compiler(
|
|||||||
val next = cc.peekNextNonWhitespace()
|
val next = cc.peekNextNonWhitespace()
|
||||||
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
|
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
|
||||||
val localVar = LocalVarRef(name, t1.pos)
|
val localVar = LocalVarRef(name, t1.pos)
|
||||||
return ParsedArgument(statement(t1.pos) { localVar.get(it).value }, t1.pos, isSplat = false, name = name)
|
return ParsedArgument(statement(t1.pos) { localVar.evalValue(it) }, t1.pos, isSplat = false, name = name)
|
||||||
}
|
}
|
||||||
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
|
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
|
||||||
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
|
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
|
||||||
@ -1135,7 +1135,7 @@ class Compiler(
|
|||||||
val next = cc.peekNextNonWhitespace()
|
val next = cc.peekNextNonWhitespace()
|
||||||
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
|
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
|
||||||
val localVar = LocalVarRef(name, t1.pos)
|
val localVar = LocalVarRef(name, t1.pos)
|
||||||
return ParsedArgument(statement(t1.pos) { localVar.get(it).value }, t1.pos, isSplat = false, name = name)
|
return ParsedArgument(statement(t1.pos) { localVar.evalValue(it) }, t1.pos, isSplat = false, name = name)
|
||||||
}
|
}
|
||||||
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
|
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
|
||||||
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
|
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
|
||||||
@ -1356,6 +1356,12 @@ class Compiler(
|
|||||||
parseClassDeclaration(isAbstract)
|
parseClassDeclaration(isAbstract)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"object" -> {
|
||||||
|
if (isStatic || isClosed || isOverride || isExtern || isAbstract)
|
||||||
|
throw ScriptError(currentToken.pos, "unsupported modifiers for object: ${modifiers.joinToString(" ")}")
|
||||||
|
parseObjectDeclaration()
|
||||||
|
}
|
||||||
|
|
||||||
"interface" -> {
|
"interface" -> {
|
||||||
if (isStatic || isClosed || isOverride || isExtern || isAbstract)
|
if (isStatic || isClosed || isOverride || isExtern || isAbstract)
|
||||||
throw ScriptError(
|
throw ScriptError(
|
||||||
@ -1421,6 +1427,12 @@ class Compiler(
|
|||||||
parseClassDeclaration()
|
parseClassDeclaration()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"object" -> {
|
||||||
|
pendingDeclStart = id.pos
|
||||||
|
pendingDeclDoc = consumePendingDoc()
|
||||||
|
parseObjectDeclaration()
|
||||||
|
}
|
||||||
|
|
||||||
"init" -> {
|
"init" -> {
|
||||||
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
val block = parseBlock()
|
val block = parseBlock()
|
||||||
@ -1847,6 +1859,81 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun parseObjectDeclaration(): Statement {
|
||||||
|
val nameToken = cc.requireToken(Token.Type.ID)
|
||||||
|
val startPos = pendingDeclStart ?: nameToken.pos
|
||||||
|
val doc = pendingDeclDoc ?: consumePendingDoc()
|
||||||
|
pendingDeclDoc = null
|
||||||
|
pendingDeclStart = null
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
val baseId = cc.requireToken(Token.Type.ID, "base class name expected")
|
||||||
|
var argsList: List<ParsedArgument>? = null
|
||||||
|
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
|
||||||
|
argsList = parseArgsNoTailBlock()
|
||||||
|
}
|
||||||
|
baseSpecs += BaseSpec(baseId.value, argsList)
|
||||||
|
} while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||||
|
|
||||||
|
pushInitScope()
|
||||||
|
|
||||||
|
// Robust body detection
|
||||||
|
var classBodyRange: MiniRange? = null
|
||||||
|
val bodyInit: Statement? = run {
|
||||||
|
val saved = cc.savePos()
|
||||||
|
val next = cc.nextNonWhitespace()
|
||||||
|
if (next.type == Token.Type.LBRACE) {
|
||||||
|
val bodyStart = next.pos
|
||||||
|
val st = withLocalNames(emptySet()) {
|
||||||
|
parseScript()
|
||||||
|
}
|
||||||
|
val rbTok = cc.next()
|
||||||
|
if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in object body")
|
||||||
|
classBodyRange = MiniRange(bodyStart, rbTok.pos)
|
||||||
|
st
|
||||||
|
} else {
|
||||||
|
cc.restorePos(saved)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val initScope = popInitScope()
|
||||||
|
val className = nameToken.value
|
||||||
|
|
||||||
|
return statement(startPos) { context ->
|
||||||
|
val parentClasses = baseSpecs.map { baseSpec ->
|
||||||
|
val rec = context[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")
|
||||||
|
}
|
||||||
|
|
||||||
|
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray())
|
||||||
|
for (i in parentClasses.indices) {
|
||||||
|
val argsList = baseSpecs[i].args
|
||||||
|
// In object, we evaluate parent args once at creation time
|
||||||
|
if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList
|
||||||
|
}
|
||||||
|
|
||||||
|
val classScope = context.createChildScope(newThisObj = newClass)
|
||||||
|
classScope.currentClassCtx = newClass
|
||||||
|
newClass.classScope = classScope
|
||||||
|
|
||||||
|
bodyInit?.execute(classScope)
|
||||||
|
|
||||||
|
// Create instance (singleton)
|
||||||
|
val instance = newClass.callOn(context.createChildScope(Arguments.EMPTY))
|
||||||
|
context.addItem(className, false, instance)
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun parseClassDeclaration(isAbstract: Boolean = false): Statement {
|
private suspend fun parseClassDeclaration(isAbstract: Boolean = false): Statement {
|
||||||
val nameToken = cc.requireToken(Token.Type.ID)
|
val nameToken = cc.requireToken(Token.Type.ID)
|
||||||
val startPos = pendingDeclStart ?: nameToken.pos
|
val startPos = pendingDeclStart ?: nameToken.pos
|
||||||
@ -2457,9 +2544,9 @@ class Compiler(
|
|||||||
val annotation = lastAnnotation
|
val annotation = lastAnnotation
|
||||||
val parentContext = codeContexts.last()
|
val parentContext = codeContexts.last()
|
||||||
|
|
||||||
t = cc.next()
|
|
||||||
// Is extension?
|
// Is extension?
|
||||||
if (t.type == Token.Type.DOT) {
|
if (cc.peekNextNonWhitespace().type == Token.Type.DOT) {
|
||||||
|
cc.nextNonWhitespace() // consume DOT
|
||||||
extTypeName = name
|
extTypeName = name
|
||||||
val receiverEnd = Pos(start.source, start.line, start.column + name.length)
|
val receiverEnd = Pos(start.source, start.line, start.column + name.length)
|
||||||
receiverMini = MiniTypeName(
|
receiverMini = MiniTypeName(
|
||||||
@ -2472,24 +2559,33 @@ class Compiler(
|
|||||||
throw ScriptError(t.pos, "illegal extension format: expected function name")
|
throw ScriptError(t.pos, "illegal extension format: expected function name")
|
||||||
name = t.value
|
name = t.value
|
||||||
nameStartPos = t.pos
|
nameStartPos = t.pos
|
||||||
t = cc.next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t.type != Token.Type.LPAREN)
|
val argsDeclaration: ArgsDeclaration =
|
||||||
throw ScriptError(t.pos, "Bad function definition: expected '(' after 'fn ${name}'")
|
if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
|
||||||
|
cc.nextNonWhitespace() // consume (
|
||||||
|
parseArgsDeclaration() ?: ArgsDeclaration(emptyList(), Token.Type.RPAREN)
|
||||||
|
} else ArgsDeclaration(emptyList(), Token.Type.RPAREN)
|
||||||
|
|
||||||
val argsDeclaration = parseArgsDeclaration()
|
// Optional return type
|
||||||
if (argsDeclaration == null || argsDeclaration.endTokenType != Token.Type.RPAREN)
|
val returnTypeMini: MiniTypeRef? = if (cc.peekNextNonWhitespace().type == Token.Type.COLON) {
|
||||||
|
parseTypeDeclarationWithMini().second
|
||||||
|
} else null
|
||||||
|
|
||||||
|
var isDelegated = false
|
||||||
|
var delegateExpression: Statement? = null
|
||||||
|
if (cc.peekNextNonWhitespace().type == Token.Type.BY) {
|
||||||
|
cc.nextNonWhitespace() // consume by
|
||||||
|
isDelegated = true
|
||||||
|
delegateExpression = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected delegate expression")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDelegated && argsDeclaration.endTokenType != Token.Type.RPAREN)
|
||||||
throw ScriptError(
|
throw ScriptError(
|
||||||
t.pos,
|
t.pos,
|
||||||
"Bad function definition: expected valid argument declaration or () after 'fn ${name}'"
|
"Bad function definition: expected valid argument declaration or () after 'fn ${name}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Optional return type
|
|
||||||
val returnTypeMini: MiniTypeRef? = if (cc.current().type == Token.Type.COLON) {
|
|
||||||
parseTypeDeclarationWithMini().second
|
|
||||||
} else null
|
|
||||||
|
|
||||||
// Capture doc locally to reuse even if we need to emit later
|
// Capture doc locally to reuse even if we need to emit later
|
||||||
val declDocLocal = pendingDeclDoc
|
val declDocLocal = pendingDeclDoc
|
||||||
|
|
||||||
@ -2526,17 +2622,13 @@ class Compiler(
|
|||||||
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") }
|
||||||
else if (isAbstract) {
|
else if (isAbstract || isDelegated) {
|
||||||
val next = cc.peekNextNonWhitespace()
|
|
||||||
if (next.type == Token.Type.ASSIGN || next.type == Token.Type.LBRACE)
|
|
||||||
throw ScriptError(next.pos, "abstract function $name cannot have a body")
|
|
||||||
null
|
null
|
||||||
} else
|
} else
|
||||||
withLocalNames(paramNames) {
|
withLocalNames(paramNames) {
|
||||||
val next = cc.peekNextNonWhitespace()
|
val next = cc.peekNextNonWhitespace()
|
||||||
if (next.type == Token.Type.ASSIGN) {
|
if (next.type == Token.Type.ASSIGN) {
|
||||||
cc.skipWsTokens()
|
cc.nextNonWhitespace() // consume '='
|
||||||
cc.next() // consume '='
|
|
||||||
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected function body expression")
|
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected function body expression")
|
||||||
// Shorthand function returns the expression value
|
// Shorthand function returns the expression value
|
||||||
statement(expr.pos) { scope ->
|
statement(expr.pos) { scope ->
|
||||||
@ -2573,6 +2665,57 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
// parentContext
|
// parentContext
|
||||||
val fnCreateStatement = statement(start) { context ->
|
val fnCreateStatement = statement(start) { context ->
|
||||||
|
if (isDelegated) {
|
||||||
|
val accessType = context.resolveQualifiedIdentifier("DelegateAccess.Callable")
|
||||||
|
val initValue = delegateExpression!!.execute(context)
|
||||||
|
val finalDelegate = try {
|
||||||
|
initValue.invokeInstanceMethod(context, "bind", Arguments(ObjString(name), accessType, context.thisObj))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
initValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extTypeName != null) {
|
||||||
|
val type = context[extTypeName!!]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
|
||||||
|
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
|
||||||
|
context.addExtension(type, name, ObjRecord(ObjUnset, isMutable = false, visibility = visibility, declaringClass = null, type = ObjRecord.Type.Delegated).apply {
|
||||||
|
delegate = finalDelegate
|
||||||
|
})
|
||||||
|
return@statement ObjVoid
|
||||||
|
}
|
||||||
|
|
||||||
|
val th = context.thisObj
|
||||||
|
if (isStatic) {
|
||||||
|
(th as ObjClass).createClassField(name, ObjUnset, false, visibility, null, start, type = ObjRecord.Type.Delegated).apply {
|
||||||
|
delegate = finalDelegate
|
||||||
|
}
|
||||||
|
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated).apply {
|
||||||
|
delegate = finalDelegate
|
||||||
|
}
|
||||||
|
} else if (th is ObjClass) {
|
||||||
|
val cls: ObjClass = th
|
||||||
|
val storageName = "${cls.className}::$name"
|
||||||
|
cls.createField(name, ObjUnset, false, visibility, null, start, declaringClass = cls, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, type = ObjRecord.Type.Delegated)
|
||||||
|
cls.instanceInitializers += statement(start) { scp ->
|
||||||
|
val accessType2 = scp.resolveQualifiedIdentifier("DelegateAccess.Callable")
|
||||||
|
val initValue2 = delegateExpression!!.execute(scp)
|
||||||
|
val finalDelegate2 = try {
|
||||||
|
initValue2.invokeInstanceMethod(scp, "bind", Arguments(ObjString(name), accessType2, scp.thisObj))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
initValue2
|
||||||
|
}
|
||||||
|
scp.addItem(storageName, false, ObjUnset, visibility, null, recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride).apply {
|
||||||
|
delegate = finalDelegate2
|
||||||
|
}
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated).apply {
|
||||||
|
delegate = finalDelegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@statement ObjVoid
|
||||||
|
}
|
||||||
|
|
||||||
// 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:
|
||||||
if (isStatic || parentContext !is CodeContext.ClassBody)
|
if (isStatic || parentContext !is CodeContext.ClassBody)
|
||||||
@ -2803,14 +2946,14 @@ class Compiler(
|
|||||||
if (!isStatic) declareLocalName(name)
|
if (!isStatic) declareLocalName(name)
|
||||||
|
|
||||||
val isDelegate = if (isAbstract) {
|
val isDelegate = if (isAbstract) {
|
||||||
if (!isProperty && (eqToken.type == Token.Type.ASSIGN || eqToken.isId("by")))
|
if (!isProperty && (eqToken.type == Token.Type.ASSIGN || eqToken.type == Token.Type.BY))
|
||||||
throw ScriptError(eqToken.pos, "abstract variable $name cannot have an initializer or delegate")
|
throw ScriptError(eqToken.pos, "abstract variable $name cannot have an initializer or delegate")
|
||||||
// Abstract variables don't have initializers
|
// Abstract variables don't have initializers
|
||||||
cc.restorePos(markBeforeEq)
|
cc.restorePos(markBeforeEq)
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
setNull = true
|
setNull = true
|
||||||
false
|
false
|
||||||
} else if (!isProperty && eqToken.isId("by")) {
|
} else if (!isProperty && eqToken.type == Token.Type.BY) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
|
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
|
||||||
@ -2858,11 +3001,35 @@ class Compiler(
|
|||||||
// when creating instance, but we need to execute it in the class initializer which
|
// when creating instance, but we need to execute it in the class initializer which
|
||||||
// is missing as for now. Add it to the compiler context?
|
// is missing as for now. Add it to the compiler context?
|
||||||
|
|
||||||
// if (isDelegate) throw ScriptError(start, "static delegates are not yet implemented")
|
|
||||||
currentInitScope += statement {
|
currentInitScope += statement {
|
||||||
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
|
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
|
||||||
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, pos)
|
if (isDelegate) {
|
||||||
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field)
|
val accessTypeStr = if (isMutable) "Var" else "Val"
|
||||||
|
val accessType = resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
|
||||||
|
val finalDelegate = try {
|
||||||
|
initValue.invokeInstanceMethod(this, "bind", Arguments(ObjString(name), accessType, thisObj))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
initValue
|
||||||
|
}
|
||||||
|
(thisObj as ObjClass).createClassField(
|
||||||
|
name,
|
||||||
|
ObjUnset,
|
||||||
|
isMutable,
|
||||||
|
visibility,
|
||||||
|
null,
|
||||||
|
start,
|
||||||
|
type = ObjRecord.Type.Delegated
|
||||||
|
).apply {
|
||||||
|
delegate = finalDelegate
|
||||||
|
}
|
||||||
|
// Also expose in current init scope
|
||||||
|
addItem(name, isMutable, ObjUnset, visibility, null, ObjRecord.Type.Delegated).apply {
|
||||||
|
delegate = finalDelegate
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start)
|
||||||
|
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field)
|
||||||
|
}
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
return NopStatement
|
return NopStatement
|
||||||
@ -3004,7 +3171,83 @@ class Compiler(
|
|||||||
if (!isStatic) declareLocalName(name)
|
if (!isStatic) declareLocalName(name)
|
||||||
|
|
||||||
if (isDelegate) {
|
if (isDelegate) {
|
||||||
TODO()
|
val declaringClassName = declaringClassNameCaptured
|
||||||
|
if (declaringClassName != null) {
|
||||||
|
val storageName = "$declaringClassName::$name"
|
||||||
|
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
|
||||||
|
if (isClassScope) {
|
||||||
|
val cls = context.thisObj as ObjClass
|
||||||
|
cls.createField(
|
||||||
|
name,
|
||||||
|
ObjUnset,
|
||||||
|
isMutable,
|
||||||
|
visibility,
|
||||||
|
setterVisibility,
|
||||||
|
start,
|
||||||
|
type = ObjRecord.Type.Delegated,
|
||||||
|
isAbstract = isAbstract,
|
||||||
|
isClosed = isClosed,
|
||||||
|
isOverride = isOverride
|
||||||
|
)
|
||||||
|
cls.instanceInitializers += statement(start) { scp ->
|
||||||
|
val initValue = initialExpression!!.execute(scp)
|
||||||
|
val accessTypeStr = if (isMutable) "Var" else "Val"
|
||||||
|
val accessType = scp.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
|
||||||
|
val finalDelegate = try {
|
||||||
|
initValue.invokeInstanceMethod(scp, "bind", Arguments(ObjString(name), accessType, scp.thisObj))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
initValue
|
||||||
|
}
|
||||||
|
scp.addItem(
|
||||||
|
storageName, isMutable, ObjUnset, visibility, setterVisibility,
|
||||||
|
recordType = ObjRecord.Type.Delegated,
|
||||||
|
isAbstract = isAbstract,
|
||||||
|
isClosed = isClosed,
|
||||||
|
isOverride = isOverride
|
||||||
|
).apply {
|
||||||
|
delegate = finalDelegate
|
||||||
|
}
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
return@statement ObjVoid
|
||||||
|
} else {
|
||||||
|
val initValue = initialExpression!!.execute(context)
|
||||||
|
val accessTypeStr = if (isMutable) "Var" else "Val"
|
||||||
|
val accessType = context.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
|
||||||
|
val finalDelegate = try {
|
||||||
|
initValue.invokeInstanceMethod(context, "bind", Arguments(ObjString(name), accessType, context.thisObj))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
initValue
|
||||||
|
}
|
||||||
|
val rec = context.addItem(
|
||||||
|
storageName, isMutable, ObjUnset, visibility, setterVisibility,
|
||||||
|
recordType = ObjRecord.Type.Delegated,
|
||||||
|
isAbstract = isAbstract,
|
||||||
|
isClosed = isClosed,
|
||||||
|
isOverride = isOverride
|
||||||
|
)
|
||||||
|
rec.delegate = finalDelegate
|
||||||
|
return@statement finalDelegate
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val initValue = initialExpression!!.execute(context)
|
||||||
|
val accessTypeStr = if (isMutable) "Var" else "Val"
|
||||||
|
val accessType = context.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
|
||||||
|
val finalDelegate = try {
|
||||||
|
initValue.invokeInstanceMethod(context, "bind", Arguments(ObjString(name), accessType, ObjNull))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
initValue
|
||||||
|
}
|
||||||
|
val rec = context.addItem(
|
||||||
|
name, isMutable, ObjUnset, visibility, setterVisibility,
|
||||||
|
recordType = ObjRecord.Type.Delegated,
|
||||||
|
isAbstract = isAbstract,
|
||||||
|
isClosed = isClosed,
|
||||||
|
isOverride = isOverride
|
||||||
|
)
|
||||||
|
rec.delegate = finalDelegate
|
||||||
|
return@statement finalDelegate
|
||||||
|
}
|
||||||
} else if (getter != null || setter != null) {
|
} else if (getter != null || setter != null) {
|
||||||
val declaringClassName = declaringClassNameCaptured!!
|
val declaringClassName = declaringClassNameCaptured!!
|
||||||
val storageName = "$declaringClassName::$name"
|
val storageName = "$declaringClassName::$name"
|
||||||
|
|||||||
@ -356,6 +356,8 @@ private class Parser(fromPos: Pos) {
|
|||||||
when (text) {
|
when (text) {
|
||||||
"in" -> Token("in", from, Token.Type.IN)
|
"in" -> Token("in", from, Token.Type.IN)
|
||||||
"is" -> Token("is", from, Token.Type.IS)
|
"is" -> Token("is", from, Token.Type.IS)
|
||||||
|
"by" -> Token("by", from, Token.Type.BY)
|
||||||
|
"object" -> Token("object", from, Token.Type.OBJECT)
|
||||||
"as" -> {
|
"as" -> {
|
||||||
// support both `as` and tight `as?` without spaces
|
// support both `as` and tight `as?` without spaces
|
||||||
if (currentChar == '?') { pos.advance(); Token("as?", from, Token.Type.ASNULL) }
|
if (currentChar == '?') { pos.advance(); Token("as?", from, Token.Type.ASNULL) }
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -621,6 +621,36 @@ open class Scope(
|
|||||||
return ref.evalValue(this)
|
return ref.evalValue(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun resolve(rec: ObjRecord, name: String): Obj {
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
val th = if (thisObj === ObjVoid) ObjNull else thisObj
|
||||||
|
return del.invokeInstanceMethod(this, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = {
|
||||||
|
// If getValue not found, return a wrapper that calls invoke
|
||||||
|
object : Statement() {
|
||||||
|
override val pos: Pos = Pos.builtIn
|
||||||
|
override suspend fun execute(scope: Scope): Obj {
|
||||||
|
val th2 = if (scope.thisObj === ObjVoid) ObjNull else scope.thisObj
|
||||||
|
val allArgs = (listOf(th2, ObjString(name)) + scope.args.list).toTypedArray()
|
||||||
|
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})!!
|
||||||
|
}
|
||||||
|
return rec.value
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) {
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
val th = if (thisObj === ObjVoid) ObjNull else thisObj
|
||||||
|
del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!rec.isMutable && rec.value !== ObjUnset) raiseIllegalAssignment("can't reassign val $name")
|
||||||
|
rec.value = newValue
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun new(): Scope =
|
fun new(): Scope =
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -36,7 +36,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
PLUS, MINUS, STAR, SLASH, PERCENT,
|
PLUS, MINUS, STAR, SLASH, PERCENT,
|
||||||
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
||||||
PLUS2, MINUS2,
|
PLUS2, MINUS2,
|
||||||
IN, NOTIN, IS, NOTIS,
|
IN, NOTIN, IS, NOTIS, BY,
|
||||||
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
|
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
|
||||||
SHUTTLE,
|
SHUTTLE,
|
||||||
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
||||||
@ -44,7 +44,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||||
LABEL, ATLABEL, // label@ at@label
|
LABEL, ATLABEL, // label@ at@label
|
||||||
// type-checking/casting
|
// type-checking/casting
|
||||||
AS, ASNULL,
|
AS, ASNULL, OBJECT,
|
||||||
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
|
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
|
||||||
ELLIPSIS, DOTDOT, DOTDOTLT,
|
ELLIPSIS, DOTDOT, DOTDOTLT,
|
||||||
NEWLINE,
|
NEWLINE,
|
||||||
|
|||||||
@ -74,7 +74,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) {
|
|||||||
Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation
|
Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation
|
||||||
|
|
||||||
// textual control keywords
|
// textual control keywords
|
||||||
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL,
|
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.OBJECT,
|
||||||
Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword
|
Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword
|
||||||
|
|
||||||
// labels / annotations
|
// labels / annotations
|
||||||
|
|||||||
@ -94,13 +94,7 @@ open class Obj {
|
|||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||||
val saved = scope.currentClassCtx
|
return rec.value.invoke(scope, this, args, decl)
|
||||||
scope.currentClassCtx = decl
|
|
||||||
try {
|
|
||||||
return rec.value.invoke(scope, this, args)
|
|
||||||
} finally {
|
|
||||||
scope.currentClassCtx = saved
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,13 +112,7 @@ open class Obj {
|
|||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||||
val saved = scope.currentClassCtx
|
return rec.value.invoke(scope, this, args, decl)
|
||||||
scope.currentClassCtx = decl
|
|
||||||
try {
|
|
||||||
return rec.value.invoke(scope, this, args)
|
|
||||||
} finally {
|
|
||||||
scope.currentClassCtx = saved
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,7 +364,14 @@ open class Obj {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
|
protected open suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
|
||||||
|
if (obj.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
return obj.copy(
|
||||||
|
value = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))),
|
||||||
|
type = ObjRecord.Type.Other
|
||||||
|
)
|
||||||
|
}
|
||||||
val value = obj.value
|
val value = obj.value
|
||||||
if (value is ObjProperty) {
|
if (value is ObjProperty) {
|
||||||
return ObjRecord(value.callGetter(scope, this, decl), obj.isMutable)
|
return ObjRecord(value.callGetter(scope, this, decl), obj.isMutable)
|
||||||
@ -425,7 +420,10 @@ open class Obj {
|
|||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(field.effectiveWriteVisibility, decl, caller))
|
if (!canAccessMember(field.effectiveWriteVisibility, decl, caller))
|
||||||
scope.raiseError(ObjIllegalAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
if (field.value is ObjProperty) {
|
if (field.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = field.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
|
||||||
|
} else if (field.value is ObjProperty) {
|
||||||
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
|
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
|
||||||
} else if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
} else if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
||||||
}
|
}
|
||||||
@ -444,13 +442,16 @@ open class Obj {
|
|||||||
scope.raiseNotImplemented()
|
scope.raiseNotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj =
|
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj =
|
||||||
if (PerfFlags.SCOPE_POOL)
|
if (PerfFlags.SCOPE_POOL)
|
||||||
scope.withChildFrame(args, newThisObj = thisObj) { child ->
|
scope.withChildFrame(args, newThisObj = thisObj) { child ->
|
||||||
|
if (declaringClass != null) child.currentClassCtx = declaringClass
|
||||||
callOn(child)
|
callOn(child)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj))
|
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj).also {
|
||||||
|
if (declaringClass != null) it.currentClassCtx = declaringClass
|
||||||
|
})
|
||||||
|
|
||||||
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
|
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
|
||||||
callOn(
|
callOn(
|
||||||
|
|||||||
@ -197,7 +197,7 @@ open class ObjClass(
|
|||||||
val list = mutableListOf<String>()
|
val list = mutableListOf<String>()
|
||||||
if (includeSelf) list += className
|
if (includeSelf) list += className
|
||||||
mroParents.forEach { list += it.className }
|
mroParents.forEach { list += it.className }
|
||||||
return list.joinToString(" → ")
|
return list.joinToString(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
override val objClass: ObjClass by lazy { ObjClassType }
|
override val objClass: ObjClass by lazy { ObjClassType }
|
||||||
@ -234,13 +234,13 @@ open class ObjClass(
|
|||||||
// This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
|
// This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
|
||||||
// 1) members-defined methods
|
// 1) members-defined methods
|
||||||
for ((k, v) in members) {
|
for ((k, v) in members) {
|
||||||
if (v.value is Statement) {
|
if (v.value is Statement || v.type == ObjRecord.Type.Delegated) {
|
||||||
instance.instanceScope.objects[k] = v
|
instance.instanceScope.objects[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2) class-scope methods registered during class-body execution
|
// 2) class-scope methods registered during class-body execution
|
||||||
classScope?.objects?.forEach { (k, rec) ->
|
classScope?.objects?.forEach { (k, rec) ->
|
||||||
if (rec.value is Statement) {
|
if (rec.value is Statement || rec.type == ObjRecord.Type.Delegated) {
|
||||||
// if not already present, copy reference for dispatch
|
// if not already present, copy reference for dispatch
|
||||||
if (!instance.instanceScope.objects.containsKey(k)) {
|
if (!instance.instanceScope.objects.containsKey(k)) {
|
||||||
instance.instanceScope.objects[k] = rec
|
instance.instanceScope.objects[k] = rec
|
||||||
@ -361,7 +361,7 @@ open class ObjClass(
|
|||||||
isClosed: Boolean = false,
|
isClosed: Boolean = false,
|
||||||
isOverride: Boolean = false,
|
isOverride: Boolean = false,
|
||||||
type: ObjRecord.Type = ObjRecord.Type.Field,
|
type: ObjRecord.Type = ObjRecord.Type.Field,
|
||||||
) {
|
): ObjRecord {
|
||||||
// Validation of override rules: only for non-system declarations
|
// Validation of override rules: only for non-system declarations
|
||||||
if (pos != Pos.builtIn) {
|
if (pos != Pos.builtIn) {
|
||||||
val existing = getInstanceMemberOrNull(name)
|
val existing = getInstanceMemberOrNull(name)
|
||||||
@ -396,7 +396,7 @@ open class ObjClass(
|
|||||||
throw ScriptError(pos, "$name is already defined in $objClass")
|
throw ScriptError(pos, "$name is already defined in $objClass")
|
||||||
|
|
||||||
// Install/override in this class
|
// Install/override in this class
|
||||||
members[name] = ObjRecord(
|
val rec = ObjRecord(
|
||||||
initialValue, isMutable, visibility, writeVisibility,
|
initialValue, isMutable, visibility, writeVisibility,
|
||||||
declaringClass = declaringClass,
|
declaringClass = declaringClass,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
@ -404,8 +404,10 @@ open class ObjClass(
|
|||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
type = type
|
type = type
|
||||||
)
|
)
|
||||||
|
members[name] = rec
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initClassScope(): Scope {
|
private fun initClassScope(): Scope {
|
||||||
@ -419,15 +421,17 @@ open class ObjClass(
|
|||||||
isMutable: Boolean = false,
|
isMutable: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
writeVisibility: Visibility? = null,
|
writeVisibility: Visibility? = null,
|
||||||
pos: Pos = Pos.builtIn
|
pos: Pos = Pos.builtIn,
|
||||||
) {
|
type: ObjRecord.Type = ObjRecord.Type.Field
|
||||||
|
): ObjRecord {
|
||||||
initClassScope()
|
initClassScope()
|
||||||
val existing = classScope!!.objects[name]
|
val existing = classScope!!.objects[name]
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
||||||
classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility)
|
val rec = classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility, recordType = type)
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addFn(
|
fun addFn(
|
||||||
@ -558,15 +562,21 @@ open class ObjClass(
|
|||||||
|
|
||||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
classScope?.objects?.get(name)?.let {
|
classScope?.objects?.get(name)?.let {
|
||||||
if (it.visibility.isPublic) return it
|
if (it.visibility.isPublic) return resolveRecord(scope, it, name, this)
|
||||||
}
|
}
|
||||||
return super.readField(scope, name)
|
return super.readField(scope, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
initClassScope().objects[name]?.let {
|
initClassScope().objects[name]?.let { rec ->
|
||||||
if (it.isMutable) it.value = newValue
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rec.isMutable) rec.value = newValue
|
||||||
else scope.raiseIllegalAssignment("can't assign $name is not mutable")
|
else scope.raiseIllegalAssignment("can't assign $name is not mutable")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
?: super.writeField(scope, name, newValue)
|
?: super.writeField(scope, name, newValue)
|
||||||
}
|
}
|
||||||
@ -575,8 +585,16 @@ open class ObjClass(
|
|||||||
scope: Scope, name: String, args: Arguments,
|
scope: Scope, name: String, args: Arguments,
|
||||||
onNotFoundResult: (suspend () -> Obj?)?
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
): Obj {
|
): Obj {
|
||||||
return classScope?.objects?.get(name)?.value?.invoke(scope, this, args)
|
getInstanceMemberOrNull(name)?.let { rec ->
|
||||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
val decl = rec.declaringClass ?: findDeclaringClassOf(name) ?: this
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = rec.delegate ?: scope.raiseError("Internal error: delegated function $name has no delegate")
|
||||||
|
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
|
||||||
|
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
|
||||||
|
}
|
||||||
|
return rec.value.invoke(scope, this, args, decl)
|
||||||
|
}
|
||||||
|
return super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||||
|
|||||||
@ -31,7 +31,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
internal lateinit var instanceScope: Scope
|
internal lateinit var instanceScope: Scope
|
||||||
|
|
||||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
// Direct (unmangled) lookup first
|
// 1. Direct (unmangled) lookup first
|
||||||
instanceScope[name]?.let { rec ->
|
instanceScope[name]?.let { rec ->
|
||||||
val decl = rec.declaringClass
|
val decl = rec.declaringClass
|
||||||
// Allow unconditional access when accessing through `this` of the same instance
|
// Allow unconditional access when accessing through `this` of the same instance
|
||||||
@ -46,33 +46,20 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (rec.type == ObjRecord.Type.Property) {
|
return resolveRecord(scope, rec, name, decl)
|
||||||
val prop = rec.value as ObjProperty
|
|
||||||
return rec.copy(value = prop.callGetter(scope, this, decl))
|
|
||||||
}
|
|
||||||
return rec
|
|
||||||
}
|
}
|
||||||
// Try MI-mangled lookup along linearization (C3 MRO): ClassName::name
|
|
||||||
val cls = objClass
|
|
||||||
|
|
||||||
// self first, then parents
|
// 2. MI-mangled instance scope lookup
|
||||||
fun findMangled(): ObjRecord? {
|
val cls = objClass
|
||||||
// self
|
fun findMangledInRead(): ObjRecord? {
|
||||||
instanceScope.objects["${cls.className}::$name"]?.let {
|
instanceScope.objects["${cls.className}::$name"]?.let { return it }
|
||||||
if (name == "c") println("[DEBUG_LOG] findMangled('c') found in self (${cls.className}): value=${it.value}")
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
// ancestors in deterministic C3 order
|
|
||||||
for (p in cls.mroParents) {
|
for (p in cls.mroParents) {
|
||||||
instanceScope.objects["${p.className}::$name"]?.let {
|
instanceScope.objects["${p.className}::$name"]?.let { return it }
|
||||||
if (name == "c") println("[DEBUG_LOG] findMangled('c') found in parent (${p.className}): value=${it.value}")
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
findMangled()?.let { rec ->
|
|
||||||
// derive declaring class by mangled prefix: try self then parents
|
findMangledInRead()?.let { rec ->
|
||||||
val declaring = when {
|
val declaring = when {
|
||||||
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||||
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||||
@ -87,16 +74,32 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (rec.type == ObjRecord.Type.Property) {
|
return resolveRecord(scope, rec, name, declaring)
|
||||||
val prop = rec.value as ObjProperty
|
|
||||||
return rec.copy(value = prop.callGetter(scope, this, declaring))
|
|
||||||
}
|
|
||||||
return rec
|
|
||||||
}
|
}
|
||||||
// Fall back to methods/properties on class
|
|
||||||
|
// 3. Fall back to super (handles class members and extensions)
|
||||||
return super.readField(scope, name)
|
return super.readField(scope, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
|
||||||
|
if (obj.type == ObjRecord.Type.Delegated) {
|
||||||
|
val storageName = "${decl?.className}::$name"
|
||||||
|
var del = instanceScope[storageName]?.delegate
|
||||||
|
if (del == null) {
|
||||||
|
for (c in objClass.mro) {
|
||||||
|
del = instanceScope["${c.className}::$name"]?.delegate
|
||||||
|
if (del != null) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
del = del ?: obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
|
||||||
|
return obj.copy(
|
||||||
|
value = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))),
|
||||||
|
type = ObjRecord.Type.Other
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return super.resolveRecord(scope, obj, name, decl)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
// Direct (unmangled) first
|
// Direct (unmangled) first
|
||||||
instanceScope[name]?.let { f ->
|
instanceScope[name]?.let { f ->
|
||||||
@ -114,6 +117,19 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
prop.callSetter(scope, this, newValue, decl)
|
prop.callSetter(scope, this, newValue, decl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (f.type == ObjRecord.Type.Delegated) {
|
||||||
|
val storageName = "${decl?.className}::$name"
|
||||||
|
var del = instanceScope[storageName]?.delegate
|
||||||
|
if (del == null) {
|
||||||
|
for (c in objClass.mro) {
|
||||||
|
del = instanceScope["${c.className}::$name"]?.delegate
|
||||||
|
if (del != null) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
del = del ?: f.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
|
||||||
|
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (f.value.assign(scope, newValue) == null)
|
if (f.value.assign(scope, newValue) == null)
|
||||||
f.value = newValue
|
f.value = newValue
|
||||||
@ -131,7 +147,6 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
|
|
||||||
val rec = findMangled()
|
val rec = findMangled()
|
||||||
if (rec != null) {
|
if (rec != null) {
|
||||||
if (name == "c") println("[DEBUG_LOG] writeField('c') found in mangled: value was ${rec.value}, setting to $newValue")
|
|
||||||
val declaring = when {
|
val declaring = when {
|
||||||
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||||
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||||
@ -149,6 +164,19 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
prop.callSetter(scope, this, newValue, declaring)
|
prop.callSetter(scope, this, newValue, declaring)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val storageName = "${declaring?.className}::$name"
|
||||||
|
var del = instanceScope[storageName]?.delegate
|
||||||
|
if (del == null) {
|
||||||
|
for (c in objClass.mro) {
|
||||||
|
del = instanceScope["${c.className}::$name"]?.delegate
|
||||||
|
if (del != null) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
del = del ?: rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
|
||||||
|
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!rec.isMutable && rec.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
if (!rec.isMutable && rec.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (rec.value.assign(scope, newValue) == null)
|
if (rec.value.assign(scope, newValue) == null)
|
||||||
rec.value = newValue
|
rec.value = newValue
|
||||||
@ -160,45 +188,42 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
override suspend fun invokeInstanceMethod(
|
override suspend fun invokeInstanceMethod(
|
||||||
scope: Scope, name: String, args: Arguments,
|
scope: Scope, name: String, args: Arguments,
|
||||||
onNotFoundResult: (suspend () -> Obj?)?
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
): Obj =
|
): Obj {
|
||||||
instanceScope[name]?.let { rec ->
|
// 1. Walk MRO to find member, handling delegation
|
||||||
if (rec.type == ObjRecord.Type.Property || rec.isAbstract) null
|
for (cls in objClass.mro) {
|
||||||
else {
|
if (cls.className == "Obj") break
|
||||||
val decl = rec.declaringClass
|
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||||
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
|
if (rec != null) {
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
scope.raiseError(
|
val storageName = "${cls.className}::$name"
|
||||||
ObjIllegalAccessException(
|
val del = instanceScope[storageName]?.delegate ?: rec.delegate
|
||||||
scope,
|
?: scope.raiseError("Internal error: delegated function $name has no delegate (tried $storageName)")
|
||||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
|
||||||
)
|
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
|
||||||
)
|
}
|
||||||
rec.value.invoke(
|
if (rec.type != ObjRecord.Type.Property && !rec.isAbstract) {
|
||||||
instanceScope,
|
val decl = rec.declaringClass ?: cls
|
||||||
this,
|
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
|
||||||
args
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
)
|
scope.raiseError(
|
||||||
}
|
ObjIllegalAccessException(
|
||||||
}
|
scope,
|
||||||
?: run {
|
"can't invoke method $name (declared in ${decl.className ?: "?"})"
|
||||||
// fallback: class-scope function (registered during class body execution)
|
|
||||||
objClass.classScope?.objects?.get(name)?.let { rec ->
|
|
||||||
if (rec.type == ObjRecord.Type.Property || rec.isAbstract) null
|
|
||||||
else {
|
|
||||||
val decl = rec.declaringClass
|
|
||||||
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
|
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
|
||||||
scope.raiseError(
|
|
||||||
ObjIllegalAccessException(
|
|
||||||
scope,
|
|
||||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
rec.value.invoke(instanceScope, this, args)
|
)
|
||||||
}
|
return rec.value.invoke(
|
||||||
|
instanceScope,
|
||||||
|
this,
|
||||||
|
args,
|
||||||
|
decl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
}
|
||||||
|
|
||||||
|
// 2. Fall back to super (handles extensions and root fallback)
|
||||||
|
return super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||||
|
}
|
||||||
|
|
||||||
private val publicFields: Map<String, ObjRecord>
|
private val publicFields: Map<String, ObjRecord>
|
||||||
get() = instanceScope.objects.filter {
|
get() = instanceScope.objects.filter {
|
||||||
@ -320,7 +345,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||||
return rec
|
return resolveRecord(scope, rec, name, decl)
|
||||||
}
|
}
|
||||||
// Then try instance locals (unmangled) only if startClass is the dynamic class itself
|
// Then try instance locals (unmangled) only if startClass is the dynamic class itself
|
||||||
if (startClass === instance.objClass) {
|
if (startClass === instance.objClass) {
|
||||||
@ -334,7 +359,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return rec
|
return resolveRecord(scope, rec, name, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finally try methods/properties starting from ancestor
|
// Finally try methods/properties starting from ancestor
|
||||||
@ -343,18 +368,8 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(r.visibility, decl, caller))
|
if (!canAccessMember(r.visibility, decl, caller))
|
||||||
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||||
return when (val value = r.value) {
|
|
||||||
is net.sergeych.lyng.Statement -> ObjRecord(
|
|
||||||
value.execute(
|
|
||||||
instance.instanceScope.createChildScope(
|
|
||||||
scope.pos,
|
|
||||||
newThisObj = instance
|
|
||||||
)
|
|
||||||
), r.isMutable
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> r
|
return resolveRecord(scope, r, name, decl)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -35,6 +35,7 @@ data class ObjRecord(
|
|||||||
val isAbstract: Boolean = false,
|
val isAbstract: Boolean = false,
|
||||||
val isClosed: Boolean = false,
|
val isClosed: Boolean = false,
|
||||||
val isOverride: Boolean = false,
|
val isOverride: Boolean = false,
|
||||||
|
var delegate: Obj? = null,
|
||||||
) {
|
) {
|
||||||
val effectiveWriteVisibility: Visibility get() = writeVisibility ?: visibility
|
val effectiveWriteVisibility: Visibility get() = writeVisibility ?: visibility
|
||||||
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
|
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
|
||||||
@ -48,6 +49,7 @@ data class ObjRecord(
|
|||||||
Class,
|
Class,
|
||||||
Enum,
|
Enum,
|
||||||
Property,
|
Property,
|
||||||
|
Delegated,
|
||||||
Other;
|
Other;
|
||||||
|
|
||||||
val isArgument get() = this == Argument
|
val isArgument get() = this == Argument
|
||||||
|
|||||||
@ -32,7 +32,11 @@ sealed interface ObjRef {
|
|||||||
* Fast path for evaluating an expression to a raw Obj value without wrapping it into ObjRecord.
|
* Fast path for evaluating an expression to a raw Obj value without wrapping it into ObjRecord.
|
||||||
* Default implementation calls [get] and returns its value. Nodes can override to avoid record traffic.
|
* Default implementation calls [get] and returns its value. Nodes can override to avoid record traffic.
|
||||||
*/
|
*/
|
||||||
suspend fun evalValue(scope: Scope): Obj = get(scope).value
|
suspend fun evalValue(scope: Scope): Obj {
|
||||||
|
val rec = get(scope)
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) return scope.resolve(rec, "unknown")
|
||||||
|
return rec.value
|
||||||
|
}
|
||||||
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||||
throw ScriptError(pos, "can't assign value")
|
throw ScriptError(pos, "can't assign value")
|
||||||
}
|
}
|
||||||
@ -79,8 +83,7 @@ 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 = PerfFlags.RVAL_FASTPATH
|
val v = a.evalValue(scope)
|
||||||
val v = if (fastRval) a.evalValue(scope) else a.get(scope).value
|
|
||||||
if (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
|
||||||
@ -108,8 +111,8 @@ 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 (PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value
|
val a = left.evalValue(scope)
|
||||||
val b = if (PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value
|
val b = right.evalValue(scope)
|
||||||
|
|
||||||
// Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS)
|
// Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS)
|
||||||
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
@ -277,7 +280,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 (PerfFlags.RVAL_FASTPATH) condition.evalValue(scope) else condition.get(scope).value
|
val condVal = condition.evalValue(scope)
|
||||||
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
|
||||||
@ -296,8 +299,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 (PerfFlags.RVAL_FASTPATH) valueRef.evalValue(scope) else valueRef.get(scope).value
|
val v0 = valueRef.evalValue(scope)
|
||||||
val t = if (PerfFlags.RVAL_FASTPATH) typeRef.evalValue(scope) else typeRef.get(scope).value
|
val t = typeRef.evalValue(scope)
|
||||||
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) {
|
||||||
@ -339,8 +342,8 @@ class AssignOpRef(
|
|||||||
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 x = target.get(scope).value
|
val x = target.evalValue(scope)
|
||||||
val y = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
|
val y = value.evalValue(scope)
|
||||||
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)
|
||||||
@ -373,7 +376,7 @@ class IncDecRef(
|
|||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val rec = target.get(scope)
|
val rec = target.get(scope)
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value")
|
if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value")
|
||||||
val v = rec.value
|
val v = scope.resolve(rec, "unknown")
|
||||||
val one = ObjInt.One
|
val one = ObjInt.One
|
||||||
// We now treat numbers as immutable and always perform write-back via setAt.
|
// We now treat numbers as immutable and always perform write-back via setAt.
|
||||||
// This avoids issues where literals are shared and mutated in-place.
|
// This avoids issues where literals are shared and mutated in-place.
|
||||||
@ -387,9 +390,8 @@ 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 = PerfFlags.RVAL_FASTPATH
|
val a = left.evalValue(scope)
|
||||||
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
|
val r = if (a != ObjNull) a else right.evalValue(scope)
|
||||||
val r = if (a != ObjNull) a else if (fastRval) right.evalValue(scope) else right.get(scope).value
|
|
||||||
return r.asReadonly
|
return r.asReadonly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -397,12 +399,10 @@ 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 = PerfFlags.RVAL_FASTPATH
|
val a = left.evalValue(scope)
|
||||||
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
|
if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly
|
||||||
val b = if (fastRval) right.evalValue(scope) else right.get(scope).value
|
val b = right.evalValue(scope)
|
||||||
if (fastPrim) {
|
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
if (a is ObjBool && b is ObjBool) {
|
if (a is ObjBool && b is ObjBool) {
|
||||||
return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
||||||
}
|
}
|
||||||
@ -414,13 +414,10 @@ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef
|
|||||||
/** Logical AND with short-circuit: a && b */
|
/** Logical AND with short-circuit: a && b */
|
||||||
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
|
val a = left.evalValue(scope)
|
||||||
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
|
if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly
|
||||||
val b = if (fastRval) right.evalValue(scope) else right.get(scope).value
|
val b = right.evalValue(scope)
|
||||||
if (fastPrim) {
|
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
if (a is ObjBool && b is ObjBool) {
|
if (a is ObjBool && b is ObjBool) {
|
||||||
return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
||||||
}
|
}
|
||||||
@ -505,10 +502,9 @@ class FieldRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
|
||||||
val fieldPic = PerfFlags.FIELD_PIC
|
val fieldPic = PerfFlags.FIELD_PIC
|
||||||
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||||
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
|
val base = target.evalValue(scope)
|
||||||
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)
|
||||||
@ -800,11 +796,10 @@ 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 = PerfFlags.RVAL_FASTPATH
|
val base = target.evalValue(scope)
|
||||||
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 = index.evalValue(scope)
|
||||||
if (fastRval) {
|
if (PerfFlags.RVAL_FASTPATH) {
|
||||||
// Primitive list index fast path: avoid virtual dispatch to getAt when shapes match
|
// Primitive list index fast path: avoid virtual dispatch to getAt when shapes match
|
||||||
if (base is ObjList && idx is ObjInt) {
|
if (base is ObjList && idx is ObjInt) {
|
||||||
val i = idx.toInt()
|
val i = idx.toInt()
|
||||||
@ -874,11 +869,10 @@ class IndexRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun evalValue(scope: Scope): Obj {
|
override suspend fun evalValue(scope: Scope): Obj {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
val base = target.evalValue(scope)
|
||||||
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 = index.evalValue(scope)
|
||||||
if (fastRval) {
|
if (PerfFlags.RVAL_FASTPATH) {
|
||||||
// Fast list[int] path
|
// Fast list[int] path
|
||||||
if (base is ObjList && idx is ObjInt) {
|
if (base is ObjList && idx is ObjInt) {
|
||||||
val i = idx.toInt()
|
val i = idx.toInt()
|
||||||
@ -1027,9 +1021,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 = PerfFlags.RVAL_FASTPATH
|
|
||||||
val usePool = PerfFlags.SCOPE_POOL
|
val usePool = PerfFlags.SCOPE_POOL
|
||||||
val callee = if (fastRval) target.evalValue(scope) else target.get(scope).value
|
val callee = target.evalValue(scope)
|
||||||
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)
|
||||||
val result: Obj = if (usePool) {
|
val result: Obj = if (usePool) {
|
||||||
@ -1117,10 +1110,9 @@ class MethodCallRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
|
||||||
val methodPic = PerfFlags.METHOD_PIC
|
val methodPic = PerfFlags.METHOD_PIC
|
||||||
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||||
val base = if (fastRval) receiver.evalValue(scope) else receiver.get(scope).value
|
val base = receiver.evalValue(scope)
|
||||||
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) {
|
||||||
@ -1327,21 +1319,21 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
override suspend fun evalValue(scope: Scope): Obj {
|
override suspend fun evalValue(scope: Scope): Obj {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||||
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
|
scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) }
|
||||||
// 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 scope.resolve(it, name) }
|
||||||
run {
|
run {
|
||||||
var s: Scope? = scope
|
var s: Scope? = scope
|
||||||
val visited = HashSet<Long>(4)
|
val visited = HashSet<Long>(4)
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
if (!visited.add(s.frameId)) break
|
if (!visited.add(s.frameId)) break
|
||||||
if (s is ClosureScope) {
|
if (s is ClosureScope) {
|
||||||
s.closureScope.chainLookupWithMembers(name)?.let { return it.value }
|
s.closureScope.chainLookupWithMembers(name)?.let { return s.resolve(it, name) }
|
||||||
}
|
}
|
||||||
s = s.parent
|
s = s.parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.chainLookupIgnoreClosure(name)?.let { return it.value }
|
scope.chainLookupIgnoreClosure(name)?.let { return scope.resolve(it, name) }
|
||||||
return try {
|
return try {
|
||||||
scope.thisObj.readField(scope, name).value
|
scope.thisObj.readField(scope, name).value
|
||||||
} catch (e: ExecutionError) {
|
} catch (e: ExecutionError) {
|
||||||
@ -1353,25 +1345,29 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) {
|
if (slot >= 0) {
|
||||||
val rec = scope.getSlotRecord(slot)
|
val rec = scope.getSlotRecord(slot)
|
||||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
return rec.value
|
return scope.resolve(rec, name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 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 scope.resolve(it, name)
|
||||||
|
}
|
||||||
run {
|
run {
|
||||||
var s: Scope? = scope
|
var s: Scope? = scope
|
||||||
val visited = HashSet<Long>(4)
|
val visited = HashSet<Long>(4)
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
if (!visited.add(s.frameId)) break
|
if (!visited.add(s.frameId)) break
|
||||||
if (s is ClosureScope) {
|
if (s is ClosureScope) {
|
||||||
s.closureScope.chainLookupWithMembers(name)?.let { return it.value }
|
s.closureScope.chainLookupWithMembers(name)?.let { return s.resolve(it, name) }
|
||||||
}
|
}
|
||||||
s = s.parent
|
s = s.parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.chainLookupIgnoreClosure(name)?.let { return it.value }
|
scope.chainLookupIgnoreClosure(name)?.let { return scope.resolve(it, name) }
|
||||||
return try {
|
return try {
|
||||||
scope.thisObj.readField(scope, name).value
|
val res = scope.thisObj.readField(scope, name).value
|
||||||
|
res
|
||||||
} catch (e: ExecutionError) {
|
} catch (e: ExecutionError) {
|
||||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||||
throw e
|
throw e
|
||||||
@ -1383,13 +1379,11 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||||
scope.getSlotIndexOf(name)?.let {
|
scope.getSlotIndexOf(name)?.let {
|
||||||
val rec = scope.getSlotRecord(it)
|
val rec = scope.getSlotRecord(it)
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
scope.assign(rec, name, newValue)
|
||||||
rec.value = newValue
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scope[name]?.let { stored ->
|
scope[name]?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
@ -1399,8 +1393,7 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
if (!visited.add(s.frameId)) break
|
if (!visited.add(s.frameId)) break
|
||||||
if (s is ClosureScope) {
|
if (s is ClosureScope) {
|
||||||
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
s.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1408,8 +1401,7 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Fallback: write to field on `this`
|
// Fallback: write to field on `this`
|
||||||
@ -1420,14 +1412,12 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
if (slot >= 0) {
|
if (slot >= 0) {
|
||||||
val rec = scope.getSlotRecord(slot)
|
val rec = scope.getSlotRecord(slot)
|
||||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
scope.assign(rec, name, newValue)
|
||||||
rec.value = newValue
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope[name]?.let { stored ->
|
scope[name]?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
@ -1437,8 +1427,7 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
if (!visited.add(s.frameId)) break
|
if (!visited.add(s.frameId)) break
|
||||||
if (s is ClosureScope) {
|
if (s is ClosureScope) {
|
||||||
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
s.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1446,8 +1435,7 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scope.thisObj.writeField(scope, name, newValue)
|
scope.thisObj.writeField(scope, name, newValue)
|
||||||
@ -1476,7 +1464,10 @@ class BoundLocalVarRef(
|
|||||||
val rec = scope.getSlotRecord(slot)
|
val rec = scope.getSlotRecord(slot)
|
||||||
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||||
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
|
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
|
||||||
return rec.value
|
// We might not have the name in BoundLocalVarRef, but let's try to find it or use a placeholder
|
||||||
|
// Actually BoundLocalVarRef is mostly used for parameters which are not delegated yet.
|
||||||
|
// But for consistency:
|
||||||
|
return scope.resolve(rec, "local")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||||
@ -1484,8 +1475,7 @@ class BoundLocalVarRef(
|
|||||||
val rec = scope.getSlotRecord(slot)
|
val rec = scope.getSlotRecord(slot)
|
||||||
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||||
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
|
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
scope.assign(rec, "local", newValue)
|
||||||
rec.value = newValue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1592,15 +1582,18 @@ class FastLocalVarRef(
|
|||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot >= 0 && actualOwner != null) {
|
if (slot >= 0 && actualOwner != null) {
|
||||||
val rec = actualOwner.getSlotRecord(slot)
|
val rec = actualOwner.getSlotRecord(slot)
|
||||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
return rec.value
|
return actualOwner.resolve(rec, name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Try per-frame local binding maps in the ancestry first
|
// Try per-frame local binding maps in the ancestry first
|
||||||
run {
|
run {
|
||||||
var s: Scope? = scope
|
var s: Scope? = scope
|
||||||
var guard = 0
|
var guard = 0
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
s.localBindings[name]?.let { return it.value }
|
s.localBindings[name]?.let {
|
||||||
|
return s.resolve(it, name)
|
||||||
|
}
|
||||||
val next = s.parent
|
val next = s.parent
|
||||||
if (next === s) break
|
if (next === s) break
|
||||||
s = next
|
s = next
|
||||||
@ -1612,7 +1605,9 @@ class FastLocalVarRef(
|
|||||||
var s: Scope? = scope
|
var s: Scope? = scope
|
||||||
var guard = 0
|
var guard = 0
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
s.objects[name]?.let { return it.value }
|
s.objects[name]?.let {
|
||||||
|
return s.resolve(it, name)
|
||||||
|
}
|
||||||
val next = s.parent
|
val next = s.parent
|
||||||
if (next === s) break
|
if (next === s) break
|
||||||
s = next
|
s = next
|
||||||
@ -1620,7 +1615,9 @@ 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 scope.resolve(it, name)
|
||||||
|
}
|
||||||
return scope.thisObj.readField(scope, name).value
|
return scope.thisObj.readField(scope, name).value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1632,8 +1629,7 @@ class FastLocalVarRef(
|
|||||||
if (slot >= 0 && actualOwner != null) {
|
if (slot >= 0 && actualOwner != null) {
|
||||||
val rec = actualOwner.getSlotRecord(slot)
|
val rec = actualOwner.getSlotRecord(slot)
|
||||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
scope.assign(rec, name, newValue)
|
||||||
rec.value = newValue
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1644,8 +1640,7 @@ class FastLocalVarRef(
|
|||||||
while (s != null) {
|
while (s != null) {
|
||||||
val rec = s.localBindings[name]
|
val rec = s.localBindings[name]
|
||||||
if (rec != null) {
|
if (rec != null) {
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
s.assign(rec, name, newValue)
|
||||||
rec.value = newValue
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val next = s.parent
|
val next = s.parent
|
||||||
@ -1656,8 +1651,7 @@ class FastLocalVarRef(
|
|||||||
}
|
}
|
||||||
// Fallback to standard name lookup
|
// Fallback to standard name lookup
|
||||||
scope[name]?.let { stored ->
|
scope[name]?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scope.thisObj.writeField(scope, name, newValue)
|
scope.thisObj.writeField(scope, name, newValue)
|
||||||
@ -1691,11 +1685,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 (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.evalValue(scope)
|
||||||
list += v
|
list += v
|
||||||
}
|
}
|
||||||
is ListEntry.Spread -> {
|
is ListEntry.Spread -> {
|
||||||
val elements = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
val elements = e.ref.evalValue(scope)
|
||||||
when (elements) {
|
when (elements) {
|
||||||
is ObjList -> {
|
is ObjList -> {
|
||||||
// Grow underlying array once when possible
|
// Grow underlying array once when possible
|
||||||
@ -1771,11 +1765,11 @@ class MapLiteralRef(private val entries: List<MapLiteralEntry>) : ObjRef {
|
|||||||
for (e in entries) {
|
for (e in entries) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is MapLiteralEntry.Named -> {
|
is MapLiteralEntry.Named -> {
|
||||||
val v = if (PerfFlags.RVAL_FASTPATH) e.value.evalValue(scope) else e.value.get(scope).value
|
val v = e.value.evalValue(scope)
|
||||||
result.map[ObjString(e.key)] = v
|
result.map[ObjString(e.key)] = v
|
||||||
}
|
}
|
||||||
is MapLiteralEntry.Spread -> {
|
is MapLiteralEntry.Spread -> {
|
||||||
val m = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
val m = e.ref.evalValue(scope)
|
||||||
if (m !is ObjMap) scope.raiseIllegalArgument("spread element in map literal must be a Map")
|
if (m !is ObjMap) scope.raiseIllegalArgument("spread element in map literal must be a Map")
|
||||||
for ((k, v) in m.map) {
|
for ((k, v) in m.map) {
|
||||||
result.map[k] = v
|
result.map[k] = v
|
||||||
@ -1796,8 +1790,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 (PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
|
val l = left?.evalValue(scope) ?: ObjNull
|
||||||
val r = right?.let { if (PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
|
val r = right?.evalValue(scope) ?: ObjNull
|
||||||
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
|
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1809,7 +1803,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 (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
|
val v = value.evalValue(scope)
|
||||||
// For properties, we should not call get() on target because it invokes the getter.
|
// For properties, we should not call get() on target because it invokes the getter.
|
||||||
// Instead, we call setAt directly.
|
// Instead, we call setAt directly.
|
||||||
if (target is FieldRef || target is IndexRef || target is LocalVarRef || target is FastLocalVarRef || target is BoundLocalVarRef) {
|
if (target is FieldRef || target is IndexRef || target is LocalVarRef || target is FastLocalVarRef || target is BoundLocalVarRef) {
|
||||||
|
|||||||
@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class DelegationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSimpleDelegation() = runTest {
|
||||||
|
eval("""
|
||||||
|
class Proxy() {
|
||||||
|
fun getValue(r, n) = 42
|
||||||
|
}
|
||||||
|
val x by Proxy()
|
||||||
|
assertEquals(42, x)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConstructorVal() = runTest {
|
||||||
|
eval("""
|
||||||
|
class Foo(val v) {
|
||||||
|
fun getV() = v
|
||||||
|
}
|
||||||
|
val f = Foo(42)
|
||||||
|
assertEquals(42, f.v)
|
||||||
|
assertEquals(42, f.getV())
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBasicValVarDelegation() = runTest {
|
||||||
|
eval("""
|
||||||
|
class MapDelegate(val map) {
|
||||||
|
fun getValue(thisRef, name) = map[name]
|
||||||
|
fun setValue(thisRef, name, value) { map[name] = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = { "x": 10 }
|
||||||
|
val x by MapDelegate(data)
|
||||||
|
var y by MapDelegate(data)
|
||||||
|
|
||||||
|
assertEquals(10, x)
|
||||||
|
assertEquals(null, y)
|
||||||
|
y = 20
|
||||||
|
assertEquals(20, data["y"])
|
||||||
|
assertEquals(20, y)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClassDelegationWithThisRef() = runTest {
|
||||||
|
eval("""
|
||||||
|
class Proxy(val target) {
|
||||||
|
fun getValue(thisRef, name) = target[name]
|
||||||
|
fun setValue(thisRef, name, value) { target[name] = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
class User(initialName) {
|
||||||
|
val storage = { "name": initialName }
|
||||||
|
var name by Proxy(storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
val u = User("Alice")
|
||||||
|
assertEquals("Alice", u.name)
|
||||||
|
u.name = "Bob"
|
||||||
|
assertEquals("Bob", u.name)
|
||||||
|
assertEquals("Bob", u.storage["name"])
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFunDelegation() = runTest {
|
||||||
|
eval("""
|
||||||
|
class ActionDelegate() {
|
||||||
|
fun invoke(thisRef, name, args...) {
|
||||||
|
"Called %s with %d args: %s"(name, args.size, args.joinToString(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun greet by ActionDelegate()
|
||||||
|
|
||||||
|
assertEquals("Called greet with 2 args: hello,world", greet("hello", "world"))
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBindHook() = runTest {
|
||||||
|
eval("""
|
||||||
|
// Note: DelegateAccess might need to be defined or built-in
|
||||||
|
// For the test, let's assume it's passed as an integer or we define it
|
||||||
|
val VAL = 0
|
||||||
|
val VAR = 1
|
||||||
|
val CALLABLE = 2
|
||||||
|
|
||||||
|
class OnlyVal() {
|
||||||
|
fun bind(name, access, thisRef) {
|
||||||
|
if (access != VAL) throw "Only val allowed"
|
||||||
|
this
|
||||||
|
}
|
||||||
|
fun getValue(thisRef, name) = 42
|
||||||
|
}
|
||||||
|
|
||||||
|
val ok by OnlyVal()
|
||||||
|
assertEquals(42, ok)
|
||||||
|
|
||||||
|
assertThrows {
|
||||||
|
eval("var bad by OnlyVal()")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStatelessObjectDelegate() = runTest {
|
||||||
|
eval("""
|
||||||
|
object Constant42 {
|
||||||
|
fun getValue(thisRef, name) = 42
|
||||||
|
}
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
val a by Constant42
|
||||||
|
val b by Constant42
|
||||||
|
}
|
||||||
|
|
||||||
|
val f = Foo()
|
||||||
|
assertEquals(42, f.a)
|
||||||
|
assertEquals(42, f.b)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLazyImplementation() = runTest {
|
||||||
|
eval("""
|
||||||
|
class Lazy(val creator) {
|
||||||
|
private var value = Unset
|
||||||
|
fun getValue(thisRef, name) {
|
||||||
|
if (this.value == Unset) {
|
||||||
|
this.value = creator()
|
||||||
|
}
|
||||||
|
this.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun lazy(creator) = Lazy(creator)
|
||||||
|
|
||||||
|
var counter = 0
|
||||||
|
val x by lazy { counter++; "computed" }
|
||||||
|
|
||||||
|
assertEquals(0, counter)
|
||||||
|
assertEquals("computed", x)
|
||||||
|
assertEquals(1, counter)
|
||||||
|
assertEquals("computed", x)
|
||||||
|
assertEquals(1, counter)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLocalDelegation() = runTest {
|
||||||
|
eval("""
|
||||||
|
class LocalProxy(val v) {
|
||||||
|
fun getValue(thisRef, name) = v
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test() {
|
||||||
|
val x by LocalProxy(123)
|
||||||
|
x
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(123, test())
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStdlibLazy() = runTest {
|
||||||
|
eval("""
|
||||||
|
var counter = 0
|
||||||
|
val x by lazy { counter++; "computed" }
|
||||||
|
|
||||||
|
assertEquals(0, counter)
|
||||||
|
assertEquals("computed", x)
|
||||||
|
assertEquals(1, counter)
|
||||||
|
assertEquals("computed", x)
|
||||||
|
|
||||||
|
assertThrows {
|
||||||
|
eval("var y by lazy { 1 }")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLazyIsDelegate() = runTest {
|
||||||
|
eval("""
|
||||||
|
val l = lazy { 42 }
|
||||||
|
assert(l is Delegate)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -277,3 +277,55 @@ fun Exception.printStackTrace() {
|
|||||||
val String.re get() = Regex(this)
|
val String.re get() = Regex(this)
|
||||||
|
|
||||||
fun TODO(message=null) = throw NotImplementedException(message ?: "not implemented")
|
fun TODO(message=null) = throw NotImplementedException(message ?: "not implemented")
|
||||||
|
|
||||||
|
/*
|
||||||
|
Provides different access types for delegates.
|
||||||
|
Used in the 'bind' hook to validate delegate usage.
|
||||||
|
*/
|
||||||
|
enum DelegateAccess {
|
||||||
|
Val,
|
||||||
|
Var,
|
||||||
|
Callable
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Base interface for all delegates.
|
||||||
|
Implementing this interface is optional as Lyng uses dynamic dispatch,
|
||||||
|
but it is recommended for documentation and clarity.
|
||||||
|
*/
|
||||||
|
interface Delegate {
|
||||||
|
/* Called when a delegated 'val' or 'var' is read. */
|
||||||
|
fun getValue(thisRef, name) = TODO("delegate getter is not implemented")
|
||||||
|
|
||||||
|
/* Called when a delegated 'var' is written. */
|
||||||
|
fun setValue(thisRef, name, newValue) = TODO("delegate setter is not implemented")
|
||||||
|
|
||||||
|
/* Called when a delegated function is invoked. */
|
||||||
|
fun invoke(thisRef, name, args...) = TODO("delegate invoke is not implemented")
|
||||||
|
|
||||||
|
/*
|
||||||
|
Called once during initialization to configure or validate the delegate.
|
||||||
|
Should return the delegate object to be used (usually 'this').
|
||||||
|
*/
|
||||||
|
fun bind(name, access, thisRef) = this
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Standard implementation of a lazy-initialized property delegate.
|
||||||
|
The provided creator lambda is called once on the first access to compute the value.
|
||||||
|
Can only be used with 'val' properties.
|
||||||
|
*/
|
||||||
|
class lazy(creatorParam) : Delegate {
|
||||||
|
private val creator = creatorParam
|
||||||
|
private var value = Unset
|
||||||
|
|
||||||
|
override fun bind(name, access, thisRef) {
|
||||||
|
if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef, name) {
|
||||||
|
if (value == Unset) value = creator()
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -318,6 +318,21 @@ fun EditorWithOverlay(
|
|||||||
try { ta.setSelectionRange(res.selStart, res.selEnd) } catch (_: Throwable) {}
|
try { ta.setSelectionRange(res.selStart, res.selEnd) } catch (_: Throwable) {}
|
||||||
pendingSelStart = res.selStart
|
pendingSelStart = res.selStart
|
||||||
pendingSelEnd = res.selEnd
|
pendingSelEnd = res.selEnd
|
||||||
|
} else if (key.length == 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey) {
|
||||||
|
// Handle single character input (like '}') for dedenting
|
||||||
|
// This is an alternative to onInput to have better control
|
||||||
|
val start = ta.selectionStart ?: 0
|
||||||
|
val end = ta.selectionEnd ?: start
|
||||||
|
val current = ta.value
|
||||||
|
val res = applyChar(current, start, end, key[0], tabSize)
|
||||||
|
if (res.text != (current.substring(0, start) + key + current.substring(end))) {
|
||||||
|
// Logic decided to change something else (e.g. dedent)
|
||||||
|
ev.preventDefault()
|
||||||
|
setCode(res.text)
|
||||||
|
try { ta.setSelectionRange(res.selStart, res.selEnd) } catch (_: Throwable) {}
|
||||||
|
pendingSelStart = res.selStart
|
||||||
|
pendingSelEnd = res.selEnd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -83,55 +83,6 @@ private fun nextNonWs(text: String, idxInclusive: Int): Int {
|
|||||||
|
|
||||||
/** Apply Enter key behavior with smart indent/undent rules. */
|
/** Apply Enter key behavior with smart indent/undent rules. */
|
||||||
fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResult {
|
fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResult {
|
||||||
// Global early rule (Rule 3): if the line after the current line is brace-only, dedent that line by one block and
|
|
||||||
// do NOT insert a newline. This uses precise line boundaries.
|
|
||||||
run {
|
|
||||||
val start = minOf(selStart, text.length)
|
|
||||||
val eol = lineEndAt(text, start)
|
|
||||||
val nextLineStart = if (eol < text.length && text[eol] == '\n') eol + 1 else eol
|
|
||||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
|
||||||
if (nextLineStart <= nextLineEnd && nextLineStart < text.length) {
|
|
||||||
val trimmedNext = text.substring(nextLineStart, nextLineEnd).trim()
|
|
||||||
if (trimmedNext == "}") {
|
|
||||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
|
||||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
|
||||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
|
||||||
val out = buildString(text.length) {
|
|
||||||
append(safeSubstring(text, 0, nextLineStart))
|
|
||||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
|
||||||
}
|
|
||||||
val caret = nextLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Absolute top-priority: caret is exactly at a line break and the next line is '}'-only (ignoring spaces).
|
|
||||||
// Dedent that next line by one block (tabSize) and do NOT insert a newline.
|
|
||||||
run {
|
|
||||||
if (selStart < text.length) {
|
|
||||||
val isCrLf = selStart + 1 < text.length && text[selStart] == '\r' && text[selStart + 1] == '\n'
|
|
||||||
val isLf = text[selStart] == '\n'
|
|
||||||
if (isCrLf || isLf) {
|
|
||||||
val nextLineStart = selStart + if (isCrLf) 2 else 1
|
|
||||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
|
||||||
if (nextLineStart <= nextLineEnd) {
|
|
||||||
val trimmed = text.substring(nextLineStart, nextLineEnd).trim()
|
|
||||||
if (trimmed == "}") {
|
|
||||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
|
||||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
|
||||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
|
||||||
val out = buildString(text.length) {
|
|
||||||
append(safeSubstring(text, 0, nextLineStart))
|
|
||||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
|
||||||
}
|
|
||||||
val caret = nextLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is a selection, replace it by newline + current line indent
|
// If there is a selection, replace it by newline + current line indent
|
||||||
if (selEnd != selStart) {
|
if (selEnd != selStart) {
|
||||||
val lineStart = lineStartAt(text, selStart)
|
val lineStart = lineStartAt(text, selStart)
|
||||||
@ -148,8 +99,6 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
|
|||||||
val lineEnd = lineEndAt(text, start)
|
val lineEnd = lineEndAt(text, start)
|
||||||
val indent = countIndentSpaces(text, lineStart, lineEnd)
|
val indent = countIndentSpaces(text, lineStart, lineEnd)
|
||||||
|
|
||||||
// (Handled by the global early rule above; no need for additional EOL variants.)
|
|
||||||
|
|
||||||
// Compute neighborhood characters early so rule precedence can use them
|
// Compute neighborhood characters early so rule precedence can use them
|
||||||
val prevIdx = prevNonWs(text, start)
|
val prevIdx = prevNonWs(text, start)
|
||||||
val nextIdx = nextNonWs(text, start)
|
val nextIdx = nextNonWs(text, start)
|
||||||
@ -158,85 +107,6 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
|
|||||||
val before = text.substring(0, start)
|
val before = text.substring(0, start)
|
||||||
val after = text.substring(start)
|
val after = text.substring(start)
|
||||||
|
|
||||||
// Rule 2: On a brace-only line '}' (caret on the same line)
|
|
||||||
// If the current line’s trimmed text is exactly '}', decrease that line’s indent by one block (not below 0),
|
|
||||||
// then insert a newline. The newly inserted line uses the (decreased) indent. Place the caret at the start of
|
|
||||||
// the newly inserted line.
|
|
||||||
run {
|
|
||||||
val trimmed = text.substring(lineStart, lineEnd).trim()
|
|
||||||
// IMPORTANT: Rule precedence — do NOT trigger this rule when caret is after '}' and the rest of the line
|
|
||||||
// up to EOL contains only spaces (Rule 5 handles that case). That scenario must insert AFTER the brace,
|
|
||||||
// not before it.
|
|
||||||
var onlySpacesAfterCaret = true
|
|
||||||
var k = start
|
|
||||||
while (k < lineEnd) { if (text[k] != ' ') { onlySpacesAfterCaret = false; break }; k++ }
|
|
||||||
val rule5Situation = (prevCh == '}') && onlySpacesAfterCaret
|
|
||||||
|
|
||||||
if (trimmed == "}" && !rule5Situation) {
|
|
||||||
val removeCount = kotlin.math.min(tabSize, indent)
|
|
||||||
val newIndent = (indent - removeCount).coerceAtLeast(0)
|
|
||||||
val crShift = if (lineStart < text.length && text[lineStart] == '\r') 1 else 0
|
|
||||||
val out = buildString(text.length + 1 + newIndent) {
|
|
||||||
append(safeSubstring(text, 0, lineStart))
|
|
||||||
append("\n")
|
|
||||||
append(" ".repeat(newIndent))
|
|
||||||
// Write the brace line but with its indent reduced by removeCount spaces
|
|
||||||
append(safeSubstring(text, lineStart + crShift + removeCount, text.length))
|
|
||||||
}
|
|
||||||
val caret = lineStart + 1 + newIndent
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// (The special case of caret after the last non-ws on a '}'-only line is covered by the rule above.)
|
|
||||||
|
|
||||||
// 0) Caret is at end-of-line and the next line is a closing brace-only line: dedent that line, no extra newline
|
|
||||||
run {
|
|
||||||
val atCr = start + 1 < text.length && text[start] == '\r' && text[start + 1] == '\n'
|
|
||||||
val atNl = start < text.length && text[start] == '\n'
|
|
||||||
val atEol = atNl || atCr
|
|
||||||
if (atEol) {
|
|
||||||
val nlAdvance = if (atCr) 2 else 1
|
|
||||||
val nextLineStart = start + nlAdvance
|
|
||||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
|
||||||
val trimmedNext = text.substring(nextLineStart, nextLineEnd).trim()
|
|
||||||
if (trimmedNext == "}") {
|
|
||||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
|
||||||
// Dedent the '}' line by one block level (tabSize), but not below column 0
|
|
||||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
|
||||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
|
||||||
val out = buildString(text.length) {
|
|
||||||
append(safeSubstring(text, 0, nextLineStart))
|
|
||||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
|
||||||
}
|
|
||||||
val caret = start + nlAdvance + kotlin.math.max(0, rbraceIndent - removeCount)
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0b) If there is a newline at or after caret and the next line starts (ignoring spaces) with '}',
|
|
||||||
// dedent that '}' line without inserting an extra newline.
|
|
||||||
run {
|
|
||||||
val nlPos = text.indexOf('\n', start)
|
|
||||||
if (nlPos >= 0) {
|
|
||||||
val nextLineStart = nlPos + 1
|
|
||||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
|
||||||
val nextLineFirstNonWs = nextNonWs(text, nextLineStart)
|
|
||||||
if (nextLineFirstNonWs in nextLineStart until nextLineEnd && text[nextLineFirstNonWs] == '}') {
|
|
||||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
|
||||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
|
||||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
|
||||||
val out = buildString(text.length) {
|
|
||||||
append(safeSubstring(text, 0, nextLineStart))
|
|
||||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
|
||||||
}
|
|
||||||
val caret = nextLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1) Between braces { | } -> two lines, inner indented
|
// 1) Between braces { | } -> two lines, inner indented
|
||||||
if (prevCh == '{' && nextCh == '}') {
|
if (prevCh == '{' && nextCh == '}') {
|
||||||
val innerIndent = indent + tabSize
|
val innerIndent = indent + tabSize
|
||||||
@ -254,119 +124,17 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
|
|||||||
}
|
}
|
||||||
// 3) Before '}'
|
// 3) Before '}'
|
||||||
if (nextCh == '}') {
|
if (nextCh == '}') {
|
||||||
// We want two things:
|
|
||||||
// - reduce indentation of the upcoming '}' line by one level
|
|
||||||
// - avoid creating an extra blank line if caret is already at EOL (the next char is a newline)
|
|
||||||
|
|
||||||
// Compute where the '}' line starts and how many leading spaces it has
|
|
||||||
val rbraceLineStart = lineStartAt(text, nextIdx)
|
|
||||||
val rbraceLineEnd = lineEndAt(text, nextIdx)
|
|
||||||
val rbraceIndent = countIndentSpaces(text, rbraceLineStart, rbraceLineEnd)
|
|
||||||
// Dedent the '}' line by one block level (tabSize), but not below column 0
|
|
||||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
|
||||||
val crShift = if (rbraceLineStart < text.length && text[rbraceLineStart] == '\r') 1 else 0
|
|
||||||
|
|
||||||
// If there is already a newline between caret and the '}', do NOT insert another newline.
|
|
||||||
// Just dedent the existing '}' line by one block and place caret at its start.
|
|
||||||
run {
|
|
||||||
val nlBetween = text.indexOf('\n', start)
|
|
||||||
if (nlBetween in start until rbraceLineStart) {
|
|
||||||
val out = buildString(text.length) {
|
|
||||||
append(safeSubstring(text, 0, rbraceLineStart))
|
|
||||||
append(safeSubstring(text, rbraceLineStart + crShift + removeCount, text.length))
|
|
||||||
}
|
|
||||||
val caret = rbraceLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNewlineAtCaret = (start < text.length && text[start] == '\n')
|
|
||||||
|
|
||||||
// New indentation for the line we create (if we actually insert one now)
|
|
||||||
val newLineIndent = (indent - tabSize).coerceAtLeast(0)
|
|
||||||
val insertion = if (hasNewlineAtCaret) "" else "\n" + " ".repeat(newLineIndent)
|
|
||||||
|
|
||||||
val out = buildString(text.length + insertion.length) {
|
|
||||||
append(before)
|
|
||||||
append(insertion)
|
|
||||||
// keep text up to the start of '}' line
|
|
||||||
append(safeSubstring(text, start, rbraceLineStart))
|
|
||||||
// drop up to tabSize spaces before '}'
|
|
||||||
append(safeSubstring(text, rbraceLineStart + crShift + removeCount, text.length))
|
|
||||||
}
|
|
||||||
val caret = if (hasNewlineAtCaret) {
|
|
||||||
// Caret moves to the beginning of the '}' line after dedent (right after the single newline)
|
|
||||||
start + 1 + kotlin.math.max(0, rbraceIndent - removeCount)
|
|
||||||
} else {
|
|
||||||
start + insertion.length
|
|
||||||
}
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
// 4) After '}' with only trailing spaces before EOL
|
|
||||||
// According to Rule 5: if the last non-whitespace before the caret is '}' and
|
|
||||||
// only spaces remain until EOL, we must:
|
|
||||||
// - dedent the current (brace) line by one block (not below 0)
|
|
||||||
// - insert a newline just AFTER '}' (do NOT move caret backward)
|
|
||||||
// - set the caret at the start of the newly inserted blank line, whose indent equals the dedented indent
|
|
||||||
if (prevCh == '}') {
|
|
||||||
var onlySpaces = true
|
|
||||||
var k = prevIdx + 1
|
|
||||||
while (k < lineEnd) { if (text[k] != ' ') { onlySpaces = false; break }; k++ }
|
|
||||||
if (onlySpaces) {
|
|
||||||
val removeCount = kotlin.math.min(tabSize, indent)
|
|
||||||
val newIndent = (indent - removeCount).coerceAtLeast(0)
|
|
||||||
val crShift = if (lineStart < text.length && text[lineStart] == '\r') 1 else 0
|
|
||||||
|
|
||||||
// Build the result:
|
|
||||||
// - keep everything before the line start
|
|
||||||
// - write the current line content up to the caret, but with its left indent reduced by removeCount
|
|
||||||
// - insert newline + spaces(newIndent)
|
|
||||||
// - drop trailing spaces after caret up to EOL
|
|
||||||
// - keep the rest of the text starting from EOL
|
|
||||||
val out = buildString(text.length) {
|
|
||||||
append(safeSubstring(text, 0, lineStart))
|
|
||||||
append(safeSubstring(text, lineStart + crShift + removeCount, start))
|
|
||||||
append("\n")
|
|
||||||
append(" ".repeat(newIndent))
|
|
||||||
append(safeSubstring(text, lineEnd, text.length))
|
|
||||||
}
|
|
||||||
val caret = (lineStart + (start - (lineStart + crShift + removeCount)) + 1 + newIndent)
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
} else {
|
|
||||||
// Default smart indent for cases where there are non-space characters after '}'
|
|
||||||
val insertion = "\n" + " ".repeat(indent)
|
|
||||||
val out = before + insertion + after
|
|
||||||
val caret = start + insertion.length
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 5) Fallback: if there is a newline ahead and the next line, trimmed, equals '}', dedent that '}' line by one block
|
|
||||||
run {
|
|
||||||
val nlPos = text.indexOf('\n', start)
|
|
||||||
if (nlPos >= 0) {
|
|
||||||
val nextLineStart = nlPos + 1
|
|
||||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
|
||||||
val trimmedNext = text.substring(nextLineStart, nextLineEnd).trim()
|
|
||||||
if (trimmedNext == "}") {
|
|
||||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
|
||||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
|
||||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
|
||||||
val out = buildString(text.length) {
|
|
||||||
append(safeSubstring(text, 0, nextLineStart))
|
|
||||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
|
||||||
}
|
|
||||||
val caret = nextLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
|
||||||
return EditResult(out, caret, caret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// default keep same indent
|
|
||||||
run {
|
|
||||||
val insertion = "\n" + " ".repeat(indent)
|
val insertion = "\n" + " ".repeat(indent)
|
||||||
val out = before + insertion + after
|
val out = before + insertion + after
|
||||||
val caret = start + insertion.length
|
val caret = start + insertion.length
|
||||||
return EditResult(out, caret, caret)
|
return EditResult(out, caret, caret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default keep same indent
|
||||||
|
val insertion = "\n" + " ".repeat(indent)
|
||||||
|
val out = before + insertion + after
|
||||||
|
val caret = start + insertion.length
|
||||||
|
return EditResult(out, caret, caret)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply Tab key: insert spaces at caret (single-caret only). */
|
/** Apply Tab key: insert spaces at caret (single-caret only). */
|
||||||
@ -423,3 +191,37 @@ fun applyShiftTab(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditR
|
|||||||
val e = maxOf(newSelStart, newSelEnd)
|
val e = maxOf(newSelStart, newSelEnd)
|
||||||
return EditResult(sb.toString(), s, e)
|
return EditResult(sb.toString(), s, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a typed character. If the character is '}', and it's the only non-whitespace on the line,
|
||||||
|
* it may be dedented.
|
||||||
|
*/
|
||||||
|
fun applyChar(text: String, selStart: Int, selEnd: Int, ch: Char, tabSize: Int): EditResult {
|
||||||
|
// Selection replacement
|
||||||
|
val current = if (selStart != selEnd) {
|
||||||
|
text.substring(0, minOf(selStart, selEnd)) + text.substring(maxOf(selStart, selEnd))
|
||||||
|
} else text
|
||||||
|
val pos = minOf(selStart, selEnd)
|
||||||
|
|
||||||
|
val before = current.substring(0, pos)
|
||||||
|
val after = current.substring(pos)
|
||||||
|
val newText = before + ch + after
|
||||||
|
val newPos = pos + 1
|
||||||
|
|
||||||
|
if (ch == '}') {
|
||||||
|
val lineStart = lineStartAt(newText, pos)
|
||||||
|
val lineEnd = lineEndAt(newText, newPos)
|
||||||
|
val trimmed = newText.substring(lineStart, lineEnd).trim()
|
||||||
|
if (trimmed == "}") {
|
||||||
|
// Dedent this line
|
||||||
|
val indent = countIndentSpaces(newText, lineStart, lineEnd)
|
||||||
|
val removeCount = minOf(tabSize, indent)
|
||||||
|
if (removeCount > 0) {
|
||||||
|
val out = newText.substring(0, lineStart) + newText.substring(lineStart + removeCount)
|
||||||
|
return EditResult(out, newPos - removeCount, newPos - removeCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EditResult(newText, newPos, newPos)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -25,81 +25,19 @@ class EditorLogicTest {
|
|||||||
private val tab = 4
|
private val tab = 4
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun enter_after_only_rbrace_undents() {
|
fun type_rbrace_dedents_if_only_char_on_line() {
|
||||||
val line = " } " // 4 spaces, brace, trailing spaces; caret after last non-ws
|
val text = " "
|
||||||
val res = applyEnter(line, line.length, line.length, tab)
|
val res = applyChar(text, 4, 4, '}', tab)
|
||||||
// Should insert newline with one indent level less (0 spaces here)
|
assertEquals("}", res.text)
|
||||||
assertEquals(" } \n" + "" , res.text.substring(0, res.text.indexOf('\n')+1))
|
assertEquals(1, res.selStart)
|
||||||
// After insertion caret should be at end of inserted indentation
|
|
||||||
// Here indent was 4 and undented by 4 -> 0 spaces after newline
|
|
||||||
val expectedCaret = line.length + 1 + 0
|
|
||||||
assertEquals(expectedCaret, res.selStart)
|
|
||||||
assertEquals(expectedCaret, res.selEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun enter_after_rbrace_with_only_spaces_to_eol_inserts_after_brace_and_dedents_and_undents_brace_line() {
|
fun type_rbrace_no_dedent_if_not_only_char() {
|
||||||
// Rule 5 exact check: last non-ws before caret is '}', remainder to EOL only spaces
|
val text = " foo "
|
||||||
val indents = listOf(0, 4, 8)
|
val res = applyChar(text, 8, 8, '}', tab)
|
||||||
for (indent in indents) {
|
assertEquals(" foo }", res.text)
|
||||||
val spaces = " ".repeat(indent)
|
assertEquals(9, res.selStart)
|
||||||
// Line ends with '}' followed by three spaces
|
|
||||||
val before = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
${'$'}spaces}
|
|
||||||
"""
|
|
||||||
).trimIndent() + " "
|
|
||||||
// Caret right after '}', before trailing spaces
|
|
||||||
val caret = before.indexOf('}') + 1
|
|
||||||
val res = applyEnter(before, caret, caret, tab)
|
|
||||||
|
|
||||||
val newBraceIndent = (indent - tab).coerceAtLeast(0)
|
|
||||||
val newLineIndent = newBraceIndent
|
|
||||||
val expected = buildString {
|
|
||||||
append("1\n")
|
|
||||||
append(" ".repeat(newBraceIndent))
|
|
||||||
append("}\n")
|
|
||||||
append(" ".repeat(newLineIndent))
|
|
||||||
}
|
|
||||||
assertEquals(expected, res.text)
|
|
||||||
// Caret must be at start of the newly inserted line (after the final newline)
|
|
||||||
assertEquals(expected.length, res.selStart)
|
|
||||||
assertEquals(res.selStart, res.selEnd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun enter_after_rbrace_with_only_spaces_to_eol_crlf_and_undents_brace_line() {
|
|
||||||
val indent = 4
|
|
||||||
val spaces = " ".repeat(indent)
|
|
||||||
val beforeLf = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
${'$'}spaces}
|
|
||||||
"""
|
|
||||||
).trimIndent() + " "
|
|
||||||
val before = beforeLf.replace("\n", "\r\n")
|
|
||||||
val caret = before.indexOf('}') + 1
|
|
||||||
val res = applyEnter(before, caret, caret, tab)
|
|
||||||
val actual = res.text.replace("\r\n", "\n")
|
|
||||||
val newIndent = (indent - tab).coerceAtLeast(0)
|
|
||||||
val expected = "1\n${" ".repeat(newIndent)}}\n${" ".repeat(newIndent)}"
|
|
||||||
assertEquals(expected, actual)
|
|
||||||
assertEquals(expected.length, res.selStart)
|
|
||||||
assertEquals(res.selStart, res.selEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun enter_before_closing_brace_outdents() {
|
|
||||||
val text = " }" // caret before '}' at index 4
|
|
||||||
val caret = 4
|
|
||||||
val res = applyEnter(text, caret, caret, tab)
|
|
||||||
// Inserted a newline with indent reduced to 0
|
|
||||||
assertEquals("\n" + "" + "}", res.text.substring(caret, caret + 2))
|
|
||||||
val expectedCaret = caret + 1 + 0
|
|
||||||
assertEquals(expectedCaret, res.selStart)
|
|
||||||
assertEquals(expectedCaret, res.selEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -123,23 +61,6 @@ class EditorLogicTest {
|
|||||||
assertEquals(1 + 1 + 4, res.selStart)
|
assertEquals(1 + 1 + 4, res.selStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun enter_after_rbrace_line_undents_ignoring_trailing_ws() {
|
|
||||||
// Line contains only '}' plus trailing spaces; caret after last non-ws
|
|
||||||
val line = " } "
|
|
||||||
val res = applyEnter(line, line.length, line.length, tab)
|
|
||||||
// Expect insertion of a newline and an undented indentation of (indent - tab)
|
|
||||||
val expectedIndentAfterNewline = 0 // 4 - 4
|
|
||||||
val expected = line + "\n" + " ".repeat(expectedIndentAfterNewline)
|
|
||||||
// Compare prefix up to inserted indentation
|
|
||||||
val prefix = res.text.substring(0, expected.length)
|
|
||||||
assertEquals(expected, prefix)
|
|
||||||
// Caret positioned at end of inserted indentation
|
|
||||||
val expectedCaret = expected.length
|
|
||||||
assertEquals(expectedCaret, res.selStart)
|
|
||||||
assertEquals(expectedCaret, res.selEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shift_tab_outdents_rbrace_only_line_no_newline() {
|
fun shift_tab_outdents_rbrace_only_line_no_newline() {
|
||||||
// Multi-line: a block followed by a number; we outdent the '}' line only
|
// Multi-line: a block followed by a number; we outdent the '}' line only
|
||||||
@ -171,222 +92,55 @@ class EditorLogicTest {
|
|||||||
assertEquals(1 + 4, res.selStart)
|
assertEquals(1 + 4, res.selStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
@Test
|
@Test
|
||||||
fun enter_before_line_with_only_rbrace_dedents_that_line() {
|
fun enter_before_line_with_only_rbrace_dedents_that_line() {
|
||||||
// Initial content as reported by user before fix
|
...
|
||||||
val before = (
|
*/
|
||||||
"""
|
|
||||||
{
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
}
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
|
|
||||||
// Place caret at the end of the line that contains just " 3" (before the line with '}')
|
|
||||||
val lines = before.split('\n')
|
|
||||||
// Build index of end of the line with the last " 3"
|
|
||||||
var idx = 0
|
|
||||||
var caret = 0
|
|
||||||
for (i in lines.indices) {
|
|
||||||
val line = lines[i]
|
|
||||||
if (line.trimEnd() == "3" && i + 1 < lines.size && lines[i + 1].trim() == "}") {
|
|
||||||
caret = idx + line.length // position after '3', before the newline
|
|
||||||
break
|
|
||||||
}
|
|
||||||
idx += line.length + 1 // +1 for newline
|
|
||||||
}
|
|
||||||
|
|
||||||
val res = applyEnter(before, caret, caret, tab)
|
|
||||||
|
|
||||||
val expected = (
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
}
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
|
|
||||||
assertEquals(expected, res.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/*
|
||||||
@Test
|
@Test
|
||||||
fun enter_eol_before_brace_only_next_line_various_indents() {
|
fun enter_eol_before_brace_only_next_line_various_indents() {
|
||||||
// Cover Rule 3 with LF newlines, at indents 0, 2, 4, 8
|
...
|
||||||
val indents = listOf(0, 2, 4, 8)
|
*/
|
||||||
for (indent in indents) {
|
|
||||||
val spaces = " ".repeat(indent)
|
|
||||||
val before = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
${'$'}spaces}
|
|
||||||
4
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
// Caret at end of the line with '3' (line before the rbrace-only line)
|
|
||||||
val caret = before.indexOf("3\n") + 1 // just before LF
|
|
||||||
val res = applyEnter(before, caret, caret, tab)
|
|
||||||
|
|
||||||
// The '}' line must be dedented by one block (clamped at 0) and caret moved to its start
|
|
||||||
val expectedIndent = (indent - tab).coerceAtLeast(0)
|
|
||||||
val expected = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
${'$'}{" ".repeat(expectedIndent)}}
|
|
||||||
4
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
assertEquals(expected, res.text, "EOL before '}' dedent failed for indent=${'$'}indent")
|
|
||||||
// Caret should be at start of that '}' line (line index 3)
|
|
||||||
val lines = res.text.split('\n')
|
|
||||||
var pos = 0
|
|
||||||
for (i in 0 until 3) pos += lines[i].length + 1
|
|
||||||
assertEquals(pos, res.selStart, "Caret pos mismatch for indent=${'$'}indent")
|
|
||||||
assertEquals(pos, res.selEnd, "Caret pos mismatch for indent=${'$'}indent")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/*
|
||||||
@Test
|
@Test
|
||||||
fun enter_eol_before_brace_only_next_line_various_indents_crlf() {
|
fun enter_eol_before_brace_only_next_line_various_indents_crlf() {
|
||||||
// Same as above but with CRLF newlines
|
...
|
||||||
val indents = listOf(0, 2, 4, 8)
|
*/
|
||||||
for (indent in indents) {
|
|
||||||
val spaces = " ".repeat(indent)
|
|
||||||
val beforeLf = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
${'$'}spaces}
|
|
||||||
4
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
val before = beforeLf.replace("\n", "\r\n")
|
|
||||||
val caret = before.indexOf("3\r\n") + 1 // at '3' index + 1 moves to end-of-line before CR
|
|
||||||
val res = applyEnter(before, caret, caret, tab)
|
|
||||||
|
|
||||||
val expectedIndent = (indent - tab).coerceAtLeast(0)
|
|
||||||
val expectedLf = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
${'$'}{" ".repeat(expectedIndent)}}
|
|
||||||
4
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
assertEquals(expectedLf, res.text.replace("\r\n", "\n"), "CRLF case failed for indent=${'$'}indent")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/*
|
||||||
@Test
|
@Test
|
||||||
fun enter_at_start_of_brace_only_line_at_cols_0_2_4() {
|
fun enter_at_start_of_brace_only_line_at_cols_0_2_4() {
|
||||||
val indents = listOf(0, 2, 4)
|
...
|
||||||
for (indent in indents) {
|
*/
|
||||||
val spaces = " ".repeat(indent)
|
|
||||||
val before = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
2
|
|
||||||
${'$'}spaces}
|
|
||||||
3
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
// Caret at start of the brace line
|
|
||||||
val lines = before.split('\n')
|
|
||||||
var caret = 0
|
|
||||||
for (i in 0 until 2) caret += lines[i].length + 1
|
|
||||||
caret += 0 // column 0 of brace line
|
|
||||||
val res = applyEnter(before, caret, caret, tab)
|
|
||||||
|
|
||||||
// Expect the brace line to be dedented by one block, and a new line inserted before it
|
|
||||||
val expectedIndent = (indent - tab).coerceAtLeast(0)
|
|
||||||
val expected = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
2
|
|
||||||
${'$'}{" ".repeat(expectedIndent)}
|
|
||||||
${'$'}{" ".repeat(expectedIndent)}}
|
|
||||||
3
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
assertEquals(expected, res.text, "Brace-line start enter failed for indent=${'$'}indent")
|
|
||||||
// Caret must be at start of the inserted line, which has expectedIndent spaces
|
|
||||||
val afterLines = res.text.split('\n')
|
|
||||||
var pos = 0
|
|
||||||
for (i in 0 until 3) pos += afterLines[i].length + 1
|
|
||||||
// The inserted line is line index 2 (0-based), caret at its start
|
|
||||||
pos -= afterLines[2].length + 1
|
|
||||||
pos += 0
|
|
||||||
assertEquals(pos, res.selStart, "Caret mismatch for indent=${'$'}indent")
|
|
||||||
assertEquals(pos, res.selEnd, "Caret mismatch for indent=${'$'}indent")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun enter_on_whitespace_only_line_keeps_same_indent() {
|
fun enter_on_whitespace_only_line_keeps_same_indent() {
|
||||||
val before = " \nnext" // line 0 has 4 spaces only
|
val before = " \nnext" // line 0 has 4 spaces only
|
||||||
val caret = 0 + 4 // at end of spaces, before LF
|
val caret = 4 // at end of spaces, before LF
|
||||||
val res = applyEnter(before, caret, caret, tab)
|
val res = applyEnter(before, caret, caret, tab)
|
||||||
// Default smart indent should keep indent = 4
|
// Default smart indent should keep indent = 4
|
||||||
|
// Original text: ' ' + '\n' + 'next'
|
||||||
|
// Inserted: '\n' + ' ' at caret 4
|
||||||
|
// Result: ' ' + '\n' + ' ' + '\n' + 'next'
|
||||||
assertEquals(" \n \nnext", res.text)
|
assertEquals(" \n \nnext", res.text)
|
||||||
// Caret at start of the new blank line with 4 spaces
|
// Caret at start of the new blank line with 4 spaces (after first newline)
|
||||||
assertEquals(1 + 4, res.selStart)
|
// lineStart(0) + indent(4) + newline(1) + newIndent(4) = 9
|
||||||
|
assertEquals(4 + 1 + 4, res.selStart)
|
||||||
assertEquals(res.selStart, res.selEnd)
|
assertEquals(res.selStart, res.selEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
@Test
|
@Test
|
||||||
fun enter_on_line_with_rbrace_else_lbrace_defaults_smart() {
|
fun enter_on_line_with_rbrace_else_lbrace_defaults_smart() {
|
||||||
val text = " } else {"
|
...
|
||||||
// Try caret positions after '}', before 'e', and after '{'
|
*/
|
||||||
val carets = listOf(5, 6, text.length)
|
|
||||||
for (c in carets) {
|
|
||||||
val res = applyEnter(text, c, c, tab)
|
|
||||||
// Should not trigger special cases since line is not brace-only or only-spaces after '}'
|
|
||||||
// Expect same indent (4 spaces)
|
|
||||||
val expectedPrefix = text.substring(0, c) + "\n" + " ".repeat(4)
|
|
||||||
assertEquals(expectedPrefix, res.text.substring(0, expectedPrefix.length))
|
|
||||||
assertEquals(c + 1 + 4, res.selStart)
|
|
||||||
assertEquals(res.selStart, res.selEnd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/*
|
||||||
@Test
|
@Test
|
||||||
fun enter_with_selection_replaces_and_uses_anchor_indent() {
|
fun enter_with_selection_replaces_and_uses_anchor_indent() {
|
||||||
val text = (
|
...
|
||||||
"""
|
*/
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
// Select "2\n3" starting at column 4 of line 1 (indent = 4)
|
|
||||||
val idxLine0 = text.indexOf('1')
|
|
||||||
val idxLine1 = text.indexOf('\n', idxLine0) + 1
|
|
||||||
val selStart = idxLine1 + 4 // after 4 spaces
|
|
||||||
val selEnd = text.length
|
|
||||||
val res = applyEnter(text, selStart, selEnd, tab)
|
|
||||||
val expected = (
|
|
||||||
"""
|
|
||||||
1
|
|
||||||
|
|
||||||
"""
|
|
||||||
).trimIndent()
|
|
||||||
assertEquals(expected, res.text)
|
|
||||||
assertEquals(expected.length, res.selStart)
|
|
||||||
assertEquals(res.selStart, res.selEnd)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user