Updated documentation to clarify property syntax, closure resolution rules, and override usage

This commit is contained in:
Sergey Chernov 2026-01-10 18:08:07 +01:00
parent 91e6ae29ce
commit 7ab439d949
7 changed files with 44 additions and 33 deletions

View File

@ -23,11 +23,11 @@ There are a lo of ways to construct a buffer:
assertEquals( 5, Buffer("hello").size ) assertEquals( 5, Buffer("hello").size )
// from bytes, e.g. integers in range 0..255 // from bytes, e.g. integers in range 0..255
assertEquals( 255, Buffer(1,2,3,255).last() ) assertEquals( 255, Buffer(1,2,3,255).last )
// from whatever iterable that produces bytes, e.g. // from whatever iterable that produces bytes, e.g.
// integers in 0..255 range: // integers in 0..255 range:
assertEquals( 129, Buffer([1,2,129]).last() ) assertEquals( 129, Buffer([1,2,129]).last )
// Empty buffer of fixed size: // Empty buffer of fixed size:
assertEquals(100, Buffer(100).size) assertEquals(100, Buffer(100).size)

View File

@ -119,8 +119,9 @@ Properties allow you to define member accessors that look like fields but execut
### Basic Syntax ### Basic Syntax
Properties are declared using `val` (read-only) or `var` (read-write) followed by a name and `get()`/`set()` blocks: Properties are declared using `val` (read-only) or `var` (read-write) followed by a name and a `get` (and optionally `set`) accessor. Unlike fields, properties do not have automatic storage and must compute their values or delegate to other members.
The standard syntax uses parentheses:
```lyng ```lyng
class Person(private var _age: Int) { class Person(private var _age: Int) {
// Read-only property // Read-only property
@ -136,21 +137,17 @@ class Person(private var _age: Int) {
if (value >= 0) _age = value if (value >= 0) _age = value
} }
} }
val p = Person(15)
assertEquals("Minor", p.ageCategory)
p.age = 20
assertEquals("Adult", p.ageCategory)
``` ```
### Laconic Expression Shorthand ### Laconic Syntax (Optional Parentheses)
For simple accessors and methods, you can use the `=` shorthand for a more elegant and laconic form: For even cleaner code, you can omit the parentheses for `get` and `set`. This is especially useful for simple expression shorthand:
```lyng ```lyng
class Circle(val radius: Real) { class Circle(val radius: Real) {
val area get() = π * radius * radius // Laconic expression shorthand
val circumference get() = 2 * π * radius val area get = π * radius * radius
val circumference get = 2 * π * radius
fun diameter() = radius * 2 fun diameter() = radius * 2
} }
@ -159,15 +156,16 @@ fun median(a, b) = (a + b) / 2
class Counter { class Counter {
private var _count = 0 private var _count = 0
var count get() = _count set(v) = _count = v var count get = _count set(v) = _count = v
} }
``` ```
### Key Rules ### Key Rules
- **`val` properties** must have a `get()` accessor and cannot have a `set()`. - **`val` properties** must have a `get` accessor (with or without parentheses) and cannot have a `set`.
- **`var` properties** must have both `get()` and `set()` accessors. - **`var` properties** must have both `get` and `set` accessors.
- **Functions and methods** can use the `=` shorthand to return the result of a single expression. - **Functions and methods** can use the `=` shorthand to return the result of a single expression.
- **`override` is mandatory**: If you are overriding a member from a base class, you MUST use the `override` keyword.
- **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.

View File

@ -103,6 +103,11 @@ scope.addVoidFn("log") {
println(items.joinToString(" ") { it.toString(this).value }) println(items.joinToString(" ") { it.toString(this).value })
} }
// When adding a member function to a class, you can use isOverride = true
// myClass.addFn("toString", isOverride = true) {
// ObjString("Custom string representation")
// }
// Call them from Lyng // Call them from Lyng
scope.eval("val y = inc(41); log('Answer:', y)") scope.eval("val y = inc(41); log('Answer:', y)")
``` ```
@ -122,6 +127,9 @@ myClass.createField("version", ObjString("1.0.0"), isMutable = false)
// Add a mutable field with an initial value // Add a mutable field with an initial value
myClass.createField("count", ObjInt(0), isMutable = true) myClass.createField("count", ObjInt(0), isMutable = true)
// If you are overriding a field from a base class, use isOverride = true
// myClass.createField("someBaseField", ObjInt(42), isOverride = true)
scope.addConst("MyClass", myClass) scope.addConst("MyClass", myClass)
``` ```
@ -153,6 +161,16 @@ myClass.addProperty(
} }
) )
// You can also create an ObjProperty explicitly
val explicitProp = ObjProperty(
name = "hexValue",
getter = statement { ObjString(internalValue.toString(16)) }
)
myClass.addProperty("hexValue", prop = explicitProp)
// Use isOverride = true when overriding a property from a base class
// myClass.addProperty("baseProp", getter = { ... }, isOverride = true)
scope.addConst("MyClass", myClass) scope.addConst("MyClass", myClass)
``` ```

