Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d9befe101 | ||
|
|
abb262d9cf | ||
|
|
eca451b5a3 | ||
|
|
54af50d6d6 | ||
|
|
ce0fc3650d | ||
|
|
ce021e85f4 |
No files matched your search
+18
-1
@@ -11,7 +11,22 @@
|
|||||||
- Integration: Updated TextMate grammar and IntelliJ plugin (highlighting + keywords).
|
- Integration: Updated TextMate grammar and IntelliJ plugin (highlighting + keywords).
|
||||||
- Documentation: New "Properties" section in `docs/OOP.md`.
|
- Documentation: New "Properties" section in `docs/OOP.md`.
|
||||||
|
|
||||||
- Docs: Scopes and Closures guidance
|
- Language: Restricted Setter Visibility
|
||||||
|
- Support for `private set` and `protected set` modifiers on `var` fields and properties.
|
||||||
|
- Allows members to be publicly readable but only writable from within the declaring class or its subclasses.
|
||||||
|
- Enforcement at runtime: throws `AccessException` on unauthorized writes.
|
||||||
|
- Supported only for declarations in class bodies (fields and properties).
|
||||||
|
- Documentation: New "Restricted Setter Visibility" section in `docs/OOP.md`.
|
||||||
|
|
||||||
|
- Language: Late-initialized `val` fields in classes
|
||||||
|
- Support for declaring `val` without an immediate initializer in class bodies.
|
||||||
|
- Compulsory initialization: every late-init `val` must be assigned at least once within the class body or an `init` block.
|
||||||
|
- Write-once enforcement: assigning to a `val` is allowed only if its current value is `Unset`.
|
||||||
|
- Access protection: reading a late-init `val` before it is assigned returns the `Unset` singleton; using `Unset` for most operations throws an `UnsetException`.
|
||||||
|
- Extension properties do not support late-init.
|
||||||
|
- Documentation: New "Late-initialized `val` fields" and "The `Unset` singleton" sections in `docs/OOP.md`.
|
||||||
|
|
||||||
|
- Docs: OOP improvements
|
||||||
- New page: `docs/scopes_and_closures.md` detailing `ClosureScope` resolution order, recursion‑safe helpers (`chainLookupIgnoreClosure`, `chainLookupWithMembers`, `baseGetIgnoreClosure`), cycle prevention, and capturing lexical environments for callbacks (`snapshotForClosure`).
|
- New page: `docs/scopes_and_closures.md` detailing `ClosureScope` resolution order, recursion‑safe helpers (`chainLookupIgnoreClosure`, `chainLookupWithMembers`, `baseGetIgnoreClosure`), cycle prevention, and capturing lexical environments for callbacks (`snapshotForClosure`).
|
||||||
- Updated: `docs/advanced_topics.md` (link to the new page), `docs/parallelism.md` (closures in `launch`/`flow`), `docs/OOP.md` (visibility from closures with preserved `currentClassCtx`), `docs/exceptions_handling.md` (compatibility alias `SymbolNotFound`).
|
- Updated: `docs/advanced_topics.md` (link to the new page), `docs/parallelism.md` (closures in `launch`/`flow`), `docs/OOP.md` (visibility from closures with preserved `currentClassCtx`), `docs/exceptions_handling.md` (compatibility alias `SymbolNotFound`).
|
||||||
- Tutorial: added quick link to Scopes and Closures.
|
- Tutorial: added quick link to Scopes and Closures.
|
||||||
@@ -83,6 +98,8 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Mutually exclusive: `--check` and `--in-place` together now produce an error and exit with code 1.
|
- Mutually exclusive: `--check` and `--in-place` together now produce an error and exit with code 1.
|
||||||
- Multi-file stdout prints headers `--- <path> ---` per file.
|
- Multi-file stdout prints headers `--- <path> ---` per file.
|
||||||
- `lyng --help` shows `fmt`; `lyng fmt --help` displays dedicated help.
|
- `lyng --help` shows `fmt`; `lyng fmt --help` displays dedicated help.
|
||||||
|
- Fix: Property accessors (`get`, `set`, `private set`, `protected set`) are now correctly indented relative to the property declaration.
|
||||||
|
- Fix: Indentation now correctly carries over into blocks that start on extra‑indented lines (e.g., nested `if` statements or property accessor bodies).
|
||||||
|
|
||||||
- CLI: Preserved legacy script invocation fast-paths:
|
- CLI: Preserved legacy script invocation fast-paths:
|
||||||
- `lyng script.lyng [args...]` executes the script directly.
|
- `lyng script.lyng [args...]` executes the script directly.
|
||||||
|
|||||||
+123
@@ -100,6 +100,28 @@ class Counter {
|
|||||||
- **No Backing Fields**: There is no magic `field` identifier. If you need to store state, you must declare a separate (usually `private`) field.
|
- **No Backing Fields**: There is no magic `field` identifier. If you need to store state, you must declare a separate (usually `private`) field.
|
||||||
- **Type Inference**: You can omit the type declaration if it can be inferred or if you don't need strict typing.
|
- **Type Inference**: You can omit the type declaration if it can be inferred or if you don't need strict typing.
|
||||||
|
|
||||||
|
### Lazy Properties with `cached`
|
||||||
|
|
||||||
|
When you want to define a property that is computed only once (on demand) and then remembered, use the built-in `cached` function. This is more efficient than a regular property with `get()` if the computation is expensive, as it avoids re-calculating the value on every access.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class DataService(val id: Int) {
|
||||||
|
// The lambda passed to cached is only executed once, the first time data() is called.
|
||||||
|
val data = cached {
|
||||||
|
println("Fetching data for " + id)
|
||||||
|
// Perform expensive operation
|
||||||
|
"Record " + id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val service = DataService(42)
|
||||||
|
// No printing yet
|
||||||
|
println(service.data()) // Prints "Fetching data for 42", then returns "Record 42"
|
||||||
|
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.
|
||||||
|
|
||||||
## 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.
|
||||||
@@ -153,6 +175,44 @@ statements discussed later, there could be default values, ellipsis, etc.
|
|||||||
|
|
||||||
Note that unlike **Kotlin**, which uses `=` for named arguments, Lyng uses `:` to avoid ambiguity with assignment expressions.
|
Note that unlike **Kotlin**, which uses `=` for named arguments, Lyng uses `:` to avoid ambiguity with assignment expressions.
|
||||||
|
|
||||||
|
### Late-initialized `val` fields
|
||||||
|
|
||||||
|
You can declare a `val` field without an immediate initializer if you provide an assignment for it within an `init` block or the class body. This is useful when the initial value depends on logic that cannot be expressed in a single expression.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class DataProcessor(data: Object) {
|
||||||
|
val result: Object
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Complex initialization logic
|
||||||
|
result = transform(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key rules for late-init `val`:
|
||||||
|
- **Compile-time Check**: The compiler ensures that every `val` declared without an initializer in a class body has at least one assignment within that class body (including `init` blocks). Failing to do so results in a syntax error.
|
||||||
|
- **Write-Once**: A `val` can only be assigned once. Even if it was declared without an initializer, once it is assigned a value (e.g., in `init`), any subsequent assignment will throw an `IllegalAssignmentException`.
|
||||||
|
- **Access before Initialization**: If you attempt to read a late-init `val` before it has been assigned (for example, by calling a method in `init` that reads the field before its assignment), it will hold a special `Unset` value. Using `Unset` for most operations (like arithmetic or method calls) will throw an `UnsetException`.
|
||||||
|
- **No Extensions**: Extension properties do not support late initialization as they do not have per-instance storage. Extension `val`s must always have an initializer or a `get()` accessor.
|
||||||
|
|
||||||
|
### The `Unset` singleton
|
||||||
|
|
||||||
|
The `Unset` singleton represents a field that has been declared but not yet initialized. While it can be compared and converted to a string, most other operations on it are forbidden to prevent accidental use of uninitialized data.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class T {
|
||||||
|
val x
|
||||||
|
fun check() {
|
||||||
|
if (x == Unset) println("Not ready")
|
||||||
|
}
|
||||||
|
init {
|
||||||
|
check() // Prints "Not ready"
|
||||||
|
x = 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
Functions defined inside a class body are methods, and unless declared
|
Functions defined inside a class body are methods, and unless declared
|
||||||
@@ -372,6 +432,69 @@ Are declared with var
|
|||||||
assert( p.isSpecial == true )
|
assert( p.isSpecial == true )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
### Restricted Setter Visibility
|
||||||
|
|
||||||
|
You can restrict the visibility of a `var` field's or property's setter by using `private set` or `protected set` modifiers. This allows the member to be publicly readable but only writable from within the class or its subclasses.
|
||||||
|
|
||||||
|
#### On Fields
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class SecretCounter {
|
||||||
|
var count = 0
|
||||||
|
private set // Can be read anywhere, but written only in SecretCounter
|
||||||
|
|
||||||
|
fun increment() { count++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
val c = SecretCounter()
|
||||||
|
println(c.count) // OK
|
||||||
|
c.count = 10 // Throws AccessException
|
||||||
|
c.increment() // OK
|
||||||
|
```
|
||||||
|
|
||||||
|
#### On Properties
|
||||||
|
|
||||||
|
You can also apply restricted visibility to custom property setters:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class Person(private var _age: Int) {
|
||||||
|
var age
|
||||||
|
get() = _age
|
||||||
|
private set(v) { if (v >= 0) _age = v }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Protected Setters and Inheritance
|
||||||
|
|
||||||
|
A `protected set` allows subclasses to modify a field that is otherwise read-only to the public:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class Base {
|
||||||
|
var state = "initial"
|
||||||
|
protected set
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived : Base() {
|
||||||
|
fun changeState(newVal) {
|
||||||
|
state = newVal // OK: protected access from subclass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val d = Derived()
|
||||||
|
println(d.state) // OK: "initial"
|
||||||
|
d.changeState("updated")
|
||||||
|
println(d.state) // OK: "updated"
|
||||||
|
d.state = "bad" // Throws AccessException: public write not allowed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Rules and Limitations
|
||||||
|
|
||||||
|
- **Only for `var`**: Restricted setter visibility cannot be used with `val` declarations, as they are inherently read-only. Attempting to use it with `val` results in a syntax error.
|
||||||
|
- **Class Body Only**: These modifiers can only be used on members declared within the class body. They are not supported for primary constructor parameters.
|
||||||
|
- **`private set`**: The setter is only accessible within the same class context (specifically, when `this` is an instance of that class).
|
||||||
|
- **`protected set`**: The setter is accessible within the declaring class and all its transitive subclasses.
|
||||||
|
- **Multiple Inheritance**: In MI scenarios, visibility is checked against the class that actually declared the member. Qualified access (e.g., `this@Base.field = value`) also respects restricted setter visibility.
|
||||||
|
|
||||||
### Private fields
|
### Private fields
|
||||||
|
|
||||||
Private fields are visible only _inside the class instance_:
|
Private fields are visible only _inside the class instance_:
|
||||||
|
|||||||
@@ -68,6 +68,26 @@ Tip: If a closure unexpectedly cannot see an outer local, check whether an inter
|
|||||||
- The `visited` sets used for cycle detection are tiny and short‑lived; in typical scripts the overhead is negligible.
|
- The `visited` sets used for cycle detection are tiny and short‑lived; in typical scripts the overhead is negligible.
|
||||||
- If profiling shows hotspots, consider limiting ancestry depth in your custom helpers or using small fixed arrays instead of hash sets—only for extremely hot code paths.
|
- If profiling shows hotspots, consider limiting ancestry depth in your custom helpers or using small fixed arrays instead of hash sets—only for extremely hot code paths.
|
||||||
|
|
||||||
|
## Practical Example: `cached`
|
||||||
|
|
||||||
|
The `cached` function (defined in `lyng.stdlib`) is a classic example of using closures to maintain state. It wraps a builder into a zero-argument function that computes once and remembers the result:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun cached(builder) {
|
||||||
|
var calculated = false
|
||||||
|
var value = null
|
||||||
|
{ // This lambda captures `calculated`, `value`, and `builder`
|
||||||
|
if( !calculated ) {
|
||||||
|
value = builder()
|
||||||
|
calculated = true
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Because Lyng now correctly isolates closures for each evaluation of a lambda literal, using `cached` inside a class instance works as expected: each instance maintains its own private `calculated` and `value` state, even if they share the same property declaration.
|
||||||
|
|
||||||
## Dos and Don’ts
|
## Dos and Don’ts
|
||||||
- Do use `chainLookupIgnoreClosure` / `chainLookupWithMembers` for ancestry traversals.
|
- Do use `chainLookupIgnoreClosure` / `chainLookupWithMembers` for ancestry traversals.
|
||||||
- Do maintain the resolution order above for predictable behavior.
|
- Do maintain the resolution order above for predictable behavior.
|
||||||
|
|||||||
+45
-1
@@ -1497,7 +1497,7 @@ See [math functions](math.md). Other general purpose functions are:
|
|||||||
| print(args...) | Open for overriding, it prints to stdout without newline. |
|
| print(args...) | Open for overriding, it prints to stdout without newline. |
|
||||||
| flow {} | create flow sequence, see [parallelism] |
|
| flow {} | create flow sequence, see [parallelism] |
|
||||||
| delay, launch, yield | see [parallelism] |
|
| delay, launch, yield | see [parallelism] |
|
||||||
| cached(builder) | remembers builder() on first invocation and return it then |
|
| cached(builder) | [Lazy evaluation with `cached`](#lazy-evaluation-with-cached) |
|
||||||
| let, also, apply, run | see above, flow controls |
|
| let, also, apply, run | see above, flow controls |
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
@@ -1545,6 +1545,50 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
|
|||||||
|
|
||||||
[Regex]: Regex.md
|
[Regex]: Regex.md
|
||||||
|
|
||||||
|
## Lazy evaluation with `cached`
|
||||||
|
|
||||||
|
Sometimes you have an expensive computation that you only want to perform if and when it is actually needed, and then remember (cache) the result for all future calls. Lyng provides the `cached(builder)` function for this purpose.
|
||||||
|
|
||||||
|
It is extremely simple to use: you pass it a block (lambda) that performs the computation, and it returns a zero-argument function that manages the caching for you.
|
||||||
|
|
||||||
|
### Basic Example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val expensive = cached {
|
||||||
|
println("Performing expensive calculation...")
|
||||||
|
2 + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
println(expensive()) // Prints "Performing expensive calculation..." then "4"
|
||||||
|
println(expensive()) // Prints only "4" (result is cached)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefits and Simplicity
|
||||||
|
|
||||||
|
1. **Lazy Execution:** The code inside the `cached` block doesn't run until you actually call the resulting function.
|
||||||
|
2. **Automatic State Management:** You don't need to manually check if a value has been computed or store it in a separate variable.
|
||||||
|
3. **Closures and Class Support:** `cached` works perfectly with closures. If you use it inside a class, it will correctly capture the instance variables, and each instance will have its own independent cache.
|
||||||
|
|
||||||
|
### Use Case: Lazy Properties in Classes
|
||||||
|
|
||||||
|
This is the most common use case for `cached`. It allows you to define expensive "fields" that are only computed if someone actually uses them:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class User(val id: Int) {
|
||||||
|
// The details will be fetched only once, on demand
|
||||||
|
val details = cached {
|
||||||
|
println("Fetching details for user " + id)
|
||||||
|
// Db.query is a hypothetical example
|
||||||
|
Db.query("SELECT * FROM users WHERE id = " + id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val u = User(101)
|
||||||
|
// ... nothing happens yet ...
|
||||||
|
val d = u.details() // Computation happens here
|
||||||
|
val sameD = u.details() // Returns the same result immediately
|
||||||
|
```
|
||||||
|
|
||||||
## Multiple Inheritance (quick start)
|
## Multiple Inheritance (quick start)
|
||||||
|
|
||||||
Lyng supports multiple inheritance (MI) with simple, predictable rules. For a full reference see OOP notes, this is a quick, copy‑paste friendly overview.
|
Lyng supports multiple inheritance (MI) with simple, predictable rules. For a full reference see OOP notes, this is a quick, copy‑paste friendly overview.
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# What's New in Lyng
|
||||||
|
|
||||||
|
This document highlights the latest additions and improvements to the Lyng language and its ecosystem.
|
||||||
|
|
||||||
|
## Language Features
|
||||||
|
|
||||||
|
### Class Properties with Accessors
|
||||||
|
Classes now support properties with custom `get()` and `set()` accessors. Properties in Lyng do **not** have automatic backing fields; they are pure accessors.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Person(private var _age: Int) {
|
||||||
|
// Read-only property
|
||||||
|
val ageCategory get() = if (_age < 18) "Minor" else "Adult"
|
||||||
|
|
||||||
|
// Read-write property
|
||||||
|
var age: Int
|
||||||
|
get() = _age
|
||||||
|
set(v) {
|
||||||
|
if (v >= 0) _age = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Private and Protected Setters
|
||||||
|
You can now restrict the visibility of a property's or field's setter using `private set` or `protected set`. This allows members to be publicly readable but only writable from within the declaring class or its subclasses.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Counter {
|
||||||
|
var count = 0
|
||||||
|
private set // Field with private setter
|
||||||
|
|
||||||
|
fun increment() { count++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdvancedCounter : Counter {
|
||||||
|
var totalOperations = 0
|
||||||
|
protected set // Settable here and in further subclasses
|
||||||
|
}
|
||||||
|
|
||||||
|
let c = Counter()
|
||||||
|
c.increment() // OK
|
||||||
|
// c.count = 10 // Error: setter is private
|
||||||
|
```
|
||||||
|
|
||||||
|
### Late-initialized `val` Fields
|
||||||
|
`val` fields in classes can be declared without an immediate initializer, provided they are assigned exactly once. If accessed before initialization, they hold the special `Unset` singleton.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Service {
|
||||||
|
val logger
|
||||||
|
|
||||||
|
fun check() {
|
||||||
|
if (logger == Unset) println("Not initialized yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
logger = Logger("Service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Named Arguments and Named Splats
|
||||||
|
Function calls now support named arguments using the `name: value` syntax. If the variable name matches the parameter name, you can use the `name:` shorthand.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun greet(name, greeting = "Hello") {
|
||||||
|
println("$greeting, $name!")
|
||||||
|
}
|
||||||
|
|
||||||
|
val name = "Alice"
|
||||||
|
greet(name:) // Shorthand for greet(name: name)
|
||||||
|
greet(greeting: "Hi", name: "Bob")
|
||||||
|
|
||||||
|
let params = { name: "Charlie", greeting: "Hey")
|
||||||
|
greet(...params) // Named splat expansion
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Inheritance (MI)
|
||||||
|
Lyng now supports multiple inheritance using the C3 Method Resolution Order (MRO). Use `this@Type` or casts for disambiguation.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class A { fun foo() = "A" }
|
||||||
|
class B { fun foo() = "B" }
|
||||||
|
|
||||||
|
class Derived : A, B {
|
||||||
|
fun test() {
|
||||||
|
println(foo()) // Resolves to A.foo (leftmost)
|
||||||
|
println(this@B.foo()) // Qualified dispatch to B.foo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let d = Derived()
|
||||||
|
println((d as B).foo()) // Disambiguation via cast
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tooling and Infrastructure
|
||||||
|
|
||||||
|
### CLI: Formatting Command
|
||||||
|
A new `fmt` subcommand has been added to the Lyng CLI.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lyng fmt MyFile.lyng # Print formatted code to stdout
|
||||||
|
lyng fmt --in-place MyFile.lyng # Format file in-place
|
||||||
|
lyng fmt --check MyFile.lyng # Check if file needs formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
### IDEA Plugin: Autocompletion
|
||||||
|
Experimental lightweight autocompletion is now available in the IntelliJ plugin. It features type-aware member suggestions and inheritance-aware completion.
|
||||||
|
|
||||||
|
You can enable it in **Settings | Lyng Formatter | Enable Lyng autocompletion**.
|
||||||
+1
-1
@@ -21,7 +21,6 @@
|
|||||||
*/
|
*/
|
||||||
package net.sergeych.lyng.idea.completion
|
package net.sergeych.lyng.idea.completion
|
||||||
|
|
||||||
import LyngAstManager
|
|
||||||
import com.intellij.codeInsight.completion.*
|
import com.intellij.codeInsight.completion.*
|
||||||
import com.intellij.codeInsight.lookup.LookupElementBuilder
|
import com.intellij.codeInsight.lookup.LookupElementBuilder
|
||||||
import com.intellij.icons.AllIcons
|
import com.intellij.icons.AllIcons
|
||||||
@@ -36,6 +35,7 @@ import net.sergeych.lyng.idea.LyngLanguage
|
|||||||
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
||||||
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
||||||
import net.sergeych.lyng.idea.util.DocsBootstrap
|
import net.sergeych.lyng.idea.util.DocsBootstrap
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
import net.sergeych.lyng.idea.util.TextCtx
|
import net.sergeych.lyng.idea.util.TextCtx
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,13 @@ class LyngEnterHandler : EnterHandlerDelegate {
|
|||||||
if (code == "}" || code == "*/") {
|
if (code == "}" || code == "*/") {
|
||||||
// Adjust indent for the previous line if it's a block or comment closer
|
// Adjust indent for the previous line if it's a block or comment closer
|
||||||
val prevStart = doc.getLineStartOffset(prevLine)
|
val prevStart = doc.getLineStartOffset(prevLine)
|
||||||
CodeStyleManager.getInstance(project).adjustLineIndent(file, prevStart)
|
if (file.context == null) {
|
||||||
|
try {
|
||||||
|
CodeStyleManager.getInstance(project).adjustLineIndent(file, prevStart)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("Failed to adjust line indent for previous line: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback for previous line: manual application
|
// Fallback for previous line: manual application
|
||||||
val desiredPrev = computeDesiredIndent(project, doc, prevLine)
|
val desiredPrev = computeDesiredIndent(project, doc, prevLine)
|
||||||
@@ -102,8 +108,13 @@ class LyngEnterHandler : EnterHandlerDelegate {
|
|||||||
}
|
}
|
||||||
// Adjust indent for the current (new) line
|
// Adjust indent for the current (new) line
|
||||||
val currentStart = doc.getLineStartOffsetSafe(currentLine)
|
val currentStart = doc.getLineStartOffsetSafe(currentLine)
|
||||||
val csm = CodeStyleManager.getInstance(project)
|
if (file.context == null) {
|
||||||
csm.adjustLineIndent(file, currentStart)
|
try {
|
||||||
|
CodeStyleManager.getInstance(project).adjustLineIndent(file, currentStart)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("Failed to adjust line indent for current line: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: if the platform didn't physically insert indentation, compute it from our formatter and apply
|
// Fallback: if the platform didn't physically insert indentation, compute it from our formatter and apply
|
||||||
val lineStart = doc.getLineStartOffset(currentLine)
|
val lineStart = doc.getLineStartOffset(currentLine)
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ class LyngTypedHandler : TypedHandlerDelegate() {
|
|||||||
}
|
}
|
||||||
// After block reindent, adjust line indent to what platform thinks (no-op in many cases)
|
// After block reindent, adjust line indent to what platform thinks (no-op in many cases)
|
||||||
val lineStart = doc.getLineStartOffset(line)
|
val lineStart = doc.getLineStartOffset(line)
|
||||||
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
if (file.context == null) {
|
||||||
|
try {
|
||||||
|
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("Failed to adjust line indent for current line: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (c == '/') {
|
} else if (c == '/') {
|
||||||
val doc = editor.document
|
val doc = editor.document
|
||||||
@@ -67,7 +73,13 @@ class LyngTypedHandler : TypedHandlerDelegate() {
|
|||||||
PsiDocumentManager.getInstance(project).commitDocument(doc)
|
PsiDocumentManager.getInstance(project).commitDocument(doc)
|
||||||
val line = doc.getLineNumber(offset - 1)
|
val line = doc.getLineNumber(offset - 1)
|
||||||
val lineStart = doc.getLineStartOffset(line)
|
val lineStart = doc.getLineStartOffset(line)
|
||||||
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
if (file.context == null) {
|
||||||
|
try {
|
||||||
|
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("Failed to adjust line indent for comment: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Manual application fallback
|
// Manual application fallback
|
||||||
val desired = computeDesiredIndent(project, doc, line)
|
val desired = computeDesiredIndent(project, doc, line)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -80,7 +80,14 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
val lineStart = doc.getLineStartOffset(line)
|
val lineStart = doc.getLineStartOffset(line)
|
||||||
// adjustLineIndent delegates to our LineIndentProvider which computes
|
// adjustLineIndent delegates to our LineIndentProvider which computes
|
||||||
// indentation from scratch; this is safe and idempotent
|
// indentation from scratch; this is safe and idempotent
|
||||||
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
if (file.context == null) {
|
||||||
|
try {
|
||||||
|
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Log as debug because this can be called many times during reformat
|
||||||
|
// and we don't want to spam warnings if it's a known platform issue with injections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// After indentation, update block/paren/bracket balances using the current line text
|
// After indentation, update block/paren/bracket balances using the current line text
|
||||||
val lineEnd = doc.getLineEndOffset(line)
|
val lineEnd = doc.getLineEndOffset(line)
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.idea.navigation
|
package net.sergeych.lyng.idea.navigation
|
||||||
|
|
||||||
import LyngAstManager
|
|
||||||
import com.intellij.lang.cacheBuilder.DefaultWordsScanner
|
import com.intellij.lang.cacheBuilder.DefaultWordsScanner
|
||||||
import com.intellij.lang.cacheBuilder.WordsScanner
|
import com.intellij.lang.cacheBuilder.WordsScanner
|
||||||
import com.intellij.lang.findUsages.FindUsagesProvider
|
import com.intellij.lang.findUsages.FindUsagesProvider
|
||||||
@@ -26,6 +25,7 @@ import com.intellij.psi.PsiElement
|
|||||||
import com.intellij.psi.tree.TokenSet
|
import com.intellij.psi.tree.TokenSet
|
||||||
import net.sergeych.lyng.idea.highlight.LyngLexer
|
import net.sergeych.lyng.idea.highlight.LyngLexer
|
||||||
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
import net.sergeych.lyng.miniast.DocLookupUtils
|
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||||
|
|
||||||
class LyngFindUsagesProvider : FindUsagesProvider {
|
class LyngFindUsagesProvider : FindUsagesProvider {
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.idea.navigation
|
package net.sergeych.lyng.idea.navigation
|
||||||
|
|
||||||
import LyngAstManager
|
|
||||||
import com.intellij.icons.AllIcons
|
import com.intellij.icons.AllIcons
|
||||||
import com.intellij.ide.IconProvider
|
import com.intellij.ide.IconProvider
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
import net.sergeych.lyng.miniast.DocLookupUtils
|
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,13 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.idea.navigation
|
package net.sergeych.lyng.idea.navigation
|
||||||
|
|
||||||
import LyngAstManager
|
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.util.TextRange
|
import com.intellij.openapi.util.TextRange
|
||||||
import com.intellij.psi.*
|
import com.intellij.psi.*
|
||||||
import com.intellij.psi.search.FilenameIndex
|
import com.intellij.psi.search.FilenameIndex
|
||||||
import com.intellij.psi.search.GlobalSearchScope
|
import com.intellij.psi.search.GlobalSearchScope
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
import net.sergeych.lyng.idea.util.TextCtx
|
import net.sergeych.lyng.idea.util.TextCtx
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -17,12 +17,12 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.idea.navigation
|
package net.sergeych.lyng.idea.navigation
|
||||||
|
|
||||||
import LyngAstManager
|
|
||||||
import com.intellij.patterns.PlatformPatterns
|
import com.intellij.patterns.PlatformPatterns
|
||||||
import com.intellij.psi.*
|
import com.intellij.psi.*
|
||||||
import com.intellij.util.ProcessingContext
|
import com.intellij.util.ProcessingContext
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
import net.sergeych.lyng.miniast.DocLookupUtils
|
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||||
|
|
||||||
class LyngPsiReferenceContributor : PsiReferenceContributor() {
|
class LyngPsiReferenceContributor : PsiReferenceContributor() {
|
||||||
|
|||||||
@@ -15,23 +15,6 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
a/*
|
|
||||||
* 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.idea.util
|
package net.sergeych.lyng.idea.util
|
||||||
|
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "1.1.0-SNAPSHOT"
|
version = "1.1.0-rc"
|
||||||
|
|
||||||
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
||||||
|
|
||||||
|
|||||||
@@ -20,5 +20,7 @@ package net.sergeych.lyng
|
|||||||
sealed class CodeContext {
|
sealed class CodeContext {
|
||||||
class Module(@Suppress("unused") val packageName: String?): CodeContext()
|
class Module(@Suppress("unused") val packageName: String?): CodeContext()
|
||||||
class Function(val name: String): CodeContext()
|
class Function(val name: String): CodeContext()
|
||||||
class ClassBody(val name: String): CodeContext()
|
class ClassBody(val name: String): CodeContext() {
|
||||||
|
val pendingInitializations = mutableMapOf<String, Pos>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -138,9 +138,16 @@ class Compiler(
|
|||||||
private var lastParsedBlockRange: MiniRange? = null
|
private var lastParsedBlockRange: MiniRange? = null
|
||||||
|
|
||||||
private suspend fun <T> inCodeContext(context: CodeContext, f: suspend () -> T): T {
|
private suspend fun <T> inCodeContext(context: CodeContext, f: suspend () -> T): T {
|
||||||
return try {
|
codeContexts.add(context)
|
||||||
codeContexts.add(context)
|
try {
|
||||||
f()
|
val res = f()
|
||||||
|
if (context is CodeContext.ClassBody) {
|
||||||
|
if (context.pendingInitializations.isNotEmpty()) {
|
||||||
|
val (name, pos) = context.pendingInitializations.entries.first()
|
||||||
|
throw ScriptError(pos, "val '$name' must be initialized in the class body or init block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
} finally {
|
} finally {
|
||||||
codeContexts.removeLast()
|
codeContexts.removeLast()
|
||||||
}
|
}
|
||||||
@@ -360,7 +367,21 @@ class Compiler(
|
|||||||
val rvalue = parseExpressionLevel(level + 1)
|
val rvalue = parseExpressionLevel(level + 1)
|
||||||
?: throw ScriptError(opToken.pos, "Expecting expression")
|
?: throw ScriptError(opToken.pos, "Expecting expression")
|
||||||
|
|
||||||
lvalue = op.generate(opToken.pos, lvalue!!, rvalue)
|
val res = op.generate(opToken.pos, lvalue!!, rvalue)
|
||||||
|
if (opToken.type == Token.Type.ASSIGN) {
|
||||||
|
val ctx = codeContexts.lastOrNull()
|
||||||
|
if (ctx is CodeContext.ClassBody) {
|
||||||
|
val target = lvalue
|
||||||
|
val name = when (target) {
|
||||||
|
is LocalVarRef -> target.name
|
||||||
|
is FastLocalVarRef -> target.name
|
||||||
|
is FieldRef -> if (target.target is LocalVarRef && target.target.name == "this") target.name else null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (name != null) ctx.pendingInitializations.remove(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lvalue = res
|
||||||
}
|
}
|
||||||
return lvalue
|
return lvalue
|
||||||
}
|
}
|
||||||
@@ -492,24 +513,32 @@ class Compiler(
|
|||||||
// there could be terminal operators or keywords:// variable to read or like
|
// there could be terminal operators or keywords:// variable to read or like
|
||||||
when (t.value) {
|
when (t.value) {
|
||||||
in stopKeywords -> {
|
in stopKeywords -> {
|
||||||
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
|
if (t.value == "init" && !(codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE)) {
|
||||||
// Allow certain statement-like constructs to act as expressions
|
// Soft keyword: init is only a keyword in class body when followed by {
|
||||||
// when they appear in expression position (e.g., `if (...) ... else ...`).
|
cc.previous()
|
||||||
// Other keywords should be handled by the outer statement parser.
|
operand = parseAccessor()
|
||||||
when (t.value) {
|
} else {
|
||||||
"if" -> {
|
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
|
||||||
val s = parseIfStatement()
|
// Allow certain statement-like constructs to act as expressions
|
||||||
operand = StatementRef(s)
|
// when they appear in expression position (e.g., `if (...) ... else ...`).
|
||||||
}
|
// Other keywords should be handled by the outer statement parser.
|
||||||
"when" -> {
|
when (t.value) {
|
||||||
val s = parseWhenStatement()
|
"if" -> {
|
||||||
operand = StatementRef(s)
|
val s = parseIfStatement()
|
||||||
}
|
operand = StatementRef(s)
|
||||||
else -> {
|
}
|
||||||
// Do not consume the keyword as part of a term; backtrack
|
|
||||||
// and return null so outer parser handles it.
|
"when" -> {
|
||||||
cc.previous()
|
val s = parseWhenStatement()
|
||||||
return null
|
operand = StatementRef(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Do not consume the keyword as part of a term; backtrack
|
||||||
|
// and return null so outer parser handles it.
|
||||||
|
cc.previous()
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,35 +657,30 @@ class Compiler(
|
|||||||
|
|
||||||
val body = parseBlock(skipLeadingBrace = true)
|
val body = parseBlock(skipLeadingBrace = true)
|
||||||
|
|
||||||
var closure: Scope? = null
|
return ValueFnRef { closureScope ->
|
||||||
|
statement {
|
||||||
val callStatement = statement {
|
// and the source closure of the lambda which might have other thisObj.
|
||||||
// and the source closure of the lambda which might have other thisObj.
|
val context = this.applyClosure(closureScope)
|
||||||
val context = this.applyClosure(closure!!)
|
// Execute lambda body in a closure-aware context. Blocks inside the lambda
|
||||||
// Execute lambda body in a closure-aware context. Blocks inside the lambda
|
// will create child scopes as usual, so re-declarations inside loops work.
|
||||||
// will create child scopes as usual, so re-declarations inside loops work.
|
if (argsDeclaration == null) {
|
||||||
if (argsDeclaration == null) {
|
// no args: automatic var 'it'
|
||||||
// no args: automatic var 'it'
|
val l = args.list
|
||||||
val l = args.list
|
val itValue: Obj = when (l.size) {
|
||||||
val itValue: Obj = when (l.size) {
|
// no args: it == void
|
||||||
// no args: it == void
|
0 -> ObjVoid
|
||||||
0 -> ObjVoid
|
// one args: it is this arg
|
||||||
// one args: it is this arg
|
1 -> l[0]
|
||||||
1 -> l[0]
|
// more args: it is a list of args
|
||||||
// more args: it is a list of args
|
else -> ObjList(l.toMutableList())
|
||||||
else -> ObjList(l.toMutableList())
|
}
|
||||||
|
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
|
||||||
|
} else {
|
||||||
|
// assign vars as declared the standard way
|
||||||
|
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
|
||||||
}
|
}
|
||||||
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
|
body.execute(context)
|
||||||
} else {
|
}.asReadonly
|
||||||
// assign vars as declared the standard way
|
|
||||||
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
|
|
||||||
}
|
|
||||||
body.execute(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValueFnRef { x ->
|
|
||||||
closure = x
|
|
||||||
callStatement.asReadonly
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,7 +1396,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
"init" -> {
|
"init" -> {
|
||||||
if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
|
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
val block = parseBlock()
|
val block = parseBlock()
|
||||||
lastParsedBlockRange?.let { range ->
|
lastParsedBlockRange?.let { range ->
|
||||||
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
||||||
@@ -2701,7 +2725,7 @@ class Compiler(
|
|||||||
cc.restorePos(markBeforeEq)
|
cc.restorePos(markBeforeEq)
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
val next = cc.peekNextNonWhitespace()
|
val next = cc.peekNextNonWhitespace()
|
||||||
if (next.isId("get") || next.isId("set")) {
|
if (next.isId("get") || next.isId("set") || next.isId("private") || next.isId("protected")) {
|
||||||
isProperty = true
|
isProperty = true
|
||||||
cc.restorePos(markBeforeEq)
|
cc.restorePos(markBeforeEq)
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
@@ -2719,7 +2743,13 @@ class Compiler(
|
|||||||
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
|
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
|
||||||
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
|
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
|
||||||
throw ScriptError(start, "val must be initialized")
|
throw ScriptError(start, "val must be initialized")
|
||||||
else {
|
else if (!isMutable && declaringClassNameCaptured != null && extTypeName == null) {
|
||||||
|
// lateinit val in class: track it
|
||||||
|
(codeContexts.lastOrNull() as? CodeContext.ClassBody)?.pendingInitializations?.put(name, start)
|
||||||
|
cc.restorePos(markBeforeEq)
|
||||||
|
cc.skipWsTokens()
|
||||||
|
setNull = true
|
||||||
|
} else {
|
||||||
cc.restorePos(markBeforeEq)
|
cc.restorePos(markBeforeEq)
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
setNull = true
|
setNull = true
|
||||||
@@ -2758,8 +2788,8 @@ class Compiler(
|
|||||||
// if (isDelegate) throw ScriptError(start, "static delegates are not yet implemented")
|
// 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, pos)
|
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, pos)
|
||||||
addItem(name, isMutable, initValue, visibility, ObjRecord.Type.Field)
|
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field)
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
return NopStatement
|
return NopStatement
|
||||||
@@ -2768,11 +2798,11 @@ class Compiler(
|
|||||||
// Check for accessors if it is a class member
|
// Check for accessors if it is a class member
|
||||||
var getter: Statement? = null
|
var getter: Statement? = null
|
||||||
var setter: Statement? = null
|
var setter: Statement? = null
|
||||||
|
var setterVisibility: Visibility? = null
|
||||||
if (declaringClassNameCaptured != null || extTypeName != null) {
|
if (declaringClassNameCaptured != null || extTypeName != null) {
|
||||||
while (true) {
|
while (true) {
|
||||||
val t = cc.peekNextNonWhitespace()
|
val t = cc.skipWsTokens()
|
||||||
if (t.isId("get")) {
|
if (t.isId("get")) {
|
||||||
cc.skipWsTokens()
|
|
||||||
cc.next() // consume 'get'
|
cc.next() // consume 'get'
|
||||||
cc.requireToken(Token.Type.LPAREN)
|
cc.requireToken(Token.Type.LPAREN)
|
||||||
cc.requireToken(Token.Type.RPAREN)
|
cc.requireToken(Token.Type.RPAREN)
|
||||||
@@ -2788,7 +2818,6 @@ class Compiler(
|
|||||||
throw ScriptError(cc.current().pos, "Expected { or = after get()")
|
throw ScriptError(cc.current().pos, "Expected { or = after get()")
|
||||||
}
|
}
|
||||||
} else if (t.isId("set")) {
|
} else if (t.isId("set")) {
|
||||||
cc.skipWsTokens()
|
|
||||||
cc.next() // consume 'set'
|
cc.next() // consume 'set'
|
||||||
cc.requireToken(Token.Type.LPAREN)
|
cc.requireToken(Token.Type.LPAREN)
|
||||||
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||||
@@ -2814,6 +2843,46 @@ class Compiler(
|
|||||||
} else {
|
} else {
|
||||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||||
}
|
}
|
||||||
|
} else if (t.isId("private") || t.isId("protected")) {
|
||||||
|
val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected
|
||||||
|
val mark = cc.savePos()
|
||||||
|
cc.next() // consume modifier
|
||||||
|
if (cc.skipWsTokens().isId("set")) {
|
||||||
|
cc.next() // consume 'set'
|
||||||
|
setterVisibility = vis
|
||||||
|
if (cc.skipWsTokens().type == Token.Type.LPAREN) {
|
||||||
|
cc.next() // consume '('
|
||||||
|
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||||
|
cc.requireToken(Token.Type.RPAREN)
|
||||||
|
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
|
cc.skipWsTokens()
|
||||||
|
val body = parseBlock()
|
||||||
|
statement(body.pos) { scope ->
|
||||||
|
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||||
|
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||||
|
body.execute(scope)
|
||||||
|
}
|
||||||
|
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
|
||||||
|
cc.skipWsTokens()
|
||||||
|
cc.next() // consume '='
|
||||||
|
val expr = parseExpression() ?: throw ScriptError(
|
||||||
|
cc.current().pos,
|
||||||
|
"Expected setter expression"
|
||||||
|
)
|
||||||
|
val st = (expr as? Statement) ?: statement(expr.pos) { s -> expr.execute(s) }
|
||||||
|
statement(st.pos) { scope ->
|
||||||
|
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||||
|
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||||
|
st.execute(scope)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cc.restorePos(mark)
|
||||||
|
break
|
||||||
|
}
|
||||||
} else break
|
} else break
|
||||||
}
|
}
|
||||||
if (getter != null || setter != null) {
|
if (getter != null || setter != null) {
|
||||||
@@ -2822,11 +2891,13 @@ class Compiler(
|
|||||||
throw ScriptError(start, "var property must have both get() and set()")
|
throw ScriptError(start, "var property must have both get() and set()")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (setter != null)
|
if (setter != null || setterVisibility != null)
|
||||||
throw ScriptError(start, "val property cannot have a setter (name: $name)")
|
throw ScriptError(start, "val property cannot have a setter or restricted visibility set (name: $name)")
|
||||||
if (getter == null)
|
if (getter == null)
|
||||||
throw ScriptError(start, "val property with accessors must have a getter (name: $name)")
|
throw ScriptError(start, "val property with accessors must have a getter (name: $name)")
|
||||||
}
|
}
|
||||||
|
} else if (setterVisibility != null && !isMutable) {
|
||||||
|
throw ScriptError(start, "val field cannot have restricted visibility set (name: $name)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2843,7 +2914,7 @@ class Compiler(
|
|||||||
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
|
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
|
||||||
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
|
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
|
||||||
|
|
||||||
context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, declaringClass = null, type = ObjRecord.Type.Property))
|
context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, writeVisibility = setterVisibility, declaringClass = null, type = ObjRecord.Type.Property))
|
||||||
|
|
||||||
return@statement prop
|
return@statement prop
|
||||||
}
|
}
|
||||||
@@ -2877,6 +2948,7 @@ class Compiler(
|
|||||||
isMutable,
|
isMutable,
|
||||||
prop,
|
prop,
|
||||||
visibility,
|
visibility,
|
||||||
|
setterVisibility,
|
||||||
recordType = ObjRecord.Type.Property
|
recordType = ObjRecord.Type.Property
|
||||||
)
|
)
|
||||||
ObjVoid
|
ObjVoid
|
||||||
@@ -2884,38 +2956,41 @@ class Compiler(
|
|||||||
ObjVoid
|
ObjVoid
|
||||||
} else {
|
} else {
|
||||||
// We are in instance scope already: perform initialization immediately
|
// We are in instance scope already: perform initialization immediately
|
||||||
context.addItem(storageName, isMutable, prop, visibility, recordType = ObjRecord.Type.Property)
|
context.addItem(storageName, isMutable, prop, visibility, setterVisibility, recordType = ObjRecord.Type.Property)
|
||||||
prop
|
prop
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (declaringClassName != null && !isStatic) {
|
val isLateInitVal = !isMutable && initialExpression == null && getter == null && setter == null
|
||||||
val storageName = "$declaringClassName::$name"
|
if (declaringClassName != null && !isStatic) {
|
||||||
// If we are in class scope now (defining instance field), defer initialization to instance time
|
val storageName = "$declaringClassName::$name"
|
||||||
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
|
// If we are in class scope now (defining instance field), defer initialization to instance time
|
||||||
if (isClassScope) {
|
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
|
||||||
val cls = context.thisObj as ObjClass
|
if (isClassScope) {
|
||||||
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
|
val cls = context.thisObj as ObjClass
|
||||||
val initStmt = statement(start) { scp ->
|
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
|
||||||
val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: ObjNull
|
val initStmt = statement(start) { scp ->
|
||||||
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
|
val initValue =
|
||||||
scp.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
initialExpression?.execute(scp)?.byValueCopy() ?: if (isLateInitVal) ObjUnset else ObjNull
|
||||||
|
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
|
||||||
|
scp.addItem(storageName, isMutable, initValue, visibility, setterVisibility, recordType = ObjRecord.Type.Field)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
cls.instanceInitializers += initStmt
|
||||||
ObjVoid
|
ObjVoid
|
||||||
|
} else {
|
||||||
|
// We are in instance scope already: perform initialization immediately
|
||||||
|
val initValue =
|
||||||
|
initialExpression?.execute(context)?.byValueCopy() ?: if (isLateInitVal) ObjUnset else ObjNull
|
||||||
|
// Preserve mutability of declaration: create record with correct mutability
|
||||||
|
context.addItem(storageName, isMutable, initValue, visibility, setterVisibility, recordType = ObjRecord.Type.Field)
|
||||||
|
initValue
|
||||||
}
|
}
|
||||||
cls.instanceInitializers += initStmt
|
|
||||||
ObjVoid
|
|
||||||
} else {
|
} else {
|
||||||
// We are in instance scope already: perform initialization immediately
|
// Not in class body: regular local/var declaration
|
||||||
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
||||||
// Preserve mutability of declaration: create record with correct mutability
|
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||||
context.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
|
||||||
initValue
|
initValue
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Not in class body: regular local/var declaration
|
|
||||||
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
|
||||||
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
|
||||||
initValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3218,7 +3293,10 @@ class Compiler(
|
|||||||
* The keywords that stop processing of expression term
|
* The keywords that stop processing of expression term
|
||||||
*/
|
*/
|
||||||
val stopKeywords =
|
val stopKeywords =
|
||||||
setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class")
|
setOf(
|
||||||
|
"break", "continue", "return", "if", "when", "do", "while", "for", "class",
|
||||||
|
"private", "protected", "val", "var", "fun", "fn", "static", "init", "enum"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -324,15 +324,14 @@ private class Parser(fromPos: Pos) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
'?' -> {
|
'?' -> {
|
||||||
when (currentChar.also { pos.advance() }) {
|
when (currentChar) {
|
||||||
':' -> Token("??", from, Token.Type.ELVIS)
|
':' -> { pos.advance(); Token("?:", from, Token.Type.ELVIS) }
|
||||||
'?' -> Token("??", from, Token.Type.ELVIS)
|
'?' -> { pos.advance(); Token("??", from, Token.Type.ELVIS) }
|
||||||
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
|
'.' -> { pos.advance(); Token("?.", from, Token.Type.NULL_COALESCE) }
|
||||||
'[' -> Token("?(", from, Token.Type.NULL_COALESCE_INDEX)
|
'[' -> { pos.advance(); Token("?[", from, Token.Type.NULL_COALESCE_INDEX) }
|
||||||
'(' -> Token("?(", from, Token.Type.NULL_COALESCE_INVOKE)
|
'(' -> { pos.advance(); Token("?(", from, Token.Type.NULL_COALESCE_INVOKE) }
|
||||||
'{' -> Token("?{", from, Token.Type.NULL_COALESCE_BLOCKINVOKE)
|
'{' -> { pos.advance(); Token("?{", from, Token.Type.NULL_COALESCE_BLOCKINVOKE) }
|
||||||
else -> {
|
else -> {
|
||||||
pos.back()
|
|
||||||
Token("?", from, Token.Type.QUESTION)
|
Token("?", from, Token.Type.QUESTION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ open class Scope(
|
|||||||
|
|
||||||
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
|
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
|
||||||
|
|
||||||
|
fun raiseUnset(message: String = "property is unset (not initialized)"): Nothing =
|
||||||
|
raiseError(ObjUnsetException(this, message))
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun raiseSymbolNotFound(name: String): Nothing =
|
fun raiseSymbolNotFound(name: String): Nothing =
|
||||||
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
|
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
|
||||||
@@ -431,6 +434,7 @@ open class Scope(
|
|||||||
name: String,
|
name: String,
|
||||||
value: Obj,
|
value: Obj,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||||
): ObjRecord =
|
): ObjRecord =
|
||||||
objects[name]?.let {
|
objects[name]?.let {
|
||||||
@@ -445,17 +449,18 @@ open class Scope(
|
|||||||
callScope.localBindings[name] = it
|
callScope.localBindings[name] = it
|
||||||
}
|
}
|
||||||
it
|
it
|
||||||
} ?: addItem(name, true, value, visibility, recordType)
|
} ?: addItem(name, true, value, visibility, writeVisibility, recordType)
|
||||||
|
|
||||||
fun addItem(
|
fun addItem(
|
||||||
name: String,
|
name: String,
|
||||||
isMutable: Boolean,
|
isMutable: Boolean,
|
||||||
value: Obj,
|
value: Obj,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
recordType: ObjRecord.Type = ObjRecord.Type.Other,
|
recordType: ObjRecord.Type = ObjRecord.Type.Other,
|
||||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
|
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
val rec = ObjRecord(value, isMutable, visibility, declaringClass = declaringClass, type = recordType)
|
val rec = ObjRecord(value, isMutable, visibility, writeVisibility, declaringClass = declaringClass, type = recordType)
|
||||||
objects[name] = rec
|
objects[name] = rec
|
||||||
// Index this binding within the current frame to help resolve locals across suspension
|
// Index this binding within the current frame to help resolve locals across suspension
|
||||||
localBindings[name] = rec
|
localBindings[name] = rec
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class Script(
|
|||||||
|
|
||||||
internal val rootScope: Scope = Scope(null).apply {
|
internal val rootScope: Scope = Scope(null).apply {
|
||||||
ObjException.addExceptionsToContext(this)
|
ObjException.addExceptionsToContext(this)
|
||||||
|
addConst("Unset", ObjUnset)
|
||||||
addFn("print") {
|
addFn("print") {
|
||||||
for ((i, a) in args.withIndex()) {
|
for ((i, a) in args.withIndex()) {
|
||||||
if (i > 0) print(' ' + a.toString(this).value)
|
if (i > 0) print(' ' + a.toString(this).value)
|
||||||
|
|||||||
@@ -113,6 +113,15 @@ object Binder {
|
|||||||
symbols += fieldSym
|
symbols += fieldSym
|
||||||
classes.last().fields += fieldSym.id
|
classes.last().fields += fieldSym.id
|
||||||
}
|
}
|
||||||
|
// Class fields (val/var in class body, if any are reported here)
|
||||||
|
for (cf in d.classFields) {
|
||||||
|
val fs = source.offsetOf(cf.nameStart)
|
||||||
|
val fe = fs + cf.name.length
|
||||||
|
val kind = if (cf.mutable) SymbolKind.Variable else SymbolKind.Value
|
||||||
|
val fieldSym = Symbol(nextId++, cf.name, kind, fs, fe, containerId = sym.id)
|
||||||
|
symbols += fieldSym
|
||||||
|
classes.last().fields += fieldSym.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: functions and top-level/class vals/vars
|
// Second pass: functions and top-level/class vals/vars
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ package net.sergeych.lyng.format
|
|||||||
*/
|
*/
|
||||||
object LyngFormatter {
|
object LyngFormatter {
|
||||||
|
|
||||||
|
private fun startsWithWord(s: String, w: String): Boolean =
|
||||||
|
s.startsWith(w) && s.getOrNull(w.length)?.let { !it.isLetterOrDigit() && it != '_' } != false
|
||||||
|
|
||||||
|
private fun isPropertyAccessor(s: String): Boolean {
|
||||||
|
val t = s.trim()
|
||||||
|
if (t.isEmpty()) return false
|
||||||
|
if (startsWithWord(t, "get") || startsWithWord(t, "set")) return true
|
||||||
|
if (startsWithWord(t, "private")) {
|
||||||
|
val rest = t.substring("private".length).trimStart()
|
||||||
|
if (startsWithWord(rest, "set")) return true
|
||||||
|
}
|
||||||
|
if (startsWithWord(t, "protected")) {
|
||||||
|
val rest = t.substring("protected".length).trimStart()
|
||||||
|
if (startsWithWord(rest, "set")) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the input with indentation recomputed from scratch, line by line. */
|
/** Returns the input with indentation recomputed from scratch, line by line. */
|
||||||
fun reindent(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
fun reindent(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
||||||
// Normalize tabs to spaces globally before any transformation; results must contain no tabs
|
// Normalize tabs to spaces globally before any transformation; results must contain no tabs
|
||||||
@@ -35,6 +53,7 @@ object LyngFormatter {
|
|||||||
var bracketBalance = 0
|
var bracketBalance = 0
|
||||||
var prevBracketContinuation = false
|
var prevBracketContinuation = false
|
||||||
var inBlockComment = false
|
var inBlockComment = false
|
||||||
|
val extraIndents = mutableListOf<Int>()
|
||||||
// We don't keep per-"[" base alignment; continuation rules define alignment.
|
// We don't keep per-"[" base alignment; continuation rules define alignment.
|
||||||
|
|
||||||
fun updateBalances(ch: Char) {
|
fun updateBalances(ch: Char) {
|
||||||
@@ -52,7 +71,7 @@ object LyngFormatter {
|
|||||||
// Always produce spaces; tabs are not allowed in resulting code
|
// Always produce spaces; tabs are not allowed in resulting code
|
||||||
" ".repeat(level * config.indentSize + continuation)
|
" ".repeat(level * config.indentSize + continuation)
|
||||||
|
|
||||||
var awaitingSingleIndent = false
|
var awaitingExtraIndent = 0
|
||||||
fun isControlHeaderNoBrace(s: String): Boolean {
|
fun isControlHeaderNoBrace(s: String): Boolean {
|
||||||
val t = s.trim()
|
val t = s.trim()
|
||||||
if (t.isEmpty()) return false
|
if (t.isEmpty()) return false
|
||||||
@@ -60,7 +79,13 @@ object LyngFormatter {
|
|||||||
val isIf = Regex("^if\\s*\\(.*\\)\\s*$").matches(t)
|
val isIf = Regex("^if\\s*\\(.*\\)\\s*$").matches(t)
|
||||||
val isElseIf = Regex("^else\\s+if\\s*\\(.*\\)\\s*$").matches(t)
|
val isElseIf = Regex("^else\\s+if\\s*\\(.*\\)\\s*$").matches(t)
|
||||||
val isElse = t == "else"
|
val isElse = t == "else"
|
||||||
return isIf || isElseIf || isElse
|
if (isIf || isElseIf || isElse) return true
|
||||||
|
|
||||||
|
// property accessors ending with ) or =
|
||||||
|
if (isPropertyAccessor(t)) {
|
||||||
|
return if (t.contains('=')) t.endsWith('=') else t.endsWith(')')
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for ((i, rawLine) in lines.withIndex()) {
|
for ((i, rawLine) in lines.withIndex()) {
|
||||||
@@ -70,7 +95,12 @@ object LyngFormatter {
|
|||||||
val trimmedLine = rawLine.trim()
|
val trimmedLine = rawLine.trim()
|
||||||
|
|
||||||
// Compute effective indent level for this line
|
// Compute effective indent level for this line
|
||||||
var effectiveLevel = blockLevel
|
val currentExtraIndent = extraIndents.sum()
|
||||||
|
var effectiveLevel = blockLevel + currentExtraIndent
|
||||||
|
|
||||||
|
val isAccessor = !inBlockComment && isPropertyAccessor(trimmedStart)
|
||||||
|
if (isAccessor) effectiveLevel += 1
|
||||||
|
|
||||||
if (inBlockComment) {
|
if (inBlockComment) {
|
||||||
if (!trimmedLine.startsWith("*/")) {
|
if (!trimmedLine.startsWith("*/")) {
|
||||||
effectiveLevel += 1
|
effectiveLevel += 1
|
||||||
@@ -83,9 +113,9 @@ object LyngFormatter {
|
|||||||
|
|
||||||
// Single-line control header (if/else/else if) without braces: indent the next
|
// Single-line control header (if/else/else if) without braces: indent the next
|
||||||
// non-empty, non-'}', non-'else' line by one extra level
|
// non-empty, non-'}', non-'else' line by one extra level
|
||||||
val applyAwaiting = awaitingSingleIndent && trimmedStart.isNotEmpty() &&
|
val applyAwaiting = awaitingExtraIndent > 0 && trimmedStart.isNotEmpty() &&
|
||||||
!trimmedStart.startsWith("else") && !trimmedStart.startsWith("}")
|
!trimmedStart.startsWith("else") && !trimmedStart.startsWith("}")
|
||||||
if (applyAwaiting) effectiveLevel += 1
|
if (applyAwaiting) effectiveLevel += awaitingExtraIndent
|
||||||
|
|
||||||
val firstChar = trimmedStart.firstOrNull()
|
val firstChar = trimmedStart.firstOrNull()
|
||||||
// While inside parentheses, continuation applies scaled by nesting level
|
// While inside parentheses, continuation applies scaled by nesting level
|
||||||
@@ -129,23 +159,43 @@ object LyngFormatter {
|
|||||||
// New line (keep EOF semantics similar to input)
|
// New line (keep EOF semantics similar to input)
|
||||||
if (i < lines.lastIndex) sb.append('\n')
|
if (i < lines.lastIndex) sb.append('\n')
|
||||||
|
|
||||||
|
val oldBlockLevel = blockLevel
|
||||||
// Update balances using this line's code content
|
// Update balances using this line's code content
|
||||||
for (part in parts) {
|
for (part in parts) {
|
||||||
if (part.type == PartType.Code) {
|
if (part.type == PartType.Code) {
|
||||||
for (ch in part.text) updateBalances(ch)
|
for (ch in part.text) updateBalances(ch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val newBlockLevel = blockLevel
|
||||||
|
if (newBlockLevel > oldBlockLevel) {
|
||||||
|
val addedThisLine = (if (applyAwaiting) awaitingExtraIndent else 0) + (if (isAccessor) 1 else 0)
|
||||||
|
repeat(newBlockLevel - oldBlockLevel) {
|
||||||
|
extraIndents.add(addedThisLine)
|
||||||
|
}
|
||||||
|
} else if (newBlockLevel < oldBlockLevel) {
|
||||||
|
repeat(oldBlockLevel - newBlockLevel) {
|
||||||
|
if (extraIndents.isNotEmpty()) extraIndents.removeAt(extraIndents.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inBlockComment = nextInBlockComment
|
inBlockComment = nextInBlockComment
|
||||||
|
|
||||||
// Update awaitingSingleIndent based on current line
|
// Update awaitingExtraIndent based on current line
|
||||||
if (applyAwaiting && trimmedStart.isNotEmpty()) {
|
if (applyAwaiting && trimmedStart.isNotEmpty()) {
|
||||||
// we have just consumed the body line
|
// we have just applied it.
|
||||||
awaitingSingleIndent = false
|
val endsWithBrace = code.trimEnd().endsWith("{")
|
||||||
|
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
|
||||||
|
// It's another header, increment
|
||||||
|
awaitingExtraIndent += if (isAccessor) 2 else 1
|
||||||
|
} else {
|
||||||
|
// It's the body, reset
|
||||||
|
awaitingExtraIndent = 0
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// start awaiting if current line is a control header without '{'
|
// start awaiting if current line is a control header without '{'
|
||||||
val endsWithBrace = code.trimEnd().endsWith("{")
|
val endsWithBrace = code.trimEnd().endsWith("{")
|
||||||
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
|
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
|
||||||
awaitingSingleIndent = true
|
awaitingExtraIndent = if (isAccessor) 2 else 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,9 +246,6 @@ object LyngFormatter {
|
|||||||
return applyControlledWrapping(spacedText, config)
|
return applyControlledWrapping(spacedText, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startsWithWord(s: String, w: String): Boolean =
|
|
||||||
s.startsWith(w) && s.getOrNull(w.length)?.let { !it.isLetterOrDigit() && it != '_' } != false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reindents a slice of [text] specified by [range] and returns a new string with that slice replaced.
|
* Reindents a slice of [text] specified by [range] and returns a new string with that slice replaced.
|
||||||
* By default, preserves the base indent of the first line in the slice (so the block stays at
|
* By default, preserves the base indent of the first line in the slice (so the block stays at
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -42,10 +42,10 @@ private val fallbackKeywordIds = setOf(
|
|||||||
"and", "or", "not",
|
"and", "or", "not",
|
||||||
// declarations & modifiers
|
// declarations & modifiers
|
||||||
"fun", "fn", "class", "enum", "val", "var", "import", "package",
|
"fun", "fn", "class", "enum", "val", "var", "import", "package",
|
||||||
"private", "protected", "static", "open", "extern",
|
"private", "protected", "static", "open", "extern", "init", "get", "set", "by",
|
||||||
// control flow and misc
|
// control flow and misc
|
||||||
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
|
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
|
||||||
"throw", "return", "break", "continue", "this", "null", "true", "false"
|
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset"
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Maps lexer token type (and sometimes value) to a [HighlightKind]. */
|
/** Maps lexer token type (and sometimes value) to a [HighlightKind]. */
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -149,6 +149,8 @@ open class Obj {
|
|||||||
// methods that to override
|
// methods that to override
|
||||||
|
|
||||||
open suspend fun compareTo(scope: Scope, other: Obj): Int {
|
open suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
|
if( other === this) return 0
|
||||||
|
if( other === ObjNull ) return 2
|
||||||
scope.raiseNotImplemented()
|
scope.raiseNotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +416,7 @@ open class Obj {
|
|||||||
|
|
||||||
val decl = field.declaringClass
|
val decl = field.declaringClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(field.visibility, decl, caller))
|
if (!canAccessMember(field.effectiveWriteVisibility, decl, caller))
|
||||||
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
if (field.value is ObjProperty) {
|
if (field.value is ObjProperty) {
|
||||||
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
|
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
|
||||||
@@ -663,6 +665,50 @@ object ObjNull : Obj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("unset")
|
||||||
|
object ObjUnset : Obj() {
|
||||||
|
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
|
||||||
|
override fun equals(other: Any?): Boolean = other === this
|
||||||
|
override fun toString(): String = "Unset"
|
||||||
|
|
||||||
|
override suspend fun readField(scope: Scope, name: String): ObjRecord = scope.raiseUnset()
|
||||||
|
override suspend fun invokeInstanceMethod(
|
||||||
|
scope: Scope,
|
||||||
|
name: String,
|
||||||
|
args: Arguments,
|
||||||
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
|
): Obj = scope.raiseUnset()
|
||||||
|
|
||||||
|
override suspend fun getAt(scope: Scope, index: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) = scope.raiseUnset()
|
||||||
|
override suspend fun callOn(scope: Scope): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun plus(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun minus(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun negate(scope: Scope): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun mul(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun div(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun mod(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun logicalNot(scope: Scope): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun logicalAnd(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun logicalOr(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun bitAnd(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun bitOr(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun bitXor(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun shl(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun shr(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun bitNot(scope: Scope): Obj = scope.raiseUnset()
|
||||||
|
|
||||||
|
override val objClass: ObjClass by lazy {
|
||||||
|
object : ObjClass("Unset") {
|
||||||
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||||
|
return ObjUnset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: get rid of it. Maybe we ise some Lyng inheritance instead
|
* TODO: get rid of it. Maybe we ise some Lyng inheritance instead
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ open class ObjClass(
|
|||||||
initialValue: Obj,
|
initialValue: Obj,
|
||||||
isMutable: Boolean = false,
|
isMutable: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
pos: Pos = Pos.builtIn,
|
pos: Pos = Pos.builtIn,
|
||||||
declaringClass: ObjClass? = this
|
declaringClass: ObjClass? = this
|
||||||
) {
|
) {
|
||||||
@@ -353,7 +354,7 @@ open class ObjClass(
|
|||||||
if (existingInSelf != null && existingInSelf.isMutable == false)
|
if (existingInSelf != null && existingInSelf.isMutable == false)
|
||||||
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(initialValue, isMutable, visibility, declaringClass = declaringClass)
|
members[name] = ObjRecord(initialValue, isMutable, visibility, writeVisibility, declaringClass = declaringClass)
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
}
|
}
|
||||||
@@ -368,13 +369,14 @@ open class ObjClass(
|
|||||||
initialValue: Obj,
|
initialValue: Obj,
|
||||||
isMutable: Boolean = false,
|
isMutable: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
pos: Pos = Pos.builtIn
|
pos: Pos = Pos.builtIn
|
||||||
) {
|
) {
|
||||||
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)
|
classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility)
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
}
|
}
|
||||||
@@ -383,11 +385,12 @@ open class ObjClass(
|
|||||||
name: String,
|
name: String,
|
||||||
isOpen: Boolean = false,
|
isOpen: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
declaringClass: ObjClass? = this,
|
declaringClass: ObjClass? = this,
|
||||||
code: suspend Scope.() -> Obj
|
code: suspend Scope.() -> Obj
|
||||||
) {
|
) {
|
||||||
val stmt = statement { code() }
|
val stmt = statement { code() }
|
||||||
createField(name, stmt, isOpen, visibility, Pos.builtIn, declaringClass)
|
createField(name, stmt, isOpen, visibility, writeVisibility, Pos.builtIn, declaringClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
||||||
@@ -397,12 +400,13 @@ open class ObjClass(
|
|||||||
getter: (suspend Scope.() -> Obj)? = null,
|
getter: (suspend Scope.() -> Obj)? = null,
|
||||||
setter: (suspend Scope.(Obj) -> Unit)? = null,
|
setter: (suspend Scope.(Obj) -> Unit)? = null,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
declaringClass: ObjClass? = this
|
declaringClass: ObjClass? = this
|
||||||
) {
|
) {
|
||||||
val g = getter?.let { statement { it() } }
|
val g = getter?.let { statement { it() } }
|
||||||
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
|
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
|
||||||
val prop = ObjProperty(name, g, s)
|
val prop = ObjProperty(name, g, s)
|
||||||
members[name] = ObjRecord(prop, false, visibility, declaringClass, type = ObjRecord.Type.Property)
|
members[name] = ObjRecord(prop, false, visibility, writeVisibility, declaringClass, type = ObjRecord.Type.Property)
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,9 @@ open class ObjException(
|
|||||||
"AccessException",
|
"AccessException",
|
||||||
"UnknownException",
|
"UnknownException",
|
||||||
"NotFoundException",
|
"NotFoundException",
|
||||||
"IllegalOperationException"
|
"IllegalOperationException",
|
||||||
|
"UnsetException",
|
||||||
|
"SyntaxError"
|
||||||
)) {
|
)) {
|
||||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
scope.addConst(name, getOrCreateExceptionClass(name))
|
||||||
}
|
}
|
||||||
@@ -245,3 +247,6 @@ class ObjIllegalOperationException(scope: Scope, message: String = "Operation is
|
|||||||
|
|
||||||
class ObjNotFoundException(scope: Scope, message: String = "not found") :
|
class ObjNotFoundException(scope: Scope, message: String = "not found") :
|
||||||
ObjException("NotFoundException", scope, message)
|
ObjException("NotFoundException", scope, message)
|
||||||
|
|
||||||
|
class ObjUnsetException(scope: Scope, message: String = "property is unset (not initialized)") :
|
||||||
|
ObjException("UnsetException", scope, message)
|
||||||
@@ -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.
|
||||||
@@ -97,8 +97,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
val decl = f.declaringClass
|
val decl = f.declaringClass
|
||||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(f.visibility, decl, caller))
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
||||||
ObjIllegalAssignmentException(
|
ObjAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
||||||
).raise()
|
).raise()
|
||||||
@@ -108,7 +108,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
prop.callSetter(scope, this, newValue, decl)
|
prop.callSetter(scope, this, newValue, decl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!f.isMutable) 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
|
||||||
return
|
return
|
||||||
@@ -131,8 +131,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
}
|
}
|
||||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
if (!canAccessMember(rec.effectiveWriteVisibility, declaring, caller))
|
||||||
ObjIllegalAssignmentException(
|
ObjAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
|
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
|
||||||
).raise()
|
).raise()
|
||||||
@@ -142,7 +142,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
prop.callSetter(scope, this, newValue, declaring)
|
prop.callSetter(scope, this, newValue, declaring)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!rec.isMutable) 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
|
||||||
return
|
return
|
||||||
@@ -271,7 +271,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
|
|
||||||
protected val comparableVars: Map<String, ObjRecord> by lazy {
|
protected val comparableVars: Map<String, ObjRecord> by lazy {
|
||||||
instanceScope.objects.filter {
|
instanceScope.objects.filter {
|
||||||
it.value.type.comparable
|
it.value.type.comparable && (it.value.type != ObjRecord.Type.Field || it.value.isMutable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,12 +350,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
instance.instanceScope.objects[mangled]?.let { f ->
|
instance.instanceScope.objects[mangled]?.let { f ->
|
||||||
val decl = f.declaringClass ?: startClass
|
val decl = f.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(f.visibility, decl, caller))
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
||||||
ObjIllegalAssignmentException(
|
ObjAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${decl.className})"
|
"can't assign to field $name (declared in ${decl.className})"
|
||||||
).raise()
|
).raise()
|
||||||
if (!f.isMutable) 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) f.value = newValue
|
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -364,12 +364,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
instance.instanceScope[name]?.let { f ->
|
instance.instanceScope[name]?.let { f ->
|
||||||
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(f.visibility, decl, caller))
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
||||||
ObjIllegalAssignmentException(
|
ObjAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
||||||
).raise()
|
).raise()
|
||||||
if (!f.isMutable) 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) f.value = newValue
|
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -377,8 +377,8 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
|
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
|
||||||
val decl = r.declaringClass ?: startClass
|
val decl = r.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(r.visibility, decl, caller))
|
if (!canAccessMember(r.effectiveWriteVisibility, decl, caller))
|
||||||
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
|
ObjAccessException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
|
||||||
if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name")
|
if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name")
|
||||||
if (r.value.assign(scope, newValue) == null) r.value = newValue
|
if (r.value.assign(scope, newValue) == null) r.value = newValue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ data class ObjRecord(
|
|||||||
var value: Obj,
|
var value: Obj,
|
||||||
val isMutable: Boolean,
|
val isMutable: Boolean,
|
||||||
val visibility: Visibility = Visibility.Public,
|
val visibility: Visibility = Visibility.Public,
|
||||||
|
val writeVisibility: Visibility? = null,
|
||||||
/** If non-null, denotes the class that declared this member (field/method). */
|
/** If non-null, denotes the class that declared this member (field/method). */
|
||||||
val declaringClass: ObjClass? = null,
|
val declaringClass: ObjClass? = null,
|
||||||
var importedFrom: Scope? = null,
|
var importedFrom: Scope? = null,
|
||||||
val isTransient: Boolean = false,
|
val isTransient: Boolean = false,
|
||||||
val type: Type = Type.Other,
|
val type: Type = Type.Other,
|
||||||
) {
|
) {
|
||||||
|
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) {
|
||||||
Field(true, true),
|
Field(true, true),
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
|||||||
@@ -442,9 +442,9 @@ class ConstRef(private val record: ObjRecord) : ObjRef {
|
|||||||
* Reference to an object's field with optional chaining.
|
* Reference to an object's field with optional chaining.
|
||||||
*/
|
*/
|
||||||
class FieldRef(
|
class FieldRef(
|
||||||
private val target: ObjRef,
|
val target: ObjRef,
|
||||||
private val name: String,
|
val name: String,
|
||||||
private val isOptional: Boolean,
|
val isOptional: Boolean,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
// 4-entry PIC for reads/writes (guarded by PerfFlags.FIELD_PIC)
|
// 4-entry PIC for reads/writes (guarded by PerfFlags.FIELD_PIC)
|
||||||
// Reads
|
// Reads
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -19,7 +19,6 @@ package net.sergeych.lyng
|
|||||||
|
|
||||||
import net.sergeych.lyng.obj.Obj
|
import net.sergeych.lyng.obj.Obj
|
||||||
import net.sergeych.lyng.obj.ObjClass
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
import net.sergeych.lyng.obj.ObjNull
|
|
||||||
import net.sergeych.lyng.obj.ObjVoid
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
|
||||||
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
||||||
@@ -46,9 +45,8 @@ abstract class Statement(
|
|||||||
abstract suspend fun execute(scope: Scope): Obj
|
abstract suspend fun execute(scope: Scope): Obj
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
if( other == ObjNull || other == ObjVoid ) return 1
|
|
||||||
if( other === this ) return 0
|
if( other === this ) return 0
|
||||||
return -1
|
return -3
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
override suspend fun callOn(scope: Scope): 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.
|
||||||
@@ -396,4 +396,123 @@ class OOTest {
|
|||||||
assertThrows { "answer is 42".totalDigits }
|
assertThrows { "answer is 42".totalDigits }
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCacheInClass() = runTest {
|
||||||
|
eval("""
|
||||||
|
class T(salt) {
|
||||||
|
private var c
|
||||||
|
|
||||||
|
init {
|
||||||
|
println("create cached with "+salt)
|
||||||
|
c = cached { salt + "." }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getResult() = c()
|
||||||
|
}
|
||||||
|
|
||||||
|
val t1 = T("foo")
|
||||||
|
val t2 = T("bar")
|
||||||
|
assertEquals("bar.", t2.getResult())
|
||||||
|
assertEquals("foo.", t1.getResult())
|
||||||
|
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLateInitValsInClasses() = runTest {
|
||||||
|
assertFails {
|
||||||
|
eval("""
|
||||||
|
class T {
|
||||||
|
val x
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFails {
|
||||||
|
eval("val String.late")
|
||||||
|
}
|
||||||
|
|
||||||
|
eval("""
|
||||||
|
// but we can "late-init" them in init block:
|
||||||
|
class OK {
|
||||||
|
val x
|
||||||
|
|
||||||
|
init {
|
||||||
|
x = "foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val ok = OK()
|
||||||
|
assertEquals("foo", ok.x)
|
||||||
|
|
||||||
|
// they can't be reassigned:
|
||||||
|
assertThrows(IllegalAssignmentException) {
|
||||||
|
ok.x = "bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
// To test access before init, we need a trick:
|
||||||
|
class AccessBefore {
|
||||||
|
val x
|
||||||
|
fun readX() = x
|
||||||
|
init {
|
||||||
|
assertEquals(x, Unset)
|
||||||
|
// if we call readX() here, x is Unset.
|
||||||
|
// Just reading it is fine, but using it should throw:
|
||||||
|
assertThrows(UnsetException) { readX() + 1 }
|
||||||
|
x = 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AccessBefore()
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPrivateSet() = runTest {
|
||||||
|
eval("""
|
||||||
|
class A {
|
||||||
|
var y = 100
|
||||||
|
private set
|
||||||
|
fun setValue(newValue) { y = newValue }
|
||||||
|
}
|
||||||
|
assertEquals(100, A().y)
|
||||||
|
assertThrows(AccessException) { A().y = 200 }
|
||||||
|
val a = A()
|
||||||
|
a.setValue(200)
|
||||||
|
assertEquals(200, a.y)
|
||||||
|
|
||||||
|
class B(initial) {
|
||||||
|
var y = initial
|
||||||
|
protected set
|
||||||
|
}
|
||||||
|
class C(initial) : B(initial) {
|
||||||
|
fun setBValue(v) { y = v }
|
||||||
|
}
|
||||||
|
val c = C(10)
|
||||||
|
assertEquals(10, c.y)
|
||||||
|
assertThrows(AccessException) { c.y = 20 }
|
||||||
|
c.setBValue(30)
|
||||||
|
assertEquals(30, c.y)
|
||||||
|
|
||||||
|
class D {
|
||||||
|
private var _y = 0
|
||||||
|
var y
|
||||||
|
get() = _y
|
||||||
|
private set(v) { _y = v }
|
||||||
|
fun setY(v) { y = v }
|
||||||
|
}
|
||||||
|
val d = D()
|
||||||
|
assertEquals(0, d.y)
|
||||||
|
assertThrows(AccessException) { d.y = 10 }
|
||||||
|
d.setY(20)
|
||||||
|
assertEquals(20, d.y)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testValPrivateSetError() = runTest {
|
||||||
|
assertFails {
|
||||||
|
eval("class E { val x = 1 private set }")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -730,4 +730,124 @@ class LyngFormatterTest {
|
|||||||
|
|
||||||
assertEquals(expected, formatted)
|
assertEquals(expected, formatted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun privateSet_indentation() {
|
||||||
|
val src = """
|
||||||
|
class A {
|
||||||
|
var x = 1
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
// What we expect: private set should be indented relative to the property
|
||||||
|
val expected = """
|
||||||
|
class A {
|
||||||
|
var x = 1
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||||
|
val out = LyngFormatter.reindent(src, cfg)
|
||||||
|
assertEquals(expected, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun propertyAccessors_indentation() {
|
||||||
|
val src = """
|
||||||
|
class A {
|
||||||
|
var x
|
||||||
|
get() = 1
|
||||||
|
private set(v) {
|
||||||
|
_x = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val expected = """
|
||||||
|
class A {
|
||||||
|
var x
|
||||||
|
get() = 1
|
||||||
|
private set(v) {
|
||||||
|
_x = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||||
|
val out = LyngFormatter.reindent(src, cfg)
|
||||||
|
assertEquals(expected, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nestedAwaitingIndentation() {
|
||||||
|
val src = """
|
||||||
|
if (cond)
|
||||||
|
if (other)
|
||||||
|
fun f() {
|
||||||
|
stmt
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val expected = """
|
||||||
|
if (cond)
|
||||||
|
if (other)
|
||||||
|
fun f() {
|
||||||
|
stmt
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||||
|
val out = LyngFormatter.reindent(src, cfg)
|
||||||
|
assertEquals(expected, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun propertyAccessor_followedByMethod() {
|
||||||
|
val src = """
|
||||||
|
var state: BarRequestState get() = getState()
|
||||||
|
set(value) = setState(value)
|
||||||
|
|
||||||
|
fun save() { cell.value = this }
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val expected = """
|
||||||
|
var state: BarRequestState get() = getState()
|
||||||
|
set(value) = setState(value)
|
||||||
|
|
||||||
|
fun save() { cell.value = this }
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||||
|
val out = LyngFormatter.reindent(src, cfg)
|
||||||
|
assertEquals(expected, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun propertyAccessor_dangling() {
|
||||||
|
val src1 = """
|
||||||
|
var x
|
||||||
|
get()
|
||||||
|
= 1
|
||||||
|
""".trimIndent()
|
||||||
|
val expected1 = """
|
||||||
|
var x
|
||||||
|
get()
|
||||||
|
= 1
|
||||||
|
""".trimIndent()
|
||||||
|
assertEquals(expected1, LyngFormatter.reindent(src1, LyngFormatConfig(indentSize = 4)))
|
||||||
|
|
||||||
|
val src2 = """
|
||||||
|
var x
|
||||||
|
get() =
|
||||||
|
1
|
||||||
|
""".trimIndent()
|
||||||
|
val expected2 = """
|
||||||
|
var x
|
||||||
|
get() =
|
||||||
|
1
|
||||||
|
""".trimIndent()
|
||||||
|
assertEquals(expected2, LyngFormatter.reindent(src2, LyngFormatConfig(indentSize = 4)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,7 @@ import net.sergeych.lyng.binding.SymbolKind
|
|||||||
import net.sergeych.lyng.highlight.HighlightKind
|
import net.sergeych.lyng.highlight.HighlightKind
|
||||||
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
|
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.miniast.MiniAstBuilder
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.miniast.MiniClassDecl
|
|
||||||
import net.sergeych.lyng.miniast.MiniFunDecl
|
|
||||||
import net.sergeych.lyng.miniast.MiniTypeName
|
|
||||||
import org.w3c.dom.HTMLStyleElement
|
import org.w3c.dom.HTMLStyleElement
|
||||||
|
|
||||||
fun ensureBootstrapCodeBlocks(html: String): String {
|
fun ensureBootstrapCodeBlocks(html: String): String {
|
||||||
@@ -367,6 +364,7 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
|
|||||||
is MiniFunDecl -> putName(d.nameStart, d.name, "hl-fn")
|
is MiniFunDecl -> putName(d.nameStart, d.name, "hl-fn")
|
||||||
is MiniClassDecl -> putName(d.nameStart, d.name, "hl-class")
|
is MiniClassDecl -> putName(d.nameStart, d.name, "hl-class")
|
||||||
is net.sergeych.lyng.miniast.MiniValDecl -> putName(d.nameStart, d.name, if (d.mutable) "hl-var" else "hl-val")
|
is net.sergeych.lyng.miniast.MiniValDecl -> putName(d.nameStart, d.name, if (d.mutable) "hl-var" else "hl-val")
|
||||||
|
is MiniEnumDecl -> putName(d.nameStart, d.name, "hl-class")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Imports: color each segment as directive/path
|
// Imports: color each segment as directive/path
|
||||||
@@ -404,6 +402,7 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
|
|||||||
}
|
}
|
||||||
is net.sergeych.lyng.miniast.MiniValDecl -> addTypeSegments(d.type)
|
is net.sergeych.lyng.miniast.MiniValDecl -> addTypeSegments(d.type)
|
||||||
is MiniClassDecl -> {}
|
is MiniClassDecl -> {}
|
||||||
|
is MiniEnumDecl -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +533,8 @@ private fun detectDeclarationAndParamOverrides(text: String): Map<Pair<Int, Int>
|
|||||||
"package", "import", "fun", "fn", "class", "enum", "val", "var",
|
"package", "import", "fun", "fn", "class", "enum", "val", "var",
|
||||||
"if", "else", "while", "do", "for", "when", "try", "catch", "finally",
|
"if", "else", "while", "do", "for", "when", "try", "catch", "finally",
|
||||||
"throw", "return", "break", "continue", "in", "is", "as", "as?", "not",
|
"throw", "return", "break", "continue", "in", "is", "as", "as?", "not",
|
||||||
"true", "false", "null", "private", "protected", "open", "extern", "static"
|
"true", "false", "null", "private", "protected", "open", "extern", "static",
|
||||||
|
"init", "get", "set", "Unset", "by"
|
||||||
)
|
)
|
||||||
fun skipWs(idx0: Int): Int {
|
fun skipWs(idx0: Int): Int {
|
||||||
var idx = idx0
|
var idx = idx0
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -127,7 +127,7 @@ fun ReferencePage() {
|
|||||||
Ul({ classes("mt-2") }) {
|
Ul({ classes("mt-2") }) {
|
||||||
d.members.forEach { m ->
|
d.members.forEach { m ->
|
||||||
when (m) {
|
when (m) {
|
||||||
is MiniMemberFunDecl, -> {
|
is MiniMemberFunDecl -> {
|
||||||
val params = m.params.joinToString(", ") { p ->
|
val params = m.params.joinToString(", ") { p ->
|
||||||
val ts = typeOf(p.type)
|
val ts = typeOf(p.type)
|
||||||
if (ts.isNotBlank()) "${p.name}${ts}" else p.name
|
if (ts.isNotBlank()) "${p.name}${ts}" else p.name
|
||||||
@@ -150,6 +150,17 @@ fun ReferencePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is MiniEnumDecl -> {
|
||||||
|
Div { Text("enum ${d.name}") }
|
||||||
|
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
||||||
|
if (d.entries.isNotEmpty()) {
|
||||||
|
Ul({ classes("mt-2") }) {
|
||||||
|
d.entries.forEach { entry ->
|
||||||
|
Li { Text(entry) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user