From b953282251404a5286a8ad186da3bd0a23a03ba6 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 10 Dec 2025 00:04:07 +0100 Subject: [PATCH] docs on updated scopes --- CHANGELOG.md | 5 +++ docs/OOP.md | 7 +++- docs/advanced_topics.md | 6 +++ docs/exceptions_handling.md | 15 ++++++++ docs/parallelism.md | 19 ++++++++++ docs/scopes_and_closures.md | 75 +++++++++++++++++++++++++++++++++++++ docs/tutorial.md | 2 +- 7 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 docs/scopes_and_closures.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dfb4e6..4fd294c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ### Unreleased +- 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. + - IDEA plugin: Lightweight autocompletion (experimental) - Global completion: local declarations, in‑scope parameters, imported modules, and stdlib symbols. - Member completion: after a dot, suggests only members of the inferred receiver type (incl. chained calls like `Path(".." ).lines().` → `Iterator` methods). No global identifiers appear after a dot. diff --git a/docs/OOP.md b/docs/OOP.md index 28f37ff..c76c011 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -603,4 +603,9 @@ Regular methods are called on instances as usual `instance.method()`. The method TBD -[argument list](declaring_arguments.md) \ No newline at end of file +[argument list](declaring_arguments.md) +### Visibility from within closures and instance scopes + +When a closure executes within a method, the closure retains the lexical class context of its creation site. This means private/protected members of that class remain accessible where expected (subject to usual visibility rules). Field resolution checks the declaring class and validates access using the preserved `currentClassCtx`. + +See also: [Scopes and Closures: resolution and safety](scopes_and_closures.md) diff --git a/docs/advanced_topics.md b/docs/advanced_topics.md index 36a3cb6..90c8fd3 100644 --- a/docs/advanced_topics.md +++ b/docs/advanced_topics.md @@ -158,3 +158,9 @@ Function annotation can have more args specified at call time. There arguments m >>> void [parallelism]: parallelism.md + +## Scopes and Closures: resolution and safety + +Closures and dynamic scope graphs require care to avoid accidental recursion and to keep name resolution predictable. See the dedicated page for detailed rules, helper APIs, and best practices: + +- Scopes and Closures: resolution and safety → [scopes_and_closures.md](scopes_and_closures.md) diff --git a/docs/exceptions_handling.md b/docs/exceptions_handling.md index 0c69f89..00c3a6b 100644 --- a/docs/exceptions_handling.md +++ b/docs/exceptions_handling.md @@ -169,3 +169,18 @@ _this functionality is not yet released_ | UnknownException | unexpected kotlin exception caught | | | | + +### Symbol resolution errors + +For compatibility, `SymbolNotFound` is an alias of `SymbolNotDefinedException`. You can catch either name in examples and tests. + +Example: + +```lyng +try { + nonExistingMethod() +} +catch(e: SymbolNotFound) { + // handle +} +``` diff --git a/docs/parallelism.md b/docs/parallelism.md index 2db6435..e0b247c 100644 --- a/docs/parallelism.md +++ b/docs/parallelism.md @@ -221,3 +221,22 @@ Lyng includes an optional optimization for function/method calls on JVM: scope f - Expected effect (from our JVM micro‑benchmarks): in deep call loops, enabling pooling reduced total time by about 1.38× in a dedicated pooling benchmark; mileage may vary depending on workload. Future work: introduce thread‑safe pooling (e.g., per‑thread pools or confinement strategies) before considering enabling it by default in multi‑threaded environments. + +### Closures inside coroutine helpers (launch/flow) + +Closures executed by `launch { ... }` and `flow { ... }` resolve names using the `ClosureScope` rules: + +1. Closure frame locals/arguments +2. Captured receiver instance/class members +3. Closure ancestry locals + each frame’s `this` members (cycle‑safe) +4. Caller `this` members +5. Caller ancestry locals + each frame’s `this` members (cycle‑safe) +6. Module pseudo‑symbols (e.g., `__PACKAGE__`) +7. Direct module/global fallback (nearest `ModuleScope` and its parent/root) + +Implications: +- Outer locals (e.g., `counter`) stay visible across suspension points. +- Global helpers like `delay(ms)` and `yield()` are available from inside closures. +- If you write your own async helpers, execute user lambdas under `ClosureScope(callScope, capturedCreatorScope)` and avoid manual ancestry walking. + +See also: [Scopes and Closures: resolution and safety](scopes_and_closures.md) diff --git a/docs/scopes_and_closures.md b/docs/scopes_and_closures.md new file mode 100644 index 0000000..e55c1fd --- /dev/null +++ b/docs/scopes_and_closures.md @@ -0,0 +1,75 @@ +# Scopes and Closures: resolution and safety + +This page documents how name resolution works with `ClosureScope`, how to avoid recursion pitfalls, and how to safely capture and execute callbacks that need access to outer locals. + +## Why this matters +Name lookup across nested scopes and closures can accidentally form recursive resolution paths or hide expected symbols (outer locals, module/global functions). The rules below ensure predictable resolution and prevent infinite recursion. + +## Resolution order in ClosureScope +When evaluating an identifier `name` inside a closure, `ClosureScope.get(name)` resolves in this order: + +1. Closure frame locals and arguments +2. Captured receiver (`closureScope.thisObj`) instance/class members +3. Closure ancestry locals + each frame’s `thisObj` members (cycle‑safe) +4. Caller `this` members +5. Caller ancestry locals + each frame’s `thisObj` members (cycle‑safe) +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. + +## 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`: + +- `chainLookupIgnoreClosure(name)` + - Walk raw `parent` chain and check only per‑frame locals/bindings/slots. + - Ignores overridden `get` (e.g., in `ClosureScope`). Cycle‑safe. +- `chainLookupWithMembers(name)` + - Like above, but after locals/bindings it also checks each frame’s `thisObj` members. + - Ignores overridden `get`. Cycle‑safe. +- `baseGetIgnoreClosure(name)` + - For the current frame only: check locals/bindings, then walk raw parents (locals/bindings), then fallback to this frame’s `thisObj` members. + +These helpers avoid ping‑pong recursion and make structural cycles harmless (lookups terminate). + +## Preventing structural cycles +- Don’t construct parent chains that can point back to a descendant. +- A debug‑time guard throws if assigning a parent would create a cycle; keep it enabled for development builds. +- Even with a cycle, chain helpers break out via a small `visited` set keyed by `frameId`. + +## Capturing lexical environments for callbacks +For dynamic objects or custom builders, capture the creator’s lexical scope so callbacks can see outer locals/parameters: + +1. Use `snapshotForClosure()` on the caller scope to capture locals/bindings/slots and parent. +2. Store this snapshot and run callbacks under `ClosureScope(callScope, captured)`. + +Kotlin sketch: +```kotlin +val captured = scope.snapshotForClosure() +val execScope = ClosureScope(currentCallScope, captured) +callback.execute(execScope) +``` + +This ensures expressions like `contractName` used inside dynamic `get { name -> ... }` resolve to outer variables defined at the creation site. + +## Closures in coroutines (launch/flow) +- The closure frame still prioritizes its own locals/args. +- Outer locals declared before suspension points remain visible through slot‑aware ancestry lookups. +- Global functions like `delay(ms)` and `yield()` are resolved via module/root fallbacks from within closures. + +Tip: If a closure unexpectedly cannot see an outer local, check whether an intermediate runtime helper introduced an extra call frame; the built‑in lookup already traverses caller ancestry, so prefer the standard helpers rather than custom dispatch. + +## Local variable references and missing symbols +- Unqualified identifier resolution first prefers locals/bindings/slots before falling back to `this` members. +- If neither locals nor members contain the symbol, missing field lookups map to `SymbolNotFound` (compatibility alias for `SymbolNotDefinedException`). + +## Performance notes +- 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. + +## Dos and Don’ts +- Do use `chainLookupIgnoreClosure` / `chainLookupWithMembers` for ancestry traversals. +- Do maintain the resolution order above for predictable behavior. +- Don’t call virtual `get` while walking parents; it risks recursion across scope types. +- Don’t attach instance scopes to transient/pool frames; bind to a stable parent scope instead. diff --git a/docs/tutorial.md b/docs/tutorial.md index 697e510..dab1611 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -6,7 +6,7 @@ In other word, the code usually works as expected when you see it. So, nothing u __Other documents to read__ maybe after this one: -- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md) +- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md), [Scopes and Closures](scopes_and_closures.md) - [OOP notes](OOP.md), [exception handling](exceptions_handling.md) - [math in Lyng](math.md), [the `when` statement](when.md) - [time](time.md) and [parallelism](parallelism.md)