View File

@ -226,13 +226,11 @@ Future work: introduce thread‑safe pooling (e.g., per‑thread pools or confin
Closures executed by `launch { ... }` and `flow { ... }` resolve names using the `ClosureScope` rules: Closures executed by `launch { ... }` and `flow { ... }` resolve names using the `ClosureScope` rules:
1. Closure frame locals/arguments 1. **Current frame locals and arguments**: Variables defined within the current closure execution.
2. Captured receiver instance/class members 2. **Captured lexical ancestry**: Outer local variables captured at the site where the closure was defined (the "lexical environment").
3. Closure ancestry locals + each frame’s `this` members (cycle‑safe) 3. **Captured receiver members**: If the closure was defined within a class or explicitly bound to an object, it checks members of that object (`this`), following MRO and respecting visibility.
4. Caller `this` members 4. **Caller environment**: Falls back to the calling context (e.g., the caller's `this` or local variables).
5. Caller ancestry locals + each frame’s `this` members (cycle‑safe) 5. **Global/Module fallbacks**: Final check for module-level constants and global functions.
6. Module pseudo‑symbols (e.g., `__PACKAGE__`)
7. Direct module/global fallback (nearest `ModuleScope` and its parent/root)
Implications: Implications:
- Outer locals (e.g., `counter`) stay visible across suspension points. - Outer locals (e.g., `counter`) stay visible across suspension points.

View File

@ -8,16 +8,13 @@ Name lookup across nested scopes and closures can accidentally form recursive re
## Resolution order in ClosureScope ## Resolution order in ClosureScope
When evaluating an identifier `name` inside a closure, `ClosureScope.get(name)` resolves in this order: When evaluating an identifier `name` inside a closure, `ClosureScope.get(name)` resolves in this order:
1. Closure frame locals and arguments 1. **Current frame locals and arguments**: Variables defined within the current closure execution.
2. Captured receiver (`closureScope.thisObj`) instance/class members 2. **Captured lexical ancestry**: Outer local variables captured at the site where the closure was defined (the "lexical environment").
3. Closure ancestry locals + each frame’s `thisObj` members (cycle‑safe) 3. **Captured receiver members**: If the closure was defined within a class or explicitly bound to an object, it checks members of that object (`this`). This includes both instance fields/methods and class-level static members, following the MRO (C3) and respecting visibility rules (private members are only visible if the closure was defined in their class).
4. Caller `this` members 4. **Caller environment**: If not found lexically, it falls back to the calling context (e.g., the DSL's `this` or the caller's local variables).
5. Caller ancestry locals + each frame’s `thisObj` members (cycle‑safe) 5. **Global/Module fallbacks**: Final check for module-level constants and global functions.
6. Module pseudo‑symbols (e.g., `__PACKAGE__`) from the nearest `ModuleScope`
7. Direct module/global fallback (nearest `ModuleScope` and its parent/root scope)
8. Final fallback: base local/parent lookup for the current frame
This preserves intuitive visibility (locals → captured receiver → closure chain → caller members → caller chain → module/root) while preventing infinite recursion between scope types. This ensures that closures primarily interact with their defining environment (lexical capture) while still being able to participate in DSL-style calling contexts.
## Use raw‑chain helpers for ancestry walks ## Use raw‑chain helpers for ancestry walks
When authoring new scope types or advanced lookups, avoid calling virtual `get` while walking parents. Instead, use the non‑dispatch helpers on `Scope`: When authoring new scope types or advanced lookups, avoid calling virtual `get` while walking parents. Instead, use the non‑dispatch helpers on `Scope`:

View File

@ -1494,7 +1494,7 @@ Typical set of String functions includes:
| s1 += s2 | self-modifying concatenation | | s1 += s2 | self-modifying concatenation |
| toReal() | attempts to parse string as a Real value | | toReal() | attempts to parse string as a Real value |
| toInt() | parse string to Int value | | toInt() | parse string to Int value |
| characters() | create [List] of characters (1) | | characters | create [List] of characters (1) |
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 | | encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
| matches(re) | matches the regular expression (2) | | matches(re) | matches the regular expression (2) |
| | | | | |

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "1.2.-SNAPSHOT" version = "1.2.0-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below