Compare commits

..
Author SHA1 Message Date
sergeych 5144a720ba unsuccussful navigation; we stop at this point 2026-01-03 17:53:54 +01:00
30 changed files with 41362 additions and 948 deletions

No files matched your search

+1 -18
View File
@@ -11,22 +11,7 @@
- Integration: Updated TextMate grammar and IntelliJ plugin (highlighting + keywords).
- Documentation: New "Properties" section in `docs/OOP.md`.
- 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
- Docs: Scopes and Closures guidance
- 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`).
- Tutorial: added quick link to Scopes and Closures.
@@ -98,8 +83,6 @@ 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.
- Multi-file stdout prints headers `--- <path> ---` per file.
- `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:
- `lyng script.lyng [args...]` executes the script directly.
+41198
View File
File diff suppressed because it is too large. Load diff
-123
View File
@@ -100,28 +100,6 @@ 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.
- **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
In addition to the primary constructor arguments, you can provide an `init` block that runs on each instance creation. This is useful for more complex initializations, side effects, or setting up fields that depend on multiple constructor parameters.
@@ -175,44 +153,6 @@ 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.
### 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
Functions defined inside a class body are methods, and unless declared
@@ -432,69 +372,6 @@ Are declared with var
assert( p.isSpecial == true )
>>> 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 are visible only _inside the class instance_:
-20
View File
@@ -68,26 +68,6 @@ 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.
- 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
- Do use `chainLookupIgnoreClosure` / `chainLookupWithMembers` for ancestry traversals.
- Do maintain the resolution order above for predictable behavior.
+1 -45
View File
@@ -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. |
| flow {} | create flow sequence, see [parallelism] |
| delay, launch, yield | see [parallelism] |
| cached(builder) | [Lazy evaluation with `cached`](#lazy-evaluation-with-cached) |
| cached(builder) | remembers builder() on first invocation and return it then |
| let, also, apply, run | see above, flow controls |
(1)
@@ -1545,50 +1545,6 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
[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)
Lyng supports multiple inheritance (MI) with simple, predictable rules. For a full reference see OOP notes, this is a quick, copy‑paste friendly overview.
-110
View File
@@ -1,110 +0,0 @@
# 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**.
@@ -85,13 +85,7 @@ class LyngEnterHandler : EnterHandlerDelegate {
if (code == "}" || code == "*/") {
// Adjust indent for the previous line if it's a block or comment closer
val prevStart = doc.getLineStartOffset(prevLine)
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}")
}
}
CodeStyleManager.getInstance(project).adjustLineIndent(file, prevStart)
// Fallback for previous line: manual application
val desiredPrev = computeDesiredIndent(project, doc, prevLine)
@@ -108,13 +102,8 @@ class LyngEnterHandler : EnterHandlerDelegate {
}
// Adjust indent for the current (new) line
val currentStart = doc.getLineStartOffsetSafe(currentLine)
if (file.context == null) {
try {
CodeStyleManager.getInstance(project).adjustLineIndent(file, currentStart)
} catch (e: Exception) {
log.warn("Failed to adjust line indent for current line: ${e.message}")
}
}
val csm = CodeStyleManager.getInstance(project)
csm.adjustLineIndent(file, currentStart)
// Fallback: if the platform didn't physically insert indentation, compute it from our formatter and apply
val lineStart = doc.getLineStartOffset(currentLine)
@@ -58,13 +58,7 @@ class LyngTypedHandler : TypedHandlerDelegate() {
}
// After block reindent, adjust line indent to what platform thinks (no-op in many cases)
val lineStart = doc.getLineStartOffset(line)
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}")
}
}
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
}
} else if (c == '/') {
val doc = editor.document
@@ -73,13 +67,7 @@ class LyngTypedHandler : TypedHandlerDelegate() {
PsiDocumentManager.getInstance(project).commitDocument(doc)
val line = doc.getLineNumber(offset - 1)
val lineStart = doc.getLineStartOffset(line)
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}")
}
}
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
// Manual application fallback
val desired = computeDesiredIndent(project, doc, line)
@@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -80,14 +80,7 @@ class LyngPreFormatProcessor : PreFormatProcessor {
val lineStart = doc.getLineStartOffset(line)
// adjustLineIndent delegates to our LineIndentProvider which computes
// indentation from scratch; this is safe and idempotent
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
}
}
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
// After indentation, update block/paren/bracket balances using the current line text
val lineEnd = doc.getLineEndOffset(line)
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "1.1.0-rc"
version = "1.1.0-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
@@ -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");
* you may not use this file except in compliance with the License.
@@ -138,7 +138,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
} else {
val value = if (hp < callArgs.size) callArgs[hp++]
else a.defaultValue?.execute(scope)
?: scope.raiseIllegalArgument("too few arguments for the call")
?: scope.raiseIllegalArgument("too few arguments for the call (${arguments.list})")
assign(a, value)
}
i++
@@ -20,7 +20,5 @@ package net.sergeych.lyng
sealed class CodeContext {
class Module(@Suppress("unused") val packageName: String?): CodeContext()
class Function(val name: String): CodeContext()
class ClassBody(val name: String): CodeContext() {
val pendingInitializations = mutableMapOf<String, Pos>()
}
class ClassBody(val name: String): CodeContext()
}
@@ -138,16 +138,9 @@ class Compiler(
private var lastParsedBlockRange: MiniRange? = null
private suspend fun <T> inCodeContext(context: CodeContext, f: suspend () -> T): T {
codeContexts.add(context)
try {
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
return try {
codeContexts.add(context)
f()
} finally {
codeContexts.removeLast()
}
@@ -367,21 +360,7 @@ class Compiler(
val rvalue = parseExpressionLevel(level + 1)
?: throw ScriptError(opToken.pos, "Expecting expression")
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
lvalue = op.generate(opToken.pos, lvalue!!, rvalue)
}
return lvalue
}
@@ -513,32 +492,24 @@ class Compiler(
// there could be terminal operators or keywords:// variable to read or like
when (t.value) {
in stopKeywords -> {
if (t.value == "init" && !(codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE)) {
// Soft keyword: init is only a keyword in class body when followed by {
cc.previous()
operand = parseAccessor()
} else {
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
// Allow certain statement-like constructs to act as expressions
// when they appear in expression position (e.g., `if (...) ... else ...`).
// Other keywords should be handled by the outer statement parser.
when (t.value) {
"if" -> {
val s = parseIfStatement()
operand = StatementRef(s)
}
"when" -> {
val s = parseWhenStatement()
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
}
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
// Allow certain statement-like constructs to act as expressions
// when they appear in expression position (e.g., `if (...) ... else ...`).
// Other keywords should be handled by the outer statement parser.
when (t.value) {
"if" -> {
val s = parseIfStatement()
operand = StatementRef(s)
}
"when" -> {
val s = parseWhenStatement()
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
}
}
}
@@ -657,30 +628,35 @@ class Compiler(
val body = parseBlock(skipLeadingBrace = true)
return ValueFnRef { closureScope ->
statement {
// and the source closure of the lambda which might have other thisObj.
val context = this.applyClosure(closureScope)
// Execute lambda body in a closure-aware context. Blocks inside the lambda
// will create child scopes as usual, so re-declarations inside loops work.
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = args.list
val itValue: Obj = when (l.size) {
// no args: it == void
0 -> ObjVoid
// one args: it is this arg
1 -> l[0]
// more args: it is a list of args
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)
var closure: Scope? = null
val callStatement = statement {
// and the source closure of the lambda which might have other thisObj.
val context = this.applyClosure(closure!!)
// Execute lambda body in a closure-aware context. Blocks inside the lambda
// will create child scopes as usual, so re-declarations inside loops work.
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = args.list
val itValue: Obj = when (l.size) {
// no args: it == void
0 -> ObjVoid
// one args: it is this arg
1 -> l[0]
// more args: it is a list of args
else -> ObjList(l.toMutableList())
}
body.execute(context)
}.asReadonly
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
} else {
// assign vars as declared the standard way
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
}
body.execute(context)
}
return ValueFnRef { x ->
closure = x
callStatement.asReadonly
}
}
@@ -1396,7 +1372,7 @@ class Compiler(
}
"init" -> {
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
val block = parseBlock()
lastParsedBlockRange?.let { range ->
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
@@ -2725,7 +2701,7 @@ class Compiler(
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
val next = cc.peekNextNonWhitespace()
if (next.isId("get") || next.isId("set") || next.isId("private") || next.isId("protected")) {
if (next.isId("get") || next.isId("set")) {
isProperty = true
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
@@ -2743,13 +2719,7 @@ class Compiler(
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
throw ScriptError(start, "val must be initialized")
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 {
else {
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
setNull = true
@@ -2788,8 +2758,8 @@ class Compiler(
// if (isDelegate) throw ScriptError(start, "static delegates are not yet implemented")
currentInitScope += statement {
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, pos)
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field)
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, pos)
addItem(name, isMutable, initValue, visibility, ObjRecord.Type.Field)
ObjVoid
}
return NopStatement
@@ -2798,11 +2768,11 @@ class Compiler(
// Check for accessors if it is a class member
var getter: Statement? = null
var setter: Statement? = null
var setterVisibility: Visibility? = null
if (declaringClassNameCaptured != null || extTypeName != null) {
while (true) {
val t = cc.skipWsTokens()
val t = cc.peekNextNonWhitespace()
if (t.isId("get")) {
cc.skipWsTokens()
cc.next() // consume 'get'
cc.requireToken(Token.Type.LPAREN)
cc.requireToken(Token.Type.RPAREN)
@@ -2818,6 +2788,7 @@ class Compiler(
throw ScriptError(cc.current().pos, "Expected { or = after get()")
}
} else if (t.isId("set")) {
cc.skipWsTokens()
cc.next() // consume 'set'
cc.requireToken(Token.Type.LPAREN)
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
@@ -2843,46 +2814,6 @@ class Compiler(
} else {
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
}
if (getter != null || setter != null) {
@@ -2891,13 +2822,11 @@ class Compiler(
throw ScriptError(start, "var property must have both get() and set()")
}
} else {
if (setter != null || setterVisibility != null)
throw ScriptError(start, "val property cannot have a setter or restricted visibility set (name: $name)")
if (setter != null)
throw ScriptError(start, "val property cannot have a setter (name: $name)")
if (getter == null)
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)")
}
}
@@ -2914,7 +2843,7 @@ class Compiler(
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, writeVisibility = setterVisibility, declaringClass = null, type = ObjRecord.Type.Property))
context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, declaringClass = null, type = ObjRecord.Type.Property))
return@statement prop
}
@@ -2948,7 +2877,6 @@ class Compiler(
isMutable,
prop,
visibility,
setterVisibility,
recordType = ObjRecord.Type.Property
)
ObjVoid
@@ -2956,41 +2884,38 @@ class Compiler(
ObjVoid
} else {
// We are in instance scope already: perform initialization immediately
context.addItem(storageName, isMutable, prop, visibility, setterVisibility, recordType = ObjRecord.Type.Property)
context.addItem(storageName, isMutable, prop, visibility, recordType = ObjRecord.Type.Property)
prop
}
} else {
val isLateInitVal = !isMutable && initialExpression == null && getter == null && setter == null
if (declaringClassName != null && !isStatic) {
val storageName = "$declaringClassName::$name"
// If we are in class scope now (defining instance field), defer initialization to instance time
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
if (isClassScope) {
val cls = context.thisObj as ObjClass
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
val initStmt = statement(start) { scp ->
val initValue =
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
if (declaringClassName != null && !isStatic) {
val storageName = "$declaringClassName::$name"
// If we are in class scope now (defining instance field), defer initialization to instance time
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
if (isClassScope) {
val cls = context.thisObj as ObjClass
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
val initStmt = statement(start) { scp ->
val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: ObjNull
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
scp.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
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 {
// Not in class body: regular local/var declaration
// We are in instance scope already: perform initialization immediately
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
// Preserve mutability of declaration: create record with correct mutability
context.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
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
}
}
}
}
@@ -3293,10 +3218,7 @@ class Compiler(
* The keywords that stop processing of expression term
*/
val stopKeywords =
setOf(
"break", "continue", "return", "if", "when", "do", "while", "for", "class",
"private", "protected", "val", "var", "fun", "fn", "static", "init", "enum"
)
setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class")
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -324,14 +324,15 @@ private class Parser(fromPos: Pos) {
}
'?' -> {
when (currentChar) {
':' -> { pos.advance(); Token("?:", from, Token.Type.ELVIS) }
'?' -> { pos.advance(); Token("??", from, Token.Type.ELVIS) }
'.' -> { pos.advance(); Token("?.", from, Token.Type.NULL_COALESCE) }
'[' -> { pos.advance(); Token("?[", from, Token.Type.NULL_COALESCE_INDEX) }
'(' -> { pos.advance(); Token("?(", from, Token.Type.NULL_COALESCE_INVOKE) }
'{' -> { pos.advance(); Token("?{", from, Token.Type.NULL_COALESCE_BLOCKINVOKE) }
when (currentChar.also { pos.advance() }) {
':' -> Token("??", from, Token.Type.ELVIS)
'?' -> Token("??", from, Token.Type.ELVIS)
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
'[' -> Token("?(", from, Token.Type.NULL_COALESCE_INDEX)
'(' -> Token("?(", from, Token.Type.NULL_COALESCE_INVOKE)
'{' -> Token("?{", from, Token.Type.NULL_COALESCE_BLOCKINVOKE)
else -> {
pos.back()
Token("?", from, Token.Type.QUESTION)
}
}
@@ -262,9 +262,6 @@ open class Scope(
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")
fun raiseSymbolNotFound(name: String): Nothing =
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
@@ -434,7 +431,6 @@ open class Scope(
name: String,
value: Obj,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
recordType: ObjRecord.Type = ObjRecord.Type.Other
): ObjRecord =
objects[name]?.let {
@@ -449,18 +445,17 @@ open class Scope(
callScope.localBindings[name] = it
}
it
} ?: addItem(name, true, value, visibility, writeVisibility, recordType)
} ?: addItem(name, true, value, visibility, recordType)
fun addItem(
name: String,
isMutable: Boolean,
value: Obj,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
recordType: ObjRecord.Type = ObjRecord.Type.Other,
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
): ObjRecord {
val rec = ObjRecord(value, isMutable, visibility, writeVisibility, declaringClass = declaringClass, type = recordType)
val rec = ObjRecord(value, isMutable, visibility, declaringClass = declaringClass, type = recordType)
objects[name] = rec
// Index this binding within the current frame to help resolve locals across suspension
localBindings[name] = rec
@@ -60,7 +60,6 @@ class Script(
internal val rootScope: Scope = Scope(null).apply {
ObjException.addExceptionsToContext(this)
addConst("Unset", ObjUnset)
addFn("print") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.toString(this).value)
@@ -113,15 +113,6 @@ object Binder {
symbols += fieldSym
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
@@ -24,24 +24,6 @@ package net.sergeych.lyng.format
*/
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. */
fun reindent(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
// Normalize tabs to spaces globally before any transformation; results must contain no tabs
@@ -53,7 +35,6 @@ object LyngFormatter {
var bracketBalance = 0
var prevBracketContinuation = false
var inBlockComment = false
val extraIndents = mutableListOf<Int>()
// We don't keep per-"[" base alignment; continuation rules define alignment.
fun updateBalances(ch: Char) {
@@ -71,7 +52,7 @@ object LyngFormatter {
// Always produce spaces; tabs are not allowed in resulting code
" ".repeat(level * config.indentSize + continuation)
var awaitingExtraIndent = 0
var awaitingSingleIndent = false
fun isControlHeaderNoBrace(s: String): Boolean {
val t = s.trim()
if (t.isEmpty()) return false
@@ -79,13 +60,7 @@ object LyngFormatter {
val isIf = Regex("^if\\s*\\(.*\\)\\s*$").matches(t)
val isElseIf = Regex("^else\\s+if\\s*\\(.*\\)\\s*$").matches(t)
val isElse = t == "else"
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
return isIf || isElseIf || isElse
}
for ((i, rawLine) in lines.withIndex()) {
@@ -95,12 +70,7 @@ object LyngFormatter {
val trimmedLine = rawLine.trim()
// Compute effective indent level for this line
val currentExtraIndent = extraIndents.sum()
var effectiveLevel = blockLevel + currentExtraIndent
val isAccessor = !inBlockComment && isPropertyAccessor(trimmedStart)
if (isAccessor) effectiveLevel += 1
var effectiveLevel = blockLevel
if (inBlockComment) {
if (!trimmedLine.startsWith("*/")) {
effectiveLevel += 1
@@ -113,9 +83,9 @@ object LyngFormatter {
// Single-line control header (if/else/else if) without braces: indent the next
// non-empty, non-'}', non-'else' line by one extra level
val applyAwaiting = awaitingExtraIndent > 0 && trimmedStart.isNotEmpty() &&
val applyAwaiting = awaitingSingleIndent && trimmedStart.isNotEmpty() &&
!trimmedStart.startsWith("else") && !trimmedStart.startsWith("}")
if (applyAwaiting) effectiveLevel += awaitingExtraIndent
if (applyAwaiting) effectiveLevel += 1
val firstChar = trimmedStart.firstOrNull()
// While inside parentheses, continuation applies scaled by nesting level
@@ -159,43 +129,23 @@ object LyngFormatter {
// New line (keep EOF semantics similar to input)
if (i < lines.lastIndex) sb.append('\n')
val oldBlockLevel = blockLevel
// Update balances using this line's code content
for (part in parts) {
if (part.type == PartType.Code) {
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
// Update awaitingExtraIndent based on current line
// Update awaitingSingleIndent based on current line
if (applyAwaiting && trimmedStart.isNotEmpty()) {
// we have just applied it.
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
}
// we have just consumed the body line
awaitingSingleIndent = false
} else {
// start awaiting if current line is a control header without '{'
val endsWithBrace = code.trimEnd().endsWith("{")
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
awaitingExtraIndent = if (isAccessor) 2 else 1
awaitingSingleIndent = true
}
}
@@ -246,6 +196,9 @@ object LyngFormatter {
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.
* By default, preserves the base indent of the first line in the slice (so the block stays at
@@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -42,10 +42,10 @@ private val fallbackKeywordIds = setOf(
"and", "or", "not",
// declarations & modifiers
"fun", "fn", "class", "enum", "val", "var", "import", "package",
"private", "protected", "static", "open", "extern", "init", "get", "set", "by",
"private", "protected", "static", "open", "extern",
// control flow and misc
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset"
"throw", "return", "break", "continue", "this", "null", "true", "false"
)
/** Maps lexer token type (and sometimes value) to a [HighlightKind]. */
@@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -149,8 +149,6 @@ open class Obj {
// methods that to override
open suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other === this) return 0
if( other === ObjNull ) return 2
scope.raiseNotImplemented()
}
@@ -416,7 +414,7 @@ open class Obj {
val decl = field.declaringClass
val caller = scope.currentClassCtx
if (!canAccessMember(field.effectiveWriteVisibility, decl, caller))
if (!canAccessMember(field.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
if (field.value is ObjProperty) {
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
@@ -665,50 +663,6 @@ 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
*/
@@ -345,7 +345,6 @@ open class ObjClass(
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
pos: Pos = Pos.builtIn,
declaringClass: ObjClass? = this
) {
@@ -354,7 +353,7 @@ open class ObjClass(
if (existingInSelf != null && existingInSelf.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass")
// Install/override in this class
members[name] = ObjRecord(initialValue, isMutable, visibility, writeVisibility, declaringClass = declaringClass)
members[name] = ObjRecord(initialValue, isMutable, visibility, declaringClass = declaringClass)
// Structural change: bump layout version for PIC invalidation
layoutVersion += 1
}
@@ -369,14 +368,13 @@ open class ObjClass(
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
pos: Pos = Pos.builtIn
) {
initClassScope()
val existing = classScope!!.objects[name]
if (existing != null)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility)
classScope!!.addItem(name, isMutable, initialValue, visibility)
// Structural change: bump layout version for PIC invalidation
layoutVersion += 1
}
@@ -385,12 +383,11 @@ open class ObjClass(
name: String,
isOpen: Boolean = false,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
declaringClass: ObjClass? = this,
code: suspend Scope.() -> Obj
) {
val stmt = statement { code() }
createField(name, stmt, isOpen, visibility, writeVisibility, Pos.builtIn, declaringClass)
createField(name, stmt, isOpen, visibility, Pos.builtIn, declaringClass)
}
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
@@ -400,13 +397,12 @@ open class ObjClass(
getter: (suspend Scope.() -> Obj)? = null,
setter: (suspend Scope.(Obj) -> Unit)? = null,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
declaringClass: ObjClass? = this
) {
val g = getter?.let { statement { it() } }
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
val prop = ObjProperty(name, g, s)
members[name] = ObjRecord(prop, false, visibility, writeVisibility, declaringClass, type = ObjRecord.Type.Property)
members[name] = ObjRecord(prop, false, visibility, declaringClass, type = ObjRecord.Type.Property)
layoutVersion += 1
}
@@ -194,9 +194,7 @@ open class ObjException(
"AccessException",
"UnknownException",
"NotFoundException",
"IllegalOperationException",
"UnsetException",
"SyntaxError"
"IllegalOperationException"
)) {
scope.addConst(name, getOrCreateExceptionClass(name))
}
@@ -247,6 +245,3 @@ class ObjIllegalOperationException(scope: Scope, message: String = "Operation is
class ObjNotFoundException(scope: Scope, message: String = "not found") :
ObjException("NotFoundException", scope, message)
class ObjUnsetException(scope: Scope, message: String = "property is unset (not initialized)") :
ObjException("UnsetException", scope, message)
@@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -97,8 +97,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val decl = f.declaringClass
if (scope.thisObj !== this || scope.currentClassCtx == null) {
val caller = scope.currentClassCtx
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
ObjAccessException(
if (!canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException(
scope,
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
).raise()
@@ -108,7 +108,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
prop.callSetter(scope, this, newValue, decl)
return
}
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null)
f.value = newValue
return
@@ -131,8 +131,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
}
if (scope.thisObj !== this || scope.currentClassCtx == null) {
val caller = scope.currentClassCtx
if (!canAccessMember(rec.effectiveWriteVisibility, declaring, caller))
ObjAccessException(
if (!canAccessMember(rec.visibility, declaring, caller))
ObjIllegalAssignmentException(
scope,
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
).raise()
@@ -142,7 +142,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
prop.callSetter(scope, this, newValue, declaring)
return
}
if (!rec.isMutable && rec.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (rec.value.assign(scope, newValue) == null)
rec.value = newValue
return
@@ -271,7 +271,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
protected val comparableVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter {
it.value.type.comparable && (it.value.type != ObjRecord.Type.Field || it.value.isMutable)
it.value.type.comparable
}
}
@@ -350,12 +350,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope.objects[mangled]?.let { f ->
val decl = f.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
ObjAccessException(
if (!canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException(
scope,
"can't assign to field $name (declared in ${decl.className})"
).raise()
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null) f.value = newValue
return
}
@@ -364,12 +364,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope[name]?.let { f ->
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
ObjAccessException(
if (!canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException(
scope,
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
).raise()
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null) f.value = newValue
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 decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!canAccessMember(r.effectiveWriteVisibility, decl, caller))
ObjAccessException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
if (!canAccessMember(r.visibility, decl, caller))
ObjIllegalAssignmentException(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.value.assign(scope, newValue) == null) r.value = newValue
}
@@ -26,14 +26,12 @@ data class ObjRecord(
var value: Obj,
val isMutable: Boolean,
val visibility: Visibility = Visibility.Public,
val writeVisibility: Visibility? = null,
/** If non-null, denotes the class that declared this member (field/method). */
val declaringClass: ObjClass? = null,
var importedFrom: Scope? = null,
val isTransient: Boolean = false,
val type: Type = Type.Other,
) {
val effectiveWriteVisibility: Visibility get() = writeVisibility ?: visibility
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
Field(true, true),
@Suppress("unused")
@@ -442,9 +442,9 @@ class ConstRef(private val record: ObjRecord) : ObjRef {
* Reference to an object's field with optional chaining.
*/
class FieldRef(
val target: ObjRef,
val name: String,
val isOptional: Boolean,
private val target: ObjRef,
private val name: String,
private val isOptional: Boolean,
) : ObjRef {
// 4-entry PIC for reads/writes (guarded by PerfFlags.FIELD_PIC)
// Reads
@@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjVoid
fun String.toSource(name: String = "eval"): Source = Source(name, this)
@@ -45,8 +46,9 @@ abstract class Statement(
abstract suspend fun execute(scope: Scope): Obj
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other == ObjNull || other == ObjVoid ) return 1
if( other === this ) return 0
return -3
return -1
}
override suspend fun callOn(scope: Scope): Obj {
+1 -120
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -396,123 +396,4 @@ class OOTest {
assertThrows { "answer is 42".totalDigits }
""".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,124 +730,4 @@ class LyngFormatterTest {
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,7 +28,10 @@ import net.sergeych.lyng.binding.SymbolKind
import net.sergeych.lyng.highlight.HighlightKind
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.miniast.MiniAstBuilder
import net.sergeych.lyng.miniast.MiniClassDecl
import net.sergeych.lyng.miniast.MiniFunDecl
import net.sergeych.lyng.miniast.MiniTypeName
import org.w3c.dom.HTMLStyleElement
fun ensureBootstrapCodeBlocks(html: String): String {
@@ -363,8 +366,8 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
when (d) {
is MiniFunDecl -> putName(d.nameStart, d.name, "hl-fn")
is MiniClassDecl -> putName(d.nameStart, d.name, "hl-class")
is net.sergeych.lyng.miniast.MiniEnumDecl -> 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 MiniEnumDecl -> putName(d.nameStart, d.name, "hl-class")
}
}
// Imports: color each segment as directive/path
@@ -402,7 +405,7 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
}
is net.sergeych.lyng.miniast.MiniValDecl -> addTypeSegments(d.type)
is MiniClassDecl -> {}
is MiniEnumDecl -> {}
is net.sergeych.lyng.miniast.MiniEnumDecl -> {}
}
}
@@ -533,8 +536,7 @@ private fun detectDeclarationAndParamOverrides(text: String): Map<Pair<Int, Int>
"package", "import", "fun", "fn", "class", "enum", "val", "var",
"if", "else", "while", "do", "for", "when", "try", "catch", "finally",
"throw", "return", "break", "continue", "in", "is", "as", "as?", "not",
"true", "false", "null", "private", "protected", "open", "extern", "static",
"init", "get", "set", "Unset", "by"
"true", "false", "null", "private", "protected", "open", "extern", "static"
)
fun skipWs(idx0: Int): Int {
var idx = idx0
+1 -8
View File
@@ -151,15 +151,8 @@ fun ReferencePage() {
}
}
is MiniEnumDecl -> {
Div { Text("enum ${d.name}") }
Div { Text("enum ${d.name} { ${d.entries.joinToString(", ")} }") }
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) }
}
}
}
}
}
}