Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aec4a3766e | ||
|
|
5f819dc87a | ||
|
|
8e766490d9 | ||
|
|
514ad96148 | ||
|
|
e0a59c8db6 | ||
|
|
75e2b63923 | ||
|
|
5f1e6564d5 | ||
|
|
f5a3fbe9a3 | ||
|
|
11eadc1d9f | ||
|
|
96e1ffc7d5 | ||
|
|
aad7c9619b | ||
|
|
9e138367ef | ||
|
|
1d9befe101 | ||
|
|
abb262d9cf | ||
|
|
eca451b5a3 | ||
|
|
54af50d6d6 | ||
|
|
ce0fc3650d | ||
|
|
ce021e85f4 | ||
|
|
bc6613ec01 | ||
|
|
59aefc5bc2 | ||
|
|
fac58675d5 | ||
|
|
22f6c149db | ||
|
|
6d8b60fcfa | ||
|
|
54ecffc803 | ||
|
|
0731d63adf | ||
|
|
434d74b07f | ||
|
|
55b27f831f | ||
|
|
5586e027ea | ||
|
|
cd2b1a9cb7 | ||
|
|
3b6504d3b1 | ||
|
|
357585d3ba | ||
|
|
3a9c567c32 | ||
|
|
8300c2a3ab | ||
|
|
17b4904ae3 | ||
|
|
7e8f1406b5 | ||
|
|
3f235878c0 | ||
|
|
5f3a54d08f | ||
|
|
157b716eb7 | ||
|
|
c9f96464f3 | ||
|
|
0732202c80 | ||
|
|
1e18a162c4 | ||
|
|
0dbfb473bf | ||
|
|
0add0ab54c | ||
|
|
3acd56f55a | ||
|
|
b7838b45ec | ||
|
|
92cb088f36 | ||
|
|
3dfe98a93d | ||
|
|
f6d51e9b94 | ||
|
|
3ac7fd7ceb | ||
|
|
99f883cfc7 | ||
|
|
76a1804dc1 | ||
|
|
35cc8fa930 | ||
|
|
6f86e6ff97 | ||
|
|
fa91afa92b | ||
|
|
00503fa105 | ||
|
|
f7a4497f3f | ||
|
|
1aef64e0fa | ||
|
|
5d8fdce637 | ||
|
|
5a8881bfd5 | ||
|
|
d487886c8f | ||
|
|
180471e4cd | ||
|
|
71a37a2906 | ||
|
|
ab05f83e77 | ||
|
|
a2d26fc777 | ||
|
|
dd1a1544c6 |
No files matched your search
@@ -17,3 +17,7 @@ xcuserdata
|
|||||||
/sample_texts/1.txt.gz
|
/sample_texts/1.txt.gz
|
||||||
/kotlin-js-store/wasm/yarn.lock
|
/kotlin-js-store/wasm/yarn.lock
|
||||||
/distributables
|
/distributables
|
||||||
|
.output*.txt
|
||||||
|
debug.log
|
||||||
|
/build.log
|
||||||
|
/test.md
|
||||||
@@ -1,3 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
~ 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.
|
||||||
|
~
|
||||||
|
-->
|
||||||
|
|
||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="lyng:site [jsBrowserDevelopmentRun]" type="GradleRunConfiguration" factoryName="Gradle">
|
<configuration default="false" name="lyng:site [jsBrowserDevelopmentRun]" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
<ExternalSystemSettings>
|
<ExternalSystemSettings>
|
||||||
@@ -17,8 +34,11 @@
|
|||||||
</ExternalSystemSettings>
|
</ExternalSystemSettings>
|
||||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||||
<DebugAllEnabled>false</DebugAllEnabled>
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
<RunAsTest>false</RunAsTest>
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<GradleProfilingDisabled>true</GradleProfilingDisabled>
|
||||||
|
<GradleCoverageDisabled>true</GradleCoverageDisabled>
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
+37
-1
@@ -2,7 +2,40 @@
|
|||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
|
|
||||||
- Docs: Scopes and Closures guidance
|
- Language: Abstract Classes and Interfaces
|
||||||
|
- Support for `abstract` modifier on classes, methods, and variables.
|
||||||
|
- Introduced `interface` as a synonym for `abstract class`, supporting full state (constructors, fields, `init` blocks) and implementation by parts via MI.
|
||||||
|
- New `closed` modifier (antonym to `open`) to prevent overriding class members.
|
||||||
|
- Refined `override` logic: mandatory keyword when re-declaring members that exist in the ancestor chain (MRO).
|
||||||
|
- MI Satisfaction: Abstract requirements are automatically satisfied by matching concrete members found later in the C3 MRO chain without requiring explicit proxy methods.
|
||||||
|
- Integration: Updated highlighters (lynglib, lyngweb, IDEA plugin), IDEA completion, and Grazie grammar checking.
|
||||||
|
- Documentation: Updated `docs/OOP.md` with sections on "Abstract Classes and Members", "Interfaces", and "Overriding and Virtual Dispatch".
|
||||||
|
|
||||||
|
- Language: Class properties with accessors
|
||||||
|
- Support for `val` (read-only) and `var` (read-write) properties in classes.
|
||||||
|
- Syntax: `val name [ : Type ] get() { body }` or `var name [ : Type ] get() { body } set(value) { body }`.
|
||||||
|
- Laconic Expression Shorthand: `val prop get() = expression` and `var prop get() = read set(v) = write`.
|
||||||
|
- Properties are pure accessors and do **not** have automatic backing fields.
|
||||||
|
- Validation: `var` properties must have both accessors; `val` must have only a getter.
|
||||||
|
- 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
|
||||||
- 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.
|
||||||
@@ -74,6 +107,9 @@ 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).
|
||||||
|
- Fix: Formatting Markdown files no longer deletes content in `.lyng` code fences and works correctly with injected files (resolves clobbering, `StringIndexOutOfBoundsException`, and `nonempty text is not covered by block` errors).
|
||||||
|
|
||||||
- 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.
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ Please visit the project homepage: [https://lynglang.com](https://lynglang.com)
|
|||||||
|
|
||||||
- simple, compact, intuitive and elegant modern code:
|
- simple, compact, intuitive and elegant modern code:
|
||||||
|
|
||||||
```
|
```lyng
|
||||||
class Point(x,y) {
|
class Point(x, y) {
|
||||||
fun dist() { sqrt(x*x + y*y) }
|
fun dist() { sqrt(x*x + y*y) }
|
||||||
}
|
}
|
||||||
Point(3,4).dist() //< 5
|
|
||||||
|
// Auto-named arguments shorthand (x: is x: x):
|
||||||
|
val x = 3
|
||||||
|
val y = 4
|
||||||
|
Point(x:, y:).dist() //< 5
|
||||||
|
|
||||||
fun swapEnds(first, args..., last, f) {
|
fun swapEnds(first, args..., last, f) {
|
||||||
f( last, ...args, first)
|
f( last, ...args, first)
|
||||||
@@ -39,6 +43,8 @@ and it is multithreaded on platforms supporting it (automatically, no code chang
|
|||||||
|
|
||||||
- [Language home](https://lynglang.com)
|
- [Language home](https://lynglang.com)
|
||||||
- [introduction and tutorial](docs/tutorial.md) - start here please
|
- [introduction and tutorial](docs/tutorial.md) - start here please
|
||||||
|
- [Testing and Assertions](docs/Testing.md)
|
||||||
|
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
||||||
- [Samples directory](docs/samples)
|
- [Samples directory](docs/samples)
|
||||||
- [Formatter (core + CLI + IDE)](docs/formatter.md)
|
- [Formatter (core + CLI + IDE)](docs/formatter.md)
|
||||||
- [Books directory](docs)
|
- [Books directory](docs)
|
||||||
@@ -184,6 +190,7 @@ Ready features:
|
|||||||
- [x] better stack reporting
|
- [x] better stack reporting
|
||||||
- [x] regular exceptions + extended `when`
|
- [x] regular exceptions + extended `when`
|
||||||
- [x] multiple inheritance for user classes
|
- [x] multiple inheritance for user classes
|
||||||
|
- [x] class properties (accessors)
|
||||||
|
|
||||||
## plan: towards v1.5 Enhancing
|
## plan: towards v1.5 Enhancing
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -82,7 +82,7 @@ function updateIdeaPluginDownloadLink() {
|
|||||||
# default target settings
|
# default target settings
|
||||||
case "com" in
|
case "com" in
|
||||||
com)
|
com)
|
||||||
SSH_HOST=sergeych@lynglang.com # host to deploy to
|
SSH_HOST=sergeych@d.lynglang.com # host to deploy to
|
||||||
SSH_PORT=22 # ssh port on it
|
SSH_PORT=22 # ssh port on it
|
||||||
ROOT=/bigstore/sergeych_pub/lyng # directory to rsync to
|
ROOT=/bigstore/sergeych_pub/lyng # directory to rsync to
|
||||||
;;
|
;;
|
||||||
@@ -98,10 +98,20 @@ esac
|
|||||||
|
|
||||||
die() { echo "ERROR: $*" 1>&2 ; exit 1; }
|
die() { echo "ERROR: $*" 1>&2 ; exit 1; }
|
||||||
|
|
||||||
|
function refreshTextmateZip() {
|
||||||
|
echo "Refreshing distributables/lyng-textmate.zip from editors/..."
|
||||||
|
mkdir -p distributables
|
||||||
|
# We use -r for recursive and -q for quiet (optional)
|
||||||
|
# -j can be used if we want to junk paths, but the request says "contents of editors/"
|
||||||
|
# usually we want to preserve the structure inside editors/
|
||||||
|
(cd editors && zip -rq ../distributables/lyng-textmate.zip .)
|
||||||
|
}
|
||||||
|
|
||||||
# Update the IDEA plugin download link in docs (temporarily), then build, then restore the doc
|
# Update the IDEA plugin download link in docs (temporarily), then build, then restore the doc
|
||||||
|
refreshTextmateZip
|
||||||
updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link"
|
updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link"
|
||||||
|
|
||||||
./gradlew site:clean site:jsBrowserDistribution
|
./gradlew site:jsBrowserDistribution
|
||||||
BUILD_RC=$?
|
BUILD_RC=$?
|
||||||
|
|
||||||
# Always restore original doc if backup exists
|
# Always restore original doc if backup exists
|
||||||
|
|||||||
Executable
+256
@@ -0,0 +1,256 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DOCS_DIR="docs"
|
||||||
|
OUTPUT_DIR="distributables"
|
||||||
|
TEMP_DIR="build/temp_docs"
|
||||||
|
MERGED_MD="$TEMP_DIR/merged.md"
|
||||||
|
OUTPUT_HTML="$OUTPUT_DIR/lyng_documentation.html"
|
||||||
|
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
mkdir -p "$TEMP_DIR"
|
||||||
|
|
||||||
|
# Files that should come first in specific order
|
||||||
|
PRIORITY_FILES=(
|
||||||
|
"tutorial.md"
|
||||||
|
"OOP.md"
|
||||||
|
"advanced_topics.md"
|
||||||
|
"declaring_arguments.md"
|
||||||
|
"scopes_and_closures.md"
|
||||||
|
"exceptions_handling.md"
|
||||||
|
"when.md"
|
||||||
|
"parallelism.md"
|
||||||
|
"Testing.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Files that should come next (reference)
|
||||||
|
REFERENCE_FILES=(
|
||||||
|
"Collection.md"
|
||||||
|
"Iterable.md"
|
||||||
|
"Iterator.md"
|
||||||
|
"List.md"
|
||||||
|
"Set.md"
|
||||||
|
"Map.md"
|
||||||
|
"Array.md"
|
||||||
|
"Buffer.md"
|
||||||
|
"RingBuffer.md"
|
||||||
|
"Range.md"
|
||||||
|
"Real.md"
|
||||||
|
"Regex.md"
|
||||||
|
"math.md"
|
||||||
|
"time.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Files that are about integration/tools
|
||||||
|
INTEGRATION_FILES=(
|
||||||
|
"serialization.md"
|
||||||
|
"json_and_kotlin_serialization.md"
|
||||||
|
"embedding.md"
|
||||||
|
"lyng_cli.md"
|
||||||
|
"lyng.io.fs.md"
|
||||||
|
"formatter.md"
|
||||||
|
"EfficientIterables.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tracking processed files to avoid duplicates
|
||||||
|
PROCESSED_PATHS=()
|
||||||
|
|
||||||
|
is_excluded() {
|
||||||
|
local full_path="$1"
|
||||||
|
if grep -q "excludeFromIndex" "$full_path"; then
|
||||||
|
return 0 # true in bash
|
||||||
|
fi
|
||||||
|
return 1 # false
|
||||||
|
}
|
||||||
|
|
||||||
|
process_file() {
|
||||||
|
local rel_path="$1"
|
||||||
|
local full_path="$DOCS_DIR/$rel_path"
|
||||||
|
|
||||||
|
if [[ ! -f "$full_path" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_excluded "$full_path"; then
|
||||||
|
echo "Skipping excluded: $rel_path"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for duplicates
|
||||||
|
for p in "${PROCESSED_PATHS[@]}"; do
|
||||||
|
if [[ "$p" == "$rel_path" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
PROCESSED_PATHS+=("$rel_path")
|
||||||
|
|
||||||
|
echo "Processing: $rel_path"
|
||||||
|
|
||||||
|
# 1. Add an anchor for the file based on its path
|
||||||
|
local anchor_name=$(echo "$rel_path" | sed 's/\//_/g')
|
||||||
|
echo "<div id=\"$anchor_name\"></div>" >> "$MERGED_MD"
|
||||||
|
echo "" >> "$MERGED_MD"
|
||||||
|
|
||||||
|
# 2. Append content with fixed links
|
||||||
|
# - [text](file.md) -> [text](#file.md)
|
||||||
|
# - [text](dir/file.md) -> [text](#dir_file.md)
|
||||||
|
# - [text](file.md#anchor) -> [text](#anchor)
|
||||||
|
# - Fix image links: [alt](../images/...) -> [alt](images/...) if needed, but none found yet.
|
||||||
|
|
||||||
|
cat "$full_path" | \
|
||||||
|
perl -pe 's/\[([^\]]+)\]\(([^)]+)\.md\)/"[$1](#" . ($2 =~ s|\/|_|gr) . ".md)"/ge' | \
|
||||||
|
perl -pe 's/\[([^\]]+)\]\(([^)]+)\.md#([^)]+)\)/[$1](#$3)/g' >> "$MERGED_MD"
|
||||||
|
|
||||||
|
echo -e "\n\n---\n\n" >> "$MERGED_MD"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start with an empty merged file
|
||||||
|
echo "% Lyng Language Documentation" > "$MERGED_MD"
|
||||||
|
echo "" >> "$MERGED_MD"
|
||||||
|
|
||||||
|
# 1. Process priority files
|
||||||
|
for f in "${PRIORITY_FILES[@]}"; do
|
||||||
|
process_file "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. Process reference files
|
||||||
|
for f in "${REFERENCE_FILES[@]}"; do
|
||||||
|
process_file "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. Process integration files
|
||||||
|
for f in "${INTEGRATION_FILES[@]}"; do
|
||||||
|
process_file "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 4. Process remaining files in docs root
|
||||||
|
for f in "$DOCS_DIR"/*.md; do
|
||||||
|
rel_f=${f#"$DOCS_DIR/"}
|
||||||
|
process_file "$rel_f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 5. Process remaining files in subdirs (like samples)
|
||||||
|
find "$DOCS_DIR" -name "*.md" | sort | while read -r f; do
|
||||||
|
rel_f=${f#"$DOCS_DIR/"}
|
||||||
|
process_file "$rel_f"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Running pandoc to generate $OUTPUT_HTML..."
|
||||||
|
|
||||||
|
# Use a basic but clean CSS
|
||||||
|
pandoc "$MERGED_MD" -o "$OUTPUT_HTML" \
|
||||||
|
--toc --toc-depth=2 \
|
||||||
|
--standalone \
|
||||||
|
--embed-resources \
|
||||||
|
--metadata title="Lyng Language Documentation" \
|
||||||
|
--css <(echo "
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2em;
|
||||||
|
color: #24292e;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background-color: rgba(27,31,35,0.05);
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: SFMono-Regular, Consolas, \"Liberation Mono\", Menlo, monospace;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
height: 0.25em;
|
||||||
|
padding: 0;
|
||||||
|
margin: 24px 0;
|
||||||
|
background-color: #e1e4e8;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
padding: 0 1em;
|
||||||
|
color: #6a737d;
|
||||||
|
border-left: 0.25em solid #dfe2e5;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
nav#TOC {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 1em;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
margin-bottom: 2.5em;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
nav#TOC ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
nav#TOC > ul {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
table th, table td {
|
||||||
|
padding: 6px 13px;
|
||||||
|
border: 1px solid #dfe2e5;
|
||||||
|
}
|
||||||
|
table tr {
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #c6cbd1;
|
||||||
|
}
|
||||||
|
table tr:nth-child(2n) {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
")
|
||||||
|
|
||||||
|
echo "-------------------------------------------------------"
|
||||||
|
echo "Done! Documentation generated successfully."
|
||||||
|
echo "Location: $OUTPUT_HTML"
|
||||||
|
echo "-------------------------------------------------------"
|
||||||
+7
-1
@@ -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.
|
||||||
@@ -29,3 +29,9 @@ tasks.register<org.gradle.api.DefaultTask>("runIde") {
|
|||||||
description = "Run IntelliJ IDEA with the Lyng plugin (:lyng-idea)"
|
description = "Run IntelliJ IDEA with the Lyng plugin (:lyng-idea)"
|
||||||
dependsOn(":lyng-idea:runIde")
|
dependsOn(":lyng-idea:runIde")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register<Exec>("generateDocs") {
|
||||||
|
group = "documentation"
|
||||||
|
description = "Generates a single-file documentation HTML using bin/generate_docs.sh"
|
||||||
|
commandLine("./bin/generate_docs.sh")
|
||||||
|
}
|
||||||
+1
-1
@@ -12,7 +12,7 @@ Is a [Iterable] with known `size`, a finite [Iterable]:
|
|||||||
(1)
|
(1)
|
||||||
: `comparator(a,b)` should return -1 if `a < b`, +1 if `a > b` or zero.
|
: `comparator(a,b)` should return -1 if `a < b`, +1 if `a > b` or zero.
|
||||||
|
|
||||||
See [List], [Set] and [Iterable]
|
See [List], [Set], [Iterable] and [Efficient Iterables in Kotlin Interop](EfficientIterables.md)
|
||||||
|
|
||||||
[Iterable]: Iterable.md
|
[Iterable]: Iterable.md
|
||||||
[List]: List.md
|
[List]: List.md
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Efficient Iterables in Kotlin Interop
|
||||||
|
|
||||||
|
Lyng provides high-performance iteration mechanisms that allow Kotlin-side code to interact with Lyng iterables efficiently and vice versa.
|
||||||
|
|
||||||
|
## 1. Enumerating Lyng Objects from Kotlin
|
||||||
|
|
||||||
|
To iterate over a Lyng object (like a `List`, `Set`, or `Range`) from Kotlin code, use the virtual `enumerate` method:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val lyngList: Obj = ...
|
||||||
|
lyngList.enumerate(scope) { item ->
|
||||||
|
println("Processing $item")
|
||||||
|
true // return true to continue, false to break
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why it's efficient:
|
||||||
|
- **Zero allocation**: Unlike traditional iterators, it doesn't create a `LyngIterator` object or any intermediate wrappers.
|
||||||
|
- **Direct access**: Subclasses like `ObjList` override `enumerate` to iterate directly over their internal Kotlin collections.
|
||||||
|
- **Reduced overhead**: It avoids multiple `invokeInstanceMethod` calls for `hasNext()` and `next()` on every step, which would normally involve dynamic dispatch and scope overhead.
|
||||||
|
|
||||||
|
## 2. Reactive Enumeration with Flow
|
||||||
|
|
||||||
|
If you prefer a reactive approach or need to integrate with Kotlin Coroutines flows, use `toFlow()`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
lyngList.toFlow(scope).collect { item ->
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Note: `toFlow()` internally uses the Lyng iterator protocol (`iterator()`, `hasNext()`, `next()`), so it's slightly less efficient than `enumerate()` for performance-critical loops, but more idiomatic for flow-based processing.*
|
||||||
|
|
||||||
|
## 3. Creating Efficient Iterables for Lyng in Kotlin
|
||||||
|
|
||||||
|
When implementing a custom object in Kotlin that should be iterable in Lyng (e.g., usable in `for (x in myObj) { ... }`), follow these steps to ensure maximum performance.
|
||||||
|
|
||||||
|
### A. Inherit from `Obj` and use `ObjIterable`
|
||||||
|
Ensure your object's class has `ObjIterable` as a parent so the Lyng compiler recognizes it as an iterable.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MyCollection(val items: List<Obj>) : Obj() {
|
||||||
|
override val objClass = MyCollection.type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = ObjClass("MyCollection", ObjIterable).apply {
|
||||||
|
// Provide a Lyng-side iterator for compatibility with
|
||||||
|
// manual iterator usage in Lyng scripts.
|
||||||
|
// Using ObjKotlinObjIterator if items are already Obj instances:
|
||||||
|
addFn("iterator") {
|
||||||
|
ObjKotlinObjIterator(thisAs<MyCollection>().items.iterator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Override `enumerate` for Maximum Performance
|
||||||
|
The Lyng compiler's `for` loops use the `enumerate` method. By overriding it in your Kotlin class, you provide a "fast path" for iteration.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MyCollection(val items: List<Obj>) : Obj() {
|
||||||
|
// ...
|
||||||
|
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
|
||||||
|
for (item in items) {
|
||||||
|
// If callback returns false, it means 'break' was called in Lyng
|
||||||
|
if (!callback(item)) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Use `ObjInt.of()` for Numeric Data
|
||||||
|
If your iterable contains integers, always use `ObjInt.of(Long)` instead of the `ObjInt(Long)` constructor. Lyng maintains a cache for small integers (-128 to 127), which significantly reduces object allocations and GC pressure during tight loops.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Efficiently creating an integer object
|
||||||
|
val obj = ObjInt.of(42L)
|
||||||
|
|
||||||
|
// Or using extension methods which also use the cache:
|
||||||
|
val obj2 = 42.toObj()
|
||||||
|
val obj3 = 42L.toObj()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Note on `toObj()` extensions:
|
||||||
|
While `<reified T> T.toObj()` is convenient, using specific extensions like `Int.toObj()` or `Long.toObj()` is slightly more efficient as they use the `ObjInt` cache.
|
||||||
|
|
||||||
|
## 4. Summary of Best Practices
|
||||||
|
|
||||||
|
- **To Consume**: Use `enumerate(scope) { item -> ... true }`.
|
||||||
|
- **To Implement**: Override `enumerate` in your `Obj` subclass.
|
||||||
|
- **To Register**: Use `ObjIterable` (or `ObjCollection`) as a parent class in your `ObjClass` definition.
|
||||||
|
- **To Optimize**: Use `ObjInt.of()` (or `.toObj()`) for all integer object allocations.
|
||||||
+93
-27
@@ -40,13 +40,13 @@ available, for example
|
|||||||
## joinToString
|
## joinToString
|
||||||
|
|
||||||
This methods convert any iterable to a string joining string representation of each element, optionally transforming it
|
This methods convert any iterable to a string joining string representation of each element, optionally transforming it
|
||||||
and joining using specified suffix.
|
and joining using specified separator.
|
||||||
|
|
||||||
Iterable.joinToString(suffux=' ', transform=null)
|
Iterable.joinToString(separator=' ', transformer=null)
|
||||||
|
|
||||||
- if `Iterable` `isEmpty`, the empty string `""` is returned.
|
- if `Iterable` `isEmpty`, the empty string `""` is returned.
|
||||||
- `suffix` is inserted between items when there are more than one.
|
- `separator` is inserted between items when there are more than one.
|
||||||
- `transform` of specified is applied to each element, otherwise its `toString()` method is used.
|
- `transformer` of specified is applied to each element, otherwise its `toString()` method is used.
|
||||||
|
|
||||||
Here is the sample:
|
Here is the sample:
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ Here is the sample:
|
|||||||
assertEquals( (1..3).joinToString { it * 10 }, "10 20 30")
|
assertEquals( (1..3).joinToString { it * 10 }, "10 20 30")
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
## `sum` and `sumBy`
|
## `sum` and `sumOf`
|
||||||
|
|
||||||
These, again, does the thing:
|
These, again, does the thing:
|
||||||
|
|
||||||
@@ -68,19 +68,79 @@ These, again, does the thing:
|
|||||||
|
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
## map and mapNotNull
|
## map, filter and their variations
|
||||||
|
|
||||||
Used to transform either the whole iterable stream or also skipping som elements from it:
|
Used to transform or filter the whole iterable stream:
|
||||||
|
|
||||||
val source = [1,2,3,4]
|
val source = [1,2,3,4]
|
||||||
// transform every element to string or null:
|
|
||||||
assertEquals(["n1", "n2", null, "n4"], source.map { if( it == 3 ) null else "n"+it } )
|
|
||||||
|
|
||||||
// transform every element to stirng, skipping 3:
|
// map: transform every element to something else
|
||||||
|
assertEquals(["n1", "n2", "n3", "n4"], source.map { "n"+it } )
|
||||||
|
|
||||||
|
// filter: keep only elements matching the predicate
|
||||||
|
assertEquals([2, 4], source.filter { it % 2 == 0 } )
|
||||||
|
|
||||||
|
// count: count elements matching the predicate
|
||||||
|
assertEquals(2, source.count { it % 2 == 0 } )
|
||||||
|
|
||||||
|
// mapNotNull: transform every element, skipping null results:
|
||||||
assertEquals(["n1", "n2", "n4"], source.mapNotNull { if( it == 3 ) null else "n"+it } )
|
assertEquals(["n1", "n2", "n4"], source.mapNotNull { if( it == 3 ) null else "n"+it } )
|
||||||
|
|
||||||
|
// filterNotNull: skip all null elements:
|
||||||
|
assertEquals([1, 2, 4], [1, 2, null, 4].filterNotNull())
|
||||||
|
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
You can also use flow variations that return a cold `Flow` instead of a `List`, which is useful for large or infinite sequences:
|
||||||
|
|
||||||
|
val source = [1, 2, 3, 4]
|
||||||
|
|
||||||
|
// filterFlow: returns a Flow of filtered elements
|
||||||
|
assert( source.filterFlow { it % 2 == 0 } is Flow )
|
||||||
|
|
||||||
|
// filterFlowNotNull: returns a Flow of non-null elements
|
||||||
|
assert( [1, null, 2].filterFlowNotNull() is Flow )
|
||||||
|
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
## minOf and maxOf
|
||||||
|
|
||||||
|
Find the minimum or maximum value of a function applied to each element:
|
||||||
|
|
||||||
|
val source = ["abc", "de", "fghi"]
|
||||||
|
assertEquals(2, source.minOf { it.length })
|
||||||
|
assertEquals(4, source.maxOf { it.length })
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
## flatten and flatMap
|
||||||
|
|
||||||
|
Work with nested collections:
|
||||||
|
|
||||||
|
val nested = [[1, 2], [3, 4]]
|
||||||
|
|
||||||
|
// flatten: combine nested collections into one list
|
||||||
|
assertEquals([1, 2, 3, 4], nested.flatten())
|
||||||
|
|
||||||
|
// flatMap: map each element to a collection and flatten the result
|
||||||
|
assertEquals([1, 10, 2, 20], [1, 2].flatMap { [it, it*10] })
|
||||||
|
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
## findFirst and findFirstOrNull
|
||||||
|
|
||||||
|
Search for the first element that satisfies the given predicate:
|
||||||
|
|
||||||
|
val source = [1, 2, 3, 4]
|
||||||
|
assertEquals( 2, source.findFirst { it % 2 == 0 } )
|
||||||
|
assertEquals( 2, source.findFirstOrNull { it % 2 == 0 } )
|
||||||
|
|
||||||
|
// findFirst throws if not found:
|
||||||
|
assertThrows( NoSuchElementException ) { source.findFirst { it > 10 } }
|
||||||
|
|
||||||
|
// findFirstOrNull returns null if not found:
|
||||||
|
assertEquals( null, source.findFirstOrNull { it > 10 } )
|
||||||
|
|
||||||
|
>>> void
|
||||||
|
|
||||||
## Instance methods:
|
## Instance methods:
|
||||||
|
|
||||||
@@ -89,52 +149,56 @@ Used to transform either the whole iterable stream or also skipping som elements
|
|||||||
| toList() | create a list from iterable |
|
| toList() | create a list from iterable |
|
||||||
| toSet() | create a set from iterable |
|
| toSet() | create a set from iterable |
|
||||||
| contains(i) | check that iterable contains `i` |
|
| contains(i) | check that iterable contains `i` |
|
||||||
| `i in iterator` | same as `contains(i)` |
|
| `i in iterable` | same as `contains(i)` |
|
||||||
| isEmpty() | check iterable is empty |
|
| isEmpty() | check iterable is empty |
|
||||||
| forEach(f) | call f for each element |
|
| forEach(f) | call f for each element |
|
||||||
| toMap() | create a map from list of key-value pairs (arrays of 2 items or like) |
|
| toMap() | create a map from list of key-value pairs (arrays of 2 items or like) |
|
||||||
|
| any(p) | true if any element matches predicate `p` |
|
||||||
|
| all(p) | true if all elements match predicate `p` |
|
||||||
| map(f) | create a list of values returned by `f` called for each element of the iterable |
|
| map(f) | create a list of values returned by `f` called for each element of the iterable |
|
||||||
| indexOf(i) | return index if the first encounter of i or a negative value if not found |
|
| indexOf(i) | return index if the first encounter of i or a negative value if not found |
|
||||||
| associateBy(kf) | create a map where keys are returned by kf that will be called for each element |
|
| associateBy(kf) | create a map where keys are returned by kf that will be called for each element |
|
||||||
|
| filter(p) | create a list of elements matching predicate `p` |
|
||||||
|
| count(p) | count elements matching predicate `p` |
|
||||||
|
| filterFlow(p) | create a [Flow] of elements matching predicate `p` |
|
||||||
|
| filterNotNull() | create a list of non-null elements |
|
||||||
|
| filterFlowNotNull() | create a [Flow] of non-null elements |
|
||||||
|
| minOf(f) | return minimum value of `f` applied to elements |
|
||||||
|
| maxOf(f) | return maximum value of `f` applied to elements |
|
||||||
|
| flatten() | flatten nested collections into a single [List] |
|
||||||
|
| flatMap(f) | map each element with `f` and flatten results into a [List] |
|
||||||
|
| findFirst(p) | return first element matching predicate `p` or throw (1) |
|
||||||
|
| findFirstOrNull(p) | return first element matching predicate `p` or `null` |
|
||||||
| first | first element (1) |
|
| first | first element (1) |
|
||||||
| last | last element (1) |
|
| last | last element (1) |
|
||||||
| take(n) | return [Iterable] of up to n first elements |
|
| take(n) | return [Iterable] of up to n first elements |
|
||||||
| taleLast(n) | return [Iterable] of up to n last elements |
|
| takeLast(n) | return [Iterable] of up to n last elements |
|
||||||
| drop(n) | return new [Iterable] without first n elements |
|
| drop(n) | return new [Iterable] without first n elements |
|
||||||
| dropLast(n) | return new [Iterable] without last n elements |
|
| dropLast(n) | return new [Iterable] without last n elements |
|
||||||
| sum() | return sum of the collection applying `+` to its elements (3) |
|
| sum() | return sum of the collection applying `+` to its elements (3) |
|
||||||
| sumOf(predicate) | sum of the modified collection items (3) |
|
| sumOf(f) | sum of the modified collection items (3) |
|
||||||
| sorted() | return [List] with collection items sorted naturally |
|
| sorted() | return [List] with collection items sorted naturally |
|
||||||
| sortedWith(comparator) | sort using a comparator that compares elements (1) |
|
| sortedWith(comparator) | sort using a comparator that compares elements (1) |
|
||||||
| sortedBy(predicate) | sort by comparing results of the predicate function |
|
| sortedBy(predicate) | sort by comparing results of the predicate function |
|
||||||
| joinToString(s,t) | convert iterable to string, see (2) |
|
| joinToString(s,t) | convert iterable to string, see (2) |
|
||||||
| reversed() | create a list containing items from this in reverse order |
|
| reversed() | create a list containing items from this in reverse order |
|
||||||
| shuffled() | create a listof shiffled elements |
|
| shuffled() | create a list of shuffled elements |
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
: throws `NoSuchElementException` if there is no such element
|
:: throws `NoSuchElementException` if there is no such element
|
||||||
|
|
||||||
(2)
|
(2)
|
||||||
: `joinToString(suffix=" ",transform=null)`: suffix is inserted between items if there are more than one, trasnfom is
|
:: `joinToString(separator=" ", transformer=null)`: separator is inserted between items if there are more than one, transformer is
|
||||||
optional function applied to each item that must return result string for an item, otherwise `item.toString()` is used.
|
optional function applied to each item that must return result string for an item, otherwise `item.toString()` is used.
|
||||||
|
|
||||||
(3)
|
(3)
|
||||||
: sum of empty collection is `null`
|
:: sum of empty collection is `null`
|
||||||
|
|
||||||
fun Iterable.toList(): List
|
|
||||||
fun Iterable.toSet(): Set
|
|
||||||
fun Iterable.indexOf(element): Int
|
|
||||||
fun Iterable.contains(element): Bool
|
|
||||||
fun Iterable.isEmpty(element): Bool
|
|
||||||
fun Iterable.forEach(block: (Any?)->Void ): Void
|
|
||||||
fun Iterable.map(block: (Any?)->Void ): List
|
|
||||||
fun Iterable.associateBy( keyMaker: (Any?)->Any): Map
|
|
||||||
|
|
||||||
## Abstract methods:
|
## Abstract methods:
|
||||||
|
|
||||||
fun iterator(): Iterator
|
fun iterator(): Iterator
|
||||||
|
|
||||||
Creates a list by iterating to the end. So, the Iterator should be finite to be used with it.
|
For high-performance Kotlin-side interop and custom iterable implementation details, see [Efficient Iterables in Kotlin Interop](EfficientIterables.md).
|
||||||
|
|
||||||
## Included in interfaces:
|
## Included in interfaces:
|
||||||
|
|
||||||
@@ -148,6 +212,8 @@ Creates a list by iterating to the end. So, the Iterator should be finite to be
|
|||||||
|
|
||||||
[List]: List.md
|
[List]: List.md
|
||||||
|
|
||||||
|
[Flow]: parallelism.md#flow
|
||||||
|
|
||||||
[Range]: Range.md
|
[Range]: Range.md
|
||||||
|
|
||||||
[Set]: Set.md
|
[Set]: Set.md
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ must throw `ObjIterationFinishedError`.
|
|||||||
|
|
||||||
Iterators are returned when implementing [Iterable] interface.
|
Iterators are returned when implementing [Iterable] interface.
|
||||||
|
|
||||||
|
For high-performance Kotlin-side interop and custom iterable implementation details, see [Efficient Iterables in Kotlin Interop](EfficientIterables.md).
|
||||||
|
|
||||||
## Implemented for classes:
|
## Implemented for classes:
|
||||||
|
|
||||||
- [List], [Range]
|
- [List], [Range]
|
||||||
|
|||||||
+30
-1
@@ -92,6 +92,34 @@ Open end ranges remove head and tail elements:
|
|||||||
assert( [1, 2, 3] !== [1, 2, 3])
|
assert( [1, 2, 3] !== [1, 2, 3])
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
## Destructuring
|
||||||
|
|
||||||
|
Lists can be used as L-values for destructuring assignments. This allows you to unpack list elements into multiple variables.
|
||||||
|
|
||||||
|
### Basic Destructuring
|
||||||
|
```lyng
|
||||||
|
val [a, b, c] = [1, 2, 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Splats (Variadic)
|
||||||
|
A single ellipsis `...` can be used to capture remaining elements into a list. It can be placed at the beginning, middle, or end of the pattern.
|
||||||
|
```lyng
|
||||||
|
val [head, rest...] = [1, 2, 3] // head=1, rest=[2, 3]
|
||||||
|
val [first, middle..., last] = [1, 2, 3, 4, 5] // first=1, middle=[2, 3, 4], last=5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Patterns
|
||||||
|
Destructuring patterns can be nested to unpack multi-dimensional lists.
|
||||||
|
```lyng
|
||||||
|
val [a, [b, c...], d] = [1, [2, 3, 4], 5]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reassignment
|
||||||
|
Destructuring can also be used to reassign existing variables:
|
||||||
|
```lyng
|
||||||
|
[x, y] = [y, x] // Swap values
|
||||||
|
```
|
||||||
|
|
||||||
## In-place sort
|
## In-place sort
|
||||||
|
|
||||||
List could be sorted in place, just like [Collection] provide sorted copies, in a very like way:
|
List could be sorted in place, just like [Collection] provide sorted copies, in a very like way:
|
||||||
@@ -130,7 +158,8 @@ List could be sorted in place, just like [Collection] provide sorted copies, in
|
|||||||
| `sort()` | in-place sort, natural order | void |
|
| `sort()` | in-place sort, natural order | void |
|
||||||
| `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | void |
|
| `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | void |
|
||||||
| `sortWith(comparator)` | in-place sort using `comarator` function (4) | void |
|
| `sortWith(comparator)` | in-place sort using `comarator` function (4) | void |
|
||||||
| `shiffle()` | in-place shiffle contents | |
|
| `shuffle()` | in-place shuffle contents | |
|
||||||
|
| `toString()` | string representation like `[a,b,c]` | |
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
: optimized implementation that override `Array` one
|
: optimized implementation that override `Array` one
|
||||||
|
|||||||
+2
-2
@@ -172,7 +172,7 @@ Maps and entries can also be merged with `+` and `+=`:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Map literals always use string keys (identifier keys are converted to strings).
|
- Map literals always use string keys (identifier keys are converted to strings).
|
||||||
- Spreads inside map literals and `+`/`+=` merges require string keys on the right-hand side; this aligns with named-argument splats.
|
- Spreads inside map literals and `+`/`+=` merges allow any objects as keys.
|
||||||
- When you need computed or non-string keys, use the constructor form `Map(...)` or build entries with `=>` and then merge.
|
- When you need computed or non-string keys, use the constructor form `Map(...)`, map literals with computed keys (if supported), or build entries with `=>` and then merge.
|
||||||
|
|
||||||
[Collection](Collection.md)
|
[Collection](Collection.md)
|
||||||
+468
-16
@@ -42,6 +42,176 @@ a _constructor_ that requires two parameters for fields. So when creating it wit
|
|||||||
Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the
|
Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the
|
||||||
example above.
|
example above.
|
||||||
|
|
||||||
|
## Singleton Objects
|
||||||
|
|
||||||
|
Singleton objects are declared using the `object` keyword. An `object` declaration defines both a class and a single instance of that class at the same time. This is perfect for stateless utilities, global configuration, or shared delegates.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object Config {
|
||||||
|
val version = "1.0.0"
|
||||||
|
val debug = true
|
||||||
|
|
||||||
|
fun printInfo() {
|
||||||
|
println("App version: " + version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
println(Config.version)
|
||||||
|
Config.printInfo()
|
||||||
|
```
|
||||||
|
|
||||||
|
Objects can also inherit from classes or interfaces:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object DefaultLogger : Logger("Default") {
|
||||||
|
override fun log(msg) {
|
||||||
|
println("[DEFAULT] " + msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Properties allow you to define member accessors that look like fields but execute code when read or written. Unlike regular fields, properties in Lyng do **not** have automatic backing fields; they are pure accessors.
|
||||||
|
|
||||||
|
### Basic Syntax
|
||||||
|
|
||||||
|
Properties are declared using `val` (read-only) or `var` (read-write) followed by a name and `get()`/`set()` blocks:
|
||||||
|
|
||||||
|
```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(value) {
|
||||||
|
if (value >= 0) _age = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val p = Person(15)
|
||||||
|
assertEquals("Minor", p.ageCategory)
|
||||||
|
p.age = 20
|
||||||
|
assertEquals("Adult", p.ageCategory)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Laconic Expression Shorthand
|
||||||
|
|
||||||
|
For simple accessors and methods, you can use the `=` shorthand for a more elegant and laconic form:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Circle(val radius: Real) {
|
||||||
|
val area get() = π * radius * radius
|
||||||
|
val circumference get() = 2 * π * radius
|
||||||
|
|
||||||
|
fun diameter() = radius * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fun median(a, b) = (a + b) / 2
|
||||||
|
|
||||||
|
class Counter {
|
||||||
|
private var _count = 0
|
||||||
|
var count get() = _count set(v) = _count = v
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Rules
|
||||||
|
|
||||||
|
- **`val` properties** must have a `get()` accessor and cannot have a `set()`.
|
||||||
|
- **`var` properties** must have both `get()` and `set()` accessors.
|
||||||
|
- **Functions and methods** can use the `=` shorthand to return the result of a single expression.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Delegation
|
||||||
|
|
||||||
|
Delegation allows you to hand over the logic of a property or function to another object. This is done using the `by` keyword.
|
||||||
|
|
||||||
|
### Property Delegation
|
||||||
|
|
||||||
|
Instead of providing `get()` and `set()` accessors, you can delegate them to an object that implements the `getValue` and `setValue` methods.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class User {
|
||||||
|
var name by MyDelegate()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Delegation
|
||||||
|
|
||||||
|
You can also delegate a whole function to an object. When the function is called, it will invoke the delegate's `invoke` method.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun remoteAction by RemoteProxy("actionName")
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Unified Delegate Interface
|
||||||
|
|
||||||
|
A delegate is any object that provides the following methods (all optional depending on usage):
|
||||||
|
|
||||||
|
- `getValue(thisRef, name)`: Called when a delegated `val` or `var` is read.
|
||||||
|
- `setValue(thisRef, name, newValue)`: Called when a delegated `var` is written.
|
||||||
|
- `invoke(thisRef, name, args...)`: Called when a delegated `fun` is invoked.
|
||||||
|
- `bind(name, access, thisRef)`: Called once during initialization to configure or validate the delegate.
|
||||||
|
|
||||||
|
For more details and advanced patterns (like `lazy`, `observable`, and shared stateless delegates), see the [Delegation Guide](delegation.md).
|
||||||
|
|
||||||
|
## Instance initialization: init block
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
class Point(val x, val y) {
|
||||||
|
var magnitude
|
||||||
|
|
||||||
|
init {
|
||||||
|
magnitude = Math.sqrt(x*x + y*y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Key features of `init` blocks:
|
||||||
|
- **Scope**: They have full access to `this` members and all primary constructor parameters.
|
||||||
|
- **Order**: In a single-inheritance scenario, `init` blocks run immediately after the instance fields are prepared but before the primary constructor body logic.
|
||||||
|
- **Multiple blocks**: You can have multiple `init` blocks; they will be executed in the order they appear in the class body.
|
||||||
|
|
||||||
|
### Initialization in Multiple Inheritance
|
||||||
|
|
||||||
|
In cases of multiple inheritance, `init` blocks are executed following the constructor chaining rule:
|
||||||
|
1. All ancestors are initialized first, following the inheritance hierarchy (diamond-safe: each ancestor is initialized exactly once).
|
||||||
|
2. The `init` blocks of each class are executed after its parents have been fully initialized.
|
||||||
|
3. For a hierarchy `class D : B, C`, the initialization order is: `B`'s chain, then `C`'s chain (skipping common ancestors with `B`), and finally `D`'s own `init` blocks.
|
||||||
|
|
||||||
|
### Initialization during Deserialization
|
||||||
|
|
||||||
|
When an object is restored from a serialized form (e.g., using `Lynon`), `init` blocks are **re-executed**. This ensures that transient state or derived fields are correctly recalculated upon restoration. However, primary constructors are **not** re-called during deserialization; only the `init` blocks and field initializers are executed to restore the instance state.
|
||||||
|
|
||||||
Class point has a _method_, or a _member function_ `length()` that uses its _fields_ `x` and `y` to
|
Class point has a _method_, or a _member function_ `length()` that uses its _fields_ `x` and `y` to
|
||||||
calculate the magnitude. Length is called
|
calculate the magnitude. Length is called
|
||||||
|
|
||||||
@@ -53,8 +223,58 @@ statements discussed later, there could be default values, ellipsis, etc.
|
|||||||
class Point(x=0,y=0)
|
class Point(x=0,y=0)
|
||||||
val p = Point()
|
val p = Point()
|
||||||
assert( p.x == 0 && p.y == 0 )
|
assert( p.x == 0 && p.y == 0 )
|
||||||
|
|
||||||
|
// Named arguments in constructor calls use colon syntax:
|
||||||
|
val p2 = Point(y: 10, x: 5)
|
||||||
|
assert( p2.x == 5 && p2.y == 10 )
|
||||||
|
|
||||||
|
// Auto-substitution shorthand for named arguments:
|
||||||
|
val x = 1
|
||||||
|
val y = 2
|
||||||
|
val p3 = Point(x:, y:)
|
||||||
|
assert( p3.x == 1 && p3.y == 2 )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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
|
||||||
@@ -79,7 +299,7 @@ Functions defined inside a class body are methods, and unless declared
|
|||||||
|
|
||||||
Lyng supports declaring a class with multiple direct base classes. The syntax is:
|
Lyng supports declaring a class with multiple direct base classes. The syntax is:
|
||||||
|
|
||||||
```
|
```lyng
|
||||||
class Foo(val a) {
|
class Foo(val a) {
|
||||||
var tag = "F"
|
var tag = "F"
|
||||||
fun runA() { "ResultA:" + a }
|
fun runA() { "ResultA:" + a }
|
||||||
@@ -128,16 +348,16 @@ Key rules and features:
|
|||||||
|
|
||||||
- Syntax
|
- Syntax
|
||||||
- `class Derived(args) : Base1(b1Args), Base2(b2Args)`
|
- `class Derived(args) : Base1(b1Args), Base2(b2Args)`
|
||||||
- Each direct base may receive constructor arguments specified in the header. Only direct bases receive header args; indirect bases must either be default‑constructible or receive their args through their direct child (future extensions may add more control).
|
- Each direct base may receive constructor arguments specified in the header. Only direct bases receive header args; indirect bases must either be default‑constructible or receive their args through their direct child.
|
||||||
|
|
||||||
- Resolution order (C3 MRO — active)
|
- Resolution order (C3 MRO)
|
||||||
- Member lookup is deterministic and follows C3 linearization (Python‑like), which provides a monotonic, predictable order for complex hierarchies and diamonds.
|
- Member lookup is deterministic and follows C3 linearization (Python‑like), which provides a monotonic, predictable order for complex hierarchies and diamonds.
|
||||||
- Intuition: for `class D() : B(), C()` where `B()` and `C()` both derive from `A()`, the C3 order is `D → B → C → A`.
|
- Intuition: for `class D() : B(), C()` where `B()` and `C()` both derive from `A()`, the C3 order is `D → B → C → A`.
|
||||||
- The first visible match along this order wins.
|
- The first visible match along this order wins.
|
||||||
|
|
||||||
- Qualified dispatch
|
- Qualified dispatch
|
||||||
- Inside a class body, use `this@Type.member(...)` to start lookup at the specified ancestor.
|
- Inside a class body, use `this@Type.member(...)` to start lookup at the specified ancestor.
|
||||||
- For arbitrary receivers, use casts: `(expr as Type).member(...)` or `(expr as? Type)?.member(...)` (safe‑call `?.` is already available in Lyng).
|
- For arbitrary receivers, use casts: `(expr as Type).member(...)` or `(expr as? Type)?.member(...)`.
|
||||||
- Qualified access does not relax visibility.
|
- Qualified access does not relax visibility.
|
||||||
|
|
||||||
- Field inheritance (`val`/`var`) and collisions
|
- Field inheritance (`val`/`var`) and collisions
|
||||||
@@ -154,10 +374,120 @@ Key rules and features:
|
|||||||
- `private`: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via `this@Type` or casts.
|
- `private`: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via `this@Type` or casts.
|
||||||
- `protected`: accessible in the declaring class and in any of its transitive subclasses (including MI), but not from unrelated contexts; qualification/casts do not bypass it.
|
- `protected`: accessible in the declaring class and in any of its transitive subclasses (including MI), but not from unrelated contexts; qualification/casts do not bypass it.
|
||||||
|
|
||||||
- Diagnostics
|
## Abstract Classes and Members
|
||||||
- When a member/field is not found, error messages include the receiver class name and the considered linearization order, with suggestions to disambiguate using `this@Type` or casts if appropriate.
|
|
||||||
- Qualifying with a non‑ancestor in `this@Type` reports a clear error mentioning the receiver lineage.
|
An `abstract` class is a class that cannot be instantiated and is intended to be inherited by other classes. It can contain `abstract` members that have no implementation and must be implemented by concrete subclasses.
|
||||||
- `as`/`as?` cast errors mention the actual and target types.
|
|
||||||
|
### Abstract Classes
|
||||||
|
|
||||||
|
To declare an abstract class, use the `abstract` modifier:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
abstract class Shape {
|
||||||
|
abstract fun area(): Real
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Abstract classes can have constructors, fields, and concrete methods, just like regular classes.
|
||||||
|
|
||||||
|
### Abstract Members
|
||||||
|
|
||||||
|
Methods and variables (`val`/`var`) can be marked as `abstract`. Abstract members must not have a body or initializer.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
abstract class Base {
|
||||||
|
abstract fun foo(): Int
|
||||||
|
abstract var bar: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Safety**: `abstract` members cannot be `private`, as they must be visible to subclasses for implementation.
|
||||||
|
- **Contract of Capability**: An `abstract val/var` represents a requirement for a capability. It can be implemented by either a **field** (storage) or a **property** (logic) in a subclass.
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
An `interface` in Lyng is a synonym for an `abstract class`. Following the principle that Lyng's Multiple Inheritance system is powerful enough to handle stateful contracts, interfaces support everything classes do, including constructors, fields, and `init` blocks.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
interface Named(val name: String) {
|
||||||
|
fun greet() { "Hello, " + name }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Person(name) : Named(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
Using `interface` instead of `abstract class` is a matter of semantic intent, signaling that the class is primarily intended to be used as a contract in MI.
|
||||||
|
|
||||||
|
### Implementation by Parts
|
||||||
|
|
||||||
|
One of the most powerful benefits of Lyng's Multiple Inheritance and C3 MRO is the ability to satisfy an interface's requirements "by parts" from different parent classes. Since an `interface` can have state and requirements, a subclass can inherit these requirements and satisfy them using members inherited from other parents in the MRO chain.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
// Interface with state (id) and abstract requirements
|
||||||
|
interface Character(val id) {
|
||||||
|
var health
|
||||||
|
var mana
|
||||||
|
|
||||||
|
fun isAlive() = health > 0
|
||||||
|
fun status() = name + " (#" + id + "): " + health + " HP, " + mana + " MP"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent class 1: provides health
|
||||||
|
class HealthPool(var health)
|
||||||
|
|
||||||
|
// Parent class 2: provides mana and name
|
||||||
|
class ManaPool(var mana) {
|
||||||
|
val name = "Hero"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite class: implements Character by combining HealthPool and ManaPool
|
||||||
|
class Warrior(id, h, m) : HealthPool(h), ManaPool(m), Character(id)
|
||||||
|
|
||||||
|
val w = Warrior(1, 100, 50)
|
||||||
|
assertEquals("Hero (#1): 100 HP, 50 MP", w.status())
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, `Warrior` inherits from `HealthPool`, `ManaPool`, and `Character`. The abstract requirements `health` and `mana` from `Character` are automatically satisfied by the matching members inherited from `HealthPool` and `ManaPool`. The `status()` method also successfully finds the `name` field from `ManaPool`. This pattern allows for highly modular and reusable "trait-like" classes that can be combined to fulfill complex contracts without boilerplate proxy methods.
|
||||||
|
|
||||||
|
## Overriding and Virtual Dispatch
|
||||||
|
|
||||||
|
When a class defines a member that already exists in one of its parents, it is called **overriding**.
|
||||||
|
|
||||||
|
### The `override` Keyword
|
||||||
|
|
||||||
|
In Lyng, the `override` keyword is **mandatory when declaring a member** that exists in the ancestor chain (MRO).
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Parent {
|
||||||
|
fun foo() = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
class Child : Parent() {
|
||||||
|
override fun foo() = 2 // Mandatory override keyword
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Implicit Satisfaction**: If a class inherits an abstract requirement and a matching implementation from different parents, the requirement is satisfied automatically without needing an explicit `override` proxy.
|
||||||
|
- **No Accidental Overrides**: If you define a member that happens to match a parent's member but you didn't use `override`, the compiler will throw an error. This prevents the "Fragile Base Class" problem.
|
||||||
|
- **Private Members**: Private members in parent classes are NOT part of the virtual interface and cannot be overridden. Defining a member with the same name in a subclass is allowed without `override` and is treated as a new, independent member.
|
||||||
|
|
||||||
|
### Visibility Widening
|
||||||
|
|
||||||
|
A subclass can increase the visibility of an overridden member (e.g., `protected` → `public`), but it is strictly forbidden from narrowing it (e.g., `public` → `protected`).
|
||||||
|
|
||||||
|
### The `closed` Modifier
|
||||||
|
|
||||||
|
To prevent a member from being overridden in subclasses, use the `closed` modifier (equivalent to `final` in other languages).
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Critical {
|
||||||
|
closed fun secureStep() { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Attempting to override a `closed` member results in a compile-time error.
|
||||||
|
|
||||||
Compatibility notes:
|
Compatibility notes:
|
||||||
|
|
||||||
@@ -274,6 +604,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
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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 IllegalAccessException
|
||||||
|
c.increment() // OK
|
||||||
|
```
|
||||||
|
|
||||||
|
#### On Properties
|
||||||
|
|
||||||
|
You can also apply restricted visibility to custom property setters:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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 IllegalAccessException: 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_:
|
||||||
@@ -393,9 +786,11 @@ As usual, private statics are not accessible from the outside:
|
|||||||
|
|
||||||
# Extending classes
|
# Extending classes
|
||||||
|
|
||||||
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method
|
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension members_ could be used.
|
||||||
that would test if some object of unknown type contains something that can be interpreted
|
|
||||||
as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type:
|
## Extension methods
|
||||||
|
|
||||||
|
For example, we want to create an extension method that would test if some object of unknown type contains something that can be interpreted as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type:
|
||||||
|
|
||||||
fun Object.isInteger() {
|
fun Object.isInteger() {
|
||||||
when(this) {
|
when(this) {
|
||||||
@@ -420,10 +815,67 @@ as an integer. In this case we _extend_ class `Object`, as it is the parent clas
|
|||||||
assert( ! "5.2".isInteger() )
|
assert( ! "5.2".isInteger() )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
__Important note__ as for version 0.6.9, extensions are in __global scope__. It means, that once applied to a global type (Int in our sample), they will be available for _all_ contexts, even new created,
|
## Extension properties
|
||||||
as they are modifying the type, not the context.
|
|
||||||
|
|
||||||
Beware of it. We might need to reconsider it later.
|
Just like methods, you can extend existing classes with properties. These can be defined using simple initialization (for `val` only) or with custom accessors.
|
||||||
|
|
||||||
|
### Simple val extension
|
||||||
|
|
||||||
|
A read-only extension can be defined by assigning an expression:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
val String.isLong = length > 10
|
||||||
|
|
||||||
|
val s = "Hello, world!"
|
||||||
|
assert(s.isLong)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties with accessors
|
||||||
|
|
||||||
|
For more complex logic, use `get()` and `set()` blocks:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Box(var value: Int)
|
||||||
|
|
||||||
|
var Box.doubledValue
|
||||||
|
get() = value * 2
|
||||||
|
set(v) = value = v / 2
|
||||||
|
|
||||||
|
val b = Box(10)
|
||||||
|
assertEquals(20, b.doubledValue)
|
||||||
|
b.doubledValue = 30
|
||||||
|
assertEquals(15, b.value)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extension members are strictly barred from accessing private members of the class they extend, maintaining encapsulation.
|
||||||
|
|
||||||
|
### Extension Scoping and Isolation
|
||||||
|
|
||||||
|
Extensions in Lyng are **scope-isolated**. This means an extension is only visible within the scope where it is defined and its child scopes. This reduces the "attack surface" and prevents extensions from polluting the global space or other modules.
|
||||||
|
|
||||||
|
#### Scope Isolation Example
|
||||||
|
|
||||||
|
You can define different extensions with the same name in different scopes:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun scopeA() {
|
||||||
|
val Int.description = "Number: " + toString()
|
||||||
|
assertEquals("Number: 42", 42.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scopeB() {
|
||||||
|
val Int.description = "Value: " + toString()
|
||||||
|
assertEquals("Value: 42", 42.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeA()
|
||||||
|
scopeB()
|
||||||
|
|
||||||
|
// Outside those scopes, Int.description is not defined
|
||||||
|
assertThrows { 42.description }
|
||||||
|
```
|
||||||
|
|
||||||
|
This isolation ensures that libraries can use extensions internally without worrying about name collisions with other libraries or the user's code. When a module is imported using `use`, its top-level extensions become available in the importing scope.
|
||||||
|
|
||||||
## dynamic symbols
|
## dynamic symbols
|
||||||
|
|
||||||
@@ -542,12 +994,12 @@ request](https://gitea.sergeych.net/SergeychWorks/lyng/issues).
|
|||||||
- ObjClass sole parent is Obj
|
- ObjClass sole parent is Obj
|
||||||
- ObjClass contains code for instance methods, class fields, hierarchy information.
|
- ObjClass contains code for instance methods, class fields, hierarchy information.
|
||||||
- Class information is also scoped.
|
- Class information is also scoped.
|
||||||
- We acoid imported classes duplication using packages and import caching, so the same imported module is the same object in all its classes.
|
- We avoid imported classes duplication using packages and import caching, so the same imported module is the same object in all its classes.
|
||||||
|
|
||||||
## Instances
|
## Instances
|
||||||
|
|
||||||
Result of executing of any expression or statement in the Lyng is the object that
|
Result of executing of any expression or statement in the Lyng is the object that
|
||||||
inherits `Obj`, but is not `Obj`. For example it could be Int, void, null, real, string, bool, etc.
|
inherits `Obj`, but is not `Obj`. For example, it could be Int, void, null, real, string, bool, etc.
|
||||||
|
|
||||||
This means whatever expression returns or the variable holds, is the first-class
|
This means whatever expression returns or the variable holds, is the first-class
|
||||||
object, no differenes. For example:
|
object, no differenes. For example:
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
# Testing and Assertions
|
||||||
|
|
||||||
|
Lyng provides several built-in functions for testing and verifying code behavior. These are available in all scripts.
|
||||||
|
|
||||||
|
## Basic Assertions
|
||||||
|
|
||||||
|
### `assert`
|
||||||
|
|
||||||
|
Assert that a condition is true.
|
||||||
|
|
||||||
|
assert(condition, message=null)
|
||||||
|
|
||||||
|
- `condition`: A boolean expression.
|
||||||
|
- `message` (optional): A string message to include in the exception if the assertion fails.
|
||||||
|
|
||||||
|
If the condition is false, it throws an `AssertionFailedException`.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
assert(1 + 1 == 2)
|
||||||
|
assert(true, "This should be true")
|
||||||
|
```
|
||||||
|
|
||||||
|
### `assertEquals` and `assertEqual`
|
||||||
|
|
||||||
|
Assert that two values are equal. `assertEqual` is an alias for `assertEquals`.
|
||||||
|
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
assertEqual(expected, actual)
|
||||||
|
|
||||||
|
If `expected != actual`, it throws an `AssertionFailedException` with a message showing both values.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
assertEquals(4, 2 * 2)
|
||||||
|
assertEqual("hello", "hel" + "lo")
|
||||||
|
```
|
||||||
|
|
||||||
|
### `assertNotEquals`
|
||||||
|
|
||||||
|
Assert that two values are not equal.
|
||||||
|
|
||||||
|
assertNotEquals(unexpected, actual)
|
||||||
|
|
||||||
|
If `unexpected == actual`, it throws an `AssertionFailedException`.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
assertNotEquals(5, 2 * 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exception Testing
|
||||||
|
|
||||||
|
### `assertThrows`
|
||||||
|
|
||||||
|
Assert that a block of code throws an exception.
|
||||||
|
|
||||||
|
assertThrows(code)
|
||||||
|
assertThrows(expectedExceptionClass, code)
|
||||||
|
|
||||||
|
- `expectedExceptionClass` (optional): The class of the exception that is expected to be thrown.
|
||||||
|
- `code`: A lambda block or statement to execute.
|
||||||
|
|
||||||
|
If the code does not throw an exception, an `AssertionFailedException` is raised.
|
||||||
|
If an `expectedExceptionClass` is provided, the thrown exception must be of that class (or its subclass), otherwise an error is raised.
|
||||||
|
|
||||||
|
`assertThrows` returns the caught exception object if successful.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
// Just assert that something is thrown
|
||||||
|
assertThrows { 1 / 0 }
|
||||||
|
|
||||||
|
// Assert that a specific exception class is thrown
|
||||||
|
assertThrows(NoSuchElementException) {
|
||||||
|
[1, 2, 3].findFirst { it > 10 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can use the returned exception
|
||||||
|
val ex = assertThrows { throw Exception("custom error") }
|
||||||
|
assertEquals("custom error", ex.message)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Other Validation Functions
|
||||||
|
|
||||||
|
While not strictly for testing, these functions help in defensive programming:
|
||||||
|
|
||||||
|
### `require`
|
||||||
|
|
||||||
|
require(condition, message="requirement not met")
|
||||||
|
|
||||||
|
Throws an `IllegalArgumentException` if the condition is false. Use this for validating function arguments.
|
||||||
|
|
||||||
|
If we want to evaluate the message lazily:
|
||||||
|
|
||||||
|
require(condition) { "requirement not met: %s"(someData) }
|
||||||
|
|
||||||
|
In this case, formatting will only occur if the condition is not met.
|
||||||
|
|
||||||
|
### `check`
|
||||||
|
|
||||||
|
check(condition, message="check failed")
|
||||||
|
|
||||||
|
Throws an `IllegalStateException` if the condition is false. Use this for validating internal state.
|
||||||
|
|
||||||
|
With lazy message evaluation:
|
||||||
|
|
||||||
|
check(condition) { "check failed: %s"(someData) }
|
||||||
|
|
||||||
|
In this case, formatting will only occur if the condition is not met.
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
|
||||||
|
It is easy to mark some code and make it throw a special exception at cone with:
|
||||||
|
|
||||||
|
TODO()
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
TODO("some message")
|
||||||
|
|
||||||
|
It raises an `NotImplementedException` with the given message. You can catch it
|
||||||
|
as any other exception when necessary.
|
||||||
|
|
||||||
|
Many IDE and editors have built-in support for marking code with TODOs.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# AI notes: avoid Kotlin/Wasm invalid IR with suspend lambdas
|
||||||
|
|
||||||
|
## Do
|
||||||
|
- Prefer explicit `object : Statement()` with `override suspend fun execute(...)` when building compiler statements.
|
||||||
|
- Keep `Statement` objects non-lambda, especially in compiler hot paths like parsing/var declarations.
|
||||||
|
- If you need conditional behavior, return early in `execute` instead of wrapping `parseExpression()` with `statement(...) { ... }`.
|
||||||
|
- When wasmJs tests hang in the browser, first check `wasmJsNodeTest` for a compile error; hangs often mean module instantiation failed.
|
||||||
|
|
||||||
|
## Don't
|
||||||
|
- Do not create suspend lambdas inside `Statement` factories (`statement { ... }`) for wasm targets.
|
||||||
|
- Do not "fix" hangs by increasing browser timeouts; it masks invalid wasm generation.
|
||||||
|
|
||||||
|
## Debugging tips
|
||||||
|
- Look for `$invokeCOROUTINE$` in wasm function names when mapping failures.
|
||||||
|
- If node test logs a wasm compile error, the browser hang is likely the same root cause.
|
||||||
+34
-14
@@ -75,6 +75,13 @@ destructuring arrays when calling functions and lambdas:
|
|||||||
getFirstAndLast( ...(1..10) ) // see "splats" section below
|
getFirstAndLast( ...(1..10) ) // see "splats" section below
|
||||||
>>> [1,10]
|
>>> [1,10]
|
||||||
|
|
||||||
|
Note that array destructuring can also be used in assignments:
|
||||||
|
|
||||||
|
val [first, middle..., last] = [1, 2, 3, 4, 5]
|
||||||
|
[x, y] = [y, x] // Swap
|
||||||
|
|
||||||
|
See [tutorial] and [List] documentation for more details on destructuring assignments.
|
||||||
|
|
||||||
# Splats
|
# Splats
|
||||||
|
|
||||||
Ellipsis allows to convert argument lists to lists. The inversa algorithm that converts [List],
|
Ellipsis allows to convert argument lists to lists. The inversa algorithm that converts [List],
|
||||||
@@ -100,41 +107,53 @@ There could be any number of splats at any positions. You can splat any other [I
|
|||||||
|
|
||||||
## Named arguments in calls
|
## Named arguments in calls
|
||||||
|
|
||||||
Lyng supports named arguments at call sites using colon syntax `name: value`:
|
Lyng supports named arguments at call sites using colon syntax `name: value`.
|
||||||
|
|
||||||
|
### Shorthand for Named Arguments
|
||||||
|
|
||||||
|
If you want to pass a variable as a named argument and the variable has the same name as the parameter, you can omit the value and use the shorthand `name:`. This is highly readable and matches the shorthand for map literals.
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
fun test(a="foo", b="bar", c="bazz") { [a, b, c] }
|
fun test(a, b, c) { [a, b, c] }
|
||||||
|
|
||||||
assertEquals(["foo", "b", "bazz"], test(b: "b"))
|
val a = 1
|
||||||
assertEquals(["a", "bar", "c"], test("a", c: "c"))
|
val b = 2
|
||||||
|
val c = 3
|
||||||
|
|
||||||
|
// Explicit:
|
||||||
|
assertEquals([1, 2, 3], test(a: a, b: b, c: c))
|
||||||
|
|
||||||
|
// Shorthand (preferred):
|
||||||
|
assertEquals([1, 2, 3], test(a:, b:, c:))
|
||||||
```
|
```
|
||||||
|
|
||||||
Rules:
|
This shorthand is elegant, reduces boilerplate, and is consistent with Lyng's map literal syntax. It works for both function calls and class constructors.
|
||||||
|
|
||||||
|
Rules for named arguments:
|
||||||
|
|
||||||
- Named arguments must follow positional arguments. After the first named argument, no positional arguments may appear inside the parentheses.
|
- Named arguments must follow positional arguments. After the first named argument, no positional arguments may appear inside the parentheses.
|
||||||
- The only exception is the syntactic trailing block after the call: `f(args) { ... }`. This block is outside the parentheses and is handled specially (see below).
|
- The only exception is the syntactic trailing block after the call: `f(args) { ... }`. This block is outside the parentheses and is handled specially (see below).
|
||||||
- A named argument cannot reassign a parameter already set positionally.
|
- A named argument cannot reassign a parameter already set positionally.
|
||||||
- If the last parameter has already been assigned by a named argument (or named splat), a trailing block is not allowed and results in an error.
|
- If the last parameter has already been assigned by a named argument (or named splat), a trailing block is not allowed and results in an error.
|
||||||
|
|
||||||
Why `:` and not `=` at call sites? In Lyng, `=` is an expression (assignment), so we use `:` to avoid ambiguity. Declarations continue to use `:` for types, while call sites use `as` / `as?` for type operations.
|
Why `:` and not `=` at call sites? In Lyng, `=` is an expression (assignment), so we use `:` to avoid ambiguity. This is a key difference from **Kotlin**, which uses `=` for named arguments. Declarations in Lyng continue to use `:` for types, while call sites use `as` / `as?` for type operations.
|
||||||
|
|
||||||
## Named splats (map splats)
|
## Named splats (map splats)
|
||||||
|
|
||||||
Splat (`...`) of a Map provides named arguments to the call. Only string keys are allowed:
|
Splat (`...`) of a Map provides named arguments to the call. Only string keys are allowed. You can use the same auto-substitution shorthand inside map literals used for splats:
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
|
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
|
||||||
val r = test("A?", ...Map("d" => "D!", "b" => "B!"))
|
|
||||||
assertEquals(["A?","B!","c","D!"], r)
|
|
||||||
```
|
|
||||||
|
|
||||||
The same with a map literal is often more concise. Define the literal, then splat the variable:
|
val b = "B!"
|
||||||
|
val d = "D!"
|
||||||
|
|
||||||
|
// Auto-substitution in map literal:
|
||||||
|
val patch = { d:, b: }
|
||||||
|
|
||||||
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
|
|
||||||
val patch = { d: "D!", b: "B!" }
|
|
||||||
val r = test("A?", ...patch)
|
val r = test("A?", ...patch)
|
||||||
assertEquals(["A?","B!","c","D!"], r)
|
assertEquals(["A?","B!","c","D!"], r)
|
||||||
>>> void
|
```
|
||||||
|
|
||||||
Constraints:
|
Constraints:
|
||||||
|
|
||||||
@@ -155,3 +174,4 @@ If a call is immediately followed by a block `{ ... }`, it is treated as an extr
|
|||||||
|
|
||||||
|
|
||||||
[tutorial]: tutorial.md
|
[tutorial]: tutorial.md
|
||||||
|
[List]: List.md
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# Delegation in Lyng
|
||||||
|
|
||||||
|
Delegation is a powerful pattern that allows you to outsource the logic of properties (`val`, `var`) and functions (`fun`) to another object. This enables code reuse, separation of concerns, and the implementation of common patterns like lazy initialization, observable properties, and remote procedure calls (RPC) with minimal boilerplate.
|
||||||
|
|
||||||
|
## The `by` Keyword
|
||||||
|
|
||||||
|
Delegation is triggered using the `by` keyword in a declaration. The expression following `by` is evaluated once when the member is initialized, and the resulting object becomes the **delegate**.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
val x by MyDelegate()
|
||||||
|
var y by MyDelegate()
|
||||||
|
fun f by MyDelegate()
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Unified Delegate Model
|
||||||
|
|
||||||
|
A delegate object can implement any of the following methods to intercept member access. All methods receive the `thisRef` (the instance containing the member) and the `name` of the member.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
interface Delegate {
|
||||||
|
// Called when a 'val' or 'var' is read
|
||||||
|
fun getValue(thisRef, name)
|
||||||
|
|
||||||
|
// Called when a 'var' is assigned
|
||||||
|
fun setValue(thisRef, name, newValue)
|
||||||
|
|
||||||
|
// Called when a 'fun' is invoked
|
||||||
|
fun invoke(thisRef, name, args...)
|
||||||
|
|
||||||
|
// Optional: Called once during initialization to "bind" the delegate
|
||||||
|
// Can be used for validation or to return a different delegate instance
|
||||||
|
fun bind(name, access, thisRef) = this
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delegate Access Types
|
||||||
|
|
||||||
|
The `bind` method receives an `access` parameter of type `DelegateAccess`, which can be one of:
|
||||||
|
- `DelegateAccess.Val`
|
||||||
|
- `DelegateAccess.Var`
|
||||||
|
- `DelegateAccess.Callable` (for `fun`)
|
||||||
|
|
||||||
|
## Usage Cases and Examples
|
||||||
|
|
||||||
|
### 1. Lazy Initialization
|
||||||
|
|
||||||
|
The classic `lazy` pattern ensures a value is computed only when first accessed and then cached. In Lyng, `lazy` is implemented as a class that follows this pattern. While classes typically start with an uppercase letter, `lazy` is an exception to make its usage feel like a native language feature.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class lazy(val creator) : Delegate {
|
||||||
|
private var value = Unset
|
||||||
|
|
||||||
|
override fun bind(name, access, thisRef) {
|
||||||
|
if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef, name) {
|
||||||
|
if (value == Unset) {
|
||||||
|
value = creator()
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
val expensiveData by lazy {
|
||||||
|
println("Performing expensive computation...")
|
||||||
|
42
|
||||||
|
}
|
||||||
|
|
||||||
|
println(expensiveData) // Computes and prints 42
|
||||||
|
println(expensiveData) // Returns 42 immediately
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Observable Properties
|
||||||
|
|
||||||
|
Delegates can be used to react to property changes.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Observable(initialValue, val onChange) {
|
||||||
|
private var value = initialValue
|
||||||
|
|
||||||
|
fun getValue(thisRef, name) = value
|
||||||
|
|
||||||
|
fun setValue(thisRef, name, newValue) {
|
||||||
|
val oldValue = value
|
||||||
|
value = newValue
|
||||||
|
onChange(name, oldValue, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
var name by Observable("Guest") { name, old, new ->
|
||||||
|
println("Property %s changed from %s to %s"(name, old, new))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val u = User()
|
||||||
|
u.name = "Alice" // Prints: Property name changed from Guest to Alice
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Function Delegation (Proxies)
|
||||||
|
|
||||||
|
You can delegate an entire function to an object. This is particularly useful for implementing decorators or RPC clients.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object LoggerDelegate {
|
||||||
|
fun invoke(thisRef, name, args...) {
|
||||||
|
println("Calling function: " + name + " with args: " + args)
|
||||||
|
// Logic here...
|
||||||
|
"Result of " + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remoteAction by LoggerDelegate
|
||||||
|
|
||||||
|
println(remoteAction(1, 2, 3))
|
||||||
|
// Prints: Calling function: remoteAction with args: [1, 2, 3]
|
||||||
|
// Prints: Result of remoteAction
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Stateless Delegates (Shared Singletons)
|
||||||
|
|
||||||
|
Because `getValue`, `setValue`, and `invoke` receive `thisRef`, a single object can act as a delegate for multiple properties across many instances without any per-property memory overhead.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object Constant42 {
|
||||||
|
fun getValue(thisRef, name) = 42
|
||||||
|
}
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
val a by Constant42
|
||||||
|
val b by Constant42
|
||||||
|
}
|
||||||
|
|
||||||
|
val f = Foo()
|
||||||
|
assertEquals(42, f.a)
|
||||||
|
assertEquals(42, f.b)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Local Delegation
|
||||||
|
|
||||||
|
Delegation is not limited to class members; you can also use it for local variables inside functions.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun test() {
|
||||||
|
val x by LocalProxy(123)
|
||||||
|
println(x)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `bind` Hook
|
||||||
|
|
||||||
|
The `bind(name, access, thisRef)` method is called exactly once when the member is being initialized. It allows the delegate to:
|
||||||
|
1. **Validate usage**: Throw an error if the delegate is used with the wrong member type (e.g., `lazy` on a `var`).
|
||||||
|
2. **Initialize state**: Set up internal state based on the property name or the containing instance.
|
||||||
|
3. **Substitute itself**: Return a different object that will act as the actual delegate.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class ValidatedDelegate() {
|
||||||
|
fun bind(name, access, thisRef) {
|
||||||
|
if (access == DelegateAccess.Var) {
|
||||||
|
throw "This delegate cannot be used with 'var'"
|
||||||
|
}
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getValue(thisRef, name) = "Validated"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Delegation in Lyng combines the elegance of Kotlin-style properties with the flexibility of dynamic function interception. By unifying `val`, `var`, and `fun` delegation into a single model, Lyng provides a consistent and powerful tool for meta-programming and code reuse.
|
||||||
+60
-5
@@ -109,7 +109,62 @@ scope.eval("val y = inc(41); log('Answer:', y)")
|
|||||||
|
|
||||||
You can register multiple names (aliases) at once: `addFn<ObjInt>("inc", "increment") { ... }`.
|
You can register multiple names (aliases) at once: `addFn<ObjInt>("inc", "increment") { ... }`.
|
||||||
|
|
||||||
### 5) Read variable values back in Kotlin
|
### 5) Add Kotlin‑backed fields
|
||||||
|
|
||||||
|
If you need a simple field (with a value) instead of a computed property, use `createField`. This adds a field to the class that will be present in all its instances.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val myClass = ObjClass("MyClass")
|
||||||
|
|
||||||
|
// Add a read-only field (constant)
|
||||||
|
myClass.createField("version", ObjString("1.0.0"), isMutable = false)
|
||||||
|
|
||||||
|
// Add a mutable field with an initial value
|
||||||
|
myClass.createField("count", ObjInt(0), isMutable = true)
|
||||||
|
|
||||||
|
scope.addConst("MyClass", myClass)
|
||||||
|
```
|
||||||
|
|
||||||
|
In Lyng:
|
||||||
|
```lyng
|
||||||
|
val instance = MyClass()
|
||||||
|
println(instance.version) // -> "1.0.0"
|
||||||
|
instance.count = 5
|
||||||
|
println(instance.count) // -> 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6) Add Kotlin‑backed properties
|
||||||
|
|
||||||
|
Properties in Lyng are pure accessors (getters and setters) and do not have automatic backing fields. You can add them to a class using `addProperty`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val myClass = ObjClass("MyClass")
|
||||||
|
var internalValue: Long = 10
|
||||||
|
|
||||||
|
myClass.addProperty(
|
||||||
|
name = "value",
|
||||||
|
getter = {
|
||||||
|
// Return current value as a Lyng object
|
||||||
|
ObjInt(internalValue)
|
||||||
|
},
|
||||||
|
setter = { newValue ->
|
||||||
|
// newValue is passed as a Lyng object (the first and only argument)
|
||||||
|
internalValue = (newValue as ObjInt).value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
scope.addConst("MyClass", myClass)
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage in Lyng:
|
||||||
|
```lyng
|
||||||
|
val instance = MyClass()
|
||||||
|
println(instance.value) // -> 10
|
||||||
|
instance.value = 42
|
||||||
|
println(instance.value) // -> 42
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7) Read variable values back in Kotlin
|
||||||
|
|
||||||
The simplest approach: evaluate an expression that yields the value and convert it.
|
The simplest approach: evaluate an expression that yields the value and convert it.
|
||||||
|
|
||||||
@@ -124,7 +179,7 @@ val kotlinName = scope.eval("name").toKotlin(scope) // -> "Lyng rocks!"
|
|||||||
|
|
||||||
Advanced: you can also grab a variable record directly via `scope.get(name)` and work with its `Obj` value, but evaluating `"name"` is often clearer and enforces Lyng semantics consistently.
|
Advanced: you can also grab a variable record directly via `scope.get(name)` and work with its `Obj` value, but evaluating `"name"` is often clearer and enforces Lyng semantics consistently.
|
||||||
|
|
||||||
### 6) Execute scripts with parameters; call Lyng functions from Kotlin
|
### 8) Execute scripts with parameters; call Lyng functions from Kotlin
|
||||||
|
|
||||||
There are two convenient patterns.
|
There are two convenient patterns.
|
||||||
|
|
||||||
@@ -157,7 +212,7 @@ val result = resultObj.toKotlin(scope) // -> 42
|
|||||||
|
|
||||||
If you need to pass complex data (lists, maps), construct the corresponding Lyng `Obj` types (`ObjList`, `ObjMap`, etc.) and pass them in `Arguments`.
|
If you need to pass complex data (lists, maps), construct the corresponding Lyng `Obj` types (`ObjList`, `ObjMap`, etc.) and pass them in `Arguments`.
|
||||||
|
|
||||||
### 7) Create your own packages and import them in Lyng
|
### 9) Create your own packages and import them in Lyng
|
||||||
|
|
||||||
Lyng supports packages that are imported from scripts. You can register packages programmatically via `ImportManager` or by providing source texts that declare `package ...`.
|
Lyng supports packages that are imported from scripts. You can register packages programmatically via `ImportManager` or by providing source texts that declare `package ...`.
|
||||||
|
|
||||||
@@ -212,7 +267,7 @@ val s = scope.eval("s").toKotlin(scope) // -> 144
|
|||||||
|
|
||||||
You can also register from parsed `Source` instances via `addSourcePackages(source)`.
|
You can also register from parsed `Source` instances via `addSourcePackages(source)`.
|
||||||
|
|
||||||
### 8) Executing from files, security, and isolation
|
### 10) Executing from files, security, and isolation
|
||||||
|
|
||||||
- To run code from a file, read it and pass to `scope.eval(text)` or compile with `Compiler.compile(Source(fileName, text))`.
|
- To run code from a file, read it and pass to `scope.eval(text)` or compile with `Compiler.compile(Source(fileName, text))`.
|
||||||
- `ImportManager` takes an optional `SecurityManager` if you need to restrict what packages or operations are available. By default, `Script.defaultImportManager` allows everything suitable for embedded use; clamp it down in sandboxed environments.
|
- `ImportManager` takes an optional `SecurityManager` if you need to restrict what packages or operations are available. By default, `Script.defaultImportManager` allows everything suitable for embedded use; clamp it down in sandboxed environments.
|
||||||
@@ -223,7 +278,7 @@ You can also register from parsed `Source` instances via `addSourcePackages(sour
|
|||||||
val isolated = net.sergeych.lyng.Scope.new()
|
val isolated = net.sergeych.lyng.Scope.new()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9) Tips and troubleshooting
|
### 11) Tips and troubleshooting
|
||||||
|
|
||||||
- All values that cross the boundary must be Lyng `Obj` instances. Convert Kotlin values explicitly (e.g., `ObjInt`, `ObjReal`, `ObjString`).
|
- All values that cross the boundary must be Lyng `Obj` instances. Convert Kotlin values explicitly (e.g., `ObjInt`, `ObjReal`, `ObjString`).
|
||||||
- Use `toKotlin(scope)` to get Kotlin values back. Collections convert to Kotlin collections recursively.
|
- Use `toKotlin(scope)` to get Kotlin values back. Collections convert to Kotlin collections recursively.
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ Serializable class that conveys information about the exception. Important membe
|
|||||||
|
|
||||||
A simple structire that stores single entry in Lyng stack, it is created automatically on exception creation:
|
A simple structire that stores single entry in Lyng stack, it is created automatically on exception creation:
|
||||||
|
|
||||||
```kotlin
|
```lyng
|
||||||
class StackTraceEntry(
|
class StackTraceEntry(
|
||||||
val sourceName: String,
|
val sourceName: String,
|
||||||
val line: Int,
|
val line: Int,
|
||||||
@@ -165,7 +165,7 @@ _this functionality is not yet released_
|
|||||||
| IllegalAssignmentException | assigning to val, etc. |
|
| IllegalAssignmentException | assigning to val, etc. |
|
||||||
| SymbolNotDefinedException | |
|
| SymbolNotDefinedException | |
|
||||||
| IterationEndException | attempt to read iterator past end, `hasNext == false` |
|
| IterationEndException | attempt to read iterator past end, `hasNext == false` |
|
||||||
| AccessException | attempt to access private members or like |
|
| IllegalAccessException | attempt to access private members or like |
|
||||||
| UnknownException | unexpected kotlin exception caught |
|
| UnknownException | unexpected kotlin exception caught |
|
||||||
| | |
|
| | |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Migration of Instant and Clock
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
Before kotlin 2.0, there was an excellent library, kotlinx.datetime, which was widely used everywhere, also in Lyng and its dependencies.
|
||||||
|
|
||||||
|
When kotlin 2.0 was released, or soon after, JetBrains made an exptic decision to remove `Instant` and `Clock` from kotlinx.datetime and replace it with _yet experimental_ analogs in `kotlin.time`.
|
||||||
|
|
||||||
|
The problem is, these were not quite the same (these weren't `@Serializable`!), so people didn't migrate with ease. Okay, then JetBrains decided to not only deprecate it but also make them unusable on Apple targets. It sort of split auditories of many published libraries to those who hate JetBrains and Apple and continue to use 1.9-2.0 compatible versions that no longer work with Kotlin 2.2 on Apple targets (but work pretty well with earlier Kotlin or on other platforms).
|
||||||
|
|
||||||
|
Later JetBrains added serializers for their new `Instant` and `Clock` types, but strangely not in the stdlib, but in newer versions of `kotlinx.serialization`. This means that plain upgrade of dependencies to 2.2 is not enough to make them work.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
We hereby publish a new version of Lyng, 1.0.8-SNAPSHOT, which uses `ktlin.time.Instant` and `kotlin.time.Clock` instead of `kotlinx.datetime.Instant` and `kotlinx.datetime.Clock; it is in other aspects compatible also with Lynon encoded binaries. Still you might need to migrate your code to use `kotlinx.datetime` types.
|
||||||
|
|
||||||
|
So, if you are getting errors with new version, plase do:
|
||||||
|
|
||||||
|
- upgrade to Kotlin 2.2
|
||||||
|
- upgrade to Lyng 1.0.8-SNAPSHOT
|
||||||
|
- replace in your code imports (or other uses) of`kotlinx.datetime.Clock` to `kotlin.time.Clock` and `kotlinx.datetime.Instant` to `kotlin.time.Instant`.
|
||||||
|
|
||||||
|
This should solve the problem and hopefully we'll see no more suh a brillant ideas from IDEA ideologspersons.
|
||||||
|
|
||||||
|
Sorry for inconvenicence and send a ray of hate to JetBrains ;)
|
||||||
@@ -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:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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.
|
||||||
|
|||||||
+170
-30
@@ -9,6 +9,7 @@ __Other documents to read__ maybe after this one:
|
|||||||
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md), [Scopes and Closures](scopes_and_closures.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)
|
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
|
||||||
- [math in Lyng](math.md), [the `when` statement](when.md)
|
- [math in Lyng](math.md), [the `when` statement](when.md)
|
||||||
|
- [Testing and Assertions](Testing.md)
|
||||||
- [time](time.md) and [parallelism](parallelism.md)
|
- [time](time.md) and [parallelism](parallelism.md)
|
||||||
- [parallelism] - multithreaded code, coroutines, etc.
|
- [parallelism] - multithreaded code, coroutines, etc.
|
||||||
- Some class
|
- Some class
|
||||||
@@ -86,6 +87,27 @@ Lyng supports simple enums for a fixed set of named constants. Declare with `enu
|
|||||||
|
|
||||||
For more details (usage patterns, `when` switching, serialization), see OOP notes: [Enums in detail](OOP.md#enums).
|
For more details (usage patterns, `when` switching, serialization), see OOP notes: [Enums in detail](OOP.md#enums).
|
||||||
|
|
||||||
|
## Singleton Objects
|
||||||
|
|
||||||
|
Singleton objects are declared using the `object` keyword. They define a class and create its single instance immediately.
|
||||||
|
|
||||||
|
object Logger {
|
||||||
|
fun log(msg) { println("[LOG] " + msg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.log("Hello singleton!")
|
||||||
|
|
||||||
|
## Delegation (briefly)
|
||||||
|
|
||||||
|
You can delegate properties and functions to other objects using the `by` keyword. This is perfect for patterns like `lazy` initialization.
|
||||||
|
|
||||||
|
val expensiveData by lazy {
|
||||||
|
// computed only once on demand
|
||||||
|
"computed"
|
||||||
|
}
|
||||||
|
|
||||||
|
For more details on these features, see [Delegation in Lyng](delegation.md) and [OOP notes](OOP.md).
|
||||||
|
|
||||||
When putting multiple statments in the same line it is convenient and recommended to use `;`:
|
When putting multiple statments in the same line it is convenient and recommended to use `;`:
|
||||||
|
|
||||||
var from; var to
|
var from; var to
|
||||||
@@ -106,6 +128,41 @@ Assignemnt is an expression that changes its lvalue and return assigned value:
|
|||||||
>>> 11
|
>>> 11
|
||||||
>>> 6
|
>>> 6
|
||||||
|
|
||||||
|
### Destructuring assignments
|
||||||
|
|
||||||
|
Lyng supports destructuring assignments for lists. This allows you to unpack list elements into multiple variables at once:
|
||||||
|
|
||||||
|
val [a, b, c] = [1, 2, 3]
|
||||||
|
assertEquals(1, a)
|
||||||
|
assertEquals(2, b)
|
||||||
|
assertEquals(3, c)
|
||||||
|
|
||||||
|
It also supports *splats* (ellipsis) to capture multiple elements into a list:
|
||||||
|
|
||||||
|
val [head, rest...] = [1, 2, 3]
|
||||||
|
assertEquals(1, head)
|
||||||
|
assertEquals([2, 3], rest)
|
||||||
|
|
||||||
|
val [first, middle..., last] = [1, 2, 3, 4, 5]
|
||||||
|
assertEquals(1, first)
|
||||||
|
assertEquals([2, 3, 4], middle)
|
||||||
|
assertEquals(5, last)
|
||||||
|
|
||||||
|
Destructuring can be nested:
|
||||||
|
|
||||||
|
val [x, [y, z...]] = [1, [2, 3, 4]]
|
||||||
|
assertEquals(1, x)
|
||||||
|
assertEquals(2, y)
|
||||||
|
assertEquals([3, 4], z)
|
||||||
|
|
||||||
|
And it can be used for reassigning existing variables, for example, to swap values:
|
||||||
|
|
||||||
|
var x = 5
|
||||||
|
var y = 10
|
||||||
|
[x, y] = [y, x]
|
||||||
|
assertEquals(10, x)
|
||||||
|
assertEquals(5, y)
|
||||||
|
|
||||||
As the assignment itself is an expression, you can use it in strange ways. Just remember
|
As the assignment itself is an expression, you can use it in strange ways. Just remember
|
||||||
to use parentheses as assignment operation insofar is left-associated and will not
|
to use parentheses as assignment operation insofar is left-associated and will not
|
||||||
allow chained assignments (we might fix it later). Use parentheses insofar:
|
allow chained assignments (we might fix it later). Use parentheses insofar:
|
||||||
@@ -311,8 +368,6 @@ Reference quality and object equality example:
|
|||||||
assert( null == null) // singletons
|
assert( null == null) // singletons
|
||||||
assert( null === null)
|
assert( null === null)
|
||||||
// but, for non-singletons:
|
// but, for non-singletons:
|
||||||
assert( 5 == 5)
|
|
||||||
assert( 5 !== 5)
|
|
||||||
assert( "foo" !== "foo" )
|
assert( "foo" !== "foo" )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
@@ -329,7 +384,7 @@ will be thrown:
|
|||||||
// WRONG! Exception will be thrown at next line:
|
// WRONG! Exception will be thrown at next line:
|
||||||
foo + "bar"
|
foo + "bar"
|
||||||
|
|
||||||
Correct pattern is:
|
The correct pattern is:
|
||||||
|
|
||||||
foo = "foo"
|
foo = "foo"
|
||||||
// now is OK:
|
// now is OK:
|
||||||
@@ -403,6 +458,19 @@ It is possible to define also vararg using ellipsis:
|
|||||||
|
|
||||||
See the [arguments reference](declaring_arguments.md) for more details.
|
See the [arguments reference](declaring_arguments.md) for more details.
|
||||||
|
|
||||||
|
## Named arguments
|
||||||
|
|
||||||
|
When calling functions, you can use named arguments with the colon syntax `name: value`. This is particularly useful when you have many parameters with default values.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun test(a="foo", b="bar", c="bazz") { [a, b, c] }
|
||||||
|
|
||||||
|
assertEquals(["foo", "b", "bazz"], test(b: "b"))
|
||||||
|
assertEquals(["a", "bar", "c"], test("a", c: "c"))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note for Kotlin users:** Lyng uses `:` instead of `=` for named arguments at call sites. This is because in Lyng, `=` is an expression that returns the assigned value, and using it in an argument list would create ambiguity.
|
||||||
|
|
||||||
## Closures
|
## Closures
|
||||||
|
|
||||||
Each __block has an isolated context that can be accessed from closures__. For example:
|
Each __block has an isolated context that can be accessed from closures__. For example:
|
||||||
@@ -478,6 +546,8 @@ one could be with ellipsis that means "the rest pf arguments as List":
|
|||||||
|
|
||||||
### Using lambda as the parameter
|
### Using lambda as the parameter
|
||||||
|
|
||||||
|
See also: [Testing and Assertions](Testing.md)
|
||||||
|
|
||||||
// note that fun returns its last calculated value,
|
// note that fun returns its last calculated value,
|
||||||
// in our case, result after in-place addition:
|
// in our case, result after in-place addition:
|
||||||
fun mapValues(iterable, transform) {
|
fun mapValues(iterable, transform) {
|
||||||
@@ -1378,31 +1448,31 @@ Part match:
|
|||||||
|
|
||||||
Typical set of String functions includes:
|
Typical set of String functions includes:
|
||||||
|
|
||||||
| fun/prop | description / notes |
|
| fun/prop | description / notes |
|
||||||
|--------------------|------------------------------------------------------------|
|
|----------------------|------------------------------------------------------------|
|
||||||
| lower() | change case to unicode upper |
|
| lower(), lowercase() | change case to unicode upper |
|
||||||
| upper() | change case to unicode lower |
|
| upper(), uppercase() | change case to unicode lower |
|
||||||
| trim() | trim space chars from both ends |
|
| trim() | trim space chars from both ends |
|
||||||
| startsWith(prefix) | true if starts with a prefix |
|
| startsWith(prefix) | true if starts with a prefix |
|
||||||
| endsWith(prefix) | true if ends with a prefix |
|
| endsWith(prefix) | true if ends with a prefix |
|
||||||
| last() | get last character of a string or throw |
|
| last() | get last character of a string or throw |
|
||||||
| take(n) | get a new string from up to n first characters |
|
| take(n) | get a new string from up to n first characters |
|
||||||
| takeLast(n) | get a new string from up to n last characters |
|
| takeLast(n) | get a new string from up to n last characters |
|
||||||
| drop(n) | get a new string dropping n first chars, or empty string |
|
| drop(n) | get a new string dropping n first chars, or empty string |
|
||||||
| dropLast(n) | get a new string dropping n last chars, or empty string |
|
| dropLast(n) | get a new string dropping n last chars, or empty string |
|
||||||
| size | size in characters like `length` because String is [Array] |
|
| size | size in characters like `length` because String is [Array] |
|
||||||
| (args...) | sprintf-like formatting, see [string formatting] |
|
| (args...) | sprintf-like formatting, see [string formatting] |
|
||||||
| [index] | character at index |
|
| [index] | character at index |
|
||||||
| [Range] | substring at range (2) |
|
| [Range] | substring at range (2) |
|
||||||
| [Regex] | find first match of regex, like [Regex.find] (2) |
|
| [Regex] | find first match of regex, like [Regex.find] (2) |
|
||||||
| s1 + s2 | concatenation |
|
| s1 + s2 | concatenation |
|
||||||
| 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) |
|
||||||
| | |
|
| | |
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
: List is mutable therefore a new copy is created on each call.
|
: List is mutable therefore a new copy is created on each call.
|
||||||
@@ -1448,7 +1518,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)
|
||||||
@@ -1466,6 +1536,8 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
|
|||||||
|
|
||||||
[List]: List.md
|
[List]: List.md
|
||||||
|
|
||||||
|
[Testing]: Testing.md
|
||||||
|
|
||||||
[Iterable]: Iterable.md
|
[Iterable]: Iterable.md
|
||||||
|
|
||||||
[Iterator]: Iterator.md
|
[Iterator]: Iterator.md
|
||||||
@@ -1494,6 +1566,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
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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.
|
||||||
@@ -1557,4 +1673,28 @@ Notes:
|
|||||||
- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualialsofication (`this@Type`) or casts do not bypass visibility.
|
- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualialsofication (`this@Type`) or casts do not bypass visibility.
|
||||||
- Safe‑call `?.` works with `as?` for optional dispatch.
|
- Safe‑call `?.` works with `as?` for optional dispatch.
|
||||||
|
|
||||||
To get details on OOP in Lyng, see [OOP notes](oop.md).
|
## Extension members
|
||||||
|
|
||||||
|
You can add new methods and properties to existing classes without modifying them.
|
||||||
|
|
||||||
|
### Extension functions
|
||||||
|
|
||||||
|
fun String.shout() = this.upper() + "!!!"
|
||||||
|
"hello".shout()
|
||||||
|
>>> "HELLO!!!"
|
||||||
|
|
||||||
|
### Extension properties
|
||||||
|
|
||||||
|
val Int.isEven = this % 2 == 0
|
||||||
|
4.isEven
|
||||||
|
>>> true
|
||||||
|
|
||||||
|
Example with custom accessors:
|
||||||
|
|
||||||
|
val String.firstChar get() = this[0]
|
||||||
|
"abc".firstChar
|
||||||
|
>>> 'a'
|
||||||
|
|
||||||
|
Extension members are **scope-isolated**: they are visible only in the scope where they are defined and its children. This prevents name collisions and improves security.
|
||||||
|
|
||||||
|
To get details on OOP in Lyng, see [OOP notes](OOP.md).
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Wasm generation hang in wasmJs browser tests
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
The wasmJs browser test runner hung after commit 5f819dc. The root cause was invalid WebAssembly generated by the Kotlin/Wasm backend when certain compiler paths emitted suspend lambdas for `Statement` execution. The invalid module failed to instantiate in the browser, and Karma kept the browser connected but never ran tests.
|
||||||
|
|
||||||
|
## Symptoms
|
||||||
|
- `:lynglib:wasmJsBrowserTest` hangs indefinitely in ChromeHeadless.
|
||||||
|
- `:lynglib:wasmJsNodeTest` fails with a WebAssembly compile error similar to:
|
||||||
|
- `struct.set expected type (ref null XXXX), found global.get of type (ref null YYYY)`
|
||||||
|
- The failing function name in the wasm name section looks like:
|
||||||
|
- `net.sergeych.lyng.$invokeCOROUTINE$.doResume`
|
||||||
|
|
||||||
|
## Root cause
|
||||||
|
The delegation/var-declaration changes introduced compiler-generated suspend lambdas inside `Statement` construction (e.g., `statement { ... }` wrappers). Kotlin/Wasm generates extra coroutine state for those suspend lambdas, which in this case produced invalid wasm IR (mismatched GC reference types). The browser loader then waits forever because the module fails to instantiate.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
Avoid suspend-lambda `Statement` construction in compiler code paths. Replace `statement { ... }` and other anonymous suspend lambdas with explicit `object : Statement()` implementations and move logic into `override suspend fun execute(...)`. This keeps the resulting wasm IR valid while preserving behavior.
|
||||||
|
|
||||||
|
## Where it was fixed
|
||||||
|
- `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt`
|
||||||
|
- `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- `./gradlew :lynglib:wasmJsNodeTest --info`
|
||||||
|
- `./gradlew :lynglib:wasmJsBrowserTest --info`
|
||||||
|
|
||||||
|
Both tests finish quickly after the change.
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Singleton Objects
|
||||||
|
Singleton objects are declared using the `object` keyword. They provide a convenient way to define a class and its single instance in one go.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
object Config {
|
||||||
|
val version = "1.2.3"
|
||||||
|
fun show() = println("Config version: " + version)
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unified Delegation Model
|
||||||
|
A powerful new delegation system allows `val`, `var`, and `fun` members to delegate their logic to other objects using the `by` keyword.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
// Property delegation
|
||||||
|
val lazyValue by lazy { "expensive" }
|
||||||
|
|
||||||
|
// Function delegation
|
||||||
|
fun remoteAction by myProxy
|
||||||
|
|
||||||
|
// Observable properties
|
||||||
|
var name by Observable("initial") { n, old, new ->
|
||||||
|
println("Changed!")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The system features a unified interface (`getValue`, `setValue`, `invoke`) and a `bind` hook for initialization-time validation and configuration. See the [Delegation Guide](delegation.md) for more.
|
||||||
|
|
||||||
|
## Tooling and Infrastructure
|
||||||
|
|
||||||
|
### 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**.
|
||||||
@@ -20,7 +20,7 @@ Files
|
|||||||
- Constants: `true`, `false`, `null`, `this`
|
- Constants: `true`, `false`, `null`, `this`
|
||||||
- Annotations: `@name` (Unicode identifiers supported)
|
- Annotations: `@name` (Unicode identifiers supported)
|
||||||
- Labels: `name:` (Unicode identifiers supported)
|
- Labels: `name:` (Unicode identifiers supported)
|
||||||
- Declarations: highlights declared names in `fun|fn name`, `class|enum Name`, `val|var name`
|
- Declarations: highlights declared names in `fun|fn name`, `class|enum|interface Name`, `val|var name`
|
||||||
- Types: built-ins (`Int|Real|String|Bool|Char|Regex`) and Capitalized identifiers (heuristic)
|
- Types: built-ins (`Int|Real|String|Bool|Char|Regex`) and Capitalized identifiers (heuristic)
|
||||||
- Operators including ranges (`..`, `..<`, `...`), null-safe (`?.`, `?[`, `?(`, `?{`, `?:`, `??`), arrows (`->`, `=>`, `::`), match operators (`=~`, `!~`), bitwise, arithmetic, etc.
|
- Operators including ranges (`..`, `..<`, `...`), null-safe (`?.`, `?[`, `?(`, `?{`, `?:`, `??`), arrows (`->`, `=>`, `::`), match operators (`=~`, `!~`), bitwise, arithmetic, etc.
|
||||||
- Shuttle operator `<=>`
|
- Shuttle operator `<=>`
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "lyng-textmate",
|
"name": "lyng-textmate",
|
||||||
"displayName": "Lyng",
|
"displayName": "Lyng",
|
||||||
"description": "TextMate grammar for the Lyng language (for JetBrains IDEs via TextMate Bundles and VS Code).",
|
"description": "TextMate grammar for the Lyng language (for JetBrains IDEs via TextMate Bundles and VS Code).",
|
||||||
"version": "0.0.3",
|
"version": "0.1.0",
|
||||||
"publisher": "lyng",
|
"publisher": "lyng",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": { "vscode": "^1.0.0" },
|
"engines": { "vscode": "^1.0.0" },
|
||||||
|
|||||||
@@ -76,8 +76,8 @@
|
|||||||
},
|
},
|
||||||
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] },
|
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] },
|
||||||
"directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] },
|
"directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] },
|
||||||
"declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(?:fun|fn)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(?:val|var)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "variable.other.declaration.lyng" } } } ] },
|
"declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(fun|fn)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum|interface)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(val|var)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "variable.other.declaration.lyng" } } } ] },
|
||||||
"keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|val|var|import|package|constructor|property|open|extern|private|protected|static)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] },
|
"keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|interface|val|var|import|package|constructor|property|abstract|override|open|closed|extern|private|protected|static|get|set)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] },
|
||||||
"constants": { "patterns": [ { "name": "constant.language.lyng", "match": "(?:\\b(?:true|false|null|this)\\b|π)" } ] },
|
"constants": { "patterns": [ { "name": "constant.language.lyng", "match": "(?:\\b(?:true|false|null|this)\\b|π)" } ] },
|
||||||
"types": { "patterns": [ { "name": "storage.type.lyng", "match": "\\b(?:Int|Real|String|Bool|Char|Regex)\\b" }, { "name": "entity.name.type.lyng", "match": "\\b[A-Z][A-Za-z0-9_]*\\b(?!\\s*\\()" } ] },
|
"types": { "patterns": [ { "name": "storage.type.lyng", "match": "\\b(?:Int|Real|String|Bool|Char|Regex)\\b" }, { "name": "entity.name.type.lyng", "match": "\\b[A-Z][A-Za-z0-9_]*\\b(?!\\s*\\()" } ] },
|
||||||
"operators": { "patterns": [ { "name": "keyword.operator.comparison.lyng", "match": "===|!==|==|!=|<=|>=|<|>" }, { "name": "keyword.operator.shuttle.lyng", "match": "<=>" }, { "name": "keyword.operator.arrow.lyng", "match": "=>|->|::" }, { "name": "keyword.operator.range.lyng", "match": "\\.\\.\\.|\\.\\.<|\\.\\." }, { "name": "keyword.operator.nullsafe.lyng", "match": "\\?\\.|\\?\\[|\\?\\(|\\?\\{|\\?:|\\?\\?" }, { "name": "keyword.operator.assignment.lyng", "match": "(?:\\+=|-=|\\*=|/=|%=|=)" }, { "name": "keyword.operator.logical.lyng", "match": "&&|\\|\\|" }, { "name": "keyword.operator.bitwise.lyng", "match": "<<|>>|&|\\||\\^|~" }, { "name": "keyword.operator.match.lyng", "match": "=~|!~" }, { "name": "keyword.operator.arithmetic.lyng", "match": "\\+\\+|--|[+\\-*/%]" }, { "name": "keyword.operator.other.lyng", "match": "[!?]" } ] },
|
"operators": { "patterns": [ { "name": "keyword.operator.comparison.lyng", "match": "===|!==|==|!=|<=|>=|<|>" }, { "name": "keyword.operator.shuttle.lyng", "match": "<=>" }, { "name": "keyword.operator.arrow.lyng", "match": "=>|->|::" }, { "name": "keyword.operator.range.lyng", "match": "\\.\\.\\.|\\.\\.<|\\.\\." }, { "name": "keyword.operator.nullsafe.lyng", "match": "\\?\\.|\\?\\[|\\?\\(|\\?\\{|\\?:|\\?\\?" }, { "name": "keyword.operator.assignment.lyng", "match": "(?:\\+=|-=|\\*=|/=|%=|=)" }, { "name": "keyword.operator.logical.lyng", "match": "&&|\\|\\|" }, { "name": "keyword.operator.bitwise.lyng", "match": "<<|>>|&|\\||\\^|~" }, { "name": "keyword.operator.match.lyng", "match": "=~|!~" }, { "name": "keyword.operator.arithmetic.lyng", "match": "\\+\\+|--|[+\\-*/%]" }, { "name": "keyword.operator.other.lyng", "match": "[!?]" } ] },
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.5.2"
|
agp = "8.5.2"
|
||||||
clikt = "5.0.3"
|
clikt = "5.0.3"
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.3.0"
|
||||||
android-minSdk = "24"
|
android-minSdk = "24"
|
||||||
android-compileSdk = "34"
|
android-compileSdk = "34"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
mp_bintools = "0.1.12"
|
mp_bintools = "0.3.2"
|
||||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||||
okioVersion = "3.10.2"
|
okioVersion = "3.10.2"
|
||||||
compiler = "3.2.0-alpha11"
|
compiler = "3.2.0-alpha11"
|
||||||
|
|||||||
@@ -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 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych.lyng"
|
group = "net.sergeych.lyng"
|
||||||
version = "0.0.3-SNAPSHOT"
|
version = "0.0.5-SNAPSHOT"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(17)
|
jvmToolchain(17)
|
||||||
|
|||||||
+11
-5
@@ -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.
|
||||||
@@ -117,6 +117,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
d.name,
|
d.name,
|
||||||
if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
|
if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
|
||||||
)
|
)
|
||||||
|
is MiniEnumDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,12 +158,17 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
is MiniFunDecl -> {
|
is MiniFunDecl -> {
|
||||||
addTypeSegments(d.returnType)
|
addTypeSegments(d.returnType)
|
||||||
d.params.forEach { addTypeSegments(it.type) }
|
d.params.forEach { addTypeSegments(it.type) }
|
||||||
|
addTypeSegments(d.receiver)
|
||||||
|
}
|
||||||
|
is MiniValDecl -> {
|
||||||
|
addTypeSegments(d.type)
|
||||||
|
addTypeSegments(d.receiver)
|
||||||
}
|
}
|
||||||
is MiniValDecl -> addTypeSegments(d.type)
|
|
||||||
is MiniClassDecl -> {
|
is MiniClassDecl -> {
|
||||||
d.ctorFields.forEach { addTypeSegments(it.type) }
|
d.ctorFields.forEach { addTypeSegments(it.type) }
|
||||||
d.classFields.forEach { addTypeSegments(it.type) }
|
d.classFields.forEach { addTypeSegments(it.type) }
|
||||||
}
|
}
|
||||||
|
is MiniEnumDecl -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,9 +185,9 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
fun keyForKind(k: SymbolKind) = when (k) {
|
fun keyForKind(k: SymbolKind) = when (k) {
|
||||||
SymbolKind.Function -> LyngHighlighterColors.FUNCTION
|
SymbolKind.Function -> LyngHighlighterColors.FUNCTION
|
||||||
SymbolKind.Class, SymbolKind.Enum -> LyngHighlighterColors.TYPE
|
SymbolKind.Class, SymbolKind.Enum -> LyngHighlighterColors.TYPE
|
||||||
SymbolKind.Param -> LyngHighlighterColors.PARAMETER
|
SymbolKind.Parameter -> LyngHighlighterColors.PARAMETER
|
||||||
SymbolKind.Val -> LyngHighlighterColors.VALUE
|
SymbolKind.Value -> LyngHighlighterColors.VALUE
|
||||||
SymbolKind.Var -> LyngHighlighterColors.VARIABLE
|
SymbolKind.Variable -> LyngHighlighterColors.VARIABLE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track covered ranges to not override later heuristics
|
// Track covered ranges to not override later heuristics
|
||||||
|
|||||||
+62
-399
@@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Lightweight BASIC completion for Lyng, MVP version.
|
* Lightweight BASIC completion for Lyng, MVP version.
|
||||||
* Uses MiniAst (best-effort) + BuiltinDocRegistry to suggest symbols.
|
* Uses MiniAst (best-effort) + BuiltinDocRegistry to suggest symbols.
|
||||||
@@ -9,18 +26,16 @@ import com.intellij.codeInsight.lookup.LookupElementBuilder
|
|||||||
import com.intellij.icons.AllIcons
|
import com.intellij.icons.AllIcons
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.editor.Document
|
import com.intellij.openapi.editor.Document
|
||||||
import com.intellij.openapi.util.Key
|
|
||||||
import com.intellij.patterns.PlatformPatterns
|
import com.intellij.patterns.PlatformPatterns
|
||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
import com.intellij.util.ProcessingContext
|
import com.intellij.util.ProcessingContext
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.sergeych.lyng.Compiler
|
|
||||||
import net.sergeych.lyng.Source
|
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
|
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.IdeLenientImportProvider
|
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.*
|
||||||
|
|
||||||
@@ -48,6 +63,12 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
StdlibDocsBootstrap.ensure()
|
StdlibDocsBootstrap.ensure()
|
||||||
val file: PsiFile = parameters.originalFile
|
val file: PsiFile = parameters.originalFile
|
||||||
if (file.language != LyngLanguage) return
|
if (file.language != LyngLanguage) return
|
||||||
|
|
||||||
|
// Disable completion inside comments
|
||||||
|
val pos = parameters.position
|
||||||
|
val et = pos.node.elementType
|
||||||
|
if (et == LyngTokenTypes.LINE_COMMENT || et == LyngTokenTypes.BLOCK_COMMENT) return
|
||||||
|
|
||||||
// Feature toggle: allow turning completion off from settings
|
// Feature toggle: allow turning completion off from settings
|
||||||
val settings = LyngFormatterSettings.getInstance(file.project)
|
val settings = LyngFormatterSettings.getInstance(file.project)
|
||||||
if (!settings.enableLyngCompletionExperimental) return
|
if (!settings.enableLyngCompletionExperimental) return
|
||||||
@@ -77,7 +98,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build MiniAst (cached) for both global and member contexts to enable local class/val inference
|
// Build MiniAst (cached) for both global and member contexts to enable local class/val inference
|
||||||
val mini = buildMiniAstCached(file, text)
|
val mini = LyngAstManager.getMiniAst(file)
|
||||||
|
val binding = LyngAstManager.getBinding(file)
|
||||||
|
|
||||||
// Delegate computation to the shared engine to keep behavior in sync with tests
|
// Delegate computation to the shared engine to keep behavior in sync with tests
|
||||||
val engineItems = try {
|
val engineItems = try {
|
||||||
@@ -104,13 +126,13 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
// Try inferring return/receiver class around the dot
|
// Try inferring return/receiver class around the dot
|
||||||
val inferred =
|
val inferred =
|
||||||
// Prefer MiniAst-based inference (return type from member call or receiver type)
|
// Prefer MiniAst-based inference (return type from member call or receiver type)
|
||||||
guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported)
|
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported, binding)
|
||||||
?: guessReceiverClassViaMini(mini, text, memberDotPos, imported)
|
?: DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDotPos, imported, binding)
|
||||||
?:
|
?:
|
||||||
guessReturnClassFromMemberCallBefore(text, memberDotPos, imported)
|
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDotPos, imported, mini)
|
||||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported, mini)
|
||||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||||
?: guessReceiverClass(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
||||||
|
|
||||||
if (inferred != null) {
|
if (inferred != null) {
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Fallback inferred receiver/return class='$inferred' — offering its members")
|
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Fallback inferred receiver/return class='$inferred' — offering its members")
|
||||||
@@ -142,6 +164,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
.withInsertHandler(ParenInsertHandler)
|
.withInsertHandler(ParenInsertHandler)
|
||||||
Kind.Class_ -> LookupElementBuilder.create(ci.name)
|
Kind.Class_ -> LookupElementBuilder.create(ci.name)
|
||||||
.withIcon(AllIcons.Nodes.Class)
|
.withIcon(AllIcons.Nodes.Class)
|
||||||
|
Kind.Enum -> LookupElementBuilder.create(ci.name)
|
||||||
|
.withIcon(AllIcons.Nodes.Enum)
|
||||||
Kind.Value -> LookupElementBuilder.create(ci.name)
|
Kind.Value -> LookupElementBuilder.create(ci.name)
|
||||||
.withIcon(AllIcons.Nodes.Field)
|
.withIcon(AllIcons.Nodes.Field)
|
||||||
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
||||||
@@ -160,18 +184,18 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
add("lyng.stdlib")
|
add("lyng.stdlib")
|
||||||
}.toList()
|
}.toList()
|
||||||
val inferredClass =
|
val inferredClass =
|
||||||
guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported)
|
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported, binding)
|
||||||
?: guessReceiverClassViaMini(mini, text, memberDotPos, imported)
|
?: DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDotPos, imported, binding)
|
||||||
?: guessReturnClassFromMemberCallBefore(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDotPos, imported, mini)
|
||||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported, mini)
|
||||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||||
?: guessReceiverClass(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
||||||
if (!inferredClass.isNullOrBlank()) {
|
if (!inferredClass.isNullOrBlank()) {
|
||||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(inferredClass)
|
val ext = BuiltinDocRegistry.extensionMemberNamesFor(inferredClass)
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}")
|
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}")
|
||||||
for (name in ext) {
|
for (name in ext) {
|
||||||
if (existing.contains(name)) continue
|
if (existing.contains(name)) continue
|
||||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name)
|
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini)
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
when (val member = resolved.second) {
|
when (val member = resolved.second) {
|
||||||
is MiniMemberFunDecl -> {
|
is MiniMemberFunDecl -> {
|
||||||
@@ -192,6 +216,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
emit(builder)
|
emit(builder)
|
||||||
existing.add(name)
|
existing.add(name)
|
||||||
}
|
}
|
||||||
|
is MiniInitDecl -> {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: emit simple method name without detailed types
|
// Fallback: emit simple method name without detailed types
|
||||||
@@ -214,12 +239,12 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
add("lyng.stdlib")
|
add("lyng.stdlib")
|
||||||
}.toList()
|
}.toList()
|
||||||
val inferred =
|
val inferred =
|
||||||
guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported)
|
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported, binding)
|
||||||
?: guessReceiverClassViaMini(mini, text, memberDotPos, imported)
|
?: DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDotPos, imported, binding)
|
||||||
?: guessReturnClassFromMemberCallBefore(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDotPos, imported, mini)
|
||||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported, mini)
|
||||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||||
?: guessReceiverClass(text, memberDotPos, imported)
|
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
||||||
if (inferred != null) {
|
if (inferred != null) {
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Enrichment inferred class='$inferred' — offering its members")
|
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Enrichment inferred class='$inferred' — offering its members")
|
||||||
offerMembers(emit, imported, inferred, sourceText = text, mini = mini)
|
offerMembers(emit, imported, inferred, sourceText = text, mini = mini)
|
||||||
@@ -249,7 +274,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
.withIcon(kindIcon)
|
.withIcon(kindIcon)
|
||||||
.withTypeText(typeOf(d.type), true)
|
.withTypeText(typeOf(d.type), true)
|
||||||
}
|
}
|
||||||
else -> LookupElementBuilder.create(name)
|
is MiniEnumDecl -> LookupElementBuilder.create(name)
|
||||||
|
.withIcon(AllIcons.Nodes.Enum)
|
||||||
}
|
}
|
||||||
emit(builder)
|
emit(builder)
|
||||||
}
|
}
|
||||||
@@ -276,9 +302,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
, sourceText: String,
|
, sourceText: String,
|
||||||
mini: MiniScript? = null
|
mini: MiniScript? = null
|
||||||
) {
|
) {
|
||||||
// Ensure modules are seeded in the registry (triggers lazy stdlib build too)
|
val classes = DocLookupUtils.aggregateClasses(imported, mini)
|
||||||
for (m in imported) BuiltinDocRegistry.docsForModule(m)
|
|
||||||
val classes = DocLookupUtils.aggregateClasses(imported)
|
|
||||||
if (DEBUG_COMPLETION) {
|
if (DEBUG_COMPLETION) {
|
||||||
val keys = classes.keys.joinToString(", ")
|
val keys = classes.keys.joinToString(", ")
|
||||||
log.info("[LYNG_DEBUG] offerMembers: imported=${imported} classes=[${keys}] target=${className}")
|
log.info("[LYNG_DEBUG] offerMembers: imported=${imported} classes=[${keys}] target=${className}")
|
||||||
@@ -297,7 +321,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
}
|
}
|
||||||
// If MiniAst didn't populate members (empty), try to scan class body text for member signatures
|
// If MiniAst didn't populate members (empty), try to scan class body text for member signatures
|
||||||
if (localClass.members.isEmpty()) {
|
if (localClass.members.isEmpty()) {
|
||||||
val scanned = scanLocalClassMembersFromText(mini, text = sourceText, cls = localClass)
|
val scanned = DocLookupUtils.scanLocalClassMembersFromText(mini, text = sourceText, cls = localClass)
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Local scan for class ${localClass.name}: found ${scanned.size} members -> ${scanned.keys}")
|
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Local scan for class ${localClass.name}: found ${scanned.size} members -> ${scanned.keys}")
|
||||||
for ((name, sig) in scanned) {
|
for ((name, sig) in scanned) {
|
||||||
when (sig.kind) {
|
when (sig.kind) {
|
||||||
@@ -329,6 +353,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
when (m) {
|
when (m) {
|
||||||
is MiniMemberFunDecl -> if (!m.isStatic) continue
|
is MiniMemberFunDecl -> if (!m.isStatic) continue
|
||||||
is MiniMemberValDecl -> if (!m.isStatic) continue
|
is MiniMemberValDecl -> if (!m.isStatic) continue
|
||||||
|
is MiniInitDecl -> continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val list = target.getOrPut(m.name) { mutableListOf() }
|
val list = target.getOrPut(m.name) { mutableListOf() }
|
||||||
@@ -401,9 +426,10 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
.firstOrNull { it.type != null } ?: rep
|
.firstOrNull { it.type != null } ?: rep
|
||||||
val builder = LookupElementBuilder.create(name)
|
val builder = LookupElementBuilder.create(name)
|
||||||
.withIcon(icon)
|
.withIcon(icon)
|
||||||
.withTypeText(typeOf((chosen as MiniMemberValDecl).type), true)
|
.withTypeText(typeOf(chosen.type), true)
|
||||||
emit(builder)
|
emit(builder)
|
||||||
}
|
}
|
||||||
|
is MiniInitDecl -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,6 +480,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
emit(builder)
|
emit(builder)
|
||||||
already.add(name)
|
already.add(name)
|
||||||
}
|
}
|
||||||
|
is MiniInitDecl -> {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs
|
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs
|
||||||
@@ -473,10 +500,10 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supplement with stdlib extension-like methods defined in root.lyng (e.g., fun String.trim(...))
|
// Supplement with stdlib extension members defined in root.lyng (e.g., fun String.trim(...))
|
||||||
run {
|
run {
|
||||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className)
|
val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Extensions for $className: count=${ext.size} -> ${ext}")
|
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Extensions for $className: count=${ext.size} -> ${ext}")
|
||||||
for (name in ext) {
|
for (name in ext) {
|
||||||
if (already.contains(name)) continue
|
if (already.contains(name)) continue
|
||||||
@@ -504,6 +531,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
already.add(name)
|
already.add(name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
is MiniInitDecl -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback: emit without detailed types if we couldn't resolve
|
// Fallback: emit without detailed types if we couldn't resolve
|
||||||
@@ -519,371 +547,6 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
|
|
||||||
// --- MiniAst-based inference helpers ---
|
// --- MiniAst-based inference helpers ---
|
||||||
|
|
||||||
private fun previousIdentifierBeforeDot(text: String, dotPos: Int): String? {
|
|
||||||
var i = dotPos - 1
|
|
||||||
// skip whitespace
|
|
||||||
while (i >= 0 && text[i].isWhitespace()) i--
|
|
||||||
val end = i + 1
|
|
||||||
while (i >= 0 && TextCtx.isIdentChar(text[i])) i--
|
|
||||||
val start = i + 1
|
|
||||||
return if (start < end) text.substring(start, end) else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun guessReceiverClassViaMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
if (mini == null) return null
|
|
||||||
val ident = previousIdentifierBeforeDot(text, dotPos) ?: return null
|
|
||||||
// 1) Local val/var in the file
|
|
||||||
val valDecl = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }
|
|
||||||
val typeFromVal = valDecl?.type?.let { simpleClassNameOf(it) }
|
|
||||||
if (!typeFromVal.isNullOrBlank()) return typeFromVal
|
|
||||||
// If initializer exists, try to sniff ClassName(
|
|
||||||
val initR = valDecl?.initRange
|
|
||||||
if (initR != null) {
|
|
||||||
val src = mini.range.start.source
|
|
||||||
val s = src.offsetOf(initR.start)
|
|
||||||
val e = src.offsetOf(initR.end).coerceAtMost(text.length)
|
|
||||||
if (s in 0..e && e <= text.length) {
|
|
||||||
val init = text.substring(s, e)
|
|
||||||
Regex("([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(init)?.let { m ->
|
|
||||||
val cls = m.groupValues[1]
|
|
||||||
return cls
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 2) Parameters in any function (best-effort without scope mapping)
|
|
||||||
val paramType = mini.declarations.filterIsInstance<MiniFunDecl>()
|
|
||||||
.asSequence()
|
|
||||||
.flatMap { it.params.asSequence() }
|
|
||||||
.firstOrNull { it.name == ident }?.type
|
|
||||||
val typeFromParam = simpleClassNameOf(paramType)
|
|
||||||
if (!typeFromParam.isNullOrBlank()) return typeFromParam
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun guessReturnClassFromMemberCallBeforeMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
if (mini == null) return null
|
|
||||||
var i = TextCtx.prevNonWs(text, dotPos - 1)
|
|
||||||
if (i < 0 || text[i] != ')') return null
|
|
||||||
// back to matching '('
|
|
||||||
i--
|
|
||||||
var depth = 0
|
|
||||||
while (i >= 0) {
|
|
||||||
when (text[i]) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
if (i < 0 || text[i] != '(') return null
|
|
||||||
var j = i - 1
|
|
||||||
while (j >= 0 && text[j].isWhitespace()) j--
|
|
||||||
val end = j + 1
|
|
||||||
while (j >= 0 && TextCtx.isIdentChar(text[j])) j--
|
|
||||||
val start = j + 1
|
|
||||||
if (start >= end) return null
|
|
||||||
val callee = text.substring(start, end)
|
|
||||||
// Ensure member call: dot before callee
|
|
||||||
var k = start - 1
|
|
||||||
while (k >= 0 && text[k].isWhitespace()) k--
|
|
||||||
if (k < 0 || text[k] != '.') return null
|
|
||||||
val prevDot = k
|
|
||||||
// Resolve receiver class via MiniAst (ident like `x`)
|
|
||||||
val receiverClass = guessReceiverClassViaMini(mini, text, prevDot, imported) ?: return null
|
|
||||||
// If receiver class is a locally declared class, resolve member on it
|
|
||||||
val localClass = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == receiverClass }
|
|
||||||
if (localClass != null) {
|
|
||||||
val mm = localClass.members.firstOrNull { it.name == callee }
|
|
||||||
if (mm != null) {
|
|
||||||
val rt = when (mm) {
|
|
||||||
is MiniMemberFunDecl -> mm.returnType
|
|
||||||
is MiniMemberValDecl -> mm.type
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
return simpleClassNameOf(rt)
|
|
||||||
} else {
|
|
||||||
// Try to scan class body text for method signature and extract return type
|
|
||||||
val sigs = scanLocalClassMembersFromText(mini, text, localClass)
|
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Local scan for return type in ${receiverClass}.${callee}: candidates=${sigs.keys}")
|
|
||||||
val sig = sigs[callee]
|
|
||||||
if (sig != null && sig.typeText != null) return sig.typeText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Else fallback to registry-based resolution (covers imported classes)
|
|
||||||
return DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee)?.second?.let { m ->
|
|
||||||
val rt = when (m) {
|
|
||||||
is MiniMemberFunDecl -> m.returnType
|
|
||||||
is MiniMemberValDecl -> m.type
|
|
||||||
}
|
|
||||||
simpleClassNameOf(rt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class ScannedSig(val kind: String, val params: List<String>?, val typeText: String?)
|
|
||||||
|
|
||||||
private fun scanLocalClassMembersFromText(mini: MiniScript, text: String, cls: MiniClassDecl): Map<String, ScannedSig> {
|
|
||||||
val src = mini.range.start.source
|
|
||||||
val start = src.offsetOf(cls.bodyRange?.start ?: cls.range.start)
|
|
||||||
val end = src.offsetOf(cls.bodyRange?.end ?: cls.range.end).coerceAtMost(text.length)
|
|
||||||
if (start !in 0..end) return emptyMap()
|
|
||||||
val body = text.substring(start, end)
|
|
||||||
val map = LinkedHashMap<String, ScannedSig>()
|
|
||||||
// fun name(params): Type
|
|
||||||
val funRe = Regex("(?m)^\\s*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([^)]*)\\)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?")
|
|
||||||
for (m in funRe.findAll(body)) {
|
|
||||||
val name = m.groupValues.getOrNull(1) ?: continue
|
|
||||||
val params = m.groupValues.getOrNull(2)?.split(',')?.mapNotNull { it.trim().takeIf { it.isNotEmpty() } } ?: emptyList()
|
|
||||||
val type = m.groupValues.getOrNull(3)?.takeIf { it.isNotBlank() }
|
|
||||||
map[name] = ScannedSig("fun", params, type)
|
|
||||||
}
|
|
||||||
// val/var name: Type
|
|
||||||
val valRe = Regex("(?m)^\\s*(val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?")
|
|
||||||
for (m in valRe.findAll(body)) {
|
|
||||||
val kind = m.groupValues.getOrNull(1) ?: continue
|
|
||||||
val name = m.groupValues.getOrNull(2) ?: continue
|
|
||||||
val type = m.groupValues.getOrNull(3)?.takeIf { it.isNotBlank() }
|
|
||||||
map.putIfAbsent(name, ScannedSig(kind, null, type))
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
// 1) Try call-based: ClassName(...).
|
|
||||||
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
|
|
||||||
|
|
||||||
// 2) Literal heuristics based on the immediate char before '.'
|
|
||||||
var i = TextCtx.prevNonWs(text, dotPos - 1)
|
|
||||||
if (i >= 0) {
|
|
||||||
when (text[i]) {
|
|
||||||
'"' -> {
|
|
||||||
// Either regular or triple-quoted string; both map to String
|
|
||||||
return "String"
|
|
||||||
}
|
|
||||||
']' -> return "List" // very rough heuristic
|
|
||||||
'}' -> return "Dict" // map/dictionary literal heuristic
|
|
||||||
')' -> {
|
|
||||||
// Parenthesized expression: walk back to matching '(' and inspect inner expression
|
|
||||||
var j = i - 1
|
|
||||||
var depth = 0
|
|
||||||
while (j >= 0) {
|
|
||||||
when (text[j]) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
if (j >= 0 && text[j] == '(') {
|
|
||||||
val innerS = (j + 1).coerceAtLeast(0)
|
|
||||||
val innerE = i.coerceAtMost(text.length)
|
|
||||||
if (innerS < innerE) {
|
|
||||||
val inner = text.substring(innerS, innerE).trim()
|
|
||||||
if (inner.startsWith('"') && inner.endsWith('"')) return "String"
|
|
||||||
if (inner.startsWith('[') && inner.endsWith(']')) return "List"
|
|
||||||
if (inner.startsWith('{') && inner.endsWith('}')) return "Dict"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Numeric literal: support decimal, hex (0x..), and scientific notation (1e-3)
|
|
||||||
var j = i
|
|
||||||
var hasDigits = false
|
|
||||||
var hasDot = false
|
|
||||||
var hasExp = false
|
|
||||||
// Walk over digits, letters for hex, dots, and exponent markers
|
|
||||||
while (j >= 0) {
|
|
||||||
val ch = text[j]
|
|
||||||
if (ch.isDigit()) { hasDigits = true; j-- ; continue }
|
|
||||||
if (ch == '.') { hasDot = true; j-- ; continue }
|
|
||||||
if (ch == 'e' || ch == 'E') { hasExp = true; j-- ; // optional sign directly before digits
|
|
||||||
if (j >= 0 && (text[j] == '+' || text[j] == '-')) j--
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch in listOf('x','X')) { // part of 0x prefix
|
|
||||||
j--
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch == 'a' || ch == 'b' || ch == 'c' || ch == 'd' || ch == 'f' ||
|
|
||||||
ch == 'A' || ch == 'B' || ch == 'C' || ch == 'D' || ch == 'F') {
|
|
||||||
// hex digit in 0x...
|
|
||||||
j--
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Now check for 0x/0X prefix
|
|
||||||
val k = j
|
|
||||||
val isHex = k >= 1 && text[k] == '0' && (text[k+1] == 'x' || text[k+1] == 'X')
|
|
||||||
if (hasDigits) {
|
|
||||||
return if (isHex) "Int" else if (hasDot || hasExp) "Real" else "Int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to infer the class of the return value of the member call immediately before the dot.
|
|
||||||
* Example: `Path(".." ).lines().<caret>` → detects `lines()` on receiver class `Path` and returns `Iterator`.
|
|
||||||
*/
|
|
||||||
private fun guessReturnClassFromMemberCallBefore(text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
var i = TextCtx.prevNonWs(text, dotPos - 1)
|
|
||||||
if (i < 0) return null
|
|
||||||
// We expect a call just before the dot, i.e., ')' ... '.'
|
|
||||||
if (text[i] != ')') return null
|
|
||||||
// Walk back to matching '('
|
|
||||||
i--
|
|
||||||
var depth = 0
|
|
||||||
while (i >= 0) {
|
|
||||||
val ch = text[i]
|
|
||||||
when (ch) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
if (i < 0 || text[i] != '(') return null
|
|
||||||
// Identify callee identifier just before '('
|
|
||||||
var j = i - 1
|
|
||||||
while (j >= 0 && text[j].isWhitespace()) j--
|
|
||||||
val end = j + 1
|
|
||||||
while (j >= 0 && TextCtx.isIdentChar(text[j])) j--
|
|
||||||
val start = j + 1
|
|
||||||
if (start >= end) return null
|
|
||||||
val callee = text.substring(start, end)
|
|
||||||
// Ensure it's a member call (there must be a dot immediately before the callee, ignoring spaces)
|
|
||||||
var k = start - 1
|
|
||||||
while (k >= 0 && text[k].isWhitespace()) k--
|
|
||||||
if (k < 0 || text[k] != '.') return null
|
|
||||||
val prevDot = k
|
|
||||||
// Infer receiver class at the previous dot
|
|
||||||
val receiverClass = guessReceiverClass(text, prevDot, imported) ?: return null
|
|
||||||
// Resolve the callee as a member of receiver class, including inheritance
|
|
||||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee) ?: return null
|
|
||||||
val member = resolved.second
|
|
||||||
val returnType = when (member) {
|
|
||||||
is MiniMemberFunDecl -> member.returnType
|
|
||||||
is MiniMemberValDecl -> member.type
|
|
||||||
}
|
|
||||||
return simpleClassNameOf(returnType)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infer return class of a top-level call right before the dot: e.g., `files().<caret>`.
|
|
||||||
* We extract callee name and resolve it among imported modules' top-level functions.
|
|
||||||
*/
|
|
||||||
private fun guessReturnClassFromTopLevelCallBefore(text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
var i = TextCtx.prevNonWs(text, dotPos - 1)
|
|
||||||
if (i < 0 || text[i] != ')') return null
|
|
||||||
// Walk back to matching '('
|
|
||||||
i--
|
|
||||||
var depth = 0
|
|
||||||
while (i >= 0) {
|
|
||||||
val ch = text[i]
|
|
||||||
when (ch) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
if (i < 0 || text[i] != '(') return null
|
|
||||||
// Extract callee ident before '('
|
|
||||||
var j = i - 1
|
|
||||||
while (j >= 0 && text[j].isWhitespace()) j--
|
|
||||||
val end = j + 1
|
|
||||||
while (j >= 0 && TextCtx.isIdentChar(text[j])) j--
|
|
||||||
val start = j + 1
|
|
||||||
if (start >= end) return null
|
|
||||||
val callee = text.substring(start, end)
|
|
||||||
// If it's a member call, bail out (handled in member-call inference)
|
|
||||||
var k = start - 1
|
|
||||||
while (k >= 0 && text[k].isWhitespace()) k--
|
|
||||||
if (k >= 0 && text[k] == '.') return null
|
|
||||||
|
|
||||||
// Resolve top-level function in imported modules
|
|
||||||
for (mod in imported) {
|
|
||||||
val decls = BuiltinDocRegistry.docsForModule(mod)
|
|
||||||
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
|
|
||||||
if (fn != null) return simpleClassNameOf(fn.returnType)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback: if we can at least extract a callee name before the dot and it exists across common classes,
|
|
||||||
* derive its return type using cross-class lookup (Iterable/Iterator/List preference). This ignores the receiver.
|
|
||||||
* Example: `something.lines().<caret>` where `something` type is unknown, but `lines()` commonly returns Iterator<String>.
|
|
||||||
*/
|
|
||||||
private fun guessReturnClassAcrossKnownCallees(text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
var i = TextCtx.prevNonWs(text, dotPos - 1)
|
|
||||||
if (i < 0 || text[i] != ')') return null
|
|
||||||
// Walk back to matching '('
|
|
||||||
i--
|
|
||||||
var depth = 0
|
|
||||||
while (i >= 0) {
|
|
||||||
val ch = text[i]
|
|
||||||
when (ch) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
if (i < 0 || text[i] != '(') return null
|
|
||||||
// Extract callee ident before '('
|
|
||||||
var j = i - 1
|
|
||||||
while (j >= 0 && text[j].isWhitespace()) j--
|
|
||||||
val end = j + 1
|
|
||||||
while (j >= 0 && TextCtx.isIdentChar(text[j])) j--
|
|
||||||
val start = j + 1
|
|
||||||
if (start >= end) return null
|
|
||||||
val callee = text.substring(start, end)
|
|
||||||
// Try cross-class resolution
|
|
||||||
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, callee) ?: return null
|
|
||||||
val member = resolved.second
|
|
||||||
val returnType = when (member) {
|
|
||||||
is MiniMemberFunDecl -> member.returnType
|
|
||||||
is MiniMemberValDecl -> member.type
|
|
||||||
}
|
|
||||||
return simpleClassNameOf(returnType)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert a MiniTypeRef to a simple class name as used by docs (e.g., Iterator from Iterator<String>). */
|
|
||||||
private fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
|
|
||||||
null -> null
|
|
||||||
is MiniTypeName -> t.segments.lastOrNull()?.name
|
|
||||||
is MiniGenericType -> simpleClassNameOf(t.base)
|
|
||||||
is MiniFunctionType -> null
|
|
||||||
is MiniTypeVar -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildMiniAst(text: String): MiniScript? {
|
|
||||||
return try {
|
|
||||||
val sink = MiniAstBuilder()
|
|
||||||
val provider = IdeLenientImportProvider.create()
|
|
||||||
val src = Source("<ide>", text)
|
|
||||||
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
|
||||||
sink.build()
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cached per PsiFile by document modification stamp
|
|
||||||
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
|
||||||
private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp")
|
|
||||||
|
|
||||||
private fun buildMiniAstCached(file: PsiFile, text: String): MiniScript? {
|
|
||||||
val doc = file.viewProvider.document ?: return null
|
|
||||||
val stamp = doc.modificationStamp
|
|
||||||
val prevStamp = file.getUserData(STAMP_KEY)
|
|
||||||
val cached = file.getUserData(MINI_KEY)
|
|
||||||
if (cached != null && prevStamp != null && prevStamp == stamp) return cached
|
|
||||||
val built = buildMiniAst(text)
|
|
||||||
// Cache even null? avoid caching failures; only cache non-null
|
|
||||||
if (built != null) {
|
|
||||||
file.putUserData(MINI_KEY, built)
|
|
||||||
file.putUserData(STAMP_KEY, stamp)
|
|
||||||
}
|
|
||||||
return built
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun offerParamsInScope(emit: (com.intellij.codeInsight.lookup.LookupElement) -> Unit, mini: MiniScript, text: String, caret: Int) {
|
private fun offerParamsInScope(emit: (com.intellij.codeInsight.lookup.LookupElement) -> Unit, mini: MiniScript, text: String, caret: Int) {
|
||||||
val src = mini.range.start.source
|
val src = mini.range.start.source
|
||||||
// Find function whose body contains caret or whose whole range contains caret
|
// Find function whose body contains caret or whose whole range contains caret
|
||||||
@@ -906,7 +569,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
// Lenient textual import extractor (duplicated from QuickDoc privately)
|
// Lenient textual import extractor (duplicated from QuickDoc privately)
|
||||||
private fun extractImportsFromText(text: String): List<String> {
|
private fun extractImportsFromText(text: String): List<String> {
|
||||||
val result = LinkedHashSet<String>()
|
val result = LinkedHashSet<String>()
|
||||||
val re = Regex("(?m)^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)")
|
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
|
||||||
re.findAll(text).forEach { m ->
|
re.findAll(text).forEach { m ->
|
||||||
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
|
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
|
||||||
if (raw.isNotEmpty()) {
|
if (raw.isNotEmpty()) {
|
||||||
|
|||||||
+176
-70
@@ -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.
|
||||||
@@ -69,59 +69,164 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val ident = text.substring(idRange.startOffset, idRange.endOffset)
|
val ident = text.substring(idRange.startOffset, idRange.endOffset)
|
||||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: ident='$ident' at ${idRange.startOffset}..${idRange.endOffset} in ${file.name}")
|
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: ident='$ident' at ${idRange.startOffset}..${idRange.endOffset} in ${file.name}")
|
||||||
|
|
||||||
// Build MiniAst for this file (fast and resilient). Best-effort; on failure continue with registry lookup only.
|
// Build MiniAst for this file (fast and resilient). Best-effort; on failure continue with partial AST.
|
||||||
val sink = MiniAstBuilder()
|
val sink = MiniAstBuilder()
|
||||||
// Use lenient import provider so unresolved imports (e.g., lyng.io.fs) don't break docs
|
|
||||||
val provider = IdeLenientImportProvider.create()
|
val provider = IdeLenientImportProvider.create()
|
||||||
val src = Source("<ide>", text)
|
val src = Source("<ide>", text)
|
||||||
var mini: MiniScript? = try {
|
val mini = try {
|
||||||
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||||
sink.build()
|
sink.build()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
// Do not bail out completely: we still can resolve built-in and imported docs (e.g., println)
|
if (DEBUG_LOG) log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini produced partial AST: ${t.message}")
|
||||||
if (DEBUG_LOG) log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini failed: ${t.message}")
|
sink.build()
|
||||||
null
|
} ?: MiniScript(MiniRange(Pos(src, 1, 1), Pos(src, 1, 1)))
|
||||||
}
|
|
||||||
val haveMini = mini != null
|
|
||||||
if (mini == null) {
|
|
||||||
// Ensure we have a dummy script object to avoid NPE in downstream helpers that expect a MiniScript
|
|
||||||
mini = MiniScript(MiniRange(Pos(src, 1, 1), Pos(src, 1, 1)))
|
|
||||||
}
|
|
||||||
val source = src
|
val source = src
|
||||||
|
|
||||||
// Try resolve to: function param at position, function/class/val declaration at position
|
// Try resolve to: function param at position, function/class/val declaration at position
|
||||||
// 1) Check declarations whose name range contains offset
|
// 1) Use unified declaration detection
|
||||||
if (haveMini) for (d in mini.declarations) {
|
DocLookupUtils.findDeclarationAt(mini, offset, ident)?.let { (name, kind) ->
|
||||||
val s = source.offsetOf(d.nameStart)
|
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched declaration '$name' kind=$kind")
|
||||||
val e = (s + d.name.length).coerceAtMost(text.length)
|
// Find the actual declaration object to render
|
||||||
if (offset in s until e) {
|
for (d in mini.declarations) {
|
||||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched decl '${d.name}' kind=${d::class.simpleName}")
|
if (d.name == name && source.offsetOf(d.nameStart) <= offset && source.offsetOf(d.nameStart) + d.name.length > offset) {
|
||||||
return renderDeclDoc(d)
|
return renderDeclDoc(d)
|
||||||
|
}
|
||||||
|
// Handle members if it was a member
|
||||||
|
if (d is MiniClassDecl) {
|
||||||
|
for (m in d.members) {
|
||||||
|
if (m.name == name && source.offsetOf(m.nameStart) <= offset && source.offsetOf(m.nameStart) + m.name.length > offset) {
|
||||||
|
return when (m) {
|
||||||
|
is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m)
|
||||||
|
is MiniMemberValDecl -> renderMemberValDoc(d.name, m)
|
||||||
|
is MiniInitDecl -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (cf in d.ctorFields) {
|
||||||
|
if (cf.name == name && source.offsetOf(cf.nameStart) <= offset && source.offsetOf(cf.nameStart) + cf.name.length > offset) {
|
||||||
|
// Render as a member val
|
||||||
|
val mv = MiniMemberValDecl(
|
||||||
|
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||||
|
name = cf.name,
|
||||||
|
mutable = cf.mutable,
|
||||||
|
type = cf.type,
|
||||||
|
doc = null,
|
||||||
|
nameStart = cf.nameStart
|
||||||
|
)
|
||||||
|
return renderMemberValDoc(d.name, mv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (cf in d.classFields) {
|
||||||
|
if (cf.name == name && source.offsetOf(cf.nameStart) <= offset && source.offsetOf(cf.nameStart) + cf.name.length > offset) {
|
||||||
|
// Render as a member val
|
||||||
|
val mv = MiniMemberValDecl(
|
||||||
|
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||||
|
name = cf.name,
|
||||||
|
mutable = cf.mutable,
|
||||||
|
type = cf.type,
|
||||||
|
doc = null,
|
||||||
|
nameStart = cf.nameStart
|
||||||
|
)
|
||||||
|
return renderMemberValDoc(d.name, mv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (d is MiniEnumDecl) {
|
||||||
|
if (d.entries.contains(name) && offset >= source.offsetOf(d.range.start) && offset <= source.offsetOf(d.range.end)) {
|
||||||
|
// For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title
|
||||||
|
return "<div class='doc-title'>enum constant ${d.name}.${name}</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
// Check parameters
|
||||||
// 2) Check parameters of functions
|
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||||
if (haveMini) for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
for (p in fn.params) {
|
||||||
for (p in fn.params) {
|
if (p.name == name && source.offsetOf(p.nameStart) <= offset && source.offsetOf(p.nameStart) + p.name.length > offset) {
|
||||||
val s = source.offsetOf(p.nameStart)
|
return renderParamDoc(fn, p)
|
||||||
val e = (s + p.name.length).coerceAtMost(text.length)
|
}
|
||||||
if (offset in s until e) {
|
|
||||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched param '${p.name}' in fun '${fn.name}'")
|
|
||||||
return renderParamDoc(fn, p)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 3) Member-context resolution first (dot immediately before identifier): handle literals and calls
|
|
||||||
|
// 3) usages in current file via Binder (resolves local variables, parameters, and classes)
|
||||||
|
try {
|
||||||
|
val binding = net.sergeych.lyng.binding.Binder.bind(text, mini)
|
||||||
|
val ref = binding.references.firstOrNull { offset in it.start until it.end }
|
||||||
|
if (ref != null) {
|
||||||
|
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||||
|
if (sym != null) {
|
||||||
|
// Find local declaration that matches this symbol
|
||||||
|
val ds = mini.declarations.firstOrNull { decl ->
|
||||||
|
val s = source.offsetOf(decl.nameStart)
|
||||||
|
decl.name == sym.name && s == sym.declStart
|
||||||
|
}
|
||||||
|
if (ds != null) return renderDeclDoc(ds)
|
||||||
|
|
||||||
|
// Check parameters
|
||||||
|
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||||
|
for (p in fn.params) {
|
||||||
|
val s = source.offsetOf(p.nameStart)
|
||||||
|
if (p.name == sym.name && s == sym.declStart) {
|
||||||
|
return renderParamDoc(fn, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check class members (fields/functions)
|
||||||
|
for (cls in mini.declarations.filterIsInstance<MiniClassDecl>()) {
|
||||||
|
for (m in cls.members) {
|
||||||
|
val s = source.offsetOf(m.nameStart)
|
||||||
|
if (m.name == sym.name && s == sym.declStart) {
|
||||||
|
return when (m) {
|
||||||
|
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
|
||||||
|
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
|
||||||
|
is MiniInitDecl -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (cf in cls.ctorFields) {
|
||||||
|
val s = source.offsetOf(cf.nameStart)
|
||||||
|
if (cf.name == sym.name && s == sym.declStart) {
|
||||||
|
// Render as a member val
|
||||||
|
val mv = MiniMemberValDecl(
|
||||||
|
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||||
|
name = cf.name,
|
||||||
|
mutable = cf.mutable,
|
||||||
|
type = cf.type,
|
||||||
|
doc = null,
|
||||||
|
nameStart = cf.nameStart
|
||||||
|
)
|
||||||
|
return renderMemberValDoc(cls.name, mv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (cf in cls.classFields) {
|
||||||
|
val s = source.offsetOf(cf.nameStart)
|
||||||
|
if (cf.name == sym.name && s == sym.declStart) {
|
||||||
|
// Render as a member val
|
||||||
|
val mv = MiniMemberValDecl(
|
||||||
|
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||||
|
name = cf.name,
|
||||||
|
mutable = cf.mutable,
|
||||||
|
type = cf.type,
|
||||||
|
doc = null,
|
||||||
|
nameStart = cf.nameStart
|
||||||
|
)
|
||||||
|
return renderMemberValDoc(cls.name, mv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (DEBUG_LOG) log.warn("[LYNG_DEBUG] QuickDoc: local binder resolution failed: ${e.message}")
|
||||||
|
}
|
||||||
|
// 4) Member-context resolution first (dot immediately before identifier): handle literals and calls
|
||||||
run {
|
run {
|
||||||
val dotPos = TextCtx.findDotLeft(text, idRange.startOffset)
|
val dotPos = TextCtx.findDotLeft(text, idRange.startOffset)
|
||||||
?: TextCtx.findDotLeft(text, offset)
|
?: TextCtx.findDotLeft(text, offset)
|
||||||
if (dotPos != null) {
|
if (dotPos != null) {
|
||||||
// Build imported modules (MiniAst-derived if available, else lenient from text) and ensure stdlib is present
|
// Build imported modules (MiniAst-derived if available, else lenient from text) and ensure stdlib is present
|
||||||
var importedModules = if (haveMini) DocLookupUtils.canonicalImportedModules(mini) else emptyList()
|
val importedModules = DocLookupUtils.canonicalImportedModules(mini, text)
|
||||||
if (importedModules.isEmpty()) {
|
|
||||||
val fromText = extractImportsFromText(text)
|
|
||||||
importedModules = if (fromText.isEmpty()) listOf("lyng.stdlib") else fromText
|
|
||||||
}
|
|
||||||
if (!importedModules.contains("lyng.stdlib")) importedModules = importedModules + "lyng.stdlib"
|
|
||||||
|
|
||||||
// Try literal and call-based receiver inference around the dot
|
// Try literal and call-based receiver inference around the dot
|
||||||
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||||
@@ -154,7 +259,27 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
} else null
|
} else null
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules)
|
else -> {
|
||||||
|
val guessed = DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules)
|
||||||
|
if (guessed != null) guessed
|
||||||
|
else {
|
||||||
|
// handle this@Type or as Type
|
||||||
|
val i2 = TextCtx.prevNonWs(text, dotPos - 1)
|
||||||
|
if (i2 >= 0) {
|
||||||
|
val identRange = TextCtx.wordRangeAt(text, i2 + 1)
|
||||||
|
if (identRange != null) {
|
||||||
|
val id = text.substring(identRange.startOffset, identRange.endOffset)
|
||||||
|
val k = TextCtx.prevNonWs(text, identRange.startOffset - 1)
|
||||||
|
if (k >= 1 && text[k] == 's' && text[k-1] == 'a' && (k-1 == 0 || !text[k-2].isLetterOrDigit())) {
|
||||||
|
id
|
||||||
|
} else if (k >= 0 && text[k] == '@') {
|
||||||
|
val k2 = TextCtx.prevNonWs(text, k - 1)
|
||||||
|
if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") id else null
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: memberCtx dotPos=${dotPos} chBeforeDot='${if (dotPos>0) text[dotPos-1] else ' '}' classGuess=${className} imports=${importedModules}")
|
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: memberCtx dotPos=${dotPos} chBeforeDot='${if (dotPos>0) text[dotPos-1] else ' '}' classGuess=${className} imports=${importedModules}")
|
||||||
if (className != null) {
|
if (className != null) {
|
||||||
@@ -163,6 +288,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniInitDecl -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
|
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
|
||||||
@@ -171,21 +297,14 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
|
// 4) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
|
||||||
if (haveMini) mini.declarations.firstOrNull { it.name == ident }?.let {
|
mini.declarations.firstOrNull { it.name == ident }?.let {
|
||||||
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
|
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
|
||||||
return renderDeclDoc(it)
|
return renderDeclDoc(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
|
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
|
||||||
// Canonicalize import names using ImportManager, as users may write shortened names (e.g., "io.fs")
|
// Canonicalize import names using ImportManager, as users may write shortened names (e.g., "io.fs")
|
||||||
var importedModules = if (haveMini) DocLookupUtils.canonicalImportedModules(mini) else emptyList()
|
var importedModules = DocLookupUtils.canonicalImportedModules(mini, text)
|
||||||
// If MiniAst failed or captured no imports, try a lightweight textual import scan
|
|
||||||
if (importedModules.isEmpty()) {
|
|
||||||
val fromText = extractImportsFromText(text)
|
|
||||||
if (fromText.isNotEmpty()) {
|
|
||||||
importedModules = fromText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Always include stdlib as a fallback context
|
// Always include stdlib as a fallback context
|
||||||
if (!importedModules.contains("lyng.stdlib")) importedModules = importedModules + "lyng.stdlib"
|
if (!importedModules.contains("lyng.stdlib")) importedModules = importedModules + "lyng.stdlib"
|
||||||
// 4a) try top-level decls
|
// 4a) try top-level decls
|
||||||
@@ -204,8 +323,9 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
// Also allow values/consts
|
// Also allow values/consts
|
||||||
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
||||||
// And classes
|
// And classes/enums
|
||||||
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
||||||
|
docs.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
||||||
}
|
}
|
||||||
// Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs
|
// Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs
|
||||||
if (ident == "println" || ident == "print") {
|
if (ident == "println" || ident == "print") {
|
||||||
@@ -224,6 +344,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniInitDecl -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -242,6 +363,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniInitDecl -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -254,6 +376,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniInitDecl -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,6 +391,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (m) {
|
return when (m) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc("String", m)
|
is MiniMemberFunDecl -> renderMemberFunDoc("String", m)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc("String", m)
|
is MiniMemberValDecl -> renderMemberValDoc("String", m)
|
||||||
|
is MiniInitDecl -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,6 +401,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniInitDecl -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,25 +412,6 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Very lenient import extractor for cases when MiniAst is unavailable.
|
|
||||||
* Looks for lines like `import xxx.yyy` and returns canonical module names
|
|
||||||
* (prefixing with `lyng.` if missing).
|
|
||||||
*/
|
|
||||||
private fun extractImportsFromText(text: String): List<String> {
|
|
||||||
val result = LinkedHashSet<String>()
|
|
||||||
val re = Regex("(?m)^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)")
|
|
||||||
re.findAll(text).forEach { m ->
|
|
||||||
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
|
|
||||||
if (raw.isNotEmpty()) {
|
|
||||||
val canon = if (raw.startsWith("lyng.")) raw else "lyng.$raw"
|
|
||||||
result.add(canon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// External docs registrars discovery via reflection to avoid hard dependencies on optional modules
|
|
||||||
private val externalDocsLoaded: Boolean by lazy { tryLoadExternalDocs() }
|
private val externalDocsLoaded: Boolean by lazy { tryLoadExternalDocs() }
|
||||||
|
|
||||||
private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded }
|
private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded }
|
||||||
@@ -347,15 +453,15 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val title = when (d) {
|
val title = when (d) {
|
||||||
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
|
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
|
||||||
is MiniClassDecl -> "class ${d.name}"
|
is MiniClassDecl -> "class ${d.name}"
|
||||||
|
is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }"
|
||||||
is MiniValDecl -> if (d.mutable) "var ${d.name}${typeOf(d.type)}" else "val ${d.name}${typeOf(d.type)}"
|
is MiniValDecl -> if (d.mutable) "var ${d.name}${typeOf(d.type)}" else "val ${d.name}${typeOf(d.type)}"
|
||||||
else -> d.name
|
|
||||||
}
|
}
|
||||||
// Show full detailed documentation, not just the summary
|
// Show full detailed documentation, not just the summary
|
||||||
val raw = d.doc?.raw
|
val raw = d.doc?.raw
|
||||||
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
||||||
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc!!))
|
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +482,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
||||||
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc!!))
|
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +495,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
||||||
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc!!))
|
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -35,6 +35,8 @@ import com.intellij.psi.codeStyle.CodeStyleManager
|
|||||||
import net.sergeych.lyng.format.LyngFormatConfig
|
import net.sergeych.lyng.format.LyngFormatConfig
|
||||||
import net.sergeych.lyng.format.LyngFormatter
|
import net.sergeych.lyng.format.LyngFormatter
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
|
import net.sergeych.lyng.idea.util.FormattingUtils.computeDesiredIndent
|
||||||
|
import net.sergeych.lyng.idea.util.FormattingUtils.findFirstNonWs
|
||||||
|
|
||||||
class LyngEnterHandler : EnterHandlerDelegate {
|
class LyngEnterHandler : EnterHandlerDelegate {
|
||||||
private val log = Logger.getInstance(LyngEnterHandler::class.java)
|
private val log = Logger.getInstance(LyngEnterHandler::class.java)
|
||||||
@@ -80,16 +82,39 @@ class LyngEnterHandler : EnterHandlerDelegate {
|
|||||||
val trimmed = prevText.trimStart()
|
val trimmed = prevText.trimStart()
|
||||||
// consider only code part before // comment
|
// consider only code part before // comment
|
||||||
val code = trimmed.substringBefore("//").trim()
|
val code = trimmed.substringBefore("//").trim()
|
||||||
if (code == "}") {
|
if (code == "}" || code == "*/") {
|
||||||
// Previously we reindented the enclosed block on Enter after a lone '}'.
|
// Adjust indent for the previous line if it's a block or comment closer
|
||||||
// Per new behavior, this action is now bound to typing '}' instead.
|
val prevStart = doc.getLineStartOffset(prevLine)
|
||||||
// Keep Enter flow limited to indenting the new line only.
|
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
|
||||||
|
val desiredPrev = computeDesiredIndent(project, doc, prevLine)
|
||||||
|
val lineStartPrev = doc.getLineStartOffset(prevLine)
|
||||||
|
val lineEndPrev = doc.getLineEndOffset(prevLine)
|
||||||
|
val firstNonWsPrev = findFirstNonWs(doc, lineStartPrev, lineEndPrev)
|
||||||
|
val currentIndentLenPrev = firstNonWsPrev - lineStartPrev
|
||||||
|
if (doc.getText(TextRange(lineStartPrev, lineStartPrev + currentIndentLenPrev)) != desiredPrev) {
|
||||||
|
WriteCommandAction.runWriteCommandAction(project) {
|
||||||
|
doc.replaceString(lineStartPrev, lineStartPrev + currentIndentLenPrev, desiredPrev)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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)
|
||||||
@@ -159,35 +184,6 @@ class LyngEnterHandler : EnterHandlerDelegate {
|
|||||||
caret.moveToOffset(target)
|
caret.moveToOffset(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun computeDesiredIndent(project: Project, doc: Document, line: Int): String {
|
|
||||||
val options = CodeStyle.getIndentOptions(project, doc)
|
|
||||||
val start = 0
|
|
||||||
val end = doc.getLineEndOffset(line)
|
|
||||||
val snippet = doc.getText(TextRange(start, end))
|
|
||||||
val isBlankLine = doc.getLineText(line).trim().isEmpty()
|
|
||||||
val snippetForCalc = if (isBlankLine) snippet + "x" else snippet
|
|
||||||
val cfg = LyngFormatConfig(
|
|
||||||
indentSize = options.INDENT_SIZE.coerceAtLeast(1),
|
|
||||||
useTabs = options.USE_TAB_CHARACTER,
|
|
||||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
|
||||||
)
|
|
||||||
val formatted = LyngFormatter.reindent(snippetForCalc, cfg)
|
|
||||||
val lastNl = formatted.lastIndexOf('\n')
|
|
||||||
val lastLine = if (lastNl >= 0) formatted.substring(lastNl + 1) else formatted
|
|
||||||
val wsLen = lastLine.indexOfFirst { it != ' ' && it != '\t' }.let { if (it < 0) lastLine.length else it }
|
|
||||||
return lastLine.substring(0, wsLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findFirstNonWs(doc: Document, start: Int, end: Int): Int {
|
|
||||||
var i = start
|
|
||||||
val text = doc.charsSequence
|
|
||||||
while (i < end) {
|
|
||||||
val ch = text[i]
|
|
||||||
if (ch != ' ' && ch != '\t') break
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Document.safeLineNumber(offset: Int): Int =
|
private fun Document.safeLineNumber(offset: Int): Int =
|
||||||
getLineNumber(offset.coerceIn(0, textLength))
|
getLineNumber(offset.coerceIn(0, textLength))
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -32,31 +32,66 @@ import net.sergeych.lyng.format.LyngFormatConfig
|
|||||||
import net.sergeych.lyng.format.LyngFormatter
|
import net.sergeych.lyng.format.LyngFormatter
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
||||||
|
import net.sergeych.lyng.idea.util.FormattingUtils.computeDesiredIndent
|
||||||
|
import net.sergeych.lyng.idea.util.FormattingUtils.findFirstNonWs
|
||||||
|
|
||||||
class LyngTypedHandler : TypedHandlerDelegate() {
|
class LyngTypedHandler : TypedHandlerDelegate() {
|
||||||
private val log = Logger.getInstance(LyngTypedHandler::class.java)
|
private val log = Logger.getInstance(LyngTypedHandler::class.java)
|
||||||
|
|
||||||
override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
|
override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
|
||||||
if (file.language != LyngLanguage) return Result.CONTINUE
|
if (file.language != LyngLanguage) return Result.CONTINUE
|
||||||
if (c != '}') return Result.CONTINUE
|
|
||||||
|
|
||||||
val doc = editor.document
|
if (c == '}') {
|
||||||
PsiDocumentManager.getInstance(project).commitDocument(doc)
|
val doc = editor.document
|
||||||
|
PsiDocumentManager.getInstance(project).commitDocument(doc)
|
||||||
|
|
||||||
val offset = editor.caretModel.offset
|
val offset = editor.caretModel.offset
|
||||||
val line = doc.getLineNumber((offset - 1).coerceAtLeast(0))
|
val line = doc.getLineNumber((offset - 1).coerceAtLeast(0))
|
||||||
if (line < 0) return Result.CONTINUE
|
if (line < 0) return Result.CONTINUE
|
||||||
|
|
||||||
val rawLine = doc.getLineText(line)
|
val rawLine = doc.getLineText(line)
|
||||||
val code = rawLine.substringBefore("//").trim()
|
val code = rawLine.substringBefore("//").trim()
|
||||||
if (code == "}") {
|
if (code == "}") {
|
||||||
val settings = LyngFormatterSettings.getInstance(project)
|
val settings = LyngFormatterSettings.getInstance(project)
|
||||||
if (settings.reindentClosedBlockOnEnter) {
|
if (settings.reindentClosedBlockOnEnter) {
|
||||||
reindentClosedBlockAroundBrace(project, file, doc, line)
|
reindentClosedBlockAroundBrace(project, file, doc, line)
|
||||||
|
}
|
||||||
|
// 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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (c == '/') {
|
||||||
|
val doc = editor.document
|
||||||
|
val offset = editor.caretModel.offset
|
||||||
|
if (offset >= 2 && doc.getText(TextRange(offset - 2, offset)) == "*/") {
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual application fallback
|
||||||
|
val desired = computeDesiredIndent(project, doc, line)
|
||||||
|
val lineEnd = doc.getLineEndOffset(line)
|
||||||
|
val firstNonWs = findFirstNonWs(doc, lineStart, lineEnd)
|
||||||
|
val currentIndentLen = firstNonWs - lineStart
|
||||||
|
if (doc.getText(TextRange(lineStart, lineStart + currentIndentLen)) != desired) {
|
||||||
|
WriteCommandAction.runWriteCommandAction(project) {
|
||||||
|
doc.replaceString(lineStart, lineStart + currentIndentLen, desired)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// After block reindent, adjust line indent to what platform thinks (no-op in many cases)
|
|
||||||
val lineStart = doc.getLineStartOffset(line)
|
|
||||||
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
|
||||||
}
|
}
|
||||||
return Result.CONTINUE
|
return Result.CONTINUE
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -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,7 +42,7 @@ private class LineBlocksRootBlock(
|
|||||||
private val file: PsiFile,
|
private val file: PsiFile,
|
||||||
private val settings: CodeStyleSettings
|
private val settings: CodeStyleSettings
|
||||||
) : Block {
|
) : Block {
|
||||||
override fun getTextRange(): TextRange = file.textRange
|
override fun getTextRange(): TextRange = TextRange(0, file.textLength)
|
||||||
|
|
||||||
override fun getSubBlocks(): List<Block> = emptyList()
|
override fun getSubBlocks(): List<Block> = emptyList()
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ private class LineBlocksRootBlock(
|
|||||||
override fun getSpacing(child1: Block?, child2: Block): Spacing? = null
|
override fun getSpacing(child1: Block?, child2: Block): Spacing? = null
|
||||||
override fun getChildAttributes(newChildIndex: Int): ChildAttributes = ChildAttributes(Indent.getNoneIndent(), null)
|
override fun getChildAttributes(newChildIndex: Int): ChildAttributes = ChildAttributes(Indent.getNoneIndent(), null)
|
||||||
override fun isIncomplete(): Boolean = false
|
override fun isIncomplete(): Boolean = false
|
||||||
override fun isLeaf(): Boolean = false
|
override fun isLeaf(): Boolean = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intentionally no sub-blocks/spacing: indentation is handled by PreFormatProcessor + LineIndentProvider
|
// Intentionally no sub-blocks/spacing: indentation is handled by PreFormatProcessor + LineIndentProvider
|
||||||
@@ -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.
|
||||||
@@ -24,9 +24,8 @@ import com.intellij.openapi.util.TextRange
|
|||||||
import com.intellij.psi.PsiDocumentManager
|
import com.intellij.psi.PsiDocumentManager
|
||||||
import com.intellij.psi.codeStyle.CommonCodeStyleSettings.IndentOptions
|
import com.intellij.psi.codeStyle.CommonCodeStyleSettings.IndentOptions
|
||||||
import com.intellij.psi.codeStyle.lineIndent.LineIndentProvider
|
import com.intellij.psi.codeStyle.lineIndent.LineIndentProvider
|
||||||
import net.sergeych.lyng.format.LyngFormatConfig
|
|
||||||
import net.sergeych.lyng.format.LyngFormatter
|
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
|
import net.sergeych.lyng.idea.util.FormattingUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightweight indentation provider for Lyng.
|
* Lightweight indentation provider for Lyng.
|
||||||
@@ -45,8 +44,7 @@ class LyngLineIndentProvider : LineIndentProvider {
|
|||||||
val options = CodeStyle.getIndentOptions(project, doc)
|
val options = CodeStyle.getIndentOptions(project, doc)
|
||||||
|
|
||||||
val line = doc.getLineNumberSafe(offset)
|
val line = doc.getLineNumberSafe(offset)
|
||||||
val indent = computeDesiredIndentFromCore(doc, line, options)
|
return FormattingUtils.computeDesiredIndent(project, doc, line)
|
||||||
return indent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isSuitableFor(language: Language?): Boolean = language == null || language == LyngLanguage
|
override fun isSuitableFor(language: Language?): Boolean = language == null || language == LyngLanguage
|
||||||
@@ -79,25 +77,4 @@ class LyngLineIndentProvider : LineIndentProvider {
|
|||||||
return spaces / size
|
return spaces / size
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun computeDesiredIndentFromCore(doc: Document, line: Int, options: IndentOptions): String {
|
|
||||||
// Build a minimal text consisting of all previous lines and the current line.
|
|
||||||
// Special case: when the current line is blank (newly created by Enter), compute the
|
|
||||||
// indent as if there was a non-whitespace character at line start (append a sentinel).
|
|
||||||
val start = 0
|
|
||||||
val end = doc.getLineEndOffset(line)
|
|
||||||
val snippet = doc.getText(TextRange(start, end))
|
|
||||||
val isBlankLine = doc.getLineText(line).trim().isEmpty()
|
|
||||||
val snippetForCalc = if (isBlankLine) snippet + "x" else snippet
|
|
||||||
val cfg = LyngFormatConfig(
|
|
||||||
indentSize = options.INDENT_SIZE.coerceAtLeast(1),
|
|
||||||
useTabs = options.USE_TAB_CHARACTER,
|
|
||||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
|
||||||
)
|
|
||||||
val formatted = LyngFormatter.reindent(snippetForCalc, cfg)
|
|
||||||
// Grab the last line's leading whitespace as the indent for the current line
|
|
||||||
val lastNl = formatted.lastIndexOf('\n')
|
|
||||||
val lastLine = if (lastNl >= 0) formatted.substring(lastNl + 1) else formatted
|
|
||||||
val wsLen = lastLine.indexOfFirst { it != ' ' && it != '\t' }.let { if (it < 0) lastLine.length else it }
|
|
||||||
return lastLine.substring(0, wsLen)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
@@ -44,25 +44,67 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
// When both spacing and wrapping are OFF, still fix indentation for the whole file to
|
// When both spacing and wrapping are OFF, still fix indentation for the whole file to
|
||||||
// guarantee visible changes on Reformat Code.
|
// guarantee visible changes on Reformat Code.
|
||||||
val runFullFileIndent = !settings.enableSpacing && !settings.enableWrapping
|
val runFullFileIndent = !settings.enableSpacing && !settings.enableWrapping
|
||||||
// Maintain a working range and a modification flag to avoid stale offsets after replacements
|
|
||||||
var modified = false
|
var modified = false
|
||||||
fun fullRange(): TextRange = TextRange(0, doc.textLength)
|
|
||||||
var workingRange: TextRange = range.intersection(fullRange()) ?: fullRange()
|
|
||||||
|
|
||||||
val startLine = if (runFullFileIndent) 0 else doc.getLineNumber(workingRange.startOffset)
|
val docW = doc as? com.intellij.injected.editor.DocumentWindow
|
||||||
val endLine = if (runFullFileIndent) (doc.lineCount - 1).coerceAtLeast(0)
|
// The host range of the entire injected fragment (or the whole file if not injected).
|
||||||
else doc.getLineNumber(workingRange.endOffset.coerceAtMost(doc.textLength))
|
fun currentHostRange(): TextRange = if (docW != null) {
|
||||||
|
TextRange(docW.injectedToHost(0), docW.injectedToHost(doc.textLength))
|
||||||
|
} else {
|
||||||
|
file.textRange
|
||||||
|
}
|
||||||
|
|
||||||
|
// The range in 'doc' coordinate system (local 0..len for injections, host offsets for normal files).
|
||||||
|
fun currentLocalRange(): TextRange = if (docW != null) {
|
||||||
|
TextRange(0, doc.textLength)
|
||||||
|
} else {
|
||||||
|
file.textRange
|
||||||
|
}
|
||||||
|
|
||||||
|
val clr = currentLocalRange()
|
||||||
|
val chr = currentHostRange()
|
||||||
|
|
||||||
|
// Convert the input range to the coordinate system of 'doc'
|
||||||
|
var workingRangeLocal: TextRange = if (docW != null) {
|
||||||
|
val hostIntersection = range.intersection(chr)
|
||||||
|
if (hostIntersection != null) {
|
||||||
|
try {
|
||||||
|
val start = docW.hostToInjected(hostIntersection.startOffset)
|
||||||
|
val end = docW.hostToInjected(hostIntersection.endOffset)
|
||||||
|
TextRange(start.coerceAtMost(end), end.coerceAtLeast(start))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
clr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
range.intersection(clr) ?: clr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
range.intersection(clr) ?: clr
|
||||||
|
}
|
||||||
|
|
||||||
|
val startLine = if (runFullFileIndent) {
|
||||||
|
doc.getLineNumber(currentLocalRange().startOffset)
|
||||||
|
} else {
|
||||||
|
doc.getLineNumber(workingRangeLocal.startOffset)
|
||||||
|
}
|
||||||
|
val endLine = if (runFullFileIndent) {
|
||||||
|
if (clr.endOffset <= clr.startOffset) doc.getLineNumber(clr.startOffset)
|
||||||
|
else doc.getLineNumber(clr.endOffset)
|
||||||
|
} else {
|
||||||
|
doc.getLineNumber(workingRangeLocal.endOffset.coerceAtMost(doc.textLength))
|
||||||
|
}
|
||||||
|
|
||||||
fun codePart(s: String): String {
|
fun codePart(s: String): String {
|
||||||
val idx = s.indexOf("//")
|
val idx = s.indexOf("//")
|
||||||
return if (idx >= 0) s.substring(0, idx) else s
|
return if (idx >= 0) s.substring(0, idx) else s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-scan to compute balances up to startLine
|
// Pre-scan to compute balances up to startLine.
|
||||||
|
val fragmentStartLine = doc.getLineNumber(currentLocalRange().startOffset)
|
||||||
var blockLevel = 0
|
var blockLevel = 0
|
||||||
var parenBalance = 0
|
var parenBalance = 0
|
||||||
var bracketBalance = 0
|
var bracketBalance = 0
|
||||||
for (ln in 0 until startLine) {
|
for (ln in fragmentStartLine until startLine) {
|
||||||
val text = doc.getText(TextRange(doc.getLineStartOffset(ln), doc.getLineEndOffset(ln)))
|
val text = doc.getText(TextRange(doc.getLineStartOffset(ln), doc.getLineEndOffset(ln)))
|
||||||
for (ch in codePart(text)) when (ch) {
|
for (ch in codePart(text)) when (ch) {
|
||||||
'{' -> blockLevel++
|
'{' -> blockLevel++
|
||||||
@@ -80,7 +122,13 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -103,15 +151,14 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
useTabs = options.USE_TAB_CHARACTER,
|
useTabs = options.USE_TAB_CHARACTER,
|
||||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
||||||
)
|
)
|
||||||
val full = fullRange()
|
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||||
val r = if (runFullFileIndent) full else workingRange.intersection(full) ?: full
|
|
||||||
val text = doc.getText(r)
|
val text = doc.getText(r)
|
||||||
val formatted = LyngFormatter.reindent(text, cfg)
|
val formatted = LyngFormatter.reindent(text, cfg)
|
||||||
if (formatted != text) {
|
if (formatted != text) {
|
||||||
doc.replaceString(r.startOffset, r.endOffset, formatted)
|
doc.replaceString(r.startOffset, r.endOffset, formatted)
|
||||||
modified = true
|
modified = true
|
||||||
psiDoc.commitDocument(doc)
|
psiDoc.commitDocument(doc)
|
||||||
workingRange = fullRange()
|
workingRangeLocal = currentLocalRange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,14 +171,14 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
applySpacing = true,
|
applySpacing = true,
|
||||||
applyWrapping = false,
|
applyWrapping = false,
|
||||||
)
|
)
|
||||||
val safe = workingRange.intersection(fullRange()) ?: fullRange()
|
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||||
val text = doc.getText(safe)
|
val text = doc.getText(r)
|
||||||
val formatted = LyngFormatter.format(text, cfg)
|
val formatted = LyngFormatter.format(text, cfg)
|
||||||
if (formatted != text) {
|
if (formatted != text) {
|
||||||
doc.replaceString(safe.startOffset, safe.endOffset, formatted)
|
doc.replaceString(r.startOffset, r.endOffset, formatted)
|
||||||
modified = true
|
modified = true
|
||||||
psiDoc.commitDocument(doc)
|
psiDoc.commitDocument(doc)
|
||||||
workingRange = fullRange()
|
workingRangeLocal = currentLocalRange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Optionally apply wrapping (after spacing) when enabled
|
// Optionally apply wrapping (after spacing) when enabled
|
||||||
@@ -143,17 +190,19 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
applySpacing = settings.enableSpacing,
|
applySpacing = settings.enableSpacing,
|
||||||
applyWrapping = true,
|
applyWrapping = true,
|
||||||
)
|
)
|
||||||
val safe2 = workingRange.intersection(fullRange()) ?: fullRange()
|
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||||
val text2 = doc.getText(safe2)
|
val text = doc.getText(r)
|
||||||
val wrapped = LyngFormatter.format(text2, cfg)
|
val wrapped = LyngFormatter.format(text, cfg)
|
||||||
if (wrapped != text2) {
|
if (wrapped != text) {
|
||||||
doc.replaceString(safe2.startOffset, safe2.endOffset, wrapped)
|
doc.replaceString(r.startOffset, r.endOffset, wrapped)
|
||||||
modified = true
|
modified = true
|
||||||
psiDoc.commitDocument(doc)
|
psiDoc.commitDocument(doc)
|
||||||
workingRange = fullRange()
|
workingRangeLocal = currentLocalRange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Return a safe range for the formatter to continue with, preventing stale offsets
|
// Return a safe range for the formatter to continue with, preventing stale offsets.
|
||||||
return if (modified) fullRange() else (range.intersection(fullRange()) ?: fullRange())
|
// For injected files, ALWAYS return a range in local coordinates.
|
||||||
|
val finalRange = currentLocalRange()
|
||||||
|
return if (modified) finalRange else (range.intersection(finalRange) ?: finalRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -579,7 +579,8 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
|
|||||||
val s = w.lowercase()
|
val s = w.lowercase()
|
||||||
return s in setOf(
|
return s in setOf(
|
||||||
// common code words / language keywords to avoid noise
|
// common code words / language keywords to avoid noise
|
||||||
"val","var","fun","class","enum","type","import","package","return","if","else","when","while","for","try","catch","finally","true","false","null",
|
"val","var","fun","class","interface","enum","type","import","package","return","if","else","when","while","for","try","catch","finally","true","false","null",
|
||||||
|
"abstract","closed","override",
|
||||||
// very common English words
|
// very common English words
|
||||||
"the","and","or","not","with","from","into","this","that","file","found","count","name","value","object"
|
"the","and","or","not","with","from","into","this","that","file","found","count","name","value","object"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ class LyngLexer : LexerBase() {
|
|||||||
private var myTokenType: IElementType? = null
|
private var myTokenType: IElementType? = null
|
||||||
|
|
||||||
private val keywords = setOf(
|
private val keywords = setOf(
|
||||||
"fun", "val", "var", "class", "type", "import", "as",
|
"fun", "val", "var", "class", "interface", "type", "import", "as",
|
||||||
|
"abstract", "closed", "override",
|
||||||
"if", "else", "for", "while", "return", "true", "false", "null",
|
"if", "else", "for", "while", "return", "true", "false", "null",
|
||||||
"when", "in", "is", "break", "continue", "try", "catch", "finally"
|
"when", "in", "is", "break", "continue", "try", "catch", "finally",
|
||||||
|
"get", "set"
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) {
|
override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) {
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation
|
||||||
|
|
||||||
|
import com.intellij.icons.AllIcons
|
||||||
|
import com.intellij.navigation.ItemPresentation
|
||||||
|
import com.intellij.openapi.util.TextRange
|
||||||
|
import com.intellij.psi.PsiDocumentManager
|
||||||
|
import com.intellij.psi.PsiElement
|
||||||
|
import com.intellij.psi.PsiFile
|
||||||
|
import com.intellij.psi.PsiNameIdentifierOwner
|
||||||
|
import com.intellij.psi.impl.light.LightElement
|
||||||
|
import com.intellij.util.IncorrectOperationException
|
||||||
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A light PSI element representing a Lyng declaration (function, class, enum, or variable).
|
||||||
|
* Used for navigation and to provide a stable anchor for "Find Usages".
|
||||||
|
*/
|
||||||
|
class LyngDeclarationElement(
|
||||||
|
private val nameElement: PsiElement,
|
||||||
|
private val name: String,
|
||||||
|
val kind: String = "declaration"
|
||||||
|
) : LightElement(nameElement.manager, LyngLanguage), PsiNameIdentifierOwner {
|
||||||
|
|
||||||
|
override fun getName(): String = name
|
||||||
|
|
||||||
|
override fun setName(name: String): PsiElement {
|
||||||
|
throw IncorrectOperationException("Renaming is not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNameIdentifier(): PsiElement = nameElement
|
||||||
|
|
||||||
|
override fun getNavigationElement(): PsiElement = nameElement
|
||||||
|
|
||||||
|
override fun getTextRange(): TextRange = nameElement.textRange
|
||||||
|
|
||||||
|
override fun getContainingFile(): PsiFile = nameElement.containingFile
|
||||||
|
|
||||||
|
override fun isValid(): Boolean = nameElement.isValid
|
||||||
|
|
||||||
|
override fun getPresentation(): ItemPresentation {
|
||||||
|
return object : ItemPresentation {
|
||||||
|
override fun getPresentableText(): String = name
|
||||||
|
override fun getLocationString(): String {
|
||||||
|
val file = containingFile
|
||||||
|
val document = PsiDocumentManager.getInstance(file.project).getDocument(file)
|
||||||
|
val line = if (document != null) document.getLineNumber(textRange.startOffset) + 1 else "?"
|
||||||
|
val column = if (document != null) {
|
||||||
|
val lineStart = document.getLineStartOffset(document.getLineNumber(textRange.startOffset))
|
||||||
|
textRange.startOffset - lineStart + 1
|
||||||
|
} else "?"
|
||||||
|
return "${file.name}:$line:$column"
|
||||||
|
}
|
||||||
|
override fun getIcon(unused: Boolean): Icon {
|
||||||
|
return when (kind) {
|
||||||
|
"Function" -> AllIcons.Nodes.Function
|
||||||
|
"Class" -> AllIcons.Nodes.Class
|
||||||
|
"Enum" -> AllIcons.Nodes.Enum
|
||||||
|
"EnumConstant" -> AllIcons.Nodes.Enum
|
||||||
|
"Variable" -> AllIcons.Nodes.Variable
|
||||||
|
"Value" -> AllIcons.Nodes.Field
|
||||||
|
"Parameter" -> AllIcons.Nodes.Parameter
|
||||||
|
"Initializer" -> AllIcons.Nodes.Method
|
||||||
|
else -> AllIcons.Nodes.Property
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "$kind:$name"
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is LyngDeclarationElement) return false
|
||||||
|
return name == other.name && nameElement == other.nameElement
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isEquivalentTo(another: PsiElement?): Boolean {
|
||||||
|
if (this === another) return true
|
||||||
|
if (another == nameElement) return true
|
||||||
|
if (another is LyngDeclarationElement) {
|
||||||
|
return name == another.name && nameElement == another.nameElement
|
||||||
|
}
|
||||||
|
return super.isEquivalentTo(another)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = nameElement.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation
|
||||||
|
|
||||||
|
import com.intellij.lang.cacheBuilder.DefaultWordsScanner
|
||||||
|
import com.intellij.lang.cacheBuilder.WordsScanner
|
||||||
|
import com.intellij.lang.findUsages.FindUsagesProvider
|
||||||
|
import com.intellij.psi.PsiDocumentManager
|
||||||
|
import com.intellij.psi.PsiElement
|
||||||
|
import com.intellij.psi.tree.TokenSet
|
||||||
|
import net.sergeych.lyng.idea.highlight.LyngLexer
|
||||||
|
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
|
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||||
|
|
||||||
|
class LyngFindUsagesProvider : FindUsagesProvider {
|
||||||
|
override fun getWordsScanner(): WordsScanner {
|
||||||
|
return DefaultWordsScanner(
|
||||||
|
LyngLexer(),
|
||||||
|
TokenSet.create(LyngTokenTypes.IDENTIFIER),
|
||||||
|
TokenSet.create(LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT),
|
||||||
|
TokenSet.create(LyngTokenTypes.STRING)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canFindUsagesFor(psiElement: PsiElement): Boolean {
|
||||||
|
return psiElement is LyngDeclarationElement || isDeclaration(psiElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDeclaration(element: PsiElement): Boolean {
|
||||||
|
val file = element.containingFile ?: return false
|
||||||
|
val mini = LyngAstManager.getMiniAst(file) ?: return false
|
||||||
|
val offset = element.textRange.startOffset
|
||||||
|
val name = element.text ?: ""
|
||||||
|
return DocLookupUtils.findDeclarationAt(mini, offset, name) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHelpId(psiElement: PsiElement): String? = null
|
||||||
|
|
||||||
|
override fun getType(element: PsiElement): String {
|
||||||
|
if (element is LyngDeclarationElement) return element.kind
|
||||||
|
val file = element.containingFile ?: return "Lyng declaration"
|
||||||
|
val mini = LyngAstManager.getMiniAst(file) ?: return "Lyng declaration"
|
||||||
|
val info = DocLookupUtils.findDeclarationAt(mini, element.textRange.startOffset, element.text ?: "")
|
||||||
|
return info?.second ?: "Lyng declaration"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDescriptiveName(element: PsiElement): String {
|
||||||
|
if (element is LyngDeclarationElement) {
|
||||||
|
val file = element.containingFile
|
||||||
|
val document = PsiDocumentManager.getInstance(file.project).getDocument(file)
|
||||||
|
val line = if (document != null) document.getLineNumber(element.textRange.startOffset) + 1 else "?"
|
||||||
|
val column = if (document != null) {
|
||||||
|
val lineStart = document.getLineStartOffset(document.getLineNumber(element.textRange.startOffset))
|
||||||
|
element.textRange.startOffset - lineStart + 1
|
||||||
|
} else "?"
|
||||||
|
return "${element.name} (${file.name}:$line:$column)"
|
||||||
|
}
|
||||||
|
val file = element.containingFile ?: return element.text ?: "unknown"
|
||||||
|
val mini = LyngAstManager.getMiniAst(file) ?: return element.text ?: "unknown"
|
||||||
|
val info = DocLookupUtils.findDeclarationAt(mini, element.textRange.startOffset, element.text ?: "")
|
||||||
|
|
||||||
|
val document = PsiDocumentManager.getInstance(file.project).getDocument(file)
|
||||||
|
val line = if (document != null) document.getLineNumber(element.textRange.startOffset) + 1 else "?"
|
||||||
|
val column = if (document != null) {
|
||||||
|
val lineStart = document.getLineStartOffset(document.getLineNumber(element.textRange.startOffset))
|
||||||
|
element.textRange.startOffset - lineStart + 1
|
||||||
|
} else "?"
|
||||||
|
|
||||||
|
val name = info?.first ?: element.text ?: "unknown"
|
||||||
|
return "$name (${file.name}:$line:$column)"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNodeText(element: PsiElement, useFullName: Boolean): String {
|
||||||
|
return (element as? LyngDeclarationElement)?.name ?: element.text ?: "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation
|
||||||
|
|
||||||
|
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
|
||||||
|
import com.intellij.openapi.editor.Editor
|
||||||
|
import com.intellij.psi.PsiElement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures Ctrl+B (Go to Definition) works on Lyng identifiers by resolving through LyngPsiReference.
|
||||||
|
*/
|
||||||
|
class LyngGotoDeclarationHandler : GotoDeclarationHandler {
|
||||||
|
override fun getGotoDeclarationTargets(sourceElement: PsiElement?, offset: Int, editor: Editor?): Array<PsiElement>? {
|
||||||
|
if (sourceElement == null) return null
|
||||||
|
|
||||||
|
val allTargets = mutableListOf<PsiElement>()
|
||||||
|
|
||||||
|
// Find reference at the element or its parent (sometimes the identifier token is wrapped)
|
||||||
|
val ref = sourceElement.reference ?: sourceElement.parent?.reference
|
||||||
|
if (ref is LyngPsiReference) {
|
||||||
|
val resolved = ref.multiResolve(false)
|
||||||
|
allTargets.addAll(resolved.mapNotNull { it.element })
|
||||||
|
} else {
|
||||||
|
// Manual check if not picked up by reference (e.g. if contributor didn't run yet)
|
||||||
|
val manualRef = LyngPsiReference(sourceElement)
|
||||||
|
val manualResolved = manualRef.multiResolve(false)
|
||||||
|
allTargets.addAll(manualResolved.mapNotNull { it.element })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTargets.isEmpty()) return null
|
||||||
|
|
||||||
|
// If there is only one target and it's equivalent to the source, return null.
|
||||||
|
// This allows IDEA to treat it as a declaration site and trigger "Show Usages".
|
||||||
|
if (allTargets.size == 1) {
|
||||||
|
val target = allTargets[0]
|
||||||
|
if (target == sourceElement || target.isEquivalentTo(sourceElement)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allTargets.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation
|
||||||
|
|
||||||
|
import com.intellij.icons.AllIcons
|
||||||
|
import com.intellij.ide.IconProvider
|
||||||
|
import com.intellij.psi.PsiElement
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
|
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||||
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
class LyngIconProvider : IconProvider() {
|
||||||
|
override fun getIcon(element: PsiElement, flags: Int): Icon? {
|
||||||
|
val file = element.containingFile ?: return null
|
||||||
|
val mini = LyngAstManager.getMiniAst(file) ?: return null
|
||||||
|
|
||||||
|
val info = DocLookupUtils.findDeclarationAt(mini, element.textRange.startOffset, element.text ?: "")
|
||||||
|
if (info != null) {
|
||||||
|
return when (info.second) {
|
||||||
|
"Function" -> AllIcons.Nodes.Function
|
||||||
|
"Class" -> AllIcons.Nodes.Class
|
||||||
|
"Enum" -> AllIcons.Nodes.Enum
|
||||||
|
"EnumConstant" -> AllIcons.Nodes.Enum
|
||||||
|
"Variable" -> AllIcons.Nodes.Variable
|
||||||
|
"Value" -> AllIcons.Nodes.Field
|
||||||
|
"Parameter" -> AllIcons.Nodes.Parameter
|
||||||
|
"Initializer" -> AllIcons.Nodes.Method
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation
|
||||||
|
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.util.TextRange
|
||||||
|
import com.intellij.psi.*
|
||||||
|
import com.intellij.psi.search.FilenameIndex
|
||||||
|
import com.intellij.psi.search.GlobalSearchScope
|
||||||
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
|
import net.sergeych.lyng.idea.util.TextCtx
|
||||||
|
import net.sergeych.lyng.miniast.*
|
||||||
|
|
||||||
|
class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiElement>(element, TextRange(0, element.textLength)) {
|
||||||
|
|
||||||
|
override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
|
||||||
|
val file = element.containingFile
|
||||||
|
val text = file.text
|
||||||
|
val offset = element.textRange.startOffset
|
||||||
|
val name = element.text ?: ""
|
||||||
|
val results = mutableListOf<ResolveResult>()
|
||||||
|
|
||||||
|
val mini = LyngAstManager.getMiniAst(file) ?: return emptyArray()
|
||||||
|
val binding = LyngAstManager.getBinding(file)
|
||||||
|
|
||||||
|
// 1. Member resolution (obj.member)
|
||||||
|
val dotPos = TextCtx.findDotLeft(text, offset)
|
||||||
|
if (dotPos != null) {
|
||||||
|
val imported = DocLookupUtils.canonicalImportedModules(mini, text)
|
||||||
|
val receiverClass = DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, imported, binding)
|
||||||
|
?: DocLookupUtils.guessReceiverClass(text, dotPos, imported, mini)
|
||||||
|
|
||||||
|
if (receiverClass != null) {
|
||||||
|
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, name, mini)
|
||||||
|
if (resolved != null) {
|
||||||
|
val owner = resolved.first
|
||||||
|
val member = resolved.second
|
||||||
|
|
||||||
|
// We need to find the actual PSI element for this member
|
||||||
|
val targetFile = findFileForClass(file.project, owner) ?: file
|
||||||
|
val targetMini = LyngAstManager.getMiniAst(targetFile)
|
||||||
|
if (targetMini != null) {
|
||||||
|
val targetSrc = targetMini.range.start.source
|
||||||
|
val off = targetSrc.offsetOf(member.nameStart)
|
||||||
|
targetFile.findElementAt(off)?.let {
|
||||||
|
val kind = when(member) {
|
||||||
|
is MiniMemberFunDecl -> "Function"
|
||||||
|
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
|
||||||
|
is MiniInitDecl -> "Initializer"
|
||||||
|
}
|
||||||
|
results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we couldn't resolve exactly, we might still want to search globally but ONLY for members
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
results.addAll(resolveGlobally(file.project, name, membersOnly = true))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2. Local resolution via Binder
|
||||||
|
if (binding != null) {
|
||||||
|
val ref = binding.references.firstOrNull { offset >= it.start && offset < it.end }
|
||||||
|
if (ref != null) {
|
||||||
|
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||||
|
if (sym != null && sym.declStart >= 0) {
|
||||||
|
file.findElementAt(sym.declStart)?.let {
|
||||||
|
results.add(PsiElementResolveResult(LyngDeclarationElement(it, sym.name, sym.kind.name)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Global project scan
|
||||||
|
// Only search globally if we haven't found a strong local match
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
results.addAll(resolveGlobally(file.project, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Filter results to exclude duplicates
|
||||||
|
// Use a more robust de-duplication that prefers the raw element if multiple refer to the same thing
|
||||||
|
val filtered = mutableListOf<ResolveResult>()
|
||||||
|
for (res in results) {
|
||||||
|
val el = res.element ?: continue
|
||||||
|
val nav = if (el is LyngDeclarationElement) el.navigationElement else el
|
||||||
|
if (filtered.none { existing ->
|
||||||
|
val exEl = existing.element
|
||||||
|
val exNav = if (exEl is LyngDeclarationElement) exEl.navigationElement else exEl
|
||||||
|
exNav == nav || (exNav != null && exNav.isEquivalentTo(nav))
|
||||||
|
}) {
|
||||||
|
filtered.add(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findFileForClass(project: Project, className: String): PsiFile? {
|
||||||
|
val psiManager = PsiManager.getInstance(project)
|
||||||
|
|
||||||
|
// 1. Try file with matching name first (optimization)
|
||||||
|
val matchingFiles = FilenameIndex.getFilesByName(project, "$className.lyng", GlobalSearchScope.projectScope(project))
|
||||||
|
for (file in matchingFiles) {
|
||||||
|
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
||||||
|
if (mini.declarations.any { (it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className) }) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to full project scan
|
||||||
|
val allFiles = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project))
|
||||||
|
for (vFile in allFiles) {
|
||||||
|
val file = psiManager.findFile(vFile) ?: continue
|
||||||
|
if (matchingFiles.contains(file)) continue // already checked
|
||||||
|
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
||||||
|
if (mini.declarations.any { (it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className) }) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(): PsiElement? {
|
||||||
|
val results = multiResolve(false)
|
||||||
|
if (results.isEmpty()) return null
|
||||||
|
val target = results[0].element ?: return null
|
||||||
|
// If the target is equivalent to our source element, return the source element itself.
|
||||||
|
// This is crucial for IDEA to recognize we are already at the declaration site
|
||||||
|
// and trigger "Show Usages" instead of performing a no-op navigation.
|
||||||
|
if (target == element || target.isEquivalentTo(element)) {
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false): List<ResolveResult> {
|
||||||
|
val results = mutableListOf<ResolveResult>()
|
||||||
|
val files = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project))
|
||||||
|
val psiManager = PsiManager.getInstance(project)
|
||||||
|
|
||||||
|
for (vFile in files) {
|
||||||
|
val file = psiManager.findFile(vFile) ?: continue
|
||||||
|
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
||||||
|
val src = mini.range.start.source
|
||||||
|
|
||||||
|
fun addIfMatch(dName: String, nameStart: net.sergeych.lyng.Pos, dKind: String) {
|
||||||
|
if (dName == name) {
|
||||||
|
val off = src.offsetOf(nameStart)
|
||||||
|
file.findElementAt(off)?.let {
|
||||||
|
results.add(PsiElementResolveResult(LyngDeclarationElement(it, dName, dKind)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (d in mini.declarations) {
|
||||||
|
if (!membersOnly) {
|
||||||
|
val dKind = when(d) {
|
||||||
|
is net.sergeych.lyng.miniast.MiniFunDecl -> "Function"
|
||||||
|
is net.sergeych.lyng.miniast.MiniClassDecl -> "Class"
|
||||||
|
is net.sergeych.lyng.miniast.MiniEnumDecl -> "Enum"
|
||||||
|
is net.sergeych.lyng.miniast.MiniValDecl -> if (d.mutable) "Variable" else "Value"
|
||||||
|
}
|
||||||
|
addIfMatch(d.name, d.nameStart, dKind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check members of classes and enums
|
||||||
|
val members = when(d) {
|
||||||
|
is MiniClassDecl -> d.members
|
||||||
|
is MiniEnumDecl -> DocLookupUtils.enumToSyntheticClass(d).members
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (m in members) {
|
||||||
|
val mKind = when(m) {
|
||||||
|
is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function"
|
||||||
|
is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value"
|
||||||
|
is net.sergeych.lyng.miniast.MiniInitDecl -> "Initializer"
|
||||||
|
}
|
||||||
|
addIfMatch(m.name, m.nameStart, mKind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVariants(): Array<Any> = emptyArray()
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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.navigation
|
||||||
|
|
||||||
|
import com.intellij.patterns.PlatformPatterns
|
||||||
|
import com.intellij.psi.*
|
||||||
|
import com.intellij.util.ProcessingContext
|
||||||
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
|
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
|
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||||
|
|
||||||
|
class LyngPsiReferenceContributor : PsiReferenceContributor() {
|
||||||
|
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
|
||||||
|
registrar.registerReferenceProvider(
|
||||||
|
PlatformPatterns.psiElement().withLanguage(LyngLanguage),
|
||||||
|
object : PsiReferenceProvider() {
|
||||||
|
override fun getReferencesByElement(
|
||||||
|
element: PsiElement,
|
||||||
|
context: ProcessingContext
|
||||||
|
): Array<PsiReference> {
|
||||||
|
if (element.node.elementType == LyngTokenTypes.IDENTIFIER) {
|
||||||
|
val file = element.containingFile
|
||||||
|
val mini = LyngAstManager.getMiniAst(file)
|
||||||
|
if (mini != null) {
|
||||||
|
val offset = element.textRange.startOffset
|
||||||
|
val name = element.text ?: ""
|
||||||
|
if (DocLookupUtils.findDeclarationAt(mini, offset, name) != null) {
|
||||||
|
return PsiReference.EMPTY_ARRAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arrayOf(LyngPsiReference(element))
|
||||||
|
}
|
||||||
|
return PsiReference.EMPTY_ARRAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import com.intellij.application.options.CodeStyle
|
||||||
|
import com.intellij.openapi.editor.Document
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.util.TextRange
|
||||||
|
import net.sergeych.lyng.format.LyngFormatConfig
|
||||||
|
import net.sergeych.lyng.format.LyngFormatter
|
||||||
|
|
||||||
|
object FormattingUtils {
|
||||||
|
fun computeDesiredIndent(project: Project, doc: Document, line: Int): String {
|
||||||
|
val options = CodeStyle.getIndentOptions(project, doc)
|
||||||
|
val start = 0
|
||||||
|
val end = doc.getLineEndOffset(line)
|
||||||
|
val snippet = doc.getText(TextRange(start, end))
|
||||||
|
val lineText = if (line < doc.lineCount) {
|
||||||
|
val ls = doc.getLineStartOffset(line)
|
||||||
|
val le = doc.getLineEndOffset(line)
|
||||||
|
doc.getText(TextRange(ls, le))
|
||||||
|
} else ""
|
||||||
|
val isBlankLine = lineText.trim().isEmpty()
|
||||||
|
val snippetForCalc = if (isBlankLine) snippet + "x" else snippet
|
||||||
|
val cfg = LyngFormatConfig(
|
||||||
|
indentSize = options.INDENT_SIZE.coerceAtLeast(1),
|
||||||
|
useTabs = options.USE_TAB_CHARACTER,
|
||||||
|
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
||||||
|
)
|
||||||
|
val formatted = LyngFormatter.reindent(snippetForCalc, cfg)
|
||||||
|
val lastNl = formatted.lastIndexOf('\n')
|
||||||
|
val lastLine = if (lastNl >= 0) formatted.substring(lastNl + 1) else formatted
|
||||||
|
val wsLen = lastLine.indexOfFirst { it != ' ' && it != '\t' }.let { if (it < 0) lastLine.length else it }
|
||||||
|
return lastLine.substring(0, wsLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findFirstNonWs(doc: Document, start: Int, end: Int): Int {
|
||||||
|
var i = start
|
||||||
|
val text = doc.charsSequence
|
||||||
|
while (i < end) {
|
||||||
|
val ch = text[i]
|
||||||
|
if (ch != ' ' && ch != '\t') break
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import com.intellij.openapi.util.Key
|
||||||
|
import com.intellij.psi.PsiFile
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.sergeych.lyng.Compiler
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.binding.Binder
|
||||||
|
import net.sergeych.lyng.binding.BindingSnapshot
|
||||||
|
import net.sergeych.lyng.miniast.MiniAstBuilder
|
||||||
|
import net.sergeych.lyng.miniast.MiniScript
|
||||||
|
|
||||||
|
object LyngAstManager {
|
||||||
|
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
||||||
|
private val BINDING_KEY = Key.create<BindingSnapshot>("lyng.binding.cache")
|
||||||
|
private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp")
|
||||||
|
|
||||||
|
fun getMiniAst(file: PsiFile): MiniScript? {
|
||||||
|
val doc = file.viewProvider.document ?: return null
|
||||||
|
val stamp = doc.modificationStamp
|
||||||
|
val prevStamp = file.getUserData(STAMP_KEY)
|
||||||
|
val cached = file.getUserData(MINI_KEY)
|
||||||
|
if (cached != null && prevStamp != null && prevStamp == stamp) return cached
|
||||||
|
|
||||||
|
val text = doc.text
|
||||||
|
val sink = MiniAstBuilder()
|
||||||
|
val built = try {
|
||||||
|
val provider = IdeLenientImportProvider.create()
|
||||||
|
val src = Source(file.name, text)
|
||||||
|
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||||
|
sink.build()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
sink.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (built != null) {
|
||||||
|
file.putUserData(MINI_KEY, built)
|
||||||
|
file.putUserData(STAMP_KEY, stamp)
|
||||||
|
// Invalidate binding too
|
||||||
|
file.putUserData(BINDING_KEY, null)
|
||||||
|
}
|
||||||
|
return built
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBinding(file: PsiFile): BindingSnapshot? {
|
||||||
|
val doc = file.viewProvider.document ?: return null
|
||||||
|
val stamp = doc.modificationStamp
|
||||||
|
val prevStamp = file.getUserData(STAMP_KEY)
|
||||||
|
val cached = file.getUserData(BINDING_KEY)
|
||||||
|
|
||||||
|
if (cached != null && prevStamp != null && prevStamp == stamp) return cached
|
||||||
|
|
||||||
|
val mini = getMiniAst(file) ?: return null
|
||||||
|
val text = doc.text
|
||||||
|
val binding = try {
|
||||||
|
Binder.bind(text, mini)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding != null) {
|
||||||
|
file.putUserData(BINDING_KEY, binding)
|
||||||
|
// stamp is already set by getMiniAst
|
||||||
|
}
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -96,6 +96,12 @@
|
|||||||
<!-- If targeting SDKs with stable RawText API, the EP below can be enabled instead: -->
|
<!-- If targeting SDKs with stable RawText API, the EP below can be enabled instead: -->
|
||||||
<!-- <copyPastePreProcessor implementation="net.sergeych.lyng.idea.editor.LyngCopyPastePreProcessor"/> -->
|
<!-- <copyPastePreProcessor implementation="net.sergeych.lyng.idea.editor.LyngCopyPastePreProcessor"/> -->
|
||||||
|
|
||||||
|
<!-- Navigation and Find Usages -->
|
||||||
|
<psi.referenceContributor language="Lyng" implementation="net.sergeych.lyng.idea.navigation.LyngPsiReferenceContributor"/>
|
||||||
|
<gotoDeclarationHandler implementation="net.sergeych.lyng.idea.navigation.LyngGotoDeclarationHandler"/>
|
||||||
|
<lang.findUsagesProvider language="Lyng" implementationClass="net.sergeych.lyng.idea.navigation.LyngFindUsagesProvider"/>
|
||||||
|
<iconProvider implementation="net.sergeych.lyng.idea.navigation.LyngIconProvider"/>
|
||||||
|
|
||||||
</extensions>
|
</extensions>
|
||||||
|
|
||||||
<actions/>
|
<actions/>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ kotlin {
|
|||||||
publishLibraryVariants("release")
|
publishLibraryVariants("release")
|
||||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget.set(JvmTarget.JVM_11)
|
jvmTarget.set(JvmTarget.JVM_17)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iosX64()
|
iosX64()
|
||||||
@@ -71,11 +71,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
all {
|
all {
|
||||||
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
||||||
// languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
languageSettings.optIn("kotlin.time.ExperimentalTime")
|
||||||
// Correct opt-in markers for coroutines
|
|
||||||
// languageSettings.optIn("kotlinx.coroutines.DelicateCoroutinesApi")
|
|
||||||
// languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
|
|
||||||
// languageSettings.optIn("kotlinx.coroutines.FlowPreview")
|
|
||||||
}
|
}
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -115,8 +111,8 @@ android {
|
|||||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
// Prevent Android Lint from failing the build due to Kotlin toolchain
|
// Prevent Android Lint from failing the build due to Kotlin toolchain
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
|
|||||||
fsGuard {
|
fsGuard {
|
||||||
val self = this.thisObj as ObjPath
|
val self = this.thisObj as ObjPath
|
||||||
val m = self.ensureMetadata()
|
val m = self.ensureMetadata()
|
||||||
m.createdAtMillis?.let { ObjInstant(kotlinx.datetime.Instant.fromEpochMilliseconds(it)) } ?: ObjNull
|
m.createdAtMillis?.let { ObjInstant(kotlin.time.Instant.fromEpochMilliseconds(it)) } ?: ObjNull
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// createdAtMillis(): Int? — milliseconds since epoch or null
|
// createdAtMillis(): Int? — milliseconds since epoch or null
|
||||||
|
|||||||
@@ -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.0.7-SNAPSHOT"
|
version = "1.1.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
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ kotlin {
|
|||||||
publishLibraryVariants("release")
|
publishLibraryVariants("release")
|
||||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget.set(JvmTarget.JVM_11)
|
jvmTarget.set(JvmTarget.JVM_17)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iosX64()
|
iosX64()
|
||||||
@@ -89,16 +89,16 @@ kotlin {
|
|||||||
languageSettings.optIn("kotlinx.coroutines.DelicateCoroutinesApi")
|
languageSettings.optIn("kotlinx.coroutines.DelicateCoroutinesApi")
|
||||||
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
|
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
|
||||||
languageSettings.optIn("kotlinx.coroutines.FlowPreview")
|
languageSettings.optIn("kotlinx.coroutines.FlowPreview")
|
||||||
|
languageSettings.optIn("kotlin.time.ExperimentalTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
kotlin.srcDir("$buildDir/generated/buildConfig/commonMain/kotlin")
|
kotlin.srcDir("$buildDir/generated/buildConfig/commonMain/kotlin")
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
//put your multiplatform dependencies here
|
//put your multiplatform dependencies here
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
api(libs.mp.bintools)
|
api(libs.mp.bintools)
|
||||||
api("net.sergeych:mp_stools:1.5.2")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
@@ -116,6 +116,18 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "net.sergeych.lynglib"
|
||||||
|
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Build-time generation of stdlib text from .lyng files into a Kotlin constant ----
|
// ---- Build-time generation of stdlib text from .lyng files into a Kotlin constant ----
|
||||||
// Implemented as a proper task type compatible with Gradle Configuration Cache
|
// Implemented as a proper task type compatible with Gradle Configuration Cache
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -49,12 +49,14 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
arguments: Arguments = scope.args,
|
arguments: Arguments = scope.args,
|
||||||
defaultAccessType: AccessType = AccessType.Var,
|
defaultAccessType: AccessType = AccessType.Var,
|
||||||
defaultVisibility: Visibility = Visibility.Public,
|
defaultVisibility: Visibility = Visibility.Public,
|
||||||
|
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
|
||||||
) {
|
) {
|
||||||
fun assign(a: Item, value: Obj) {
|
fun assign(a: Item, value: Obj) {
|
||||||
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
|
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
|
||||||
value.byValueCopy(),
|
value.byValueCopy(),
|
||||||
a.visibility ?: defaultVisibility,
|
a.visibility ?: defaultVisibility,
|
||||||
recordType = ObjRecord.Type.Argument)
|
recordType = ObjRecord.Type.Argument,
|
||||||
|
declaringClass = declaringClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare positional args and parameter count, handle tail-block binding
|
// Prepare positional args and parameter count, handle tail-block binding
|
||||||
@@ -136,7 +138,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
} else {
|
} else {
|
||||||
val value = if (hp < callArgs.size) callArgs[hp++]
|
val value = if (hp < callArgs.size) callArgs[hp++]
|
||||||
else a.defaultValue?.execute(scope)
|
else a.defaultValue?.execute(scope)
|
||||||
?: scope.raiseIllegalArgument("too few arguments for the call")
|
?: scope.raiseIllegalArgument("too few arguments for the call (missing ${a.name})")
|
||||||
assign(a, value)
|
assign(a, value)
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
|
|||||||
@@ -53,22 +53,75 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
|
|||||||
super.objects[name]?.let { return it }
|
super.objects[name]?.let { return it }
|
||||||
super.localBindings[name]?.let { return it }
|
super.localBindings[name]?.let { return it }
|
||||||
|
|
||||||
|
// 1a) Priority: if we are in a class context, prefer our own private members to support
|
||||||
|
// non-virtual private dispatch. This prevents a subclass from accidentally
|
||||||
|
// capturing a private member call from a base class.
|
||||||
|
// We only return non-field/non-property members (methods) here; fields must
|
||||||
|
// be resolved via instance storage in priority 2.
|
||||||
|
currentClassCtx?.let { ctx ->
|
||||||
|
ctx.members[name]?.let { rec ->
|
||||||
|
if (rec.visibility == Visibility.Private &&
|
||||||
|
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
|
||||||
|
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property) return rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Members on the captured receiver instance
|
// 2) Members on the captured receiver instance
|
||||||
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)
|
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst ->
|
||||||
?.instanceScope
|
// Check direct locals in instance scope (unmangled)
|
||||||
?.objects
|
inst.instanceScope.objects[name]?.let { rec ->
|
||||||
?.get(name)
|
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
|
||||||
?.let { return it }
|
canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
|
||||||
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
}
|
||||||
|
// Check mangled names for fields along MRO
|
||||||
|
for (cls in inst.objClass.mro) {
|
||||||
|
inst.instanceScope.objects["${cls.className}::$name"]?.let { rec ->
|
||||||
|
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
|
||||||
|
canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) return rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findExtension(closureScope.thisObj.objClass, name)?.let { return it }
|
||||||
|
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||||
|
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
|
||||||
|
// Return only non-field/non-property members (methods) from class-level records.
|
||||||
|
// Fields and properties must be resolved via instance storage (mangled) or readField.
|
||||||
|
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
|
||||||
|
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
|
||||||
|
!rec.isAbstract) return rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion
|
// 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion
|
||||||
closureScope.chainLookupWithMembers(name)?.let { return it }
|
closureScope.chainLookupWithMembers(name, currentClassCtx)?.let { return it }
|
||||||
|
|
||||||
// 4) Caller `this` members
|
// 4) Caller `this` members
|
||||||
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
(callScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst ->
|
||||||
|
// Check direct locals in instance scope (unmangled)
|
||||||
|
inst.instanceScope.objects[name]?.let { rec ->
|
||||||
|
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
|
||||||
|
canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
|
||||||
|
}
|
||||||
|
// Check mangled names for fields along MRO
|
||||||
|
for (cls in inst.objClass.mro) {
|
||||||
|
inst.instanceScope.objects["${cls.className}::$name"]?.let { rec ->
|
||||||
|
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
|
||||||
|
canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) return rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
findExtension(callScope.thisObj.objClass, name)?.let { return it }
|
||||||
|
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||||
|
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
|
||||||
|
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
|
||||||
|
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
|
||||||
|
!rec.isAbstract) return rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 5) Caller chain (locals/parents + members)
|
// 5) Caller chain (locals/parents + members)
|
||||||
callScope.chainLookupWithMembers(name)?.let { return it }
|
callScope.chainLookupWithMembers(name, currentClassCtx)?.let { return it }
|
||||||
|
|
||||||
// 6) Module pseudo-symbols (e.g., __PACKAGE__) — walk caller ancestry and query ModuleScope directly
|
// 6) Module pseudo-symbols (e.g., __PACKAGE__) — walk caller ancestry and query ModuleScope directly
|
||||||
if (name.startsWith("__")) {
|
if (name.startsWith("__")) {
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -25,11 +25,12 @@ class CompilerContext(val tokens: List<Token>) {
|
|||||||
var loopLevel = 0
|
var loopLevel = 0
|
||||||
|
|
||||||
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
|
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
|
||||||
if (++loopLevel == 0) breakFound = false
|
val oldBreakFound = breakFound
|
||||||
|
breakFound = false
|
||||||
val result = f()
|
val result = f()
|
||||||
return Pair(breakFound, result).also {
|
val currentBreakFound = breakFound
|
||||||
--loopLevel
|
breakFound = oldBreakFound || currentBreakFound
|
||||||
}
|
return Pair(currentBreakFound, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentIndex = 0
|
var currentIndex = 0
|
||||||
@@ -108,7 +109,6 @@ class CompilerContext(val tokens: List<Token>) {
|
|||||||
val t = next()
|
val t = next()
|
||||||
return if (t.type != tokenType) {
|
return if (t.type != tokenType) {
|
||||||
if (!isOptional) {
|
if (!isOptional) {
|
||||||
println("unexpected: $t (needed $tokenType)")
|
|
||||||
throw ScriptError(t.pos, errorMessage)
|
throw ScriptError(t.pos, errorMessage)
|
||||||
} else {
|
} else {
|
||||||
previous()
|
previous()
|
||||||
|
|||||||
@@ -45,19 +45,36 @@ class ModuleScope(
|
|||||||
if (record.visibility.isPublic) {
|
if (record.visibility.isPublic) {
|
||||||
val newName = symbols?.let { ss: Map<String, String> ->
|
val newName = symbols?.let { ss: Map<String, String> ->
|
||||||
ss[symbol]
|
ss[symbol]
|
||||||
?.also { symbolsToImport!!.remove(it) }
|
?.also { symbolsToImport!!.remove(symbol) }
|
||||||
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached")
|
?: return@let null
|
||||||
} ?: symbol
|
} ?: if (symbols == null) symbol else null
|
||||||
val existing = scope.objects[newName]
|
|
||||||
if (existing != null ) {
|
if (newName != null) {
|
||||||
if (existing.importedFrom != record.importedFrom)
|
val existing = scope.objects[newName]
|
||||||
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
|
if (existing != null) {
|
||||||
// already imported
|
if (existing.importedFrom != record.importedFrom)
|
||||||
|
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
|
||||||
|
// already imported
|
||||||
|
} else {
|
||||||
|
// when importing records, we keep track of its package (not otherwise needed)
|
||||||
|
if (record.importedFrom == null) record.importedFrom = this
|
||||||
|
scope.objects[newName] = record
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
// when importing records, we keep track of its package (not otherwise needed)
|
}
|
||||||
if (record.importedFrom == null) record.importedFrom = this
|
for ((cls, map) in this.extensions) {
|
||||||
scope.objects[newName] = record
|
for ((symbol, record) in map) {
|
||||||
|
if (record.visibility.isPublic) {
|
||||||
|
val newName = symbols?.let { ss: Map<String, String> ->
|
||||||
|
ss[symbol]
|
||||||
|
?.also { symbolsToImport!!.remove(symbol) }
|
||||||
|
?: return@let null
|
||||||
|
} ?: if (symbols == null) symbol else null
|
||||||
|
|
||||||
|
if (newName != null) {
|
||||||
|
scope.addExtension(cls, newName, record)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,6 +356,8 @@ private class Parser(fromPos: Pos) {
|
|||||||
when (text) {
|
when (text) {
|
||||||
"in" -> Token("in", from, Token.Type.IN)
|
"in" -> Token("in", from, Token.Type.IN)
|
||||||
"is" -> Token("is", from, Token.Type.IS)
|
"is" -> Token("is", from, Token.Type.IS)
|
||||||
|
"by" -> Token("by", from, Token.Type.BY)
|
||||||
|
"object" -> Token("object", from, Token.Type.OBJECT)
|
||||||
"as" -> {
|
"as" -> {
|
||||||
// support both `as` and tight `as?` without spaces
|
// support both `as` and tight `as?` without spaces
|
||||||
if (currentChar == '?') { pos.advance(); Token("as?", from, Token.Type.ASNULL) }
|
if (currentChar == '?') { pos.advance(); Token("as?", from, Token.Type.ASNULL) }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -62,6 +62,30 @@ open class Scope(
|
|||||||
*/
|
*/
|
||||||
internal val localBindings: MutableMap<String, ObjRecord> = mutableMapOf()
|
internal val localBindings: MutableMap<String, ObjRecord> = mutableMapOf()
|
||||||
|
|
||||||
|
internal val extensions: MutableMap<ObjClass, MutableMap<String, ObjRecord>> = mutableMapOf()
|
||||||
|
|
||||||
|
fun addExtension(cls: ObjClass, name: String, record: ObjRecord) {
|
||||||
|
extensions.getOrPut(cls) { mutableMapOf() }[name] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
|
||||||
|
var s: Scope? = this
|
||||||
|
val visited = HashSet<Long>(4)
|
||||||
|
while (s != null) {
|
||||||
|
if (!visited.add(s.frameId)) break
|
||||||
|
// Proximity rule: check all extensions in the current scope before going to parent.
|
||||||
|
// Priority within scope: more specific class in MRO wins.
|
||||||
|
for (cls in receiverClass.mro) {
|
||||||
|
s.extensions[cls]?.get(name)?.let { return it }
|
||||||
|
}
|
||||||
|
if (s is ClosureScope) {
|
||||||
|
s.closureScope.findExtension(receiverClass, name)?.let { return it }
|
||||||
|
}
|
||||||
|
s = s.parent
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/** Debug helper: ensure assigning [candidateParent] does not create a structural cycle. */
|
/** Debug helper: ensure assigning [candidateParent] does not create a structural cycle. */
|
||||||
private fun ensureNoCycle(candidateParent: Scope?) {
|
private fun ensureNoCycle(candidateParent: Scope?) {
|
||||||
if (candidateParent == null) return
|
if (candidateParent == null) return
|
||||||
@@ -82,10 +106,17 @@ open class Scope(
|
|||||||
* intertwined closure frames. They traverse the plain parent chain and consult only locals
|
* intertwined closure frames. They traverse the plain parent chain and consult only locals
|
||||||
* and bindings of each frame. Instance/class member fallback must be decided by the caller.
|
* and bindings of each frame. Instance/class member fallback must be decided by the caller.
|
||||||
*/
|
*/
|
||||||
private fun tryGetLocalRecord(s: Scope, name: String): ObjRecord? {
|
private fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
|
||||||
s.objects[name]?.let { return it }
|
s.objects[name]?.let { rec ->
|
||||||
s.localBindings[name]?.let { return it }
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||||
s.getSlotIndexOf(name)?.let { return s.getSlotRecord(it) }
|
}
|
||||||
|
s.localBindings[name]?.let { rec ->
|
||||||
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||||
|
}
|
||||||
|
s.getSlotIndexOf(name)?.let { idx ->
|
||||||
|
val rec = s.getSlotRecord(idx)
|
||||||
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +126,7 @@ open class Scope(
|
|||||||
val visited = HashSet<Long>(4)
|
val visited = HashSet<Long>(4)
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
if (!visited.add(s.frameId)) return null
|
if (!visited.add(s.frameId)) return null
|
||||||
tryGetLocalRecord(s, name)?.let { return it }
|
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
||||||
s = s.parent
|
s = s.parent
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -110,17 +141,25 @@ open class Scope(
|
|||||||
*/
|
*/
|
||||||
internal fun baseGetIgnoreClosure(name: String): ObjRecord? {
|
internal fun baseGetIgnoreClosure(name: String): ObjRecord? {
|
||||||
// 1) locals/bindings in this frame
|
// 1) locals/bindings in this frame
|
||||||
tryGetLocalRecord(this, name)?.let { return it }
|
tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
|
||||||
// 2) walk parents for plain locals/bindings only
|
// 2) walk parents for plain locals/bindings only
|
||||||
var s = parent
|
var s = parent
|
||||||
val visited = HashSet<Long>(4)
|
val visited = HashSet<Long>(4)
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
if (!visited.add(s.frameId)) return null
|
if (!visited.add(s.frameId)) return null
|
||||||
tryGetLocalRecord(s, name)?.let { return it }
|
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
||||||
s = s.parent
|
s = s.parent
|
||||||
}
|
}
|
||||||
// 3) fallback to instance/class members of this frame's thisObj
|
// 3) fallback to instance/class members of this frame's thisObj
|
||||||
return thisObj.objClass.getInstanceMemberOrNull(name)
|
for (cls in thisObj.objClass.mro) {
|
||||||
|
this.extensions[cls]?.get(name)?.let { return it }
|
||||||
|
}
|
||||||
|
return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||||
|
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
|
||||||
|
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
|
||||||
|
else rec
|
||||||
|
} else null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,13 +169,22 @@ open class Scope(
|
|||||||
* This completely avoids invoking overridden `get` implementations, preventing
|
* This completely avoids invoking overridden `get` implementations, preventing
|
||||||
* ping-pong recursion between `ClosureScope` frames.
|
* ping-pong recursion between `ClosureScope` frames.
|
||||||
*/
|
*/
|
||||||
internal fun chainLookupWithMembers(name: String): ObjRecord? {
|
internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx): ObjRecord? {
|
||||||
var s: Scope? = this
|
var s: Scope? = this
|
||||||
val visited = HashSet<Long>(4)
|
val visited = HashSet<Long>(4)
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
if (!visited.add(s.frameId)) return null
|
if (!visited.add(s.frameId)) return null
|
||||||
tryGetLocalRecord(s, name)?.let { return it }
|
tryGetLocalRecord(s, name, caller)?.let { return it }
|
||||||
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
for (cls in s.thisObj.objClass.mro) {
|
||||||
|
s.extensions[cls]?.get(name)?.let { return it }
|
||||||
|
}
|
||||||
|
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||||
|
if (canAccessMember(rec.visibility, rec.declaringClass, caller)) {
|
||||||
|
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) {
|
||||||
|
// ignore fields, properties and abstracts here, they will be handled by the caller via readField
|
||||||
|
} else return rec
|
||||||
|
}
|
||||||
|
}
|
||||||
s = s.parent
|
s = s.parent
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -152,6 +200,10 @@ open class Scope(
|
|||||||
// copy locals and bindings
|
// copy locals and bindings
|
||||||
snap.objects.putAll(this.objects)
|
snap.objects.putAll(this.objects)
|
||||||
snap.localBindings.putAll(this.localBindings)
|
snap.localBindings.putAll(this.localBindings)
|
||||||
|
// copy extensions
|
||||||
|
for ((cls, map) in extensions) {
|
||||||
|
snap.extensions[cls] = map.toMutableMap()
|
||||||
|
}
|
||||||
// copy slots map preserving indices
|
// copy slots map preserving indices
|
||||||
if (this.slotCount() > 0) {
|
if (this.slotCount() > 0) {
|
||||||
var i = 0
|
var i = 0
|
||||||
@@ -217,6 +269,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"))
|
||||||
@@ -273,13 +328,22 @@ open class Scope(
|
|||||||
if (name == "this") thisObj.asReadonly
|
if (name == "this") thisObj.asReadonly
|
||||||
else {
|
else {
|
||||||
// Prefer direct locals/bindings declared in this frame
|
// Prefer direct locals/bindings declared in this frame
|
||||||
(objects[name]
|
(objects[name]?.let { rec ->
|
||||||
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
|
||||||
|
}
|
||||||
// Then, check known local bindings in this frame (helps after suspension)
|
// Then, check known local bindings in this frame (helps after suspension)
|
||||||
?: localBindings[name]
|
?: localBindings[name]?.let { rec ->
|
||||||
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
|
||||||
|
}
|
||||||
// Walk up ancestry
|
// Walk up ancestry
|
||||||
?: parent?.get(name)
|
?: parent?.get(name)
|
||||||
// Finally, fallback to class members on thisObj
|
// Finally, fallback to class members on thisObj
|
||||||
?: thisObj.objClass.getInstanceMemberOrNull(name)
|
?: thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||||
|
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
|
||||||
|
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
|
||||||
|
else rec
|
||||||
|
} else null
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +361,10 @@ open class Scope(
|
|||||||
return idx
|
return idx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateSlotFor(name: String, record: ObjRecord) {
|
||||||
|
nameToSlot[name]?.let { slots[it] = record }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset this scope instance so it can be safely reused as a fresh child frame.
|
* Reset this scope instance so it can be safely reused as a fresh child frame.
|
||||||
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||||
@@ -313,6 +381,7 @@ open class Scope(
|
|||||||
slots.clear()
|
slots.clear()
|
||||||
nameToSlot.clear()
|
nameToSlot.clear()
|
||||||
localBindings.clear()
|
localBindings.clear()
|
||||||
|
extensions.clear()
|
||||||
// Now safe to validate and re-parent
|
// Now safe to validate and re-parent
|
||||||
ensureNoCycle(parent)
|
ensureNoCycle(parent)
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
@@ -375,7 +444,11 @@ open class Scope(
|
|||||||
name: String,
|
name: String,
|
||||||
value: Obj,
|
value: Obj,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
writeVisibility: Visibility? = null,
|
||||||
|
recordType: ObjRecord.Type = ObjRecord.Type.Other,
|
||||||
|
isAbstract: Boolean = false,
|
||||||
|
isClosed: Boolean = false,
|
||||||
|
isOverride: Boolean = false
|
||||||
): ObjRecord =
|
): ObjRecord =
|
||||||
objects[name]?.let {
|
objects[name]?.let {
|
||||||
if( !it.isMutable )
|
if( !it.isMutable )
|
||||||
@@ -389,16 +462,28 @@ 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, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride)
|
||||||
|
|
||||||
fun addItem(
|
fun addItem(
|
||||||
name: String,
|
name: String,
|
||||||
isMutable: Boolean,
|
isMutable: Boolean,
|
||||||
value: Obj,
|
value: Obj,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
writeVisibility: Visibility? = null,
|
||||||
|
recordType: ObjRecord.Type = ObjRecord.Type.Other,
|
||||||
|
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx,
|
||||||
|
isAbstract: Boolean = false,
|
||||||
|
isClosed: Boolean = false,
|
||||||
|
isOverride: Boolean = false
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
val rec = ObjRecord(value, isMutable, visibility, declaringClass = currentClassCtx, type = recordType)
|
val rec = ObjRecord(
|
||||||
|
value, isMutable, visibility, writeVisibility,
|
||||||
|
declaringClass = declaringClass,
|
||||||
|
type = recordType,
|
||||||
|
isAbstract = isAbstract,
|
||||||
|
isClosed = isClosed,
|
||||||
|
isOverride = isOverride
|
||||||
|
)
|
||||||
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
|
||||||
@@ -415,9 +500,12 @@ open class Scope(
|
|||||||
callScope.allocateSlotFor(name, rec)
|
callScope.allocateSlotFor(name, rec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Map to a slot for fast local access (if not already mapped)
|
// Map to a slot for fast local access (ensure consistency)
|
||||||
if (getSlotIndexOf(name) == null) {
|
val idx = getSlotIndexOf(name)
|
||||||
|
if (idx == null) {
|
||||||
allocateSlotFor(name, rec)
|
allocateSlotFor(name, rec)
|
||||||
|
} else {
|
||||||
|
slots[idx] = rec
|
||||||
}
|
}
|
||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
@@ -428,13 +516,13 @@ open class Scope(
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Scope.() -> Unit) {
|
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Scope.() -> Unit) {
|
||||||
addFn<ObjVoid>(*names) {
|
addFn(*names) {
|
||||||
fn(this)
|
fn(this)
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Obj> addFn(vararg names: String, crossinline fn: suspend Scope.() -> T) {
|
fun addFn(vararg names: String, fn: suspend Scope.() -> Obj) {
|
||||||
val newFn = object : Statement() {
|
val newFn = object : Statement() {
|
||||||
override val pos: Pos = Pos.builtIn
|
override val pos: Pos = Pos.builtIn
|
||||||
|
|
||||||
@@ -516,6 +604,53 @@ open class Scope(
|
|||||||
|
|
||||||
open fun applyClosure(closure: Scope): Scope = ClosureScope(this, closure)
|
open fun applyClosure(closure: Scope): Scope = ClosureScope(this, closure)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and evaluate a qualified identifier exactly as compiled code would.
|
||||||
|
* For input like `A.B.C`, it builds the same ObjRef chain the compiler emits:
|
||||||
|
* `LocalVarRef("A", Pos.builtIn)` followed by `FieldRef` for each segment, then evaluates it.
|
||||||
|
* This mirrors `eval("A.B.C")` resolution semantics without invoking the compiler.
|
||||||
|
*/
|
||||||
|
suspend fun resolveQualifiedIdentifier(qualifiedName: String): Obj {
|
||||||
|
val trimmed = qualifiedName.trim()
|
||||||
|
if (trimmed.isEmpty()) raiseSymbolNotFound("empty identifier")
|
||||||
|
val parts = trimmed.split('.')
|
||||||
|
var ref: ObjRef = LocalVarRef(parts[0], Pos.builtIn)
|
||||||
|
for (i in 1 until parts.size) {
|
||||||
|
ref = FieldRef(ref, parts[i], false)
|
||||||
|
}
|
||||||
|
return ref.evalValue(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resolve(rec: ObjRecord, name: String): Obj {
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
val th = if (thisObj === ObjVoid) ObjNull else thisObj
|
||||||
|
if (del.objClass.getInstanceMemberOrNull("getValue") == null) {
|
||||||
|
return object : Statement() {
|
||||||
|
override val pos: Pos = Pos.builtIn
|
||||||
|
override suspend fun execute(scope: Scope): Obj {
|
||||||
|
val th2 = if (scope.thisObj === ObjVoid) ObjNull else scope.thisObj
|
||||||
|
val allArgs = (listOf(th2, ObjString(name)) + scope.args.list).toTypedArray()
|
||||||
|
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return del.invokeInstanceMethod(this, "getValue", Arguments(th, ObjString(name)))
|
||||||
|
}
|
||||||
|
return rec.value
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) {
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
val th = if (thisObj === ObjVoid) ObjNull else thisObj
|
||||||
|
del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!rec.isMutable && rec.value !== ObjUnset) raiseIllegalAssignment("can't reassign val $name")
|
||||||
|
rec.value = newValue
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun new(): Scope =
|
fun new(): Scope =
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -21,6 +21,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
||||||
import net.sergeych.lyng.miniast.addConstDoc
|
import net.sergeych.lyng.miniast.addConstDoc
|
||||||
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
import net.sergeych.lyng.miniast.addVoidFnDoc
|
import net.sergeych.lyng.miniast.addVoidFnDoc
|
||||||
import net.sergeych.lyng.miniast.type
|
import net.sergeych.lyng.miniast.type
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
@@ -60,6 +61,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)
|
||||||
@@ -203,8 +205,34 @@ class Script(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
addFn("assertThrows") {
|
addFnDoc(
|
||||||
val code = requireOnlyArg<Statement>()
|
"assertThrows",
|
||||||
|
doc = """
|
||||||
|
Asserts that the provided code block throws an exception, with or without exception:
|
||||||
|
```lyng
|
||||||
|
assertThrows { /* ode */ }
|
||||||
|
assertThrows(IllegalArgumentException) { /* code */ }
|
||||||
|
```
|
||||||
|
If an expected exception class is provided,
|
||||||
|
it checks that the thrown exception is of that class. If no expected class is provided, any exception
|
||||||
|
will be accepted.
|
||||||
|
""".trimIndent()
|
||||||
|
) {
|
||||||
|
val code: Statement
|
||||||
|
val expectedClass: ObjClass?
|
||||||
|
when (args.size) {
|
||||||
|
1 -> {
|
||||||
|
code = requiredArg<Statement>(0)
|
||||||
|
expectedClass = null
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
code = requiredArg<Statement>(1)
|
||||||
|
expectedClass = requiredArg<ObjClass>(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> raiseIllegalArgument("Expected 1 or 2 arguments, got ${args.size}")
|
||||||
|
}
|
||||||
val result = try {
|
val result = try {
|
||||||
code.execute(this)
|
code.execute(this)
|
||||||
null
|
null
|
||||||
@@ -213,26 +241,51 @@ class Script(
|
|||||||
} catch (_: ScriptError) {
|
} catch (_: ScriptError) {
|
||||||
ObjNull
|
ObjNull
|
||||||
}
|
}
|
||||||
result ?: raiseError(ObjAssertionFailedException(this, "Expected exception but nothing was thrown"))
|
if (result == null) raiseError(
|
||||||
|
ObjAssertionFailedException(
|
||||||
|
this,
|
||||||
|
"Expected exception but nothing was thrown"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expectedClass?.let {
|
||||||
|
if (result !is ObjException)
|
||||||
|
raiseError("Expected $expectedClass, got non-lyng exception $result")
|
||||||
|
if (result.exceptionClass != expectedClass) {
|
||||||
|
raiseError("Expected $expectedClass, got ${result.exceptionClass}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("dynamic") {
|
addFn("dynamic") {
|
||||||
ObjDynamic.create(this, requireOnlyArg())
|
ObjDynamic.create(this, requireOnlyArg())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val root = this
|
||||||
|
val mathClass = ObjClass("Math").apply {
|
||||||
|
addFn("sqrt") {
|
||||||
|
ObjReal(sqrt(args.firstAndOnly().toDouble()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addItem("Math", false, ObjInstance(mathClass).apply {
|
||||||
|
instanceScope = Scope(root, thisObj = this)
|
||||||
|
})
|
||||||
|
|
||||||
addFn("require") {
|
addFn("require") {
|
||||||
val condition = requiredArg<ObjBool>(0)
|
val condition = requiredArg<ObjBool>(0)
|
||||||
if (!condition.value) {
|
if (!condition.value) {
|
||||||
val message = args.list.getOrNull(1)?.toString() ?: "requirement not met"
|
var message = args.list.getOrNull(1)
|
||||||
raiseIllegalArgument(message)
|
if (message is Statement) message = message.execute(this)
|
||||||
|
raiseIllegalArgument(message?.toString() ?: "requirement not met")
|
||||||
}
|
}
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
addFn("check") {
|
addFn("check") {
|
||||||
val condition = requiredArg<ObjBool>(0)
|
val condition = requiredArg<ObjBool>(0)
|
||||||
if (!condition.value) {
|
if (!condition.value) {
|
||||||
val message = args.list.getOrNull(1)?.toString() ?: "check failed"
|
var message = args.list.getOrNull(1)
|
||||||
raiseIllegalState(message)
|
if (message is Statement) message = message.execute(this)
|
||||||
|
raiseIllegalState(message?.toString() ?: "check failed")
|
||||||
}
|
}
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
@@ -270,6 +323,7 @@ class Script(
|
|||||||
addConst("Deferred", ObjDeferred.type)
|
addConst("Deferred", ObjDeferred.type)
|
||||||
addConst("CompletableDeferred", ObjCompletableDeferred.type)
|
addConst("CompletableDeferred", ObjCompletableDeferred.type)
|
||||||
addConst("Mutex", ObjMutex.type)
|
addConst("Mutex", ObjMutex.type)
|
||||||
|
addConst("Flow", ObjFlow.type)
|
||||||
|
|
||||||
addConst("Regex", ObjRegex.type)
|
addConst("Regex", ObjRegex.type)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -36,7 +36,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
PLUS, MINUS, STAR, SLASH, PERCENT,
|
PLUS, MINUS, STAR, SLASH, PERCENT,
|
||||||
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
||||||
PLUS2, MINUS2,
|
PLUS2, MINUS2,
|
||||||
IN, NOTIN, IS, NOTIS,
|
IN, NOTIN, IS, NOTIS, BY,
|
||||||
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
|
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
|
||||||
SHUTTLE,
|
SHUTTLE,
|
||||||
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
||||||
@@ -44,7 +44,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||||
LABEL, ATLABEL, // label@ at@label
|
LABEL, ATLABEL, // label@ at@label
|
||||||
// type-checking/casting
|
// type-checking/casting
|
||||||
AS, ASNULL,
|
AS, ASNULL, OBJECT,
|
||||||
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
|
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
|
||||||
ELLIPSIS, DOTDOT, DOTDOTLT,
|
ELLIPSIS, DOTDOT, DOTDOTLT,
|
||||||
NEWLINE,
|
NEWLINE,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
|
|
||||||
enum class Visibility {
|
enum class Visibility {
|
||||||
Public, Private, Protected;//, Internal
|
Public, Protected, Private;//, Internal
|
||||||
val isPublic by lazy { this == Public }
|
val isPublic by lazy { this == Public }
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val isProtected by lazy { this == Protected }
|
val isProtected by lazy { this == Protected }
|
||||||
@@ -26,7 +26,7 @@ enum class Visibility {
|
|||||||
|
|
||||||
/** MI-aware visibility check: whether [caller] can access a member declared in [decl] with [visibility]. */
|
/** MI-aware visibility check: whether [caller] can access a member declared in [decl] with [visibility]. */
|
||||||
fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass?, caller: net.sergeych.lyng.obj.ObjClass?): Boolean {
|
fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass?, caller: net.sergeych.lyng.obj.ObjClass?): Boolean {
|
||||||
return when (visibility) {
|
val res = when (visibility) {
|
||||||
Visibility.Public -> true
|
Visibility.Public -> true
|
||||||
Visibility.Private -> (decl != null && caller === decl)
|
Visibility.Private -> (decl != null && caller === decl)
|
||||||
Visibility.Protected -> when {
|
Visibility.Protected -> when {
|
||||||
@@ -36,4 +36,5 @@ fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass
|
|||||||
else -> (caller.allParentsSet.contains(decl))
|
else -> (caller.allParentsSet.contains(decl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -25,12 +25,9 @@ import net.sergeych.lyng.Source
|
|||||||
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.MiniClassDecl
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.miniast.MiniFunDecl
|
|
||||||
import net.sergeych.lyng.miniast.MiniScript
|
|
||||||
import net.sergeych.lyng.miniast.MiniValDecl
|
|
||||||
|
|
||||||
enum class SymbolKind { Class, Enum, Function, Val, Var, Param }
|
enum class SymbolKind { Class, Enum, Function, Value, Variable, Parameter }
|
||||||
|
|
||||||
data class Symbol(
|
data class Symbol(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@@ -111,7 +108,16 @@ object Binder {
|
|||||||
for (cf in d.ctorFields) {
|
for (cf in d.ctorFields) {
|
||||||
val fs = source.offsetOf(cf.nameStart)
|
val fs = source.offsetOf(cf.nameStart)
|
||||||
val fe = fs + cf.name.length
|
val fe = fs + cf.name.length
|
||||||
val kind = if (cf.mutable) SymbolKind.Var else SymbolKind.Val
|
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
|
||||||
|
}
|
||||||
|
// 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)
|
val fieldSym = Symbol(nextId++, cf.name, kind, fs, fe, containerId = sym.id)
|
||||||
symbols += fieldSym
|
symbols += fieldSym
|
||||||
classes.last().fields += fieldSym.id
|
classes.last().fields += fieldSym.id
|
||||||
@@ -138,7 +144,7 @@ object Binder {
|
|||||||
for (p in d.params) {
|
for (p in d.params) {
|
||||||
val ps = source.offsetOf(p.nameStart)
|
val ps = source.offsetOf(p.nameStart)
|
||||||
val pe = ps + p.name.length
|
val pe = ps + p.name.length
|
||||||
val pk = SymbolKind.Param
|
val pk = SymbolKind.Parameter
|
||||||
val paramSym = Symbol(nextId++, p.name, pk, ps, pe, containerId = sym.id)
|
val paramSym = Symbol(nextId++, p.name, pk, ps, pe, containerId = sym.id)
|
||||||
fnScope.locals += paramSym.id
|
fnScope.locals += paramSym.id
|
||||||
symbols += paramSym
|
symbols += paramSym
|
||||||
@@ -147,7 +153,7 @@ object Binder {
|
|||||||
}
|
}
|
||||||
is MiniValDecl -> {
|
is MiniValDecl -> {
|
||||||
val (s, e) = nameOffsets(d.nameStart, d.name)
|
val (s, e) = nameOffsets(d.nameStart, d.name)
|
||||||
val kind = if (d.mutable) SymbolKind.Var else SymbolKind.Val
|
val kind = if (d.mutable) SymbolKind.Variable else SymbolKind.Value
|
||||||
val ownerClass = classContaining(s)
|
val ownerClass = classContaining(s)
|
||||||
if (ownerClass != null) {
|
if (ownerClass != null) {
|
||||||
// class field
|
// class field
|
||||||
@@ -160,6 +166,12 @@ object Binder {
|
|||||||
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
|
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is MiniEnumDecl -> {
|
||||||
|
val (s, e) = nameOffsets(d.nameStart, d.name)
|
||||||
|
val sym = Symbol(nextId++, d.name, SymbolKind.Enum, s, e, containerId = null)
|
||||||
|
symbols += sym
|
||||||
|
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +203,7 @@ object Binder {
|
|||||||
.maxByOrNull { it.rangeEnd - it.rangeStart }
|
.maxByOrNull { it.rangeEnd - it.rangeStart }
|
||||||
if (containerFn != null) {
|
if (containerFn != null) {
|
||||||
val fnSymId = containerFn.id
|
val fnSymId = containerFn.id
|
||||||
val kind = if (d.mutable) SymbolKind.Var else SymbolKind.Val
|
val kind = if (d.mutable) SymbolKind.Variable else SymbolKind.Value
|
||||||
val localSym = Symbol(nextId++, d.name, kind, s, e, containerId = fnSymId)
|
val localSym = Symbol(nextId++, d.name, kind, s, e, containerId = fnSymId)
|
||||||
symbols += localSym
|
symbols += localSym
|
||||||
containerFn.locals += localSym.id
|
containerFn.locals += localSym.id
|
||||||
@@ -231,7 +243,7 @@ object Binder {
|
|||||||
val inFn = functions.asSequence()
|
val inFn = functions.asSequence()
|
||||||
.filter { it.rangeEnd > it.rangeStart && nameStart >= it.rangeStart && nameStart <= it.rangeEnd }
|
.filter { it.rangeEnd > it.rangeStart && nameStart >= it.rangeStart && nameStart <= it.rangeEnd }
|
||||||
.maxByOrNull { it.rangeEnd - it.rangeStart }
|
.maxByOrNull { it.rangeEnd - it.rangeStart }
|
||||||
val kind = if (kw.equals("var", true)) SymbolKind.Var else SymbolKind.Val
|
val kind = if (kw.equals("var", true)) SymbolKind.Variable else SymbolKind.Value
|
||||||
if (inFn != null) {
|
if (inFn != null) {
|
||||||
val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = inFn.id)
|
val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = inFn.id)
|
||||||
symbols += localSym
|
symbols += localSym
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -34,18 +52,26 @@ object LyngFormatter {
|
|||||||
var parenBalance = 0
|
var parenBalance = 0
|
||||||
var bracketBalance = 0
|
var bracketBalance = 0
|
||||||
var prevBracketContinuation = false
|
var prevBracketContinuation = 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 codePart(s: String): String {
|
fun updateBalances(ch: Char) {
|
||||||
val idx = s.indexOf("//")
|
when (ch) {
|
||||||
return if (idx >= 0) s.substring(0, idx) else s
|
'{' -> blockLevel++
|
||||||
|
'}' -> if (blockLevel > 0) blockLevel--
|
||||||
|
'(' -> parenBalance++
|
||||||
|
')' -> if (parenBalance > 0) parenBalance--
|
||||||
|
'[' -> bracketBalance++
|
||||||
|
']' -> if (bracketBalance > 0) bracketBalance--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun indentOf(level: Int, continuation: Int): String =
|
fun indentOf(level: Int, continuation: Int): String =
|
||||||
// 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
|
||||||
@@ -53,36 +79,46 @@ 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()) {
|
||||||
val line = rawLine
|
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
|
||||||
val code = codePart(line)
|
val code = parts.filter { it.type == PartType.Code }.joinToString("") { it.text }
|
||||||
val trimmedStart = code.dropWhile { it == ' ' || it == '\t' }
|
val trimmedStart = code.dropWhile { it == ' ' || it == '\t' }
|
||||||
|
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()
|
||||||
if (trimmedStart.startsWith("}")) effectiveLevel = (effectiveLevel - 1).coerceAtLeast(0)
|
var effectiveLevel = blockLevel + currentExtraIndent
|
||||||
|
|
||||||
|
val isAccessor = !inBlockComment && isPropertyAccessor(trimmedStart)
|
||||||
|
if (isAccessor) effectiveLevel += 1
|
||||||
|
|
||||||
|
if (inBlockComment) {
|
||||||
|
if (!trimmedLine.startsWith("*/")) {
|
||||||
|
effectiveLevel += 1
|
||||||
|
}
|
||||||
|
} else if (trimmedStart.startsWith("}")) {
|
||||||
|
effectiveLevel = (effectiveLevel - 1).coerceAtLeast(0)
|
||||||
|
}
|
||||||
// else/catch/finally should align with the parent block level; no extra dedent here,
|
// else/catch/finally should align with the parent block level; no extra dedent here,
|
||||||
// because the preceding '}' has already reduced [blockLevel] appropriately.
|
// because the preceding '}' has already reduced [blockLevel] appropriately.
|
||||||
|
|
||||||
// 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()
|
||||||
// Do not apply continuation on a line that starts with a closer ')' or ']'
|
// While inside parentheses, continuation applies scaled by nesting level
|
||||||
val startsWithCloser = firstChar == ')' || firstChar == ']'
|
|
||||||
// Kotlin-like rule: continuation persists while inside parentheses; it applies
|
|
||||||
// even on the line that starts with ')'. For brackets, do not apply on the ']' line itself.
|
|
||||||
// Continuation rules:
|
|
||||||
// - For brackets: one-shot continuation for first element after '[', and while inside brackets
|
|
||||||
// (except on the ']' line) continuation equals one unit.
|
|
||||||
// - For parentheses: continuation depth scales with nested level; e.g., inside two nested
|
|
||||||
// parentheses lines get 2 * continuationIndentSize. No continuation on a line that starts with ')'.
|
|
||||||
val parenContLevels = if (parenBalance > 0 && firstChar != ')') parenBalance else 0
|
val parenContLevels = if (parenBalance > 0 && firstChar != ')') parenBalance else 0
|
||||||
val continuation = when {
|
val continuation = when {
|
||||||
// One-shot continuation when previous line ended with '[' to align first element
|
// One-shot continuation when previous line ended with '[' to align first element
|
||||||
@@ -91,6 +127,7 @@ object LyngFormatter {
|
|||||||
bracketBalance > 0 && firstChar != ']' -> config.continuationIndentSize
|
bracketBalance > 0 && firstChar != ']' -> config.continuationIndentSize
|
||||||
// While inside parentheses, continuation applies scaled by nesting level
|
// While inside parentheses, continuation applies scaled by nesting level
|
||||||
parenContLevels > 0 -> config.continuationIndentSize * parenContLevels
|
parenContLevels > 0 -> config.continuationIndentSize * parenContLevels
|
||||||
|
trimmedStart.startsWith(".") -> config.indentSize
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,8 +137,8 @@ object LyngFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Replace leading whitespace with the exact target indent; but keep fully blank lines truly empty
|
// Replace leading whitespace with the exact target indent; but keep fully blank lines truly empty
|
||||||
val contentStart = line.indexOfFirst { it != ' ' && it != '\t' }.let { if (it < 0) line.length else it }
|
val contentStart = rawLine.indexOfFirst { it != ' ' && it != '\t' }.let { if (it < 0) rawLine.length else it }
|
||||||
var content = line.substring(contentStart)
|
var content = rawLine.substring(contentStart)
|
||||||
// Collapse spaces right after an opening '[' to avoid "[ 1"; make it "[1"
|
// Collapse spaces right after an opening '[' to avoid "[ 1"; make it "[1"
|
||||||
if (content.startsWith("[")) {
|
if (content.startsWith("[")) {
|
||||||
content = "[" + content.drop(1).trimStart()
|
content = "[" + content.drop(1).trimStart()
|
||||||
@@ -115,104 +152,89 @@ object LyngFormatter {
|
|||||||
}
|
}
|
||||||
// Determine base indent using structural level and continuation only (spaces only)
|
// Determine base indent using structural level and continuation only (spaces only)
|
||||||
val indentString = indentOf(effectiveLevel, continuation)
|
val indentString = indentOf(effectiveLevel, continuation)
|
||||||
if (content.isEmpty()) {
|
if (content.isNotEmpty()) {
|
||||||
// preserve truly blank line as empty to avoid trailing spaces on empty lines
|
|
||||||
// (also keeps continuation blocks visually clean)
|
|
||||||
// do nothing, just append nothing; newline will be appended below if needed
|
|
||||||
} else {
|
|
||||||
sb.append(indentString).append(content)
|
sb.append(indentString).append(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (ch in code) when (ch) {
|
for (part in parts) {
|
||||||
'{' -> blockLevel++
|
if (part.type == PartType.Code) {
|
||||||
'}' -> if (blockLevel > 0) blockLevel--
|
for (ch in part.text) updateBalances(ch)
|
||||||
'(' -> parenBalance++
|
}
|
||||||
')' -> if (parenBalance > 0) parenBalance--
|
}
|
||||||
'[' -> bracketBalance++
|
val newBlockLevel = blockLevel
|
||||||
']' -> if (bracketBalance > 0) bracketBalance--
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update awaitingSingleIndent based on current line
|
inBlockComment = nextInBlockComment
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare one-shot bracket continuation if the current line ends with '['
|
// Prepare one-shot bracket continuation if the current line ends with '['
|
||||||
// (first element line gets continuation even before balances update propagate).
|
|
||||||
val endsWithBracket = code.trimEnd().endsWith("[")
|
val endsWithBracket = code.trimEnd().endsWith("[")
|
||||||
// Reset one-shot flag after we used it on this line
|
|
||||||
if (prevBracketContinuation) prevBracketContinuation = false
|
|
||||||
// Set for the next iteration if current line ends with '['
|
|
||||||
// Record whether THIS line ends with an opening '[' so the NEXT line gets a one-shot
|
|
||||||
// continuation indent for the first element.
|
|
||||||
if (endsWithBracket) {
|
if (endsWithBracket) {
|
||||||
// One-shot continuation for the very next line
|
// One-shot continuation for the very next line
|
||||||
prevBracketContinuation = true
|
prevBracketContinuation = true
|
||||||
} else {
|
} else {
|
||||||
// Reset the one-shot flag if the previous line didn't end with '['
|
// Reset the one-shot flag if it was used or if line doesn't end with '['
|
||||||
prevBracketContinuation = false
|
prevBracketContinuation = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Full format. Currently performs indentation only; spacing/wrapping can be added later. */
|
|
||||||
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
||||||
// Phase 1: indentation
|
// Phase 1: indentation
|
||||||
val indented = reindent(text, config)
|
val indented = reindent(text, config)
|
||||||
if (!config.applySpacing && !config.applyWrapping) return indented
|
if (!config.applySpacing && !config.applyWrapping) return indented
|
||||||
|
|
||||||
// Phase 2: minimal, safe spacing (PSI-free). Skip block comments completely and
|
// Phase 2: minimal, safe spacing (PSI-free).
|
||||||
// only apply spacing to the part before '//' on each line.
|
|
||||||
val lines = indented.split('\n')
|
val lines = indented.split('\n')
|
||||||
val out = StringBuilder(indented.length)
|
val out = StringBuilder(indented.length)
|
||||||
var inBlockComment = false
|
var inBlockComment = false
|
||||||
for ((i, rawLine) in lines.withIndex()) {
|
for ((i, rawLine) in lines.withIndex()) {
|
||||||
var line = rawLine
|
var line = rawLine
|
||||||
if (config.applySpacing) {
|
if (config.applySpacing) {
|
||||||
if (inBlockComment) {
|
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
|
||||||
// Pass-through until we see the end of the block comment on some line
|
val sb = StringBuilder()
|
||||||
val end = line.indexOf("*/")
|
for (part in parts) {
|
||||||
if (end >= 0) {
|
if (part.type == PartType.Code) {
|
||||||
inBlockComment = false
|
sb.append(applyMinimalSpacingRules(part.text))
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If this line opens a block comment, apply spacing only before the opener
|
|
||||||
val startIdx = line.indexOf("/*")
|
|
||||||
val endIdx = line.indexOf("*/")
|
|
||||||
if (startIdx >= 0 && (endIdx < 0 || endIdx < startIdx)) {
|
|
||||||
val before = line.substring(0, startIdx)
|
|
||||||
val after = line.substring(startIdx)
|
|
||||||
val commentIdx = before.indexOf("//")
|
|
||||||
val code = if (commentIdx >= 0) before.substring(0, commentIdx) else before
|
|
||||||
val tail = if (commentIdx >= 0) before.substring(commentIdx) else ""
|
|
||||||
val spaced = applyMinimalSpacing(code)
|
|
||||||
line = (spaced + tail) + after
|
|
||||||
inBlockComment = true
|
|
||||||
} else {
|
} else {
|
||||||
// Normal code line: respect single-line comments
|
sb.append(part.text)
|
||||||
val commentIdx = line.indexOf("//")
|
|
||||||
if (commentIdx >= 0) {
|
|
||||||
val code = line.substring(0, commentIdx)
|
|
||||||
val tail = line.substring(commentIdx)
|
|
||||||
val spaced = applyMinimalSpacing(code)
|
|
||||||
line = spaced + tail
|
|
||||||
} else {
|
|
||||||
line = applyMinimalSpacing(line)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
line = sb.toString()
|
||||||
|
inBlockComment = nextInBlockComment
|
||||||
}
|
}
|
||||||
out.append(line.trimEnd())
|
out.append(line.trimEnd())
|
||||||
if (i < lines.lastIndex) out.append('\n')
|
if (i < lines.lastIndex) out.append('\n')
|
||||||
@@ -224,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
|
||||||
@@ -269,7 +288,20 @@ object LyngFormatter {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
// Normalize collected base indent: replace tabs with spaces
|
// Normalize collected base indent: replace tabs with spaces
|
||||||
var baseIndent = if (onlyWs) base.toString().replace("\t", " ".repeat(config.indentSize)) else ""
|
var baseIndent = if (onlyWs) {
|
||||||
|
if (start == lineStart) {
|
||||||
|
// Range starts at line start, pick up this line's indentation as base
|
||||||
|
var k = start
|
||||||
|
val lineIndent = StringBuilder()
|
||||||
|
while (k < text.length && text[k] != '\n' && (text[k] == ' ' || text[k] == '\t')) {
|
||||||
|
lineIndent.append(text[k])
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
lineIndent.toString().replace("\t", " ".repeat(config.indentSize))
|
||||||
|
} else {
|
||||||
|
base.toString().replace("\t", " ".repeat(config.indentSize))
|
||||||
|
}
|
||||||
|
} else ""
|
||||||
var parentBaseIndent: String? = baseIndent
|
var parentBaseIndent: String? = baseIndent
|
||||||
if (baseIndent.isEmpty()) {
|
if (baseIndent.isEmpty()) {
|
||||||
// Fallback: use the indent of the nearest previous non-empty line as base.
|
// Fallback: use the indent of the nearest previous non-empty line as base.
|
||||||
@@ -369,7 +401,80 @@ object LyngFormatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyMinimalSpacing(code: String): String {
|
private enum class PartType { Code, StringLiteral, BlockComment, LineComment }
|
||||||
|
private data class Part(val text: String, val type: PartType)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a line into parts: code, string literals, and comments.
|
||||||
|
* Tracks [inBlockComment] state across lines.
|
||||||
|
*/
|
||||||
|
private fun splitIntoParts(
|
||||||
|
text: String,
|
||||||
|
inBlockCommentInitial: Boolean
|
||||||
|
): Pair<List<Part>, Boolean> {
|
||||||
|
val result = mutableListOf<Part>()
|
||||||
|
var i = 0
|
||||||
|
var last = 0
|
||||||
|
var inBlockComment = inBlockCommentInitial
|
||||||
|
var inString = false
|
||||||
|
var quoteChar = ' '
|
||||||
|
|
||||||
|
while (i < text.length) {
|
||||||
|
if (inBlockComment) {
|
||||||
|
if (text.startsWith("*/", i)) {
|
||||||
|
result.add(Part(text.substring(last, i + 2), PartType.BlockComment))
|
||||||
|
inBlockComment = false
|
||||||
|
i += 2
|
||||||
|
last = i
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
} else if (inString) {
|
||||||
|
if (text[i] == quoteChar) {
|
||||||
|
var escapeCount = 0
|
||||||
|
var j = i - 1
|
||||||
|
while (j >= 0 && text[j] == '\\') {
|
||||||
|
escapeCount++
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
if (escapeCount % 2 == 0) {
|
||||||
|
inString = false
|
||||||
|
result.add(Part(text.substring(last, i + 1), PartType.StringLiteral))
|
||||||
|
last = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
if (text.startsWith("//", i)) {
|
||||||
|
if (i > last) result.add(Part(text.substring(last, i), PartType.Code))
|
||||||
|
result.add(Part(text.substring(i), PartType.LineComment))
|
||||||
|
last = text.length
|
||||||
|
break
|
||||||
|
} else if (text.startsWith("/*", i)) {
|
||||||
|
if (i > last) result.add(Part(text.substring(last, i), PartType.Code))
|
||||||
|
inBlockComment = true
|
||||||
|
last = i
|
||||||
|
i += 2
|
||||||
|
} else if (text[i] == '"' || text[i] == '\'') {
|
||||||
|
if (i > last) result.add(Part(text.substring(last, i), PartType.Code))
|
||||||
|
inString = true
|
||||||
|
quoteChar = text[i]
|
||||||
|
last = i
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (last < text.length) {
|
||||||
|
val leftover = text.substring(last)
|
||||||
|
val type = if (inBlockComment) PartType.BlockComment else PartType.Code
|
||||||
|
result.add(Part(leftover, type))
|
||||||
|
}
|
||||||
|
return result to inBlockComment
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyMinimalSpacingRules(code: String): String {
|
||||||
var s = code
|
var s = code
|
||||||
// Ensure space before '(' for control-flow keywords
|
// Ensure space before '(' for control-flow keywords
|
||||||
s = s.replace(Regex("\\b(if|for|while)\\("), "$1 (")
|
s = s.replace(Regex("\\b(if|for|while)\\("), "$1 (")
|
||||||
@@ -408,15 +513,16 @@ private fun applyMinimalSpacing(code: String): String {
|
|||||||
s = s.replace(Regex(", \\)"), ",)")
|
s = s.replace(Regex(", \\)"), ",)")
|
||||||
s = s.replace(Regex(", \\]"), ",]")
|
s = s.replace(Regex(", \\]"), ",]")
|
||||||
// Equality/boolean operators: ensure spaces around
|
// Equality/boolean operators: ensure spaces around
|
||||||
s = s.replace(Regex("\\s*(==|!=|<=|>=|&&|\\|\\|)\\s*"), " $1 ")
|
s = s.replace(Regex("\\s*(==|!=|<=|>=|&&|\\|\\||\\+=|-=|\\*=|/=|=>|->)\\s*"), " $1 ")
|
||||||
// Assignment '=' (not part of '==', '!=', '<=', '>=', '=>'): collapse whitespace to single spaces around '='
|
// Assignment '=' (not part of '==', '!=', '<=', '>=', '=>', '+=', '-=', '*=', '/=', '->'): collapse whitespace to single spaces around '='
|
||||||
s = s.replace(Regex("(?<![=!<>])\\s*=\\s*(?![=>])"), " = ")
|
s = s.replace(Regex("(?<![=!<>+\\-*/])\\s*=\\s*(?![=>])"), " = ")
|
||||||
// Multiply/Divide/Mod as binary: spaces around
|
// Multiply/Divide/Mod as binary: spaces around. Avoid '*=', '/='
|
||||||
s = s.replace(Regex("\\s*([*/%])\\s*"), " $1 ")
|
s = s.replace(Regex("(?<![*])\\s*([*/%])\\s*(?![=])"), " $1 ")
|
||||||
// Addition as binary: spaces around '+'. We try not to break '++' and '+=' here.
|
// Addition as binary: spaces around '+'. We try not to break '++' and '+=' here.
|
||||||
s = s.replace(Regex("(?<![+])\\s*\\+\\s*(?![+=])"), " + ")
|
s = s.replace(Regex("(?<![+])\\s*\\+\\s*(?![+=])"), " + ")
|
||||||
// Subtraction as binary: add spaces when it looks binary. Keep unary '-' tight (after start or ( [ { = : ,)
|
// Subtraction as binary: add spaces when it looks binary. Keep unary '-' tight (after start or ( [ { = : ,)
|
||||||
s = s.replace(Regex("(?<=[^\\s(\\[\\{=:,])-(?=[^=])"), " - ")
|
// and avoid splitting '-=', '--', '->'
|
||||||
|
s = s.replace(Regex("(?<=[^\\s(\\[\\{=:,])\\s*-\\s*(?=[^=\\->])"), " - ")
|
||||||
// Colon in types/extends: remove spaces before, ensure one space after (keep '::' intact)
|
// Colon in types/extends: remove spaces before, ensure one space after (keep '::' intact)
|
||||||
s = s.replace(Regex("(?<!:)\\s*:(?!:)\\s*"), ": ")
|
s = s.replace(Regex("(?<!:)\\s*:(?!:)\\s*"), ": ")
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -41,11 +41,12 @@ private val fallbackKeywordIds = setOf(
|
|||||||
// boolean operators
|
// boolean operators
|
||||||
"and", "or", "not",
|
"and", "or", "not",
|
||||||
// declarations & modifiers
|
// declarations & modifiers
|
||||||
"fun", "fn", "class", "enum", "val", "var", "import", "package",
|
"fun", "fn", "class", "interface", "enum", "val", "var", "import", "package",
|
||||||
"private", "protected", "static", "open", "extern",
|
"abstract", "closed", "override",
|
||||||
|
"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]. */
|
||||||
@@ -73,7 +74,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) {
|
|||||||
Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation
|
Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation
|
||||||
|
|
||||||
// textual control keywords
|
// textual control keywords
|
||||||
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL,
|
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.OBJECT,
|
||||||
Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword
|
Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword
|
||||||
|
|
||||||
// labels / annotations
|
// labels / annotations
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -105,20 +105,24 @@ object BuiltinDocRegistry : BuiltinDocSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List names of extension-like methods defined for [className] in the stdlib text (`root.lyng`).
|
* List names of extension members defined for [className] in the stdlib text (`root.lyng`).
|
||||||
* We do a lightweight regex scan like: `fun ClassName.methodName(` and collect distinct names.
|
* We do a lightweight regex scan like: `fun ClassName.methodName(` or `val ClassName.propName`
|
||||||
|
* and collect distinct names.
|
||||||
*/
|
*/
|
||||||
fun extensionMethodNamesFor(className: String): List<String> {
|
fun extensionMemberNamesFor(className: String): List<String> {
|
||||||
val src = try { rootLyng } catch (_: Throwable) { null } ?: return emptyList()
|
val src = try { rootLyng } catch (_: Throwable) { null } ?: return emptyList()
|
||||||
val out = LinkedHashSet<String>()
|
val out = LinkedHashSet<String>()
|
||||||
// Match lines like: fun String.trim(...)
|
// Match lines like: fun String.trim(...) or val Int.isEven = ... (allowing modifiers)
|
||||||
val re = Regex("(?m)^\\s*fun\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\(")
|
val re = Regex("^\\s*(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*(?:fun|val|var)\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\b", RegexOption.MULTILINE)
|
||||||
re.findAll(src).forEach { m ->
|
re.findAll(src).forEach { m ->
|
||||||
val name = m.groupValues.getOrNull(1)?.trim()
|
val name = m.groupValues.getOrNull(1)?.trim()
|
||||||
if (!name.isNullOrEmpty()) out.add(name)
|
if (!name.isNullOrEmpty()) out.add(name)
|
||||||
}
|
}
|
||||||
return out.toList()
|
return out.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use extensionMemberNamesFor", ReplaceWith("extensionMemberNamesFor(className)"))
|
||||||
|
fun extensionMethodNamesFor(className: String): List<String> = extensionMemberNamesFor(className)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Builders ----------------
|
// ---------------- Builders ----------------
|
||||||
@@ -367,20 +371,20 @@ private object StdlibInlineDocIndex {
|
|||||||
else -> {
|
else -> {
|
||||||
// Non-comment, non-blank: try to match a declaration just after comments
|
// Non-comment, non-blank: try to match a declaration just after comments
|
||||||
if (buf.isNotEmpty()) {
|
if (buf.isNotEmpty()) {
|
||||||
// fun Class.name( ... )
|
// fun/val/var Class.name( ... ) (allowing modifiers)
|
||||||
val mExt = Regex("^fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(line)
|
val mExt = Regex("^(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*(?:fun|val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
|
||||||
if (mExt != null) {
|
if (mExt != null) {
|
||||||
val (cls, name) = mExt.destructured
|
val (cls, name) = mExt.destructured
|
||||||
flushTo(Key.Method(cls, name))
|
flushTo(Key.Method(cls, name))
|
||||||
} else {
|
} else {
|
||||||
// fun name( ... )
|
// fun name( ... ) (allowing modifiers)
|
||||||
val mTop = Regex("^fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(line)
|
val mTop = Regex("^(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(line)
|
||||||
if (mTop != null) {
|
if (mTop != null) {
|
||||||
val (name) = mTop.destructured
|
val (name) = mTop.destructured
|
||||||
flushTo(Key.TopFun(name))
|
flushTo(Key.TopFun(name))
|
||||||
} else {
|
} else {
|
||||||
// class Name
|
// class/interface Name (allowing modifiers)
|
||||||
val mClass = Regex("^class\\s+([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
|
val mClass = Regex("^(?:(?:abstract|private|protected|static|open|extern)\\s+)*(?:class|interface)\\s+([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
|
||||||
if (mClass != null) {
|
if (mClass != null) {
|
||||||
val (name) = mClass.destructured
|
val (name) = mClass.destructured
|
||||||
flushTo(Key.Clazz(name))
|
flushTo(Key.Clazz(name))
|
||||||
@@ -600,6 +604,11 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
|||||||
method(name = "printStackTrace", doc = StdlibInlineDocIndex.methodDoc("Exception", "printStackTrace") ?: "Print this exception and its stack trace to standard output.")
|
method(name = "printStackTrace", doc = StdlibInlineDocIndex.methodDoc("Exception", "printStackTrace") ?: "Print this exception and its stack trace to standard output.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod.classDoc(name = "Enum", doc = StdlibInlineDocIndex.classDoc("Enum") ?: "Base class for all enums.") {
|
||||||
|
method(name = "name", doc = "Returns the name of this enum constant.", returns = type("lyng.String"))
|
||||||
|
method(name = "ordinal", doc = "Returns the ordinal of this enum constant.", returns = type("lyng.Int"))
|
||||||
|
}
|
||||||
|
|
||||||
mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.") {
|
mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.") {
|
||||||
// Only include inline-source method here; Kotlin-embedded methods are now documented via DocHelpers near definitions.
|
// Only include inline-source method here; Kotlin-embedded methods are now documented via DocHelpers near definitions.
|
||||||
method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex"))
|
method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex"))
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Pure-Kotlin, PSI-free completion engine used for isolated tests and non-IDE harnesses.
|
* Pure-Kotlin, PSI-free completion engine used for isolated tests and non-IDE harnesses.
|
||||||
* Mirrors the IntelliJ MVP logic: MiniAst + BuiltinDocRegistry + lenient imports.
|
* Mirrors the IntelliJ MVP logic: MiniAst + BuiltinDocRegistry + lenient imports.
|
||||||
@@ -16,7 +33,7 @@ data class CompletionItem(
|
|||||||
val typeText: String? = null,
|
val typeText: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class Kind { Function, Class_, Value, Method, Field }
|
enum class Kind { Function, Class_, Enum, Value, Method, Field }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform-free, lenient import provider that never fails on unknown packages.
|
* Platform-free, lenient import provider that never fails on unknown packages.
|
||||||
@@ -45,43 +62,33 @@ object CompletionEngineLight {
|
|||||||
StdlibDocsBootstrap.ensure()
|
StdlibDocsBootstrap.ensure()
|
||||||
val prefix = prefixAt(text, caret)
|
val prefix = prefixAt(text, caret)
|
||||||
val mini = buildMiniAst(text)
|
val mini = buildMiniAst(text)
|
||||||
// Build imported modules as a UNION of MiniAst-derived and textual extraction, always including stdlib
|
val imported: List<String> = DocLookupUtils.canonicalImportedModules(mini ?: return emptyList(), text)
|
||||||
run {
|
|
||||||
// no-op block to keep local scope tidy
|
|
||||||
}
|
|
||||||
val fromMini: List<String> = mini?.let { DocLookupUtils.canonicalImportedModules(it) } ?: emptyList()
|
|
||||||
val fromText: List<String> = extractImportsFromText(text)
|
|
||||||
val imported: List<String> = LinkedHashSet<String>().apply {
|
|
||||||
fromMini.forEach { add(it) }
|
|
||||||
fromText.forEach { add(it) }
|
|
||||||
add("lyng.stdlib")
|
|
||||||
}.toList()
|
|
||||||
|
|
||||||
val cap = 200
|
val cap = 200
|
||||||
val out = ArrayList<CompletionItem>(64)
|
val out = ArrayList<CompletionItem>(64)
|
||||||
|
|
||||||
// Member context detection: dot immediately before caret or before current word start
|
// Member context detection: dot immediately before caret or before current word start
|
||||||
val word = wordRangeAt(text, caret)
|
val word = DocLookupUtils.wordRangeAt(text, caret)
|
||||||
val memberDot = findDotLeft(text, word?.first ?: caret)
|
val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret)
|
||||||
if (memberDot != null) {
|
if (memberDot != null) {
|
||||||
// 0) Try chained member call return type inference
|
// 0) Try chained member call return type inference
|
||||||
guessReturnClassFromMemberCallBefore(text, memberDot, imported)?.let { cls ->
|
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
|
||||||
offerMembersAdd(out, prefix, imported, cls)
|
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
// 0a) Top-level call before dot
|
// 0a) Top-level call before dot
|
||||||
guessReturnClassFromTopLevelCallBefore(text, memberDot, imported)?.let { cls ->
|
DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDot, imported, mini)?.let { cls ->
|
||||||
offerMembersAdd(out, prefix, imported, cls)
|
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
// 0b) Across-known-callees (Iterable/Iterator/List preference)
|
// 0b) Across-known-callees (Iterable/Iterator/List preference)
|
||||||
guessReturnClassAcrossKnownCallees(text, memberDot, imported)?.let { cls ->
|
DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDot, imported, mini)?.let { cls ->
|
||||||
offerMembersAdd(out, prefix, imported, cls)
|
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
// 1) Receiver inference fallback
|
// 1) Receiver inference fallback
|
||||||
guessReceiverClass(text, memberDot, imported)?.let { cls ->
|
(DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
|
||||||
offerMembersAdd(out, prefix, imported, cls)
|
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
// In member context and unknown receiver/return type: show nothing (no globals after dot)
|
// In member context and unknown receiver/return type: show nothing (no globals after dot)
|
||||||
@@ -93,9 +100,11 @@ object CompletionEngineLight {
|
|||||||
val decls = m.declarations
|
val decls = m.declarations
|
||||||
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
|
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
|
||||||
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
||||||
|
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
||||||
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
|
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
|
||||||
funs.forEach { offerDeclAdd(out, prefix, it) }
|
funs.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
classes.forEach { offerDeclAdd(out, prefix, it) }
|
classes.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
|
enums.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
vals.forEach { offerDeclAdd(out, prefix, it) }
|
vals.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,9 +118,11 @@ object CompletionEngineLight {
|
|||||||
val decls = BuiltinDocRegistry.docsForModule(mod)
|
val decls = BuiltinDocRegistry.docsForModule(mod)
|
||||||
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
|
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
|
||||||
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
||||||
|
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
||||||
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
|
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
|
||||||
funs.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
funs.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
classes.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
classes.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
|
enums.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
vals.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
vals.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
if (out.size >= cap || externalAdded >= budget) break
|
if (out.size >= cap || externalAdded >= budget) break
|
||||||
}
|
}
|
||||||
@@ -130,13 +141,14 @@ object CompletionEngineLight {
|
|||||||
add(CompletionItem(d.name, Kind.Function, tailText = tail, typeText = typeOf(d.returnType)))
|
add(CompletionItem(d.name, Kind.Function, tailText = tail, typeText = typeOf(d.returnType)))
|
||||||
}
|
}
|
||||||
is MiniClassDecl -> add(CompletionItem(d.name, Kind.Class_))
|
is MiniClassDecl -> add(CompletionItem(d.name, Kind.Class_))
|
||||||
|
is MiniEnumDecl -> add(CompletionItem(d.name, Kind.Enum))
|
||||||
is MiniValDecl -> add(CompletionItem(d.name, Kind.Value, typeText = typeOf(d.type)))
|
is MiniValDecl -> add(CompletionItem(d.name, Kind.Value, typeText = typeOf(d.type)))
|
||||||
// else -> add(CompletionItem(d.name, Kind.Value))
|
// else -> add(CompletionItem(d.name, Kind.Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun offerMembersAdd(out: MutableList<CompletionItem>, prefix: String, imported: List<String>, className: String) {
|
private fun offerMembersAdd(out: MutableList<CompletionItem>, prefix: String, imported: List<String>, className: String, mini: MiniScript? = null) {
|
||||||
val classes = DocLookupUtils.aggregateClasses(imported)
|
val classes = DocLookupUtils.aggregateClasses(imported, mini)
|
||||||
val visited = mutableSetOf<String>()
|
val visited = mutableSetOf<String>()
|
||||||
val directMap = LinkedHashMap<String, MutableList<MiniMemberDecl>>()
|
val directMap = LinkedHashMap<String, MutableList<MiniMemberDecl>>()
|
||||||
val inheritedMap = LinkedHashMap<String, MutableList<MiniMemberDecl>>()
|
val inheritedMap = LinkedHashMap<String, MutableList<MiniMemberDecl>>()
|
||||||
@@ -183,6 +195,7 @@ object CompletionEngineLight {
|
|||||||
val ci = CompletionItem(name, Kind.Field, typeText = typeOf((chosen as MiniMemberValDecl).type))
|
val ci = CompletionItem(name, Kind.Field, typeText = typeOf((chosen as MiniMemberValDecl).type))
|
||||||
if (ci.name.startsWith(prefix, true)) out += ci
|
if (ci.name.startsWith(prefix, true)) out += ci
|
||||||
}
|
}
|
||||||
|
is MiniInitDecl -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,10 +203,10 @@ object CompletionEngineLight {
|
|||||||
emitGroup(directMap)
|
emitGroup(directMap)
|
||||||
emitGroup(inheritedMap)
|
emitGroup(inheritedMap)
|
||||||
|
|
||||||
// Supplement with stdlib extension-like methods defined in root.lyng (e.g., fun String.re(...))
|
// Supplement with stdlib extension members defined in root.lyng (e.g., fun String.re(...))
|
||||||
run {
|
run {
|
||||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className)
|
val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
|
||||||
for (name in ext) {
|
for (name in ext) {
|
||||||
if (already.contains(name)) continue
|
if (already.contains(name)) continue
|
||||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
|
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
|
||||||
@@ -210,6 +223,7 @@ object CompletionEngineLight {
|
|||||||
if (ci.name.startsWith(prefix, true)) out += ci
|
if (ci.name.startsWith(prefix, true)) out += ci
|
||||||
already.add(name)
|
already.add(name)
|
||||||
}
|
}
|
||||||
|
is MiniInitDecl -> {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: emit simple method name without detailed types
|
// Fallback: emit simple method name without detailed types
|
||||||
@@ -223,167 +237,16 @@ object CompletionEngineLight {
|
|||||||
|
|
||||||
// --- Inference helpers (text-only, PSI-free) ---
|
// --- Inference helpers (text-only, PSI-free) ---
|
||||||
|
|
||||||
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>): String? {
|
private suspend fun buildMiniAst(text: String): MiniScript? {
|
||||||
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
|
|
||||||
var i = prevNonWs(text, dotPos - 1)
|
|
||||||
if (i >= 0) {
|
|
||||||
when (text[i]) {
|
|
||||||
'"' -> return "String"
|
|
||||||
']' -> return "List"
|
|
||||||
'}' -> return "Dict"
|
|
||||||
')' -> {
|
|
||||||
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
|
|
||||||
var j = i - 1
|
|
||||||
var depth = 0
|
|
||||||
while (j >= 0) {
|
|
||||||
when (text[j]) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
if (j >= 0 && text[j] == '(') {
|
|
||||||
val innerS = (j + 1).coerceAtLeast(0)
|
|
||||||
val innerE = i.coerceAtMost(text.length)
|
|
||||||
if (innerS < innerE) {
|
|
||||||
val inner = text.substring(innerS, innerE).trim()
|
|
||||||
if (inner.startsWith('"') && inner.endsWith('"')) return "String"
|
|
||||||
if (inner.startsWith('[') && inner.endsWith(']')) return "List"
|
|
||||||
if (inner.startsWith('{') && inner.endsWith('}')) return "Dict"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Numeric literal: decimal/int/hex/scientific
|
|
||||||
var j = i
|
|
||||||
var hasDigits = false
|
|
||||||
var hasDot = false
|
|
||||||
var hasExp = false
|
|
||||||
while (j >= 0) {
|
|
||||||
val ch = text[j]
|
|
||||||
if (ch.isDigit()) { hasDigits = true; j--; continue }
|
|
||||||
if (ch == '.') { hasDot = true; j--; continue }
|
|
||||||
if (ch == 'e' || ch == 'E') { hasExp = true; j--; if (j >= 0 && (text[j] == '+' || text[j] == '-')) j--; continue }
|
|
||||||
if (ch in listOf('x','X','a','b','c','d','f','A','B','C','D','F')) { j--; continue }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (hasDigits) return if (hasDot || hasExp) "Real" else "Int"
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun guessReturnClassFromMemberCallBefore(text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
var i = prevNonWs(text, dotPos - 1)
|
|
||||||
if (i < 0 || text[i] != ')') return null
|
|
||||||
i--
|
|
||||||
var depth = 0
|
|
||||||
while (i >= 0) {
|
|
||||||
when (text[i]) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
if (i < 0 || text[i] != '(') return null
|
|
||||||
var j = i - 1
|
|
||||||
while (j >= 0 && text[j].isWhitespace()) j--
|
|
||||||
val end = j + 1
|
|
||||||
while (j >= 0 && isIdentChar(text[j])) j--
|
|
||||||
val start = j + 1
|
|
||||||
if (start >= end) return null
|
|
||||||
val callee = text.substring(start, end)
|
|
||||||
var k = start - 1
|
|
||||||
while (k >= 0 && text[k].isWhitespace()) k--
|
|
||||||
if (k < 0 || text[k] != '.') return null
|
|
||||||
val prevDot = k
|
|
||||||
val receiverClass = guessReceiverClass(text, prevDot, imported) ?: return null
|
|
||||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee) ?: return null
|
|
||||||
val member = resolved.second
|
|
||||||
val ret = when (member) {
|
|
||||||
is MiniMemberFunDecl -> member.returnType
|
|
||||||
is MiniMemberValDecl -> member.type
|
|
||||||
}
|
|
||||||
return simpleClassNameOf(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun guessReturnClassFromTopLevelCallBefore(text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
var i = prevNonWs(text, dotPos - 1)
|
|
||||||
if (i < 0 || text[i] != ')') return null
|
|
||||||
i--
|
|
||||||
var depth = 0
|
|
||||||
while (i >= 0) {
|
|
||||||
when (text[i]) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
if (i < 0 || text[i] != '(') return null
|
|
||||||
var j = i - 1
|
|
||||||
while (j >= 0 && text[j].isWhitespace()) j--
|
|
||||||
val end = j + 1
|
|
||||||
while (j >= 0 && isIdentChar(text[j])) j--
|
|
||||||
val start = j + 1
|
|
||||||
if (start >= end) return null
|
|
||||||
val callee = text.substring(start, end)
|
|
||||||
var k = start - 1
|
|
||||||
while (k >= 0 && text[k].isWhitespace()) k--
|
|
||||||
if (k >= 0 && text[k] == '.') return null // was a member call
|
|
||||||
for (mod in imported) {
|
|
||||||
val decls = BuiltinDocRegistry.docsForModule(mod)
|
|
||||||
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
|
|
||||||
if (fn != null) return simpleClassNameOf(fn.returnType)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun guessReturnClassAcrossKnownCallees(text: String, dotPos: Int, imported: List<String>): String? {
|
|
||||||
var i = prevNonWs(text, dotPos - 1)
|
|
||||||
if (i < 0 || text[i] != ')') return null
|
|
||||||
i--
|
|
||||||
var depth = 0
|
|
||||||
while (i >= 0) {
|
|
||||||
when (text[i]) {
|
|
||||||
')' -> depth++
|
|
||||||
'(' -> if (depth == 0) break else depth--
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
if (i < 0 || text[i] != '(') return null
|
|
||||||
var j = i - 1
|
|
||||||
while (j >= 0 && text[j].isWhitespace()) j--
|
|
||||||
val end = j + 1
|
|
||||||
while (j >= 0 && isIdentChar(text[j])) j--
|
|
||||||
val start = j + 1
|
|
||||||
if (start >= end) return null
|
|
||||||
val callee = text.substring(start, end)
|
|
||||||
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, callee) ?: return null
|
|
||||||
val member = resolved.second
|
|
||||||
val ret = when (member) {
|
|
||||||
is MiniMemberFunDecl -> member.returnType
|
|
||||||
is MiniMemberValDecl -> member.type
|
|
||||||
}
|
|
||||||
return simpleClassNameOf(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
|
|
||||||
null -> null
|
|
||||||
is MiniTypeName -> t.segments.lastOrNull()?.name
|
|
||||||
is MiniGenericType -> simpleClassNameOf(t.base)
|
|
||||||
is MiniFunctionType -> null
|
|
||||||
is MiniTypeVar -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MiniAst and small utils ---
|
|
||||||
|
|
||||||
private suspend fun buildMiniAst(text: String): MiniScript? = try {
|
|
||||||
val sink = MiniAstBuilder()
|
val sink = MiniAstBuilder()
|
||||||
val src = Source("<engine>", text)
|
return try {
|
||||||
val provider = LenientImportProvider.create()
|
val src = Source("<engine>", text)
|
||||||
Compiler.compileWithMini(src, provider, sink)
|
val provider = LenientImportProvider.create()
|
||||||
sink.build()
|
Compiler.compileWithMini(src, provider, sink)
|
||||||
} catch (_: Throwable) {
|
sink.build()
|
||||||
null
|
} catch (_: Throwable) {
|
||||||
|
sink.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun typeOf(t: MiniTypeRef?): String = when (t) {
|
private fun typeOf(t: MiniTypeRef?): String = when (t) {
|
||||||
@@ -404,42 +267,8 @@ object CompletionEngineLight {
|
|||||||
private fun prefixAt(text: String, offset: Int): String {
|
private fun prefixAt(text: String, offset: Int): String {
|
||||||
val off = offset.coerceIn(0, text.length)
|
val off = offset.coerceIn(0, text.length)
|
||||||
var i = (off - 1).coerceAtLeast(0)
|
var i = (off - 1).coerceAtLeast(0)
|
||||||
while (i >= 0 && isIdentChar(text[i])) i--
|
while (i >= 0 && DocLookupUtils.isIdentChar(text[i])) i--
|
||||||
val start = i + 1
|
val start = i + 1
|
||||||
return if (start in 0..text.length && start <= off) text.substring(start, off) else ""
|
return if (start in 0..text.length && start <= off) text.substring(start, off) else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun wordRangeAt(text: String, offset: Int): Pair<Int, Int>? {
|
|
||||||
if (text.isEmpty()) return null
|
|
||||||
val off = offset.coerceIn(0, text.length)
|
|
||||||
var s = off
|
|
||||||
var e = off
|
|
||||||
while (s > 0 && isIdentChar(text[s - 1])) s--
|
|
||||||
while (e < text.length && isIdentChar(text[e])) e++
|
|
||||||
return if (s < e) s to e else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findDotLeft(text: String, offset: Int): Int? {
|
|
||||||
var i = (offset - 1).coerceAtLeast(0)
|
|
||||||
while (i >= 0 && text[i].isWhitespace()) i--
|
|
||||||
return if (i >= 0 && text[i] == '.') i else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun prevNonWs(text: String, start: Int): Int {
|
|
||||||
var i = start.coerceAtMost(text.length - 1)
|
|
||||||
while (i >= 0 && text[i].isWhitespace()) i--
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
|
|
||||||
|
|
||||||
private fun extractImportsFromText(text: String): List<String> {
|
|
||||||
val result = LinkedHashSet<String>()
|
|
||||||
val re = Regex("(?m)^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)")
|
|
||||||
re.findAll(text).forEach { m ->
|
|
||||||
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
|
|
||||||
if (raw.isNotEmpty()) result.add(if (raw.startsWith("lyng.")) raw else "lyng.$raw")
|
|
||||||
}
|
|
||||||
return result.toList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Shared QuickDoc lookup helpers reusable outside the IDEA plugin.
|
* Shared QuickDoc lookup helpers reusable outside the IDEA plugin.
|
||||||
*/
|
*/
|
||||||
package net.sergeych.lyng.miniast
|
package net.sergeych.lyng.miniast
|
||||||
|
|
||||||
|
import net.sergeych.lyng.binding.BindingSnapshot
|
||||||
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
|
|
||||||
object DocLookupUtils {
|
object DocLookupUtils {
|
||||||
|
fun findDeclarationAt(mini: MiniScript, offset: Int, name: String): Pair<String, String>? {
|
||||||
|
val src = mini.range.start.source
|
||||||
|
fun matches(p: net.sergeych.lyng.Pos, len: Int) = src.offsetOf(p).let { s -> offset >= s && offset < s + len }
|
||||||
|
|
||||||
|
for (d in mini.declarations) {
|
||||||
|
if (matches(d.nameStart, d.name.length)) {
|
||||||
|
val kind = when (d) {
|
||||||
|
is MiniFunDecl -> "Function"
|
||||||
|
is MiniClassDecl -> "Class"
|
||||||
|
is MiniEnumDecl -> "Enum"
|
||||||
|
is MiniValDecl -> if (d.mutable) "Variable" else "Value"
|
||||||
|
}
|
||||||
|
return d.name to kind
|
||||||
|
}
|
||||||
|
|
||||||
|
val members = when (d) {
|
||||||
|
is MiniFunDecl -> {
|
||||||
|
for (p in d.params) {
|
||||||
|
if (matches(p.nameStart, p.name.length)) return p.name to "Parameter"
|
||||||
|
}
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
is MiniEnumDecl -> {
|
||||||
|
// Enum entries don't have explicit nameStart yet, but we can check their text range if we had it.
|
||||||
|
// For now, heuristic: if offset is within enum range and matches an entry name.
|
||||||
|
// To be more precise, we should check that we are NOT at the 'enum' or enum name.
|
||||||
|
if (offset >= src.offsetOf(d.range.start) && offset <= src.offsetOf(d.range.end)) {
|
||||||
|
if (d.entries.contains(name) && !matches(d.nameStart, d.name.length)) {
|
||||||
|
// verify we are actually at the entry word in text
|
||||||
|
val off = src.offsetOf(d.range.start)
|
||||||
|
val end = src.offsetOf(d.range.end)
|
||||||
|
val text = src.text.substring(off, end)
|
||||||
|
// This is still a bit loose but better
|
||||||
|
return name to "EnumConstant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
is MiniClassDecl -> {
|
||||||
|
for (cf in d.ctorFields) {
|
||||||
|
if (matches(cf.nameStart, cf.name.length)) return cf.name to (if (cf.mutable) "Variable" else "Value")
|
||||||
|
}
|
||||||
|
for (cf in d.classFields) {
|
||||||
|
if (matches(cf.nameStart, cf.name.length)) return cf.name to (if (cf.mutable) "Variable" else "Value")
|
||||||
|
}
|
||||||
|
d.members
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (m in members) {
|
||||||
|
if (matches(m.nameStart, m.name.length)) {
|
||||||
|
val kind = when (m) {
|
||||||
|
is MiniMemberFunDecl -> "Function"
|
||||||
|
is MiniMemberValDecl -> if (m.isStatic) "Value" else (if (m.mutable) "Variable" else "Value")
|
||||||
|
is MiniInitDecl -> "Initializer"
|
||||||
|
}
|
||||||
|
return m.name to kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findTypeByRange(mini: MiniScript?, name: String, startOffset: Int): MiniTypeRef? {
|
||||||
|
if (mini == null) return null
|
||||||
|
val src = mini.range.start.source
|
||||||
|
|
||||||
|
for (d in mini.declarations) {
|
||||||
|
if (d.name == name && src.offsetOf(d.nameStart) == startOffset) {
|
||||||
|
return when (d) {
|
||||||
|
is MiniValDecl -> d.type
|
||||||
|
is MiniFunDecl -> d.returnType
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d is MiniFunDecl) {
|
||||||
|
for (p in d.params) {
|
||||||
|
if (p.name == name && src.offsetOf(p.nameStart) == startOffset) return p.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d is MiniClassDecl) {
|
||||||
|
for (m in d.members) {
|
||||||
|
if (m.name == name && src.offsetOf(m.nameStart) == startOffset) {
|
||||||
|
return when (m) {
|
||||||
|
is MiniMemberFunDecl -> m.returnType
|
||||||
|
is MiniMemberValDecl -> m.type
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (cf in d.ctorFields) {
|
||||||
|
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type
|
||||||
|
}
|
||||||
|
for (cf in d.classFields) {
|
||||||
|
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert MiniAst imports to fully-qualified module names expected by BuiltinDocRegistry.
|
* Convert MiniAst imports to fully-qualified module names expected by BuiltinDocRegistry.
|
||||||
* Heuristics:
|
* Heuristics:
|
||||||
* - If an import does not start with "lyng.", prefix it with "lyng." (e.g., "io.fs" -> "lyng.io.fs").
|
* - If an import does not start with "lyng.", prefix it with "lyng." (e.g., "io.fs" -> "lyng.io.fs").
|
||||||
* - Keep original if it already starts with "lyng.".
|
* - Keep original if it already starts with "lyng.".
|
||||||
* - Always include "lyng.stdlib" to make builtins available for docs.
|
* - Always include "lyng.stdlib" to make builtins available for docs.
|
||||||
|
* - If [sourceText] is provided, uses textual extraction as a fallback if MiniAst has no imports.
|
||||||
*/
|
*/
|
||||||
fun canonicalImportedModules(mini: MiniScript): List<String> {
|
fun canonicalImportedModules(mini: MiniScript, sourceText: String? = null): List<String> {
|
||||||
val raw = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
|
val raw = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
|
||||||
val result = LinkedHashSet<String>()
|
val result = LinkedHashSet<String>()
|
||||||
for (name in raw) {
|
for (name in raw) {
|
||||||
val canon = if (name.startsWith("lyng.")) name else "lyng.$name"
|
val canon = if (name.startsWith("lyng.")) name else "lyng.$name"
|
||||||
result.add(canon)
|
result.add(canon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.isEmpty() && sourceText != null) {
|
||||||
|
result.addAll(extractImportsFromText(sourceText))
|
||||||
|
}
|
||||||
|
|
||||||
// Always make stdlib available as a fallback context for common types,
|
// Always make stdlib available as a fallback context for common types,
|
||||||
// even when there are no explicit imports in the file
|
// even when there are no explicit imports in the file
|
||||||
result.add("lyng.stdlib")
|
result.add("lyng.stdlib")
|
||||||
return result.toList()
|
return result.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aggregateClasses(importedModules: List<String>): Map<String, MiniClassDecl> {
|
fun extractImportsFromText(text: String): List<String> {
|
||||||
|
val result = LinkedHashSet<String>()
|
||||||
|
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
|
||||||
|
re.findAll(text).forEach { m ->
|
||||||
|
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
|
||||||
|
if (raw.isNotEmpty()) {
|
||||||
|
val canon = if (raw.startsWith("lyng.")) raw else "lyng.$raw"
|
||||||
|
result.add(canon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aggregateClasses(importedModules: List<String>, localMini: MiniScript? = null): Map<String, MiniClassDecl> {
|
||||||
// Collect all class decls by name across modules, then merge duplicates by unioning members and bases.
|
// Collect all class decls by name across modules, then merge duplicates by unioning members and bases.
|
||||||
val buckets = LinkedHashMap<String, MutableList<MiniClassDecl>>()
|
val buckets = LinkedHashMap<String, MutableList<MiniClassDecl>>()
|
||||||
for (mod in importedModules) {
|
for (mod in importedModules) {
|
||||||
@@ -32,6 +178,23 @@ object DocLookupUtils {
|
|||||||
for (cls in docs.filterIsInstance<MiniClassDecl>()) {
|
for (cls in docs.filterIsInstance<MiniClassDecl>()) {
|
||||||
buckets.getOrPut(cls.name) { mutableListOf() }.add(cls)
|
buckets.getOrPut(cls.name) { mutableListOf() }.add(cls)
|
||||||
}
|
}
|
||||||
|
for (en in docs.filterIsInstance<MiniEnumDecl>()) {
|
||||||
|
buckets.getOrPut(en.name) { mutableListOf() }.add(enumToSyntheticClass(en))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add local declarations
|
||||||
|
localMini?.declarations?.forEach { d ->
|
||||||
|
when (d) {
|
||||||
|
is MiniClassDecl -> {
|
||||||
|
buckets.getOrPut(d.name) { mutableListOf() }.add(d)
|
||||||
|
}
|
||||||
|
is MiniEnumDecl -> {
|
||||||
|
val syn = enumToSyntheticClass(d)
|
||||||
|
buckets.getOrPut(d.name) { mutableListOf() }.add(syn)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeClassDecls(name: String, list: List<MiniClassDecl>): MiniClassDecl {
|
fun mergeClassDecls(name: String, list: List<MiniClassDecl>): MiniClassDecl {
|
||||||
@@ -72,8 +235,8 @@ object DocLookupUtils {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String): Pair<String, MiniMemberDecl>? {
|
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
|
||||||
val classes = aggregateClasses(importedModules)
|
val classes = aggregateClasses(importedModules, localMini)
|
||||||
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? {
|
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? {
|
||||||
val cls = classes[name] ?: return null
|
val cls = classes[name] ?: return null
|
||||||
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
||||||
@@ -86,12 +249,12 @@ object DocLookupUtils {
|
|||||||
return dfs(className, mutableSetOf())
|
return dfs(className, mutableSetOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findMemberAcrossClasses(importedModules: List<String>, member: String): Pair<String, MiniMemberDecl>? {
|
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
|
||||||
val classes = aggregateClasses(importedModules)
|
val classes = aggregateClasses(importedModules, localMini)
|
||||||
// Preferred order for ambiguous common ops
|
// Preferred order for ambiguous common ops
|
||||||
val preference = listOf("Iterable", "Iterator", "List")
|
val preference = listOf("Iterable", "Iterator", "List")
|
||||||
for (name in preference) {
|
for (name in preference) {
|
||||||
resolveMemberWithInheritance(importedModules, name, member)?.let { return it }
|
resolveMemberWithInheritance(importedModules, name, member, localMini)?.let { return it }
|
||||||
}
|
}
|
||||||
for ((name, cls) in classes) {
|
for ((name, cls) in classes) {
|
||||||
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
||||||
@@ -104,12 +267,8 @@ object DocLookupUtils {
|
|||||||
* We walk left from the dot, find a matching `)` and then the identifier immediately before the `(`.
|
* We walk left from the dot, find a matching `)` and then the identifier immediately before the `(`.
|
||||||
* If that identifier matches a known class in any of the imported modules, return it.
|
* If that identifier matches a known class in any of the imported modules, return it.
|
||||||
*/
|
*/
|
||||||
fun guessClassFromCallBefore(text: String, dotPos: Int, importedModules: List<String>): String? {
|
fun guessClassFromCallBefore(text: String, dotPos: Int, importedModules: List<String>, localMini: MiniScript? = null): String? {
|
||||||
var i = (dotPos - 1).coerceAtLeast(0)
|
var i = (dotPos - 1).coerceAtLeast(0)
|
||||||
// Skip spaces
|
|
||||||
while (i >= 0 && text[i].isWhitespace()) i++
|
|
||||||
// Note: the previous line is wrong direction; correct implementation below
|
|
||||||
i = (dotPos - 1).coerceAtLeast(0)
|
|
||||||
while (i >= 0 && text[i].isWhitespace()) i--
|
while (i >= 0 && text[i].isWhitespace()) i--
|
||||||
if (i < 0 || text[i] != ')') return null
|
if (i < 0 || text[i] != ')') return null
|
||||||
// Walk back to matching '(' accounting nested parentheses
|
// Walk back to matching '(' accounting nested parentheses
|
||||||
@@ -134,9 +293,396 @@ object DocLookupUtils {
|
|||||||
if (start >= end) return null
|
if (start >= end) return null
|
||||||
val name = text.substring(start, end)
|
val name = text.substring(start, end)
|
||||||
// Validate against imported classes
|
// Validate against imported classes
|
||||||
val classes = aggregateClasses(importedModules)
|
val classes = aggregateClasses(importedModules, localMini)
|
||||||
return if (classes.containsKey(name)) name else null
|
return if (classes.containsKey(name)) name else null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
|
fun guessReceiverClassViaMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? {
|
||||||
|
if (mini == null) return null
|
||||||
|
val i = prevNonWs(text, dotPos - 1)
|
||||||
|
if (i < 0) return null
|
||||||
|
val wordRange = wordRangeAt(text, i + 1) ?: return null
|
||||||
|
val ident = text.substring(wordRange.first, wordRange.second)
|
||||||
|
|
||||||
|
// 0) Use binding if available for precise resolution
|
||||||
|
if (binding != null) {
|
||||||
|
val ref = binding.references.firstOrNull { wordRange.first >= it.start && wordRange.first < it.end }
|
||||||
|
if (ref != null) {
|
||||||
|
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||||
|
if (sym != null) {
|
||||||
|
val type = findTypeByRange(mini, sym.name, sym.declStart)
|
||||||
|
simpleClassNameOf(type)?.let { return it }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check if it's a declaration (e.g. static access to a class)
|
||||||
|
val sym = binding.symbols.firstOrNull { it.declStart == wordRange.first && it.name == ident }
|
||||||
|
if (sym != null) {
|
||||||
|
val type = findTypeByRange(mini, sym.name, sym.declStart)
|
||||||
|
simpleClassNameOf(type)?.let { return it }
|
||||||
|
// if it's a class/enum, return its name directly
|
||||||
|
if (sym.kind == net.sergeych.lyng.binding.SymbolKind.Class || sym.kind == net.sergeych.lyng.binding.SymbolKind.Enum) return sym.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Global declarations in current file (val/var/fun/class/enum)
|
||||||
|
val d = mini.declarations.firstOrNull { it.name == ident }
|
||||||
|
if (d != null) {
|
||||||
|
return when (d) {
|
||||||
|
is MiniClassDecl -> d.name
|
||||||
|
is MiniEnumDecl -> d.name
|
||||||
|
is MiniValDecl -> simpleClassNameOf(d.type)
|
||||||
|
is MiniFunDecl -> simpleClassNameOf(d.returnType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Parameters in any function (best-effort fallback without scope mapping)
|
||||||
|
for (fd in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||||
|
for (p in fd.params) {
|
||||||
|
if (p.name == ident) return simpleClassNameOf(p.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Recursive chaining: Base.ident.
|
||||||
|
val dotBefore = findDotLeft(text, wordRange.first)
|
||||||
|
if (dotBefore != null) {
|
||||||
|
val receiverClass = guessReceiverClassViaMini(mini, text, dotBefore, imported, binding)
|
||||||
|
?: guessReceiverClass(text, dotBefore, imported, mini)
|
||||||
|
if (receiverClass != null) {
|
||||||
|
val resolved = resolveMemberWithInheritance(imported, receiverClass, ident, mini)
|
||||||
|
if (resolved != null) {
|
||||||
|
val rt = when (val m = resolved.second) {
|
||||||
|
is MiniMemberFunDecl -> m.returnType
|
||||||
|
is MiniMemberValDecl -> m.type
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return simpleClassNameOf(rt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Check if it's a known class (static access)
|
||||||
|
val classes = aggregateClasses(imported, mini)
|
||||||
|
if (classes.containsKey(ident)) return ident
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun guessReturnClassFromMemberCallBeforeMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? {
|
||||||
|
if (mini == null) return null
|
||||||
|
var i = prevNonWs(text, dotPos - 1)
|
||||||
|
if (i < 0 || text[i] != ')') return null
|
||||||
|
// back to matching '('
|
||||||
|
i--
|
||||||
|
var depth = 0
|
||||||
|
while (i >= 0) {
|
||||||
|
when (text[i]) {
|
||||||
|
')' -> depth++
|
||||||
|
'(' -> if (depth == 0) break else depth--
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
if (i < 0 || text[i] != '(') return null
|
||||||
|
var j = i - 1
|
||||||
|
while (j >= 0 && text[j].isWhitespace()) j--
|
||||||
|
val end = j + 1
|
||||||
|
while (j >= 0 && isIdentChar(text[j])) j--
|
||||||
|
val start = j + 1
|
||||||
|
if (start >= end) return null
|
||||||
|
val callee = text.substring(start, end)
|
||||||
|
// Ensure member call: dot before callee
|
||||||
|
var k = start - 1
|
||||||
|
while (k >= 0 && text[k].isWhitespace()) k--
|
||||||
|
if (k < 0 || text[k] != '.') return null
|
||||||
|
val prevDot = k
|
||||||
|
// Resolve receiver class via MiniAst (ident like `x`)
|
||||||
|
val receiverClass = guessReceiverClassViaMini(mini, text, prevDot, imported, binding) ?: return null
|
||||||
|
// If receiver class is a locally declared class, resolve member on it
|
||||||
|
val localClass = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == receiverClass }
|
||||||
|
if (localClass != null) {
|
||||||
|
val mm = localClass.members.firstOrNull { it.name == callee }
|
||||||
|
if (mm != null) {
|
||||||
|
val rt = when (mm) {
|
||||||
|
is MiniMemberFunDecl -> mm.returnType
|
||||||
|
is MiniMemberValDecl -> mm.type
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return simpleClassNameOf(rt)
|
||||||
|
} else {
|
||||||
|
// Try to scan class body text for method signature and extract return type
|
||||||
|
val sigs = scanLocalClassMembersFromText(mini, text, localClass)
|
||||||
|
val sig = sigs[callee]
|
||||||
|
if (sig != null && sig.typeText != null) return sig.typeText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Else fallback to registry-based resolution (covers imported classes)
|
||||||
|
return resolveMemberWithInheritance(imported, receiverClass, callee, mini)?.second?.let { m ->
|
||||||
|
val rt = when (m) {
|
||||||
|
is MiniMemberFunDecl -> m.returnType
|
||||||
|
is MiniMemberValDecl -> m.type
|
||||||
|
is MiniInitDecl -> null
|
||||||
|
}
|
||||||
|
simpleClassNameOf(rt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ScannedSig(val kind: String, val params: List<String>?, val typeText: String?)
|
||||||
|
|
||||||
|
fun scanLocalClassMembersFromText(mini: MiniScript, text: String, cls: MiniClassDecl): Map<String, ScannedSig> {
|
||||||
|
val src = mini.range.start.source
|
||||||
|
val start = src.offsetOf(cls.bodyRange?.start ?: cls.range.start)
|
||||||
|
val end = src.offsetOf(cls.bodyRange?.end ?: cls.range.end).coerceAtMost(text.length)
|
||||||
|
if (start !in 0..end) return emptyMap()
|
||||||
|
val body = text.substring(start, end)
|
||||||
|
val map = LinkedHashMap<String, ScannedSig>()
|
||||||
|
// fun name(params): Type (allowing modifiers like abstract, override, closed)
|
||||||
|
val funRe = Regex("^\\s*(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([^)]*)\\)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
|
||||||
|
for (m in funRe.findAll(body)) {
|
||||||
|
val name = m.groupValues.getOrNull(1) ?: continue
|
||||||
|
val params = m.groupValues.getOrNull(2)?.split(',')?.mapNotNull { it.trim().takeIf { it.isNotEmpty() } } ?: emptyList()
|
||||||
|
val type = m.groupValues.getOrNull(3)?.takeIf { it.isNotBlank() }
|
||||||
|
map[name] = ScannedSig("fun", params, type)
|
||||||
|
}
|
||||||
|
// val/var name: Type (allowing modifiers)
|
||||||
|
val valRe = Regex("^\\s*(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*(val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
|
||||||
|
for (m in valRe.findAll(body)) {
|
||||||
|
val kind = m.groupValues.getOrNull(1) ?: continue
|
||||||
|
val name = m.groupValues.getOrNull(2) ?: continue
|
||||||
|
val type = m.groupValues.getOrNull(3)?.takeIf { it.isNotBlank() }
|
||||||
|
if (!map.containsKey(name)) map[name] = ScannedSig(kind, null, type)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
|
||||||
|
guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
|
||||||
|
var i = prevNonWs(text, dotPos - 1)
|
||||||
|
if (i >= 0) {
|
||||||
|
when (text[i]) {
|
||||||
|
'"' -> return "String"
|
||||||
|
']' -> return "List"
|
||||||
|
'}' -> return "Dict"
|
||||||
|
')' -> {
|
||||||
|
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
|
||||||
|
var j = i - 1
|
||||||
|
var depth = 0
|
||||||
|
while (j >= 0) {
|
||||||
|
when (text[j]) {
|
||||||
|
')' -> depth++
|
||||||
|
'(' -> if (depth == 0) break else depth--
|
||||||
|
}
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
if (j >= 0 && text[j] == '(') {
|
||||||
|
val innerS = (j + 1).coerceAtLeast(0)
|
||||||
|
val innerE = i.coerceAtMost(text.length)
|
||||||
|
if (innerS < innerE) {
|
||||||
|
val inner = text.substring(innerS, innerE).trim()
|
||||||
|
if (inner.startsWith('"') && inner.endsWith('"')) return "String"
|
||||||
|
if (inner.startsWith('[') && inner.endsWith(']')) return "List"
|
||||||
|
if (inner.startsWith('{') && inner.endsWith('}')) return "Dict"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Numeric literal: decimal/int/hex/scientific
|
||||||
|
var j = i
|
||||||
|
var hasDigits = false
|
||||||
|
var hasDot = false
|
||||||
|
var hasExp = false
|
||||||
|
while (j >= 0) {
|
||||||
|
val ch = text[j]
|
||||||
|
if (ch.isDigit()) {
|
||||||
|
hasDigits = true; j--; continue
|
||||||
|
}
|
||||||
|
if (ch == '.') {
|
||||||
|
hasDot = true; j--; continue
|
||||||
|
}
|
||||||
|
if (ch == 'e' || ch == 'E') {
|
||||||
|
hasExp = true; j--; if (j >= 0 && (text[j] == '+' || text[j] == '-')) j--; continue
|
||||||
|
}
|
||||||
|
if (ch in listOf('x', 'X', 'a', 'b', 'c', 'd', 'f', 'A', 'B', 'C', 'D', 'F')) {
|
||||||
|
j--; continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (hasDigits) return if (hasDot || hasExp) "Real" else "Int"
|
||||||
|
|
||||||
|
// 3) this@Type or as Type
|
||||||
|
val identRange = wordRangeAt(text, i + 1)
|
||||||
|
if (identRange != null) {
|
||||||
|
val ident = text.substring(identRange.first, identRange.second)
|
||||||
|
// if it's "as Type", we want Type
|
||||||
|
var k = prevNonWs(text, identRange.first - 1)
|
||||||
|
if (k >= 1 && text[k] == 's' && text[k - 1] == 'a' && (k - 1 == 0 || !text[k - 2].isLetterOrDigit())) {
|
||||||
|
return ident
|
||||||
|
}
|
||||||
|
// if it's "this@Type", we want Type
|
||||||
|
if (k >= 0 && text[k] == '@') {
|
||||||
|
val k2 = prevNonWs(text, k - 1)
|
||||||
|
if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") {
|
||||||
|
return ident
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Check if it's a known class (static access)
|
||||||
|
val classes = aggregateClasses(imported, mini)
|
||||||
|
if (classes.containsKey(ident)) return ident
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun guessReturnClassFromMemberCallBefore(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
|
||||||
|
var i = prevNonWs(text, dotPos - 1)
|
||||||
|
if (i < 0 || text[i] != ')') return null
|
||||||
|
i--
|
||||||
|
var depth = 0
|
||||||
|
while (i >= 0) {
|
||||||
|
when (text[i]) {
|
||||||
|
')' -> depth++
|
||||||
|
'(' -> if (depth == 0) break else depth--
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
if (i < 0 || text[i] != '(') return null
|
||||||
|
var j = i - 1
|
||||||
|
while (j >= 0 && text[j].isWhitespace()) j--
|
||||||
|
val end = j + 1
|
||||||
|
while (j >= 0 && isIdentChar(text[j])) j--
|
||||||
|
val start = j + 1
|
||||||
|
if (start >= end) return null
|
||||||
|
val callee = text.substring(start, end)
|
||||||
|
var k = start - 1
|
||||||
|
while (k >= 0 && text[k].isWhitespace()) k--
|
||||||
|
if (k < 0 || text[k] != '.') return null
|
||||||
|
val prevDot = k
|
||||||
|
val receiverClass = guessReceiverClass(text, prevDot, imported, mini) ?: return null
|
||||||
|
val resolved = resolveMemberWithInheritance(imported, receiverClass, callee, mini) ?: return null
|
||||||
|
val member = resolved.second
|
||||||
|
val ret = when (member) {
|
||||||
|
is MiniMemberFunDecl -> member.returnType
|
||||||
|
is MiniMemberValDecl -> member.type
|
||||||
|
is MiniInitDecl -> null
|
||||||
|
}
|
||||||
|
return simpleClassNameOf(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun guessReturnClassFromTopLevelCallBefore(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
|
||||||
|
var i = prevNonWs(text, dotPos - 1)
|
||||||
|
if (i < 0 || text[i] != ')') return null
|
||||||
|
i--
|
||||||
|
var depth = 0
|
||||||
|
while (i >= 0) {
|
||||||
|
when (text[i]) {
|
||||||
|
')' -> depth++
|
||||||
|
'(' -> if (depth == 0) break else depth--
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
if (i < 0 || text[i] != '(') return null
|
||||||
|
var j = i - 1
|
||||||
|
while (j >= 0 && text[j].isWhitespace()) j--
|
||||||
|
val end = j + 1
|
||||||
|
while (j >= 0 && isIdentChar(text[j])) j--
|
||||||
|
val start = j + 1
|
||||||
|
if (start >= end) return null
|
||||||
|
val callee = text.substring(start, end)
|
||||||
|
var k = start - 1
|
||||||
|
while (k >= 0 && text[k].isWhitespace()) k--
|
||||||
|
if (k >= 0 && text[k] == '.') return null // was a member call
|
||||||
|
for (mod in imported) {
|
||||||
|
val decls = BuiltinDocRegistry.docsForModule(mod)
|
||||||
|
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
|
||||||
|
if (fn != null) return simpleClassNameOf(fn.returnType)
|
||||||
|
}
|
||||||
|
// Also check local declarations
|
||||||
|
mini?.declarations?.filterIsInstance<MiniFunDecl>()?.firstOrNull { it.name == callee }?.let { return simpleClassNameOf(it.returnType) }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun guessReturnClassAcrossKnownCallees(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
|
||||||
|
var i = prevNonWs(text, dotPos - 1)
|
||||||
|
if (i < 0 || text[i] != ')') return null
|
||||||
|
i--
|
||||||
|
var depth = 0
|
||||||
|
while (i >= 0) {
|
||||||
|
when (text[i]) {
|
||||||
|
')' -> depth++
|
||||||
|
'(' -> if (depth == 0) break else depth--
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
if (i < 0 || text[i] != '(') return null
|
||||||
|
var j = i - 1
|
||||||
|
while (j >= 0 && text[j].isWhitespace()) j--
|
||||||
|
val end = j + 1
|
||||||
|
while (j >= 0 && isIdentChar(text[j])) j--
|
||||||
|
val start = j + 1
|
||||||
|
if (start >= end) return null
|
||||||
|
val callee = text.substring(start, end)
|
||||||
|
val resolved = findMemberAcrossClasses(imported, callee, mini) ?: return null
|
||||||
|
val member = resolved.second
|
||||||
|
val ret = when (member) {
|
||||||
|
is MiniMemberFunDecl -> member.returnType
|
||||||
|
is MiniMemberValDecl -> member.type
|
||||||
|
is MiniInitDecl -> null
|
||||||
|
}
|
||||||
|
return simpleClassNameOf(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
|
||||||
|
null -> null
|
||||||
|
is MiniTypeName -> t.segments.lastOrNull()?.name
|
||||||
|
is MiniGenericType -> simpleClassNameOf(t.base)
|
||||||
|
is MiniFunctionType -> null
|
||||||
|
is MiniTypeVar -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findDotLeft(text: String, offset: Int): Int? {
|
||||||
|
var i = (offset - 1).coerceAtLeast(0)
|
||||||
|
while (i >= 0 && text[i].isWhitespace()) i--
|
||||||
|
return if (i >= 0 && text[i] == '.') i else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prevNonWs(text: String, start: Int): Int {
|
||||||
|
var i = start.coerceAtMost(text.length - 1)
|
||||||
|
while (i >= 0 && text[i].isWhitespace()) i--
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wordRangeAt(text: String, offset: Int): Pair<Int, Int>? {
|
||||||
|
if (text.isEmpty()) return null
|
||||||
|
val off = offset.coerceIn(0, text.length)
|
||||||
|
var s = off
|
||||||
|
var e = off
|
||||||
|
while (s > 0 && isIdentChar(text[s - 1])) s--
|
||||||
|
while (e < text.length && isIdentChar(text[e])) e++
|
||||||
|
return if (s < e) s to e else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
|
||||||
|
|
||||||
|
fun enumToSyntheticClass(en: MiniEnumDecl): MiniClassDecl {
|
||||||
|
val staticMembers = mutableListOf<MiniMemberDecl>()
|
||||||
|
// entries: List
|
||||||
|
staticMembers.add(MiniMemberValDecl(en.range, "entries", false, null, null, en.nameStart, isStatic = true))
|
||||||
|
// valueOf(name: String): Enum
|
||||||
|
staticMembers.add(MiniMemberFunDecl(en.range, "valueOf", listOf(MiniParam("name", null, en.nameStart)), null, null, en.nameStart, isStatic = true))
|
||||||
|
|
||||||
|
// Also add each entry as a static member (const)
|
||||||
|
for (entry in en.entries) {
|
||||||
|
staticMembers.add(MiniMemberValDecl(en.range, entry, false, MiniTypeName(en.range, listOf(MiniTypeName.Segment(en.name, en.range)), false), null, en.nameStart, isStatic = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
return MiniClassDecl(
|
||||||
|
range = en.range,
|
||||||
|
name = en.name,
|
||||||
|
bases = listOf("Enum"),
|
||||||
|
bodyRange = null,
|
||||||
|
doc = en.doc,
|
||||||
|
nameStart = en.nameStart,
|
||||||
|
members = staticMembers
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ fun ObjClass.addFnDoc(
|
|||||||
code: suspend Scope.() -> Obj
|
code: suspend Scope.() -> Obj
|
||||||
) {
|
) {
|
||||||
// Register runtime method
|
// Register runtime method
|
||||||
addFn(name, isOpen, visibility, code)
|
addFn(name, isOpen, visibility, code = code)
|
||||||
// Register docs for the member under this class
|
// Register docs for the member under this class
|
||||||
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
|
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
|
||||||
classDoc(this@addFnDoc.className, doc = "") {
|
classDoc(this@addFnDoc.className, doc = "") {
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -109,7 +109,8 @@ data class MiniFunDecl(
|
|||||||
val returnType: MiniTypeRef?,
|
val returnType: MiniTypeRef?,
|
||||||
val body: MiniBlock?,
|
val body: MiniBlock?,
|
||||||
override val doc: MiniDoc?,
|
override val doc: MiniDoc?,
|
||||||
override val nameStart: Pos
|
override val nameStart: Pos,
|
||||||
|
val receiver: MiniTypeRef? = null
|
||||||
) : MiniDecl
|
) : MiniDecl
|
||||||
|
|
||||||
data class MiniValDecl(
|
data class MiniValDecl(
|
||||||
@@ -119,7 +120,8 @@ data class MiniValDecl(
|
|||||||
val type: MiniTypeRef?,
|
val type: MiniTypeRef?,
|
||||||
val initRange: MiniRange?,
|
val initRange: MiniRange?,
|
||||||
override val doc: MiniDoc?,
|
override val doc: MiniDoc?,
|
||||||
override val nameStart: Pos
|
override val nameStart: Pos,
|
||||||
|
val receiver: MiniTypeRef? = null
|
||||||
) : MiniDecl
|
) : MiniDecl
|
||||||
|
|
||||||
data class MiniClassDecl(
|
data class MiniClassDecl(
|
||||||
@@ -135,6 +137,14 @@ data class MiniClassDecl(
|
|||||||
val members: List<MiniMemberDecl> = emptyList()
|
val members: List<MiniMemberDecl> = emptyList()
|
||||||
) : MiniDecl
|
) : MiniDecl
|
||||||
|
|
||||||
|
data class MiniEnumDecl(
|
||||||
|
override val range: MiniRange,
|
||||||
|
override val name: String,
|
||||||
|
val entries: List<String>,
|
||||||
|
override val doc: MiniDoc?,
|
||||||
|
override val nameStart: Pos
|
||||||
|
) : MiniDecl
|
||||||
|
|
||||||
data class MiniCtorField(
|
data class MiniCtorField(
|
||||||
val name: String,
|
val name: String,
|
||||||
val mutable: Boolean,
|
val mutable: Boolean,
|
||||||
@@ -183,6 +193,15 @@ data class MiniMemberValDecl(
|
|||||||
override val isStatic: Boolean = false,
|
override val isStatic: Boolean = false,
|
||||||
) : MiniMemberDecl
|
) : MiniMemberDecl
|
||||||
|
|
||||||
|
data class MiniInitDecl(
|
||||||
|
override val range: MiniRange,
|
||||||
|
override val nameStart: Pos,
|
||||||
|
) : MiniMemberDecl {
|
||||||
|
override val name: String get() = "init"
|
||||||
|
override val doc: MiniDoc? get() = null
|
||||||
|
override val isStatic: Boolean get() = false
|
||||||
|
}
|
||||||
|
|
||||||
// Streaming sink to collect mini-AST during parsing. Implementations may assemble a tree or process events.
|
// Streaming sink to collect mini-AST during parsing. Implementations may assemble a tree or process events.
|
||||||
interface MiniAstSink {
|
interface MiniAstSink {
|
||||||
fun onScriptStart(start: Pos) {}
|
fun onScriptStart(start: Pos) {}
|
||||||
@@ -193,7 +212,9 @@ interface MiniAstSink {
|
|||||||
fun onImport(node: MiniImport) {}
|
fun onImport(node: MiniImport) {}
|
||||||
fun onFunDecl(node: MiniFunDecl) {}
|
fun onFunDecl(node: MiniFunDecl) {}
|
||||||
fun onValDecl(node: MiniValDecl) {}
|
fun onValDecl(node: MiniValDecl) {}
|
||||||
|
fun onInitDecl(node: MiniInitDecl) {}
|
||||||
fun onClassDecl(node: MiniClassDecl) {}
|
fun onClassDecl(node: MiniClassDecl) {}
|
||||||
|
fun onEnumDecl(node: MiniEnumDecl) {}
|
||||||
|
|
||||||
fun onBlock(node: MiniBlock) {}
|
fun onBlock(node: MiniBlock) {}
|
||||||
fun onIdentifier(node: MiniIdentifier) {}
|
fun onIdentifier(node: MiniIdentifier) {}
|
||||||
@@ -263,6 +284,12 @@ class MiniAstBuilder : MiniAstSink {
|
|||||||
lastDoc = null
|
lastDoc = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onEnumDecl(node: MiniEnumDecl) {
|
||||||
|
val attach = node.copy(doc = node.doc ?: lastDoc)
|
||||||
|
currentScript?.declarations?.add(attach)
|
||||||
|
lastDoc = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBlock(node: MiniBlock) {
|
override fun onBlock(node: MiniBlock) {
|
||||||
blocks.addLast(node)
|
blocks.addLast(node)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -83,23 +83,40 @@ open class Obj {
|
|||||||
scope: Scope,
|
scope: Scope,
|
||||||
name: String,
|
name: String,
|
||||||
args: Arguments = Arguments.EMPTY,
|
args: Arguments = Arguments.EMPTY,
|
||||||
onNotFoundResult: (() -> Obj?)? = null
|
onNotFoundResult: (suspend () -> Obj?)? = null
|
||||||
): Obj {
|
): Obj {
|
||||||
val rec = objClass.getInstanceMemberOrNull(name)
|
// 1. Hierarchy members (excluding root fallback)
|
||||||
if (rec != null) {
|
for (cls in objClass.mro) {
|
||||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
if (cls.className == "Obj") break
|
||||||
val caller = scope.currentClassCtx
|
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (rec != null && !rec.isAbstract && rec.type != ObjRecord.Type.Property) {
|
||||||
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
val decl = rec.declaringClass ?: cls
|
||||||
// Propagate declaring class as current class context during method execution
|
val caller = scope.currentClassCtx
|
||||||
val saved = scope.currentClassCtx
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.currentClassCtx = decl
|
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||||
try {
|
return rec.value.invoke(scope, this, args, decl)
|
||||||
return rec.value.invoke(scope, this, args)
|
|
||||||
} finally {
|
|
||||||
scope.currentClassCtx = saved
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Extensions in scope
|
||||||
|
val extension = scope.findExtension(objClass, name)
|
||||||
|
if (extension != null) {
|
||||||
|
return extension.value.invoke(scope, this, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Root object fallback
|
||||||
|
for (cls in objClass.mro) {
|
||||||
|
if (cls.className == "Obj") {
|
||||||
|
cls.members[name]?.let { rec ->
|
||||||
|
val decl = rec.declaringClass ?: cls
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||||
|
return rec.value.invoke(scope, this, args, decl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return onNotFoundResult?.invoke()
|
return onNotFoundResult?.invoke()
|
||||||
?: scope.raiseError(
|
?: scope.raiseError(
|
||||||
"no such member: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}. " +
|
"no such member: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}. " +
|
||||||
@@ -120,6 +137,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +146,40 @@ open class Obj {
|
|||||||
return invokeInstanceMethod(scope, "contains", other).toBool()
|
return invokeInstanceMethod(scope, "contains", other).toBool()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call [callback] for each element of this obj considering it provides [Iterator]
|
||||||
|
* methods `hasNext` and `next`.
|
||||||
|
*
|
||||||
|
* IF callback returns false, iteration is stopped.
|
||||||
|
*/
|
||||||
|
open suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
|
||||||
|
val iterator = invokeInstanceMethod(scope, "iterator")
|
||||||
|
val hasNext = iterator.getInstanceMethod(scope, "hasNext")
|
||||||
|
val next = iterator.getInstanceMethod(scope, "next")
|
||||||
|
var closeIt = false
|
||||||
|
try {
|
||||||
|
while (hasNext.invoke(scope, iterator).toBool()) {
|
||||||
|
val nextValue = next.invoke(scope, iterator)
|
||||||
|
val shouldContinue = try {
|
||||||
|
callback(nextValue)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// iteration aborted due to exception in callback
|
||||||
|
closeIt = true
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
if (!shouldContinue) {
|
||||||
|
closeIt = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (closeIt) {
|
||||||
|
// Best-effort cancel on premature termination
|
||||||
|
iterator.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default toString implementation:
|
* Default toString implementation:
|
||||||
*
|
*
|
||||||
@@ -142,15 +195,19 @@ open class Obj {
|
|||||||
* @param calledFromLyng true if called from Lyng's `toString`. Normally this parameter should be ignored,
|
* @param calledFromLyng true if called from Lyng's `toString`. Normally this parameter should be ignored,
|
||||||
* but it is used to avoid endless recursion in [Obj.toString] base implementation
|
* but it is used to avoid endless recursion in [Obj.toString] base implementation
|
||||||
*/
|
*/
|
||||||
open suspend fun toString(scope: Scope,calledFromLyng: Boolean=false): ObjString {
|
open suspend fun toString(scope: Scope, calledFromLyng: Boolean = false): ObjString {
|
||||||
return if (this is ObjString) this
|
if (this is ObjString) return this
|
||||||
else if( !calledFromLyng ) {
|
return if (!calledFromLyng) {
|
||||||
invokeInstanceMethod(scope, "toString") {
|
invokeInstanceMethod(scope, "toString", Arguments.EMPTY) {
|
||||||
ObjString(this.toString())
|
defaultToString(scope)
|
||||||
} as ObjString
|
} as ObjString
|
||||||
} else { ObjString(this.toString()) }
|
} else {
|
||||||
|
defaultToString(scope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open suspend fun defaultToString(scope: Scope): ObjString = ObjString(this.toString())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class of the object: definition of member functions (top-level), etc.
|
* Class of the object: definition of member functions (top-level), etc.
|
||||||
* Note that using lazy allows to avoid endless recursion here
|
* Note that using lazy allows to avoid endless recursion here
|
||||||
@@ -228,7 +285,7 @@ open class Obj {
|
|||||||
|
|
||||||
open suspend fun assign(scope: Scope, other: Obj): Obj? = null
|
open suspend fun assign(scope: Scope, other: Obj): Obj? = null
|
||||||
|
|
||||||
open fun getValue(scope: Scope) = this
|
open suspend fun getValue(scope: Scope) = this
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a += b
|
* a += b
|
||||||
@@ -275,34 +332,100 @@ open class Obj {
|
|||||||
// suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
|
// suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
|
||||||
|
|
||||||
open suspend fun readField(scope: Scope, name: String): ObjRecord {
|
open suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
// could be property or class field:
|
// 1. Hierarchy members (excluding root fallback)
|
||||||
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError(
|
for (cls in objClass.mro) {
|
||||||
|
if (cls.className == "Obj") break
|
||||||
|
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||||
|
if (rec != null) {
|
||||||
|
if (!rec.isAbstract) {
|
||||||
|
return resolveRecord(scope, rec, name, rec.declaringClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Extensions
|
||||||
|
val extension = scope.findExtension(objClass, name)
|
||||||
|
if (extension != null) {
|
||||||
|
return resolveRecord(scope, extension, name, extension.declaringClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Root fallback
|
||||||
|
for (cls in objClass.mro) {
|
||||||
|
if (cls.className == "Obj") {
|
||||||
|
cls.members[name]?.let {
|
||||||
|
val decl = it.declaringClass ?: cls
|
||||||
|
return resolveRecord(scope, it, name, decl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.raiseError(
|
||||||
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
|
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
|
||||||
)
|
)
|
||||||
val decl = obj.declaringClass ?: objClass.findDeclaringClassOf(name)
|
}
|
||||||
val caller = scope.currentClassCtx
|
|
||||||
if (!canAccessMember(obj.visibility, decl, caller))
|
protected open suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
|
||||||
scope.raiseError(ObjAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
if (obj.type == ObjRecord.Type.Delegated) {
|
||||||
return when (val value = obj.value) {
|
val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
|
||||||
is Statement -> {
|
return obj.copy(
|
||||||
ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
|
value = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))),
|
||||||
}
|
type = ObjRecord.Type.Other
|
||||||
// could be writable property naturally
|
)
|
||||||
// null -> ObjNull.asReadonly
|
|
||||||
else -> obj
|
|
||||||
}
|
}
|
||||||
|
val value = obj.value
|
||||||
|
if (value is ObjProperty) {
|
||||||
|
return ObjRecord(value.callGetter(scope, this, decl), obj.isMutable)
|
||||||
|
}
|
||||||
|
if (value is Statement && decl != null) {
|
||||||
|
return ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
|
||||||
|
}
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
// Check visibility for non-property members here if they weren't checked before
|
||||||
|
if (!canAccessMember(obj.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
willMutate(scope)
|
willMutate(scope)
|
||||||
val field = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError(
|
var field: ObjRecord? = null
|
||||||
|
// 1. Hierarchy members (excluding root fallback)
|
||||||
|
for (cls in objClass.mro) {
|
||||||
|
if (cls.className == "Obj") break
|
||||||
|
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||||
|
if (rec != null && !rec.isAbstract) {
|
||||||
|
field = rec
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. Extensions
|
||||||
|
if (field == null) {
|
||||||
|
field = scope.findExtension(objClass, name)
|
||||||
|
}
|
||||||
|
// 3. Root fallback
|
||||||
|
if (field == null) {
|
||||||
|
for (cls in objClass.mro) {
|
||||||
|
if (cls.className == "Obj") {
|
||||||
|
field = cls.members[name]
|
||||||
|
if (field != null) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field == null) scope.raiseError(
|
||||||
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
|
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
|
||||||
)
|
)
|
||||||
val decl = field.declaringClass ?: objClass.findDeclaringClassOf(name)
|
|
||||||
|
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(ObjIllegalAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
if (field.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = field.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
|
||||||
|
} else if (field.value is ObjProperty) {
|
||||||
|
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
|
||||||
|
} else if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun getAt(scope: Scope, index: Obj): Obj {
|
open suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||||
@@ -319,13 +442,17 @@ open class Obj {
|
|||||||
scope.raiseNotImplemented()
|
scope.raiseNotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj =
|
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj {
|
||||||
if (PerfFlags.SCOPE_POOL)
|
if (PerfFlags.SCOPE_POOL) {
|
||||||
scope.withChildFrame(args, newThisObj = thisObj) { child ->
|
return scope.withChildFrame(args, newThisObj = thisObj) { child ->
|
||||||
|
if (declaringClass != null) child.currentClassCtx = declaringClass
|
||||||
callOn(child)
|
callOn(child)
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj))
|
val child = scope.createChildScope(scope.pos, args = args, newThisObj = thisObj)
|
||||||
|
if (declaringClass != null) child.currentClassCtx = declaringClass
|
||||||
|
return callOn(child)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
|
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
|
||||||
callOn(
|
callOn(
|
||||||
@@ -440,8 +567,8 @@ open class Obj {
|
|||||||
is Obj -> obj
|
is Obj -> obj
|
||||||
is Double -> ObjReal(obj)
|
is Double -> ObjReal(obj)
|
||||||
is Float -> ObjReal(obj.toDouble())
|
is Float -> ObjReal(obj.toDouble())
|
||||||
is Int -> ObjInt(obj.toLong())
|
is Int -> ObjInt.of(obj.toLong())
|
||||||
is Long -> ObjInt(obj)
|
is Long -> ObjInt.of(obj)
|
||||||
is String -> ObjString(obj)
|
is String -> ObjString(obj)
|
||||||
is CharSequence -> ObjString(obj.toString())
|
is CharSequence -> ObjString(obj.toString())
|
||||||
is Boolean -> ObjBool(obj)
|
is Boolean -> ObjBool(obj)
|
||||||
@@ -547,6 +674,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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ class ObjChar(val value: Char): Obj() {
|
|||||||
returns = type("lyng.Int"),
|
returns = type("lyng.Int"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
) { ObjInt(thisAs<ObjChar>().value.code.toLong()) }
|
) { ObjInt(thisAs<ObjChar>().value.code.toLong()) }
|
||||||
|
addFn("isDigit") {
|
||||||
|
thisAs<ObjChar>().value.isDigit().toObj()
|
||||||
|
}
|
||||||
|
addFn("isSpace") {
|
||||||
|
thisAs<ObjChar>().value.isWhitespace().toObj()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
@@ -91,6 +91,8 @@ open class ObjClass(
|
|||||||
vararg parents: ObjClass,
|
vararg parents: ObjClass,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
|
|
||||||
|
var isAbstract: Boolean = false
|
||||||
|
|
||||||
// Stable identity and simple structural version for PICs
|
// Stable identity and simple structural version for PICs
|
||||||
val classId: Long = ClassIdGen.nextId()
|
val classId: Long = ClassIdGen.nextId()
|
||||||
var layoutVersion: Int = 0
|
var layoutVersion: Int = 0
|
||||||
@@ -175,7 +177,17 @@ open class ObjClass(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Full C3 MRO including this class at index 0. */
|
/** Full C3 MRO including this class at index 0. */
|
||||||
val mro: List<ObjClass> by lazy { c3Linearize(this, mutableMapOf()) }
|
val mro: List<ObjClass> by lazy {
|
||||||
|
val base = c3Linearize(this, mutableMapOf())
|
||||||
|
if (this.className == "Obj" || base.any { it.className == "Obj" }) base
|
||||||
|
else {
|
||||||
|
// During very early bootstrap rootObjectType might not be initialized yet.
|
||||||
|
// We use a safe check here.
|
||||||
|
@Suppress("UNNECESSARY_SAFE_CALL")
|
||||||
|
val root = net.sergeych.lyng.obj.Obj.rootObjectType
|
||||||
|
if (root != null) base + root else base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Parents in C3 order (no self). */
|
/** Parents in C3 order (no self). */
|
||||||
val mroParents: List<ObjClass> by lazy { mro.drop(1) }
|
val mroParents: List<ObjClass> by lazy { mro.drop(1) }
|
||||||
@@ -185,7 +197,7 @@ open class ObjClass(
|
|||||||
val list = mutableListOf<String>()
|
val list = mutableListOf<String>()
|
||||||
if (includeSelf) list += className
|
if (includeSelf) list += className
|
||||||
mroParents.forEach { list += it.className }
|
mroParents.forEach { list += it.className }
|
||||||
return list.joinToString(" → ")
|
return list.joinToString(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
override val objClass: ObjClass by lazy { ObjClassType }
|
override val objClass: ObjClass by lazy { ObjClassType }
|
||||||
@@ -200,98 +212,136 @@ open class ObjClass(
|
|||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
|
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
|
||||||
|
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
if (isAbstract) scope.raiseError("can't instantiate abstract class $className")
|
||||||
|
val instance = createInstance(scope)
|
||||||
|
initializeInstance(instance, scope.args, runConstructors = true)
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of this class and initialize its [ObjInstance.instanceScope] with
|
||||||
|
* methods. Does NOT run initializers or constructors.
|
||||||
|
*/
|
||||||
|
internal fun createInstance(scope: Scope): ObjInstance {
|
||||||
val instance = ObjInstance(this)
|
val instance = ObjInstance(this)
|
||||||
// Avoid capturing a transient (pooled) call frame as the parent of the instance scope.
|
// Avoid capturing a transient (pooled) call frame as the parent of the instance scope.
|
||||||
// Bind instance scope to the caller's parent chain directly so name resolution (e.g., stdlib like sqrt)
|
// Bind instance scope to the caller's parent chain directly so name resolution (e.g., stdlib like sqrt)
|
||||||
// remains stable even when call frames are pooled and reused.
|
// remains stable even when call frames are pooled and reused.
|
||||||
val stableParent = scope.parent
|
val stableParent = classScope ?: scope.parent
|
||||||
instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance)
|
instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance)
|
||||||
|
instance.instanceScope.currentClassCtx = null
|
||||||
// Expose instance methods (and other callable members) directly in the instance scope for fast lookup
|
// Expose instance methods (and other callable members) directly in the instance scope for fast lookup
|
||||||
// This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
|
// This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
|
||||||
// 1) members-defined methods
|
// 1) members-defined methods
|
||||||
for ((k, v) in members) {
|
for ((k, v) in members) {
|
||||||
if (v.value is Statement) {
|
if (v.value is Statement || v.type == ObjRecord.Type.Delegated) {
|
||||||
instance.instanceScope.objects[k] = v
|
instance.instanceScope.objects[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2) class-scope methods registered during class-body execution
|
// 2) class-scope methods registered during class-body execution
|
||||||
classScope?.objects?.forEach { (k, rec) ->
|
classScope?.objects?.forEach { (k, rec) ->
|
||||||
if (rec.value is Statement) {
|
if (rec.value is Statement || rec.type == ObjRecord.Type.Delegated) {
|
||||||
// if not already present, copy reference for dispatch
|
// if not already present, copy reference for dispatch
|
||||||
if (!instance.instanceScope.objects.containsKey(k)) {
|
if (!instance.instanceScope.objects.containsKey(k)) {
|
||||||
instance.instanceScope.objects[k] = rec
|
instance.instanceScope.objects[k] = rec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Constructor chaining MVP: initialize base classes left-to-right, then this class.
|
return instance
|
||||||
// Ensure each ancestor is initialized at most once (diamond-safe).
|
}
|
||||||
val visited = hashSetOf<ObjClass>()
|
|
||||||
|
|
||||||
suspend fun initClass(c: ObjClass, argsForThis: Arguments?, isRoot: Boolean = false) {
|
/**
|
||||||
if (!visited.add(c)) return
|
* Run initializers and optionally constructors for the given [instance].
|
||||||
// For the most-derived class, bind its constructor params BEFORE parents so base arg thunks can see them
|
* Handles Multiple Inheritance correctly (diamond-safe).
|
||||||
if (isRoot) {
|
*/
|
||||||
c.constructorMeta?.let { meta ->
|
internal suspend fun initializeInstance(
|
||||||
val argsHere = argsForThis ?: Arguments.EMPTY
|
instance: ObjInstance,
|
||||||
// Assign constructor params into instance scope (unmangled)
|
args: Arguments?,
|
||||||
meta.assignToContext(instance.instanceScope, argsHere)
|
runConstructors: Boolean
|
||||||
// Also expose them under MI-mangled storage keys `${Class}::name` so qualified views can access them
|
) {
|
||||||
// and so that base-class casts like `(obj as Base).field` work.
|
val visited = hashSetOf<ObjClass>()
|
||||||
for (p in meta.params) {
|
initClassInternal(instance, visited, this, args, isRoot = true, runConstructors = runConstructors)
|
||||||
val rec = instance.instanceScope.objects[p.name]
|
}
|
||||||
if (rec != null) {
|
|
||||||
val mangled = "${c.className}::${p.name}"
|
private suspend fun initClassInternal(
|
||||||
// Always point the mangled name to the current record to keep writes consistent
|
instance: ObjInstance,
|
||||||
// across re-bindings (e.g., second pass before ctor)
|
visited: MutableSet<ObjClass>,
|
||||||
instance.instanceScope.objects[mangled] = rec
|
c: ObjClass,
|
||||||
}
|
argsForThis: Arguments?,
|
||||||
}
|
@Suppress("UNUSED_PARAMETER") isRoot: Boolean = false,
|
||||||
|
runConstructors: Boolean = true
|
||||||
|
) {
|
||||||
|
if (!visited.add(c)) return
|
||||||
|
|
||||||
|
// Bind constructor parameters (both mangled and unmangled)
|
||||||
|
// These are needed for:
|
||||||
|
// 1) base constructor argument evaluation (if called from a derived class)
|
||||||
|
// 2) this class's field initializers and `init` blocks
|
||||||
|
// 3) this class's constructor body
|
||||||
|
// 4) `compareTo` and other structural operations
|
||||||
|
c.constructorMeta?.let { meta ->
|
||||||
|
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||||
|
// Assign constructor params into instance scope (unmangled)
|
||||||
|
meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c)
|
||||||
|
// Also expose them under MI-mangled storage keys `${Class}::name` so qualified views can access them
|
||||||
|
// and so that base-class casts like `(obj as Base).field` work.
|
||||||
|
for (p in meta.params) {
|
||||||
|
val rec = instance.instanceScope.objects[p.name]
|
||||||
|
if (rec != null) {
|
||||||
|
val mangled = "${c.className}::${p.name}"
|
||||||
|
// Always point the mangled name to the current record to keep writes consistent
|
||||||
|
// across re-bindings
|
||||||
|
instance.instanceScope.objects[mangled] = rec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Initialize direct parents first, in order
|
|
||||||
for (p in c.directParents) {
|
|
||||||
val raw = c.directParentArgs[p]?.toArguments(instance.instanceScope, false)
|
|
||||||
val limited = if (raw != null) {
|
|
||||||
val need = p.constructorMeta?.params?.size ?: 0
|
|
||||||
if (need == 0) Arguments.EMPTY else Arguments(raw.list.take(need), tailBlockMode = false)
|
|
||||||
} else Arguments.EMPTY
|
|
||||||
initClass(p, limited)
|
|
||||||
}
|
|
||||||
// Execute per-instance initializers collected from class body for this class
|
|
||||||
if (c.instanceInitializers.isNotEmpty()) {
|
|
||||||
val savedCtx = instance.instanceScope.currentClassCtx
|
|
||||||
instance.instanceScope.currentClassCtx = c
|
|
||||||
try {
|
|
||||||
for (initStmt in c.instanceInitializers) {
|
|
||||||
initStmt.execute(instance.instanceScope)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
instance.instanceScope.currentClassCtx = savedCtx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Then run this class' constructor, if any
|
|
||||||
c.instanceConstructor?.let { ctor ->
|
|
||||||
// Bind this class's constructor parameters into the instance scope now, right before ctor
|
|
||||||
c.constructorMeta?.let { meta ->
|
|
||||||
val argsHere = argsForThis ?: Arguments.EMPTY
|
|
||||||
meta.assignToContext(instance.instanceScope, argsHere)
|
|
||||||
// Ensure mangled aliases exist for qualified access starting from this class
|
|
||||||
for (p in meta.params) {
|
|
||||||
val rec = instance.instanceScope.objects[p.name]
|
|
||||||
if (rec != null) {
|
|
||||||
val mangled = "${c.className}::${p.name}"
|
|
||||||
// Overwrite to ensure alias refers to the latest ObjRecord after re-binding
|
|
||||||
instance.instanceScope.objects[mangled] = rec
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val execScope = instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
|
|
||||||
ctor.execute(execScope)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initClass(this, instance.instanceScope.args, isRoot = true)
|
// Initialize direct parents first, in order
|
||||||
return instance
|
for (p in c.directParents) {
|
||||||
|
val raw = c.directParentArgs[p]?.toArguments(instance.instanceScope, false)
|
||||||
|
val limited = if (raw != null) {
|
||||||
|
val need = p.constructorMeta?.params?.size ?: 0
|
||||||
|
if (need == 0) Arguments.EMPTY else Arguments(raw.list.take(need), tailBlockMode = false)
|
||||||
|
} else Arguments.EMPTY
|
||||||
|
initClassInternal(instance, visited, p, limited, false, runConstructors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-bind this class's parameters right before running its initializers and constructor.
|
||||||
|
// This ensures that unmangled names in the instance scope correctly refer to THIS class's
|
||||||
|
// parameters even if they were shadowed/overwritten by parent class initialization.
|
||||||
|
c.constructorMeta?.let { meta ->
|
||||||
|
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||||
|
meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c)
|
||||||
|
// Re-sync mangled names to point to the fresh records to keep them consistent
|
||||||
|
for (p in meta.params) {
|
||||||
|
val rec = instance.instanceScope.objects[p.name]
|
||||||
|
if (rec != null) {
|
||||||
|
val mangled = "${c.className}::${p.name}"
|
||||||
|
instance.instanceScope.objects[mangled] = rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute per-instance initializers collected from class body for this class
|
||||||
|
if (c.instanceInitializers.isNotEmpty()) {
|
||||||
|
val savedCtx = instance.instanceScope.currentClassCtx
|
||||||
|
instance.instanceScope.currentClassCtx = c
|
||||||
|
try {
|
||||||
|
for (initStmt in c.instanceInitializers) {
|
||||||
|
initStmt.execute(instance.instanceScope)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
instance.instanceScope.currentClassCtx = savedCtx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Then run this class' constructor, if any
|
||||||
|
if (runConstructors) {
|
||||||
|
c.instanceConstructor?.let { ctor ->
|
||||||
|
val execScope =
|
||||||
|
instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
|
||||||
|
ctor.execute(execScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
|
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
|
||||||
@@ -304,16 +354,60 @@ open class ObjClass(
|
|||||||
initialValue: Obj,
|
initialValue: Obj,
|
||||||
isMutable: Boolean = false,
|
isMutable: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
pos: Pos = Pos.builtIn
|
writeVisibility: Visibility? = null,
|
||||||
) {
|
pos: Pos = Pos.builtIn,
|
||||||
|
declaringClass: ObjClass? = this,
|
||||||
|
isAbstract: Boolean = false,
|
||||||
|
isClosed: Boolean = false,
|
||||||
|
isOverride: Boolean = false,
|
||||||
|
type: ObjRecord.Type = ObjRecord.Type.Field,
|
||||||
|
): ObjRecord {
|
||||||
|
// Validation of override rules: only for non-system declarations
|
||||||
|
if (pos != Pos.builtIn) {
|
||||||
|
val existing = getInstanceMemberOrNull(name)
|
||||||
|
var actualOverride = false
|
||||||
|
if (existing != null && existing.declaringClass != this) {
|
||||||
|
// If the existing member is private in the ancestor, it's not visible for overriding.
|
||||||
|
// It should be treated as a new member in this class.
|
||||||
|
if (!existing.visibility.isPublic && !canAccessMember(existing.visibility, existing.declaringClass, this)) {
|
||||||
|
// It's effectively not there for us, so actualOverride remains false
|
||||||
|
} else {
|
||||||
|
actualOverride = true
|
||||||
|
// It's an override (implicit or explicit)
|
||||||
|
if (existing.isClosed)
|
||||||
|
throw ScriptError(pos, "can't override closed member $name from ${existing.declaringClass?.className}")
|
||||||
|
|
||||||
|
if (!isOverride)
|
||||||
|
throw ScriptError(pos, "member $name overrides parent member but 'override' keyword is missing")
|
||||||
|
|
||||||
|
if (visibility.ordinal > existing.visibility.ordinal)
|
||||||
|
throw ScriptError(pos, "can't narrow visibility of $name from ${existing.visibility} to $visibility")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOverride && !actualOverride) {
|
||||||
|
throw ScriptError(pos, "member $name is marked 'override' but does not override anything")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Allow overriding ancestors: only prevent redefinition if THIS class already defines an immutable member
|
// Allow overriding ancestors: only prevent redefinition if THIS class already defines an immutable member
|
||||||
val existingInSelf = members[name]
|
val existingInSelf = members[name]
|
||||||
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 = this)
|
val rec = ObjRecord(
|
||||||
|
initialValue, isMutable, visibility, writeVisibility,
|
||||||
|
declaringClass = declaringClass,
|
||||||
|
isAbstract = isAbstract,
|
||||||
|
isClosed = isClosed,
|
||||||
|
isOverride = isOverride,
|
||||||
|
type = type
|
||||||
|
)
|
||||||
|
members[name] = rec
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initClassScope(): Scope {
|
private fun initClassScope(): Scope {
|
||||||
@@ -326,28 +420,64 @@ open class ObjClass(
|
|||||||
initialValue: Obj,
|
initialValue: Obj,
|
||||||
isMutable: Boolean = false,
|
isMutable: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
pos: Pos = Pos.builtIn
|
writeVisibility: Visibility? = null,
|
||||||
) {
|
pos: Pos = Pos.builtIn,
|
||||||
|
type: ObjRecord.Type = ObjRecord.Type.Field
|
||||||
|
): ObjRecord {
|
||||||
initClassScope()
|
initClassScope()
|
||||||
val existing = classScope!!.objects[name]
|
val existing = classScope!!.objects[name]
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
||||||
classScope!!.addItem(name, isMutable, initialValue, visibility)
|
val rec = classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility, recordType = type)
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addFn(
|
fun addFn(
|
||||||
name: String,
|
name: String,
|
||||||
isOpen: Boolean = false,
|
isMutable: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
code: suspend Scope.() -> Obj
|
writeVisibility: Visibility? = null,
|
||||||
|
declaringClass: ObjClass? = this,
|
||||||
|
isAbstract: Boolean = false,
|
||||||
|
isClosed: Boolean = false,
|
||||||
|
isOverride: Boolean = false,
|
||||||
|
pos: Pos = Pos.builtIn,
|
||||||
|
code: (suspend Scope.() -> Obj)? = null
|
||||||
) {
|
) {
|
||||||
val stmt = statement { code() }
|
val stmt = code?.let { statement { it() } } ?: ObjNull
|
||||||
createField(name, stmt, isOpen, visibility)
|
createField(
|
||||||
|
name, stmt, isMutable, visibility, writeVisibility, pos, declaringClass,
|
||||||
|
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
|
||||||
|
type = ObjRecord.Type.Fun
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
||||||
|
|
||||||
|
fun addProperty(
|
||||||
|
name: String,
|
||||||
|
getter: (suspend Scope.() -> Obj)? = null,
|
||||||
|
setter: (suspend Scope.(Obj) -> Unit)? = null,
|
||||||
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
|
declaringClass: ObjClass? = this,
|
||||||
|
isAbstract: Boolean = false,
|
||||||
|
isClosed: Boolean = false,
|
||||||
|
isOverride: Boolean = false,
|
||||||
|
pos: Pos = Pos.builtIn,
|
||||||
|
) {
|
||||||
|
val g = getter?.let { statement { it() } }
|
||||||
|
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
|
||||||
|
val prop = if (isAbstract) ObjNull else ObjProperty(name, g, s)
|
||||||
|
createField(
|
||||||
|
name, prop, false, visibility, writeVisibility, pos, declaringClass,
|
||||||
|
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
|
||||||
|
type = ObjRecord.Type.Property
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
|
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
|
||||||
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
|
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
|
||||||
createClassField(name, statement { code() }, isOpen)
|
createClassField(name, statement { code() }, isOpen)
|
||||||
@@ -380,6 +510,38 @@ open class ObjClass(
|
|||||||
getInstanceMemberOrNull(name)
|
getInstanceMemberOrNull(name)
|
||||||
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
|
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
|
||||||
|
|
||||||
|
fun findFirstConcreteMember(name: String): ObjRecord? {
|
||||||
|
for (cls in mro) {
|
||||||
|
cls.members[name]?.let {
|
||||||
|
if (!it.isAbstract) return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkAbstractSatisfaction(pos: Pos) {
|
||||||
|
if (isAbstract) return
|
||||||
|
|
||||||
|
val missing = mutableSetOf<String>()
|
||||||
|
for (cls in mroParents) {
|
||||||
|
for ((name, rec) in cls.members) {
|
||||||
|
if (rec.isAbstract) {
|
||||||
|
val current = findFirstConcreteMember(name)
|
||||||
|
if (current == null) {
|
||||||
|
missing.add(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.isNotEmpty()) {
|
||||||
|
throw ScriptError(
|
||||||
|
pos,
|
||||||
|
"class $className is not abstract and does not implement abstract members: ${missing.joinToString(", ")}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve member starting from a specific ancestor class [start], not from this class.
|
* Resolve member starting from a specific ancestor class [start], not from this class.
|
||||||
* Searches [start] first, then traverses its linearized parents.
|
* Searches [start] first, then traverses its linearized parents.
|
||||||
@@ -400,25 +562,39 @@ open class ObjClass(
|
|||||||
|
|
||||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
classScope?.objects?.get(name)?.let {
|
classScope?.objects?.get(name)?.let {
|
||||||
if (it.visibility.isPublic) return it
|
if (it.visibility.isPublic) return resolveRecord(scope, it, name, this)
|
||||||
}
|
}
|
||||||
return super.readField(scope, name)
|
return super.readField(scope, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
initClassScope().objects[name]?.let {
|
initClassScope().objects[name]?.let { rec ->
|
||||||
if (it.isMutable) it.value = newValue
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
|
||||||
|
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rec.isMutable) rec.value = newValue
|
||||||
else scope.raiseIllegalAssignment("can't assign $name is not mutable")
|
else scope.raiseIllegalAssignment("can't assign $name is not mutable")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
?: super.writeField(scope, name, newValue)
|
?: super.writeField(scope, name, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun invokeInstanceMethod(
|
override suspend fun invokeInstanceMethod(
|
||||||
scope: Scope, name: String, args: Arguments,
|
scope: Scope, name: String, args: Arguments,
|
||||||
onNotFoundResult: (() -> Obj?)?
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
): Obj {
|
): Obj {
|
||||||
return classScope?.objects?.get(name)?.value?.invoke(scope, this, args)
|
getInstanceMemberOrNull(name)?.let { rec ->
|
||||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
val decl = rec.declaringClass ?: findDeclaringClassOf(name) ?: this
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val del = rec.delegate ?: scope.raiseError("Internal error: delegated function $name has no delegate")
|
||||||
|
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
|
||||||
|
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
|
||||||
|
}
|
||||||
|
return rec.value.invoke(scope, this, args, decl)
|
||||||
|
}
|
||||||
|
return super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
|
|||||||
scope: Scope,
|
scope: Scope,
|
||||||
name: String,
|
name: String,
|
||||||
args: Arguments,
|
args: Arguments,
|
||||||
onNotFoundResult: (() -> Obj?)?
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
): Obj {
|
): Obj {
|
||||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||||
val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))
|
val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
/*
|
/*
|
||||||
|
* 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.obj/*
|
||||||
* Copyright 2025 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@@ -18,7 +35,6 @@
|
|||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.obj.*
|
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -71,7 +71,7 @@ open class ObjException(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Fallback textual entry if StackTraceEntry class is not available in this scope
|
// Fallback textual entry if StackTraceEntry class is not available in this scope
|
||||||
result.list += ObjString("${'$'}{pos.source.objSourceName}:${'$'}{pos.line}:${'$'}{pos.column}: ${'$'}{pos.currentLine}")
|
result.list += ObjString("${pos.source.objSourceName}:${pos.line}:${pos.column}: ${pos.currentLine}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s = s.parent
|
s = s.parent
|
||||||
@@ -89,7 +89,7 @@ open class ObjException(
|
|||||||
|
|
||||||
override val objClass: ObjClass = exceptionClass
|
override val objClass: ObjClass = exceptionClass
|
||||||
|
|
||||||
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||||
val at = getStackTrace().list.firstOrNull()?.toString(scope)
|
val at = getStackTrace().list.firstOrNull()?.toString(scope)
|
||||||
?: ObjString("(unknown)")
|
?: ObjString("(unknown)")
|
||||||
return ObjString("${objClass.className}: $message at $at")
|
return ObjString("${objClass.className}: $message at $at")
|
||||||
@@ -191,10 +191,13 @@ open class ObjException(
|
|||||||
"IllegalAssignmentException",
|
"IllegalAssignmentException",
|
||||||
"SymbolNotDefinedException",
|
"SymbolNotDefinedException",
|
||||||
"IterationEndException",
|
"IterationEndException",
|
||||||
"AccessException",
|
"IllegalAccessException",
|
||||||
"UnknownException",
|
"UnknownException",
|
||||||
"NotFoundException",
|
"NotFoundException",
|
||||||
"IllegalOperationException"
|
"IllegalOperationException",
|
||||||
|
"UnsetException",
|
||||||
|
"NotImplementedException",
|
||||||
|
"SyntaxError"
|
||||||
)) {
|
)) {
|
||||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
scope.addConst(name, getOrCreateExceptionClass(name))
|
||||||
}
|
}
|
||||||
@@ -234,8 +237,8 @@ class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is no
|
|||||||
class ObjIterationFinishedException(scope: Scope) :
|
class ObjIterationFinishedException(scope: Scope) :
|
||||||
ObjException("IterationEndException", scope, "iteration finished")
|
ObjException("IterationEndException", scope, "iteration finished")
|
||||||
|
|
||||||
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
|
class ObjIllegalAccessException(scope: Scope, message: String = "access not allowed error") :
|
||||||
ObjException("AccessException", scope, message)
|
ObjException("IllegalAccessException", scope, message)
|
||||||
|
|
||||||
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
|
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
|
||||||
ObjException("UnknownException", scope, message)
|
ObjException("UnknownException", scope, message)
|
||||||
@@ -245,3 +248,9 @@ 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)
|
||||||
|
|
||||||
|
class ObjNotImplementedException(scope: Scope, message: String = "not implemented") :
|
||||||
|
ObjException("NotImplementedException", 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.
|
||||||
@@ -31,77 +31,106 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
internal lateinit var instanceScope: Scope
|
internal lateinit var instanceScope: Scope
|
||||||
|
|
||||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
// Direct (unmangled) lookup first
|
// 1. Direct (unmangled) lookup first
|
||||||
instanceScope[name]?.let {
|
instanceScope[name]?.let { rec ->
|
||||||
val decl = it.declaringClass ?: objClass.findDeclaringClassOf(name)
|
val decl = rec.declaringClass
|
||||||
// When execution passes through suspension points (e.g., withLock),
|
|
||||||
// currentClassCtx could be lost. Fall back to the instance scope class ctx
|
|
||||||
// to preserve correct visibility semantics for private/protected members
|
|
||||||
// accessed from within the class methods.
|
|
||||||
// Allow unconditional access when accessing through `this` of the same instance
|
// Allow unconditional access when accessing through `this` of the same instance
|
||||||
if (scope.thisObj === this) return it
|
// BUT only if we are in the class context (not extension)
|
||||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||||
val caller = caller0 // do not default to objClass for outsiders
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(it.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(
|
scope.raiseError(
|
||||||
ObjAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
return it
|
return resolveRecord(scope, rec, name, decl)
|
||||||
}
|
}
|
||||||
// Try MI-mangled lookup along linearization (C3 MRO): ClassName::name
|
|
||||||
val cls = objClass
|
|
||||||
|
|
||||||
// self first, then parents
|
// 2. MI-mangled instance scope lookup
|
||||||
fun findMangled(): ObjRecord? {
|
val cls = objClass
|
||||||
// self
|
fun findMangledInRead(): ObjRecord? {
|
||||||
instanceScope.objects["${cls.className}::$name"]?.let { return it }
|
instanceScope.objects["${cls.className}::$name"]?.let { return it }
|
||||||
// ancestors in deterministic C3 order
|
|
||||||
for (p in cls.mroParents) {
|
for (p in cls.mroParents) {
|
||||||
instanceScope.objects["${p.className}::$name"]?.let { return it }
|
instanceScope.objects["${p.className}::$name"]?.let { return it }
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
findMangled()?.let { rec ->
|
|
||||||
// derive declaring class by mangled prefix: try self then parents
|
findMangledInRead()?.let { rec ->
|
||||||
val declaring = when {
|
val declaring = when {
|
||||||
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||||
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||||
}
|
}
|
||||||
if (scope.thisObj === this) return rec
|
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
val caller = caller0 // do not default to objClass for outsiders
|
if (!canAccessMember(rec.visibility, declaring, caller))
|
||||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
scope.raiseError(
|
||||||
scope.raiseError(
|
ObjIllegalAccessException(
|
||||||
ObjAccessException(
|
scope,
|
||||||
scope,
|
"can't access field $name (declared in ${declaring?.className ?: "?"})"
|
||||||
"can't access field $name (declared in ${declaring?.className ?: "?"})"
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
return rec
|
return resolveRecord(scope, rec, name, declaring)
|
||||||
}
|
}
|
||||||
// Fall back to methods/properties on class
|
|
||||||
|
// 3. Fall back to super (handles class members and extensions)
|
||||||
return super.readField(scope, name)
|
return super.readField(scope, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
|
||||||
|
if (obj.type == ObjRecord.Type.Delegated) {
|
||||||
|
val storageName = "${decl?.className}::$name"
|
||||||
|
var del = instanceScope[storageName]?.delegate
|
||||||
|
if (del == null) {
|
||||||
|
for (c in objClass.mro) {
|
||||||
|
del = instanceScope["${c.className}::$name"]?.delegate
|
||||||
|
if (del != null) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
del = del ?: obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
|
||||||
|
return obj.copy(
|
||||||
|
value = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))),
|
||||||
|
type = ObjRecord.Type.Other
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return super.resolveRecord(scope, obj, name, decl)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
// Direct (unmangled) first
|
// Direct (unmangled) first
|
||||||
instanceScope[name]?.let { f ->
|
instanceScope[name]?.let { f ->
|
||||||
val decl = f.declaringClass ?: objClass.findDeclaringClassOf(name)
|
val decl = f.declaringClass
|
||||||
if (scope.thisObj === this) {
|
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||||
// direct self-assignment allowed; enforce mutability below
|
val caller = scope.currentClassCtx
|
||||||
} else {
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
||||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
ObjIllegalAccessException(
|
||||||
val caller = caller0 // do not default to objClass for outsiders
|
|
||||||
if (!canAccessMember(f.visibility, decl, caller))
|
|
||||||
ObjIllegalAssignmentException(
|
|
||||||
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.type == ObjRecord.Type.Property) {
|
||||||
|
val prop = f.value as ObjProperty
|
||||||
|
prop.callSetter(scope, this, newValue, decl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (f.type == ObjRecord.Type.Delegated) {
|
||||||
|
val storageName = "${decl?.className}::$name"
|
||||||
|
var del = instanceScope[storageName]?.delegate
|
||||||
|
if (del == null) {
|
||||||
|
for (c in objClass.mro) {
|
||||||
|
del = instanceScope["${c.className}::$name"]?.delegate
|
||||||
|
if (del != null) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
del = del ?: f.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
|
||||||
|
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (f.value.assign(scope, newValue) == null)
|
if (f.value.assign(scope, newValue) == null)
|
||||||
f.value = newValue
|
f.value = newValue
|
||||||
return
|
return
|
||||||
@@ -122,16 +151,33 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||||
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||||
}
|
}
|
||||||
if (scope.thisObj !== this) {
|
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
val caller = caller0 // do not default to objClass for outsiders
|
if (!canAccessMember(rec.effectiveWriteVisibility, declaring, caller))
|
||||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
ObjIllegalAccessException(
|
||||||
ObjIllegalAssignmentException(
|
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
|
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
|
||||||
).raise()
|
).raise()
|
||||||
}
|
}
|
||||||
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
|
val prop = rec.value as ObjProperty
|
||||||
|
prop.callSetter(scope, this, newValue, declaring)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
|
val storageName = "${declaring?.className}::$name"
|
||||||
|
var del = instanceScope[storageName]?.delegate
|
||||||
|
if (del == null) {
|
||||||
|
for (c in objClass.mro) {
|
||||||
|
del = instanceScope["${c.className}::$name"]?.delegate
|
||||||
|
if (del != null) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
del = del ?: rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
|
||||||
|
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!rec.isMutable && rec.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (rec.value.assign(scope, newValue) == null)
|
if (rec.value.assign(scope, newValue) == null)
|
||||||
rec.value = newValue
|
rec.value = newValue
|
||||||
return
|
return
|
||||||
@@ -141,55 +187,43 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
|
|
||||||
override suspend fun invokeInstanceMethod(
|
override suspend fun invokeInstanceMethod(
|
||||||
scope: Scope, name: String, args: Arguments,
|
scope: Scope, name: String, args: Arguments,
|
||||||
onNotFoundResult: (() -> Obj?)?
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
): Obj =
|
): Obj {
|
||||||
instanceScope[name]?.let { rec ->
|
// 1. Walk MRO to find member, handling delegation
|
||||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
for (cls in objClass.mro) {
|
||||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
if (cls.className == "Obj") break
|
||||||
val caller = caller0 ?: if (scope.thisObj === this) objClass else null
|
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (rec != null) {
|
||||||
scope.raiseError(
|
if (rec.type == ObjRecord.Type.Delegated) {
|
||||||
ObjAccessException(
|
val storageName = "${cls.className}::$name"
|
||||||
scope,
|
val del = instanceScope[storageName]?.delegate ?: rec.delegate
|
||||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
?: scope.raiseError("Internal error: delegated function $name has no delegate (tried $storageName)")
|
||||||
)
|
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
|
||||||
)
|
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
|
||||||
// execute with lexical class context propagated to declaring class
|
}
|
||||||
val saved = instanceScope.currentClassCtx
|
if (rec.type != ObjRecord.Type.Property && !rec.isAbstract) {
|
||||||
instanceScope.currentClassCtx = decl
|
val decl = rec.declaringClass ?: cls
|
||||||
try {
|
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
|
||||||
rec.value.invoke(
|
|
||||||
instanceScope,
|
|
||||||
this,
|
|
||||||
args
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
instanceScope.currentClassCtx = saved
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?: run {
|
|
||||||
// fallback: class-scope function (registered during class body execution)
|
|
||||||
objClass.classScope?.objects?.get(name)?.let { rec ->
|
|
||||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
|
||||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
|
||||||
val caller = caller0 ?: if (scope.thisObj === this) objClass else null
|
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(
|
scope.raiseError(
|
||||||
ObjAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
"can't invoke method $name (declared in ${decl.className ?: "?"})"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val saved = instanceScope.currentClassCtx
|
return rec.value.invoke(
|
||||||
instanceScope.currentClassCtx = decl
|
instanceScope,
|
||||||
try {
|
this,
|
||||||
rec.value.invoke(instanceScope, this, args)
|
args,
|
||||||
} finally {
|
decl
|
||||||
instanceScope.currentClassCtx = saved
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
}
|
||||||
|
|
||||||
|
// 2. Fall back to super (handles extensions and root fallback)
|
||||||
|
return super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||||
|
}
|
||||||
|
|
||||||
private val publicFields: Map<String, ObjRecord>
|
private val publicFields: Map<String, ObjRecord>
|
||||||
get() = instanceScope.objects.filter {
|
get() = instanceScope.objects.filter {
|
||||||
@@ -197,7 +231,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
!it.key.contains("::") && it.value.visibility.isPublic && it.value.type.serializable
|
!it.key.contains("::") && it.value.visibility.isPublic && it.value.type.serializable
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||||
return ObjString(buildString {
|
return ObjString(buildString {
|
||||||
append("${objClass.className}(")
|
append("${objClass.className}(")
|
||||||
var first = true
|
var first = true
|
||||||
@@ -221,10 +255,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
|
|
||||||
// using objlist allow for some optimizations:
|
// using objlist allow for some optimizations:
|
||||||
val params = meta.params.map { readField(scope, it.name).value }
|
val params = meta.params.map { readField(scope, it.name).value }
|
||||||
println("serializing $objClass with params: $params")
|
|
||||||
encoder.encodeAnyList(scope, params)
|
encoder.encodeAnyList(scope, params)
|
||||||
val vars = serializingVars.values.map { it.value }
|
val vars = serializingVars.values.map { it.value }
|
||||||
println("encoding vars: $vars")
|
|
||||||
if (vars.isNotEmpty<Obj>()) {
|
if (vars.isNotEmpty<Obj>()) {
|
||||||
encoder.encodeAnyList(scope, vars)
|
encoder.encodeAnyList(scope, vars)
|
||||||
}
|
}
|
||||||
@@ -246,7 +278,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
for (i in serializingVars) {
|
for (i in serializingVars) {
|
||||||
// remove T:: prefix from the field name for JSON
|
// remove T:: prefix from the field name for JSON
|
||||||
val parts = i.key.split("::")
|
val parts = i.key.split("::")
|
||||||
result[if( parts.size == 1 ) parts[0] else parts.last()] = i.value.value.toJson(scope)
|
result[if (parts.size == 1) parts[0] else parts.last()] = i.value.value.toJson(scope)
|
||||||
}
|
}
|
||||||
return JsonObject(result)
|
return JsonObject(result)
|
||||||
}
|
}
|
||||||
@@ -259,7 +291,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
instanceScope.objects.filter {
|
instanceScope.objects.filter {
|
||||||
it.value.type.serializable &&
|
it.value.type.serializable &&
|
||||||
it.value.type == ObjRecord.Type.Field &&
|
it.value.type == ObjRecord.Type.Field &&
|
||||||
it.value.isMutable }
|
it.value.isMutable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) {
|
internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) {
|
||||||
@@ -276,7 +309,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,8 +344,8 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
val decl = rec.declaringClass ?: startClass
|
val decl = rec.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||||
return rec
|
return resolveRecord(scope, rec, name, decl)
|
||||||
}
|
}
|
||||||
// Then try instance locals (unmangled) only if startClass is the dynamic class itself
|
// Then try instance locals (unmangled) only if startClass is the dynamic class itself
|
||||||
if (startClass === instance.objClass) {
|
if (startClass === instance.objClass) {
|
||||||
@@ -321,12 +354,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(
|
scope.raiseError(
|
||||||
ObjAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return rec
|
return resolveRecord(scope, rec, name, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finally try methods/properties starting from ancestor
|
// Finally try methods/properties starting from ancestor
|
||||||
@@ -334,19 +367,9 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
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.visibility, decl, caller))
|
||||||
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||||
return when (val value = r.value) {
|
|
||||||
is net.sergeych.lyng.Statement -> ObjRecord(
|
|
||||||
value.execute(
|
|
||||||
instance.instanceScope.createChildScope(
|
|
||||||
scope.pos,
|
|
||||||
newThisObj = instance
|
|
||||||
)
|
|
||||||
), r.isMutable
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> r
|
return resolveRecord(scope, r, name, decl)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
@@ -355,12 +378,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(
|
ObjIllegalAccessException(
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -369,12 +392,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(
|
ObjIllegalAccessException(
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -382,8 +405,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()
|
ObjIllegalAccessException(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
|
||||||
}
|
}
|
||||||
@@ -392,14 +415,14 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
scope: Scope,
|
scope: Scope,
|
||||||
name: String,
|
name: String,
|
||||||
args: Arguments,
|
args: Arguments,
|
||||||
onNotFoundResult: (() -> Obj?)?
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
): Obj {
|
): Obj {
|
||||||
// Qualified method dispatch must start from the specified ancestor, not from the instance scope.
|
// Qualified method dispatch must start from the specified ancestor, not from the instance scope.
|
||||||
memberFromAncestor(name)?.let { rec ->
|
memberFromAncestor(name)?.let { rec ->
|
||||||
val decl = rec.declaringClass ?: startClass
|
val decl = rec.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl.className})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke method $name (declared in ${decl.className})"))
|
||||||
val saved = instance.instanceScope.currentClassCtx
|
val saved = instance.instanceScope.currentClassCtx
|
||||||
instance.instanceScope.currentClassCtx = decl
|
instance.instanceScope.currentClassCtx = decl
|
||||||
try {
|
try {
|
||||||
@@ -415,7 +438,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
scope.raiseError(
|
scope.raiseError(
|
||||||
ObjAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(na
|
|||||||
if (args.size > actualSize)
|
if (args.size > actualSize)
|
||||||
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
|
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
|
||||||
val newScope = scope.createChildScope(args = Arguments(args))
|
val newScope = scope.createChildScope(args = Arguments(args))
|
||||||
return (callOn(newScope) as ObjInstance).apply {
|
val instance = createInstance(newScope)
|
||||||
|
initializeInstance(instance, newScope.args, runConstructors = false)
|
||||||
|
return instance.apply {
|
||||||
deserializeStateVars(scope, decoder)
|
deserializeStateVars(scope, decoder)
|
||||||
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
|
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
|
||||||
}
|
}
|
||||||
@@ -41,7 +43,7 @@ class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(na
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
addFn("toString", true) {
|
addFn("toString", true) {
|
||||||
ObjString(thisObj.toString())
|
thisObj.toString(this, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,6 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.datetime.isDistantFuture
|
|
||||||
import kotlinx.datetime.isDistantPast
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
@@ -28,6 +24,10 @@ import net.sergeych.lynon.LynonDecoder
|
|||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonSettings
|
import net.sergeych.lynon.LynonSettings
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import kotlin.time.isDistantFuture
|
||||||
|
import kotlin.time.isDistantPast
|
||||||
|
|
||||||
class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTruncateMode=LynonSettings.InstantTruncateMode.Microsecond) : Obj() {
|
class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTruncateMode=LynonSettings.InstantTruncateMode.Microsecond) : Obj() {
|
||||||
override val objClass: ObjClass get() = type
|
override val objClass: ObjClass get() = type
|
||||||
|
|||||||
@@ -24,36 +24,32 @@ import net.sergeych.lynon.LynonDecoder
|
|||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
|
|
||||||
class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Numeric {
|
class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Numeric {
|
||||||
override val longValue get() = value
|
override val longValue get() = value
|
||||||
override val doubleValue get() = value.toDouble()
|
override val doubleValue get() = value.toDouble()
|
||||||
override val toObjInt get() = this
|
override val toObjInt get() = this
|
||||||
override val toObjReal = ObjReal(doubleValue)
|
override val toObjReal = ObjReal.of(doubleValue)
|
||||||
|
|
||||||
override fun byValueCopy(): Obj = ObjInt(value)
|
override fun byValueCopy(): Obj = this
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return value.hashCode()
|
return value.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAndIncrement(scope: Scope): Obj {
|
override suspend fun getAndIncrement(scope: Scope): Obj {
|
||||||
ensureNotConst(scope)
|
return this
|
||||||
return ObjInt(value).also { value++ }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAndDecrement(scope: Scope): Obj {
|
override suspend fun getAndDecrement(scope: Scope): Obj {
|
||||||
ensureNotConst(scope)
|
return this
|
||||||
return ObjInt(value).also { value-- }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun incrementAndGet(scope: Scope): Obj {
|
override suspend fun incrementAndGet(scope: Scope): Obj {
|
||||||
ensureNotConst(scope)
|
return of(value + 1)
|
||||||
return ObjInt(++value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun decrementAndGet(scope: Scope): Obj {
|
override suspend fun decrementAndGet(scope: Scope): Obj {
|
||||||
ensureNotConst(scope)
|
return of(value - 1)
|
||||||
return ObjInt(--value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
@@ -67,41 +63,35 @@ class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
|||||||
|
|
||||||
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
ObjInt(this.value + other.value)
|
of(this.value + other.value)
|
||||||
else
|
else
|
||||||
ObjReal(this.doubleValue + other.toDouble())
|
ObjReal.of(this.doubleValue + other.toDouble())
|
||||||
|
|
||||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
ObjInt(this.value - other.value)
|
of(this.value - other.value)
|
||||||
else
|
else
|
||||||
ObjReal(this.doubleValue - other.toDouble())
|
ObjReal.of(this.doubleValue - other.toDouble())
|
||||||
|
|
||||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt) {
|
if (other is ObjInt) {
|
||||||
ObjInt(this.value * other.value)
|
of(this.value * other.value)
|
||||||
} else ObjReal(this.value * other.toDouble())
|
} else ObjReal.of(this.value * other.toDouble())
|
||||||
|
|
||||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
ObjInt(this.value / other.value)
|
of(this.value / other.value)
|
||||||
else ObjReal(this.value / other.toDouble())
|
else ObjReal.of(this.value / other.toDouble())
|
||||||
|
|
||||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
ObjInt(this.value % other.value)
|
of(this.value % other.value)
|
||||||
else ObjReal(this.value.toDouble() % other.toDouble())
|
else ObjReal.of(this.value.toDouble() % other.toDouble())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We are by-value type ([byValueCopy] is implemented) so we can do in-place
|
* Numbers are now immutable, so we can't do in-place assignment.
|
||||||
* assignment
|
|
||||||
*/
|
*/
|
||||||
override suspend fun assign(scope: Scope, other: Obj): Obj? {
|
override suspend fun assign(scope: Scope, other: Obj): Obj? = null
|
||||||
return if (!isConst && other is ObjInt) {
|
|
||||||
value = other.value
|
|
||||||
this
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun toKotlin(scope: Scope): Any {
|
override suspend fun toKotlin(scope: Scope): Any {
|
||||||
return value
|
return value
|
||||||
@@ -117,31 +107,31 @@ class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun negate(scope: Scope): Obj {
|
override suspend fun negate(scope: Scope): Obj {
|
||||||
return ObjInt(-value)
|
return of(-value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bitwise operations
|
// Bitwise operations
|
||||||
override suspend fun bitAnd(scope: Scope, other: Obj): Obj =
|
override suspend fun bitAnd(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt) ObjInt(this.value and other.value)
|
if (other is ObjInt) of(this.value and other.value)
|
||||||
else scope.raiseIllegalArgument("bitwise and '&' requires Int, got ${other.objClass.className}")
|
else scope.raiseIllegalArgument("bitwise and '&' requires Int, got ${other.objClass.className}")
|
||||||
|
|
||||||
override suspend fun bitOr(scope: Scope, other: Obj): Obj =
|
override suspend fun bitOr(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt) ObjInt(this.value or other.value)
|
if (other is ObjInt) of(this.value or other.value)
|
||||||
else scope.raiseIllegalArgument("bitwise or '|' requires Int, got ${other.objClass.className}")
|
else scope.raiseIllegalArgument("bitwise or '|' requires Int, got ${other.objClass.className}")
|
||||||
|
|
||||||
override suspend fun bitXor(scope: Scope, other: Obj): Obj =
|
override suspend fun bitXor(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt) ObjInt(this.value xor other.value)
|
if (other is ObjInt) of(this.value xor other.value)
|
||||||
else scope.raiseIllegalArgument("bitwise xor '^' requires Int, got ${other.objClass.className}")
|
else scope.raiseIllegalArgument("bitwise xor '^' requires Int, got ${other.objClass.className}")
|
||||||
|
|
||||||
override suspend fun shl(scope: Scope, other: Obj): Obj =
|
override suspend fun shl(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt) ObjInt(this.value shl (other.value.toInt() and 63))
|
if (other is ObjInt) of(this.value shl (other.value.toInt() and 63))
|
||||||
else scope.raiseIllegalArgument("shift left '<<' requires Int, got ${other.objClass.className}")
|
else scope.raiseIllegalArgument("shift left '<<' requires Int, got ${other.objClass.className}")
|
||||||
|
|
||||||
override suspend fun shr(scope: Scope, other: Obj): Obj =
|
override suspend fun shr(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt) ObjInt(this.value shr (other.value.toInt() and 63))
|
if (other is ObjInt) of(this.value shr (other.value.toInt() and 63))
|
||||||
else scope.raiseIllegalArgument("shift right '>>' requires Int, got ${other.objClass.className}")
|
else scope.raiseIllegalArgument("shift right '>>' requires Int, got ${other.objClass.className}")
|
||||||
|
|
||||||
override suspend fun bitNot(scope: Scope): Obj = ObjInt(this.value.inv())
|
override suspend fun bitNot(scope: Scope): Obj = of(this.value.inv())
|
||||||
|
|
||||||
override suspend fun lynonType(): LynonType = when (value) {
|
override suspend fun lynonType(): LynonType = when (value) {
|
||||||
0L -> LynonType.Int0
|
0L -> LynonType.Int0
|
||||||
@@ -168,20 +158,32 @@ class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Zero = ObjInt(0, true)
|
private val cache = Array(256) { ObjInt((it - 128).toLong(), true) }
|
||||||
val One = ObjInt(1, true)
|
|
||||||
|
fun of(value: Long): ObjInt {
|
||||||
|
return if (value in -128L..127L) cache[(value + 128).toInt()]
|
||||||
|
else ObjInt(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
val Zero = of(0)
|
||||||
|
val One = of(1)
|
||||||
val type = object : ObjClass("Int") {
|
val type = object : ObjClass("Int") {
|
||||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||||
when (lynonType) {
|
when (lynonType) {
|
||||||
null -> ObjInt(decoder.unpackSigned())
|
null -> of(decoder.unpackSigned())
|
||||||
LynonType.Int0 -> Zero
|
LynonType.Int0 -> Zero
|
||||||
LynonType.IntPositive -> ObjInt(decoder.unpackUnsigned().toLong())
|
LynonType.IntPositive -> of(decoder.unpackUnsigned().toLong())
|
||||||
LynonType.IntNegative -> ObjInt(-decoder.unpackUnsigned().toLong())
|
LynonType.IntNegative -> of(-decoder.unpackUnsigned().toLong())
|
||||||
LynonType.IntSigned -> ObjInt(decoder.unpackSigned())
|
LynonType.IntSigned -> of(decoder.unpackSigned())
|
||||||
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
|
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
|
||||||
}
|
}
|
||||||
|
}.apply {
|
||||||
|
addFn("toInt") {
|
||||||
|
thisObj
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Int.toObj() = ObjInt(this.toLong())
|
fun Int.toObj() = ObjInt.of(this.toLong())
|
||||||
|
fun Long.toObj() = ObjInt.of(this)
|
||||||
@@ -104,11 +104,14 @@ val ObjIterable by lazy {
|
|||||||
returns = type("lyng.Map"),
|
returns = type("lyng.Map"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
) {
|
) {
|
||||||
val result = ObjMap()
|
val result = mutableMapOf<Obj, Obj>()
|
||||||
thisObj.toFlow(this).collect { pair ->
|
thisObj.toFlow(this).collect { pair ->
|
||||||
result.map[pair.getAt(this, 0)] = pair.getAt(this, 1)
|
when (pair) {
|
||||||
|
is ObjMapEntry -> result[pair.key] = pair.value
|
||||||
|
else -> result[pair.getAt(this, 0)] = pair.getAt(this, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result
|
ObjMap(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFnDoc(
|
addFnDoc(
|
||||||
|
|||||||
@@ -76,36 +76,3 @@ fun Obj.toFlow(scope: Scope): Flow<Obj> = flow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Call [callback] for each element of this obj considering it provides [Iterator]
|
|
||||||
* methods `hasNext` and `next`.
|
|
||||||
*
|
|
||||||
* IF callback returns false, iteration is stopped.
|
|
||||||
*/
|
|
||||||
suspend fun Obj.enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
|
|
||||||
val iterator = invokeInstanceMethod(scope, "iterator")
|
|
||||||
val hasNext = iterator.getInstanceMethod(scope, "hasNext")
|
|
||||||
val next = iterator.getInstanceMethod(scope, "next")
|
|
||||||
var closeIt = false
|
|
||||||
try {
|
|
||||||
while (hasNext.invoke(scope, iterator).toBool()) {
|
|
||||||
val nextValue = next.invoke(scope, iterator)
|
|
||||||
val shouldContinue = try {
|
|
||||||
callback(nextValue)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// iteration aborted due to exception in callback
|
|
||||||
closeIt = true
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
if (!shouldContinue) {
|
|
||||||
closeIt = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (closeIt) {
|
|
||||||
// Best-effort cancel on premature termination
|
|
||||||
iterator.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -105,7 +105,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else ->
|
else ->
|
||||||
scope.raiseError("'+': can't concatenate $this with $other")
|
scope.raiseError("'+': can't concatenate ${this.toString(scope)} with ${other.toString(scope)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -140,6 +140,12 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
|||||||
return list.contains(other)
|
return list.contains(other)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
|
||||||
|
for (item in list) {
|
||||||
|
if (!callback(item)) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val objClass: ObjClass
|
override val objClass: ObjClass
|
||||||
get() = type
|
get() = type
|
||||||
|
|
||||||
@@ -198,7 +204,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
|||||||
return JsonArray(list.map { it.toJson(scope) })
|
return JsonArray(list.map { it.toJson(scope) })
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||||
return ObjString(buildString {
|
return ObjString(buildString {
|
||||||
append("[")
|
append("[")
|
||||||
var first = true
|
var first = true
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
|
|||||||
else -> scope.raiseIndexOutOfBounds()
|
else -> scope.raiseIndexOutOfBounds()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||||
return ObjString("(${key.toString(scope).value} => ${value.toString(scope).value})")
|
return ObjString("(${key.toString(scope).value} => ${value.toString(scope).value})")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||||
val reusult = buildString {
|
val reusult = buildString {
|
||||||
append("Map(")
|
append("Map(")
|
||||||
var first = true
|
var first = true
|
||||||
@@ -174,18 +174,21 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
val map = mutableMapOf<Obj, Obj>()
|
val map = mutableMapOf<Obj, Obj>()
|
||||||
if (list.isEmpty()) return map
|
if (list.isEmpty()) return map
|
||||||
|
|
||||||
val first = list.first()
|
|
||||||
if (first.isInstanceOf(ObjArray)) {
|
|
||||||
if (first.invokeInstanceMethod(scope, "size").toInt() != 2)
|
|
||||||
scope.raiseIllegalArgument(
|
|
||||||
"list to construct map entry should exactly be 2 element Array like [key,value], got $list"
|
|
||||||
)
|
|
||||||
} else scope.raiseIllegalArgument("first element of map list be a Collection of 2 elements; got $first")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
list.forEach {
|
list.forEach {
|
||||||
map[it.getAt(scope, ObjInt.Zero)] = it.getAt(scope, ObjInt.One)
|
when (it) {
|
||||||
|
is ObjMapEntry -> map[it.key] = it.value
|
||||||
|
else -> {
|
||||||
|
if (it.isInstanceOf(ObjArray)) {
|
||||||
|
if (it.invokeInstanceMethod(scope, "size").toInt() != 2)
|
||||||
|
scope.raiseIllegalArgument(
|
||||||
|
"Array to construct map entry should exactly be 2 elements [key,value], got $it"
|
||||||
|
)
|
||||||
|
map[it.getAt(scope, ObjInt.Zero)] = it.getAt(scope, ObjInt.One)
|
||||||
|
} else {
|
||||||
|
scope.raiseIllegalArgument("elements to construct map must be MapEntry or Array of 2 elements; got $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
@@ -296,13 +299,11 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
is ObjMap -> {
|
is ObjMap -> {
|
||||||
// Rightmost wins: copy all entries from `other` over existing ones
|
// Rightmost wins: copy all entries from `other` over existing ones
|
||||||
for ((k, v) in other.map) {
|
for ((k, v) in other.map) {
|
||||||
val key = k as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got $k")
|
map[k] = v
|
||||||
map[key] = v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ObjMapEntry -> {
|
is ObjMapEntry -> {
|
||||||
val key = other.key as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got ${other.key}")
|
map[other.key] = other.value
|
||||||
map[key] = other.value
|
|
||||||
}
|
}
|
||||||
is ObjList -> {
|
is ObjList -> {
|
||||||
// Treat as list of map entries
|
// Treat as list of map entries
|
||||||
@@ -311,8 +312,7 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
is ObjMapEntry -> e
|
is ObjMapEntry -> e
|
||||||
else -> scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e")
|
else -> scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e")
|
||||||
}
|
}
|
||||||
val key = entry.key as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got ${entry.key}")
|
map[entry.key] = entry.value
|
||||||
map[key] = entry.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> scope.raiseIllegalArgument("map can only be merged with Map, MapEntry, or List<MapEntry>")
|
else -> scope.raiseIllegalArgument("map can only be merged with Map, MapEntry, or List<MapEntry>")
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
* 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.obj
|
||||||
|
|
||||||
|
import net.sergeych.lyng.Arguments
|
||||||
|
import net.sergeych.lyng.ClosureScope
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.Statement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property accessor storage. Per instructions, properties do NOT have
|
||||||
|
* automatic backing fields. They are pure accessors.
|
||||||
|
*/
|
||||||
|
class ObjProperty(
|
||||||
|
val name: String,
|
||||||
|
val getter: Statement?,
|
||||||
|
val setter: Statement?
|
||||||
|
) : Obj() {
|
||||||
|
|
||||||
|
suspend fun callGetter(scope: Scope, instance: Obj, declaringClass: ObjClass? = null): Obj {
|
||||||
|
val g = getter ?: scope.raiseError("property $name has no getter")
|
||||||
|
// Execute getter in a child scope of the instance with 'this' properly set
|
||||||
|
// Use ClosureScope to match extension function behavior (access to instance scope + call scope)
|
||||||
|
val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope)
|
||||||
|
val execScope = ClosureScope(scope, instanceScope).createChildScope(newThisObj = instance)
|
||||||
|
execScope.currentClassCtx = declaringClass
|
||||||
|
return g.execute(execScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun callSetter(scope: Scope, instance: Obj, value: Obj, declaringClass: ObjClass? = null) {
|
||||||
|
val s = setter ?: scope.raiseError("property $name has no setter")
|
||||||
|
// Execute setter in a child scope of the instance with 'this' properly set and the value as an argument
|
||||||
|
// Use ClosureScope to match extension function behavior
|
||||||
|
val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope)
|
||||||
|
val execScope = ClosureScope(scope, instanceScope).createChildScope(args = Arguments(value), newThisObj = instance)
|
||||||
|
execScope.currentClassCtx = declaringClass
|
||||||
|
s.execute(execScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "Property($name)"
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
|
|||||||
|
|
||||||
override val objClass: ObjClass = type
|
override val objClass: ObjClass = type
|
||||||
|
|
||||||
override suspend fun toString(scope: Scope,calledFromLyng: Boolean): ObjString {
|
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||||
val result = StringBuilder()
|
val result = StringBuilder()
|
||||||
result.append("${start?.inspect(scope) ?: '∞'} ..")
|
result.append("${start?.inspect(scope) ?: '∞'} ..")
|
||||||
if (!isEndInclusive) result.append('<')
|
if (!isEndInclusive) result.append('<')
|
||||||
@@ -114,6 +114,36 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
|
|||||||
start is ObjChar && end is ObjChar
|
start is ObjChar && end is ObjChar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
|
||||||
|
if (isIntRange) {
|
||||||
|
val s = (start as ObjInt).value
|
||||||
|
val e = (end as ObjInt).value
|
||||||
|
if (isEndInclusive) {
|
||||||
|
for (i in s..e) {
|
||||||
|
if (!callback(ObjInt.of(i))) break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i in s..<e) {
|
||||||
|
if (!callback(ObjInt.of(i))) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isCharRange) {
|
||||||
|
val s = (start as ObjChar).value
|
||||||
|
val e = (end as ObjChar).value
|
||||||
|
if (isEndInclusive) {
|
||||||
|
for (c in s..e) {
|
||||||
|
if (!callback(ObjChar(c))) break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (c in s..<e) {
|
||||||
|
if (!callback(ObjChar(c))) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.enumerate(scope, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
return (other as? ObjRange)?.let {
|
return (other as? ObjRange)?.let {
|
||||||
if( start == other.start && end == other.end ) 0 else -1
|
if( start == other.start && end == other.end ) 0 else -1
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ import kotlin.math.floor
|
|||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
data class ObjReal(val value: Double) : Obj(), Numeric {
|
data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||||
override val longValue: Long by lazy { floor(value).toLong() }
|
override val longValue: Long get() = floor(value).toLong()
|
||||||
override val doubleValue: Double by lazy { value }
|
override val doubleValue: Double get() = value
|
||||||
override val toObjInt: ObjInt by lazy { ObjInt(longValue) }
|
override val toObjInt: ObjInt get() = ObjInt.of(longValue)
|
||||||
override val toObjReal: ObjReal by lazy { ObjReal(value) }
|
override val toObjReal: ObjReal get() = this
|
||||||
|
|
||||||
override val objClass: ObjClass = type
|
override val objClass: ObjClass = type
|
||||||
|
|
||||||
override fun byValueCopy(): Obj = ObjReal(value)
|
override fun byValueCopy(): Obj = this
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
if (other !is Numeric) return -2
|
if (other !is Numeric) return -2
|
||||||
@@ -65,19 +65,19 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
||||||
ObjReal(this.value + other.toDouble())
|
of(this.value + other.toDouble())
|
||||||
|
|
||||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||||
ObjReal(this.value - other.toDouble())
|
of(this.value - other.toDouble())
|
||||||
|
|
||||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||||
ObjReal(this.value * other.toDouble())
|
of(this.value * other.toDouble())
|
||||||
|
|
||||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||||
ObjReal(this.value / other.toDouble())
|
of(this.value / other.toDouble())
|
||||||
|
|
||||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||||
ObjReal(this.value % other.toDouble())
|
of(this.value % other.toDouble())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns unboxed Double value
|
* Returns unboxed Double value
|
||||||
@@ -96,7 +96,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun negate(scope: Scope): Obj {
|
override suspend fun negate(scope: Scope): Obj {
|
||||||
return ObjReal(-value)
|
return of(-value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun lynonType(): LynonType = LynonType.Real
|
override suspend fun lynonType(): LynonType = LynonType.Real
|
||||||
@@ -110,9 +110,18 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val Zero = ObjReal(0.0)
|
||||||
|
val One = ObjReal(1.0)
|
||||||
|
|
||||||
|
fun of(value: Double): ObjReal = when (value) {
|
||||||
|
0.0 -> Zero
|
||||||
|
1.0 -> One
|
||||||
|
else -> ObjReal(value)
|
||||||
|
}
|
||||||
|
|
||||||
val type: ObjClass = object : ObjClass("Real") {
|
val type: ObjClass = object : ObjClass("Real") {
|
||||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||||
ObjReal(decoder.unpackDouble())
|
of(decoder.unpackDouble())
|
||||||
}.apply {
|
}.apply {
|
||||||
// roundToInt: number rounded to the nearest integer
|
// roundToInt: number rounded to the nearest integer
|
||||||
addConstDoc(
|
addConstDoc(
|
||||||
@@ -130,7 +139,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
returns = type("lyng.Int"),
|
returns = type("lyng.Int"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
) {
|
) {
|
||||||
ObjInt(thisAs<ObjReal>().value.toLong())
|
ObjInt.of(thisAs<ObjReal>().value.toLong())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -26,12 +26,18 @@ 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 isAbstract: Boolean = false,
|
||||||
|
val isClosed: Boolean = false,
|
||||||
|
val isOverride: Boolean = false,
|
||||||
|
var delegate: Obj? = null,
|
||||||
) {
|
) {
|
||||||
|
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")
|
||||||
@@ -42,6 +48,8 @@ data class ObjRecord(
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
Class,
|
Class,
|
||||||
Enum,
|
Enum,
|
||||||
|
Property,
|
||||||
|
Delegated,
|
||||||
Other;
|
Other;
|
||||||
|
|
||||||
val isArgument get() = this == Argument
|
val isArgument get() = this == Argument
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -27,14 +27,33 @@ import net.sergeych.lyng.*
|
|||||||
*/
|
*/
|
||||||
sealed interface ObjRef {
|
sealed interface ObjRef {
|
||||||
suspend fun get(scope: Scope): ObjRecord
|
suspend fun get(scope: Scope): ObjRecord
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fast path for evaluating an expression to a raw Obj value without wrapping it into ObjRecord.
|
* Fast path for evaluating an expression to a raw Obj value without wrapping it into ObjRecord.
|
||||||
* Default implementation calls [get] and returns its value. Nodes can override to avoid record traffic.
|
* Default implementation calls [get] and returns its value. Nodes can override to avoid record traffic.
|
||||||
*/
|
*/
|
||||||
suspend fun evalValue(scope: Scope): Obj = get(scope).value
|
suspend fun evalValue(scope: Scope): Obj {
|
||||||
|
val rec = get(scope)
|
||||||
|
if (rec.type == ObjRecord.Type.Delegated) return scope.resolve(rec, "unknown")
|
||||||
|
return rec.value
|
||||||
|
}
|
||||||
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||||
throw ScriptError(pos, "can't assign value")
|
throw ScriptError(pos, "can't assign value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls [block] for each variable name that this reference targets for writing.
|
||||||
|
* Used for declaring local variables in destructuring.
|
||||||
|
*/
|
||||||
|
fun forEachVariable(block: (String) -> Unit) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls [block] for each variable name that this reference targets for writing,
|
||||||
|
* including its source position if available.
|
||||||
|
*/
|
||||||
|
fun forEachVariableWithPos(block: (String, Pos) -> Unit) {
|
||||||
|
forEachVariable { block(it, Pos.UNKNOWN) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Runtime-computed read-only reference backed by a lambda. */
|
/** Runtime-computed read-only reference backed by a lambda. */
|
||||||
@@ -64,8 +83,7 @@ enum class BinOp {
|
|||||||
/** R-value reference for unary operations. */
|
/** R-value reference for unary operations. */
|
||||||
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
|
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
val v = a.evalValue(scope)
|
||||||
val v = if (fastRval) a.evalValue(scope) else a.get(scope).value
|
|
||||||
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
val rFast: Obj? = when (op) {
|
val rFast: Obj? = when (op) {
|
||||||
UnaryOp.NOT -> if (v is ObjBool) if (!v.value) ObjTrue else ObjFalse else null
|
UnaryOp.NOT -> if (v is ObjBool) if (!v.value) ObjTrue else ObjFalse else null
|
||||||
@@ -93,8 +111,8 @@ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
|
|||||||
/** R-value reference for binary operations. */
|
/** R-value reference for binary operations. */
|
||||||
class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val a = if (PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value
|
val a = left.evalValue(scope)
|
||||||
val b = if (PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value
|
val b = right.evalValue(scope)
|
||||||
|
|
||||||
// Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS)
|
// Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS)
|
||||||
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
@@ -117,16 +135,16 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
|
|||||||
val av = a.value
|
val av = a.value
|
||||||
val bv = b.value
|
val bv = b.value
|
||||||
val r: Obj? = when (op) {
|
val r: Obj? = when (op) {
|
||||||
BinOp.PLUS -> ObjInt(av + bv)
|
BinOp.PLUS -> ObjInt.of(av + bv)
|
||||||
BinOp.MINUS -> ObjInt(av - bv)
|
BinOp.MINUS -> ObjInt.of(av - bv)
|
||||||
BinOp.STAR -> ObjInt(av * bv)
|
BinOp.STAR -> ObjInt.of(av * bv)
|
||||||
BinOp.SLASH -> if (bv != 0L) ObjInt(av / bv) else null
|
BinOp.SLASH -> if (bv != 0L) ObjInt.of(av / bv) else null
|
||||||
BinOp.PERCENT -> if (bv != 0L) ObjInt(av % bv) else null
|
BinOp.PERCENT -> if (bv != 0L) ObjInt.of(av % bv) else null
|
||||||
BinOp.BAND -> ObjInt(av and bv)
|
BinOp.BAND -> ObjInt.of(av and bv)
|
||||||
BinOp.BXOR -> ObjInt(av xor bv)
|
BinOp.BXOR -> ObjInt.of(av xor bv)
|
||||||
BinOp.BOR -> ObjInt(av or bv)
|
BinOp.BOR -> ObjInt.of(av or bv)
|
||||||
BinOp.SHL -> ObjInt(av shl (bv.toInt() and 63))
|
BinOp.SHL -> ObjInt.of(av shl (bv.toInt() and 63))
|
||||||
BinOp.SHR -> ObjInt(av shr (bv.toInt() and 63))
|
BinOp.SHR -> ObjInt.of(av shr (bv.toInt() and 63))
|
||||||
BinOp.EQ -> if (av == bv) ObjTrue else ObjFalse
|
BinOp.EQ -> if (av == bv) ObjTrue else ObjFalse
|
||||||
BinOp.NEQ -> if (av != bv) ObjTrue else ObjFalse
|
BinOp.NEQ -> if (av != bv) ObjTrue else ObjFalse
|
||||||
BinOp.LT -> if (av < bv) ObjTrue else ObjFalse
|
BinOp.LT -> if (av < bv) ObjTrue else ObjFalse
|
||||||
@@ -201,11 +219,11 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
|
|||||||
val ad: Double = if (a is ObjInt) a.doubleValue else (a as ObjReal).value
|
val ad: Double = if (a is ObjInt) a.doubleValue else (a as ObjReal).value
|
||||||
val bd: Double = if (b is ObjInt) b.doubleValue else (b as ObjReal).value
|
val bd: Double = if (b is ObjInt) b.doubleValue else (b as ObjReal).value
|
||||||
val rNum: Obj? = when (op) {
|
val rNum: Obj? = when (op) {
|
||||||
BinOp.PLUS -> ObjReal(ad + bd)
|
BinOp.PLUS -> ObjReal.of(ad + bd)
|
||||||
BinOp.MINUS -> ObjReal(ad - bd)
|
BinOp.MINUS -> ObjReal.of(ad - bd)
|
||||||
BinOp.STAR -> ObjReal(ad * bd)
|
BinOp.STAR -> ObjReal.of(ad * bd)
|
||||||
BinOp.SLASH -> ObjReal(ad / bd)
|
BinOp.SLASH -> ObjReal.of(ad / bd)
|
||||||
BinOp.PERCENT -> ObjReal(ad % bd)
|
BinOp.PERCENT -> ObjReal.of(ad % bd)
|
||||||
BinOp.LT -> if (ad < bd) ObjTrue else ObjFalse
|
BinOp.LT -> if (ad < bd) ObjTrue else ObjFalse
|
||||||
BinOp.LTE -> if (ad <= bd) ObjTrue else ObjFalse
|
BinOp.LTE -> if (ad <= bd) ObjTrue else ObjFalse
|
||||||
BinOp.GT -> if (ad > bd) ObjTrue else ObjFalse
|
BinOp.GT -> if (ad > bd) ObjTrue else ObjFalse
|
||||||
@@ -262,7 +280,7 @@ class ConditionalRef(
|
|||||||
private val ifFalse: ObjRef
|
private val ifFalse: ObjRef
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val condVal = if (PerfFlags.RVAL_FASTPATH) condition.evalValue(scope) else condition.get(scope).value
|
val condVal = condition.evalValue(scope)
|
||||||
val condTrue = when (condVal) {
|
val condTrue = when (condVal) {
|
||||||
is ObjBool -> condVal.value
|
is ObjBool -> condVal.value
|
||||||
is ObjInt -> condVal.value != 0L
|
is ObjInt -> condVal.value != 0L
|
||||||
@@ -281,8 +299,8 @@ class CastRef(
|
|||||||
private val atPos: Pos,
|
private val atPos: Pos,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val v0 = if (PerfFlags.RVAL_FASTPATH) valueRef.evalValue(scope) else valueRef.get(scope).value
|
val v0 = valueRef.evalValue(scope)
|
||||||
val t = if (PerfFlags.RVAL_FASTPATH) typeRef.evalValue(scope) else typeRef.get(scope).value
|
val t = typeRef.evalValue(scope)
|
||||||
val target = (t as? ObjClass) ?: scope.raiseClassCastError("${'$'}t is not the class instance")
|
val target = (t as? ObjClass) ?: scope.raiseClassCastError("${'$'}t is not the class instance")
|
||||||
// unwrap qualified views
|
// unwrap qualified views
|
||||||
val v = when (v0) {
|
val v = when (v0) {
|
||||||
@@ -324,8 +342,8 @@ class AssignOpRef(
|
|||||||
private val atPos: Pos,
|
private val atPos: Pos,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val x = target.get(scope).value
|
val x = target.evalValue(scope)
|
||||||
val y = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
|
val y = value.evalValue(scope)
|
||||||
val inPlace: Obj? = when (op) {
|
val inPlace: Obj? = when (op) {
|
||||||
BinOp.PLUS -> x.plusAssign(scope, y)
|
BinOp.PLUS -> x.plusAssign(scope, y)
|
||||||
BinOp.MINUS -> x.minusAssign(scope, y)
|
BinOp.MINUS -> x.minusAssign(scope, y)
|
||||||
@@ -358,33 +376,22 @@ class IncDecRef(
|
|||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val rec = target.get(scope)
|
val rec = target.get(scope)
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value")
|
if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value")
|
||||||
val v = rec.value
|
val v = scope.resolve(rec, "unknown")
|
||||||
val one = ObjInt.One
|
val one = ObjInt.One
|
||||||
return if (v.isConst) {
|
// We now treat numbers as immutable and always perform write-back via setAt.
|
||||||
// Mirror existing semantics in Compiler for const values
|
// This avoids issues where literals are shared and mutated in-place.
|
||||||
val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one)
|
// For post-inc: return ORIGINAL value; for pre-inc: return NEW value.
|
||||||
// write back
|
val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one)
|
||||||
target.setAt(atPos, scope, result)
|
target.setAt(atPos, scope, result)
|
||||||
// For post-inc: previous code returned NEW value; for pre-inc: returned ORIGINAL value
|
return (if (isPost) v else result).asReadonly
|
||||||
if (isPost) result.asReadonly else v.asReadonly
|
|
||||||
} else {
|
|
||||||
val res = when {
|
|
||||||
isIncrement && isPost -> v.getAndIncrement(scope)
|
|
||||||
isIncrement && !isPost -> v.incrementAndGet(scope)
|
|
||||||
!isIncrement && isPost -> v.getAndDecrement(scope)
|
|
||||||
else -> v.decrementAndGet(scope)
|
|
||||||
}
|
|
||||||
res.asReadonly
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Elvis operator reference: a ?: b */
|
/** Elvis operator reference: a ?: b */
|
||||||
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
val a = left.evalValue(scope)
|
||||||
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
|
val r = if (a != ObjNull) a else right.evalValue(scope)
|
||||||
val r = if (a != ObjNull) a else if (fastRval) right.evalValue(scope) else right.get(scope).value
|
|
||||||
return r.asReadonly
|
return r.asReadonly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,12 +399,10 @@ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
|||||||
/** Logical OR with short-circuit: a || b */
|
/** Logical OR with short-circuit: a || b */
|
||||||
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
val a = left.evalValue(scope)
|
||||||
val fastPrim = PerfFlags.PRIMITIVE_FASTOPS
|
|
||||||
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
|
|
||||||
if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly
|
if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly
|
||||||
val b = if (fastRval) right.evalValue(scope) else right.get(scope).value
|
val b = right.evalValue(scope)
|
||||||
if (fastPrim) {
|
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
if (a is ObjBool && b is ObjBool) {
|
if (a is ObjBool && b is ObjBool) {
|
||||||
return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
||||||
}
|
}
|
||||||
@@ -409,13 +414,10 @@ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef
|
|||||||
/** Logical AND with short-circuit: a && b */
|
/** Logical AND with short-circuit: a && b */
|
||||||
class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
// Hoist flags to locals for JIT friendliness
|
val a = left.evalValue(scope)
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
|
||||||
val fastPrim = PerfFlags.PRIMITIVE_FASTOPS
|
|
||||||
val a = if (fastRval) left.evalValue(scope) else left.get(scope).value
|
|
||||||
if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly
|
if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly
|
||||||
val b = if (fastRval) right.evalValue(scope) else right.get(scope).value
|
val b = right.evalValue(scope)
|
||||||
if (fastPrim) {
|
if (PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
if (a is ObjBool && b is ObjBool) {
|
if (a is ObjBool && b is ObjBool) {
|
||||||
return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
||||||
}
|
}
|
||||||
@@ -437,9 +439,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
|
||||||
@@ -500,10 +502,9 @@ class FieldRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
|
||||||
val fieldPic = PerfFlags.FIELD_PIC
|
val fieldPic = PerfFlags.FIELD_PIC
|
||||||
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||||
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
|
val base = target.evalValue(scope)
|
||||||
if (base == ObjNull && isOptional) return ObjNull.asMutable
|
if (base == ObjNull && isOptional) return ObjNull.asMutable
|
||||||
if (fieldPic) {
|
if (fieldPic) {
|
||||||
val (key, ver) = receiverKeyAndVersion(base)
|
val (key, ver) = receiverKeyAndVersion(base)
|
||||||
@@ -590,7 +591,7 @@ class FieldRef(
|
|||||||
val scope0 = (obj as ObjClass).classScope!!
|
val scope0 = (obj as ObjClass).classScope!!
|
||||||
val r0 = scope0.getSlotRecord(capturedIdx)
|
val r0 = scope0.getSlotRecord(capturedIdx)
|
||||||
if (!r0.visibility.isPublic)
|
if (!r0.visibility.isPublic)
|
||||||
sc.raiseError(ObjAccessException(sc, "can't access non-public field $name"))
|
sc.raiseError(ObjIllegalAccessException(sc, "can't access non-public field $name"))
|
||||||
r0
|
r0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -620,12 +621,16 @@ class FieldRef(
|
|||||||
val (k, v) = receiverKeyAndVersion(base)
|
val (k, v) = receiverKeyAndVersion(base)
|
||||||
val rec = tRecord
|
val rec = tRecord
|
||||||
if (rec != null && tKey == k && tVer == v && tFrameId == scope.frameId) {
|
if (rec != null && tKey == k && tVer == v && tFrameId == scope.frameId) {
|
||||||
// visibility/mutability checks
|
// If it is a property, we must go through writeField (slow path for now)
|
||||||
if (!rec.isMutable) scope.raiseError(ObjIllegalAssignmentException(scope, "can't reassign val $name"))
|
// or handle it here.
|
||||||
if (!rec.visibility.isPublic)
|
if (rec.type != ObjRecord.Type.Property) {
|
||||||
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name"))
|
// visibility/mutability checks
|
||||||
if (rec.value.assign(scope, newValue) == null) rec.value = newValue
|
if (!rec.isMutable) scope.raiseError(ObjIllegalAssignmentException(scope, "can't reassign val $name"))
|
||||||
return
|
if (!rec.visibility.isPublic)
|
||||||
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access non-public field $name"))
|
||||||
|
if (rec.value.assign(scope, newValue) == null) rec.value = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fieldPic) {
|
if (fieldPic) {
|
||||||
@@ -791,11 +796,10 @@ class IndexRef(
|
|||||||
else -> 0L to -1
|
else -> 0L to -1
|
||||||
}
|
}
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
val base = target.evalValue(scope)
|
||||||
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
|
|
||||||
if (base == ObjNull && isOptional) return ObjNull.asMutable
|
if (base == ObjNull && isOptional) return ObjNull.asMutable
|
||||||
val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value
|
val idx = index.evalValue(scope)
|
||||||
if (fastRval) {
|
if (PerfFlags.RVAL_FASTPATH) {
|
||||||
// Primitive list index fast path: avoid virtual dispatch to getAt when shapes match
|
// Primitive list index fast path: avoid virtual dispatch to getAt when shapes match
|
||||||
if (base is ObjList && idx is ObjInt) {
|
if (base is ObjList && idx is ObjInt) {
|
||||||
val i = idx.toInt()
|
val i = idx.toInt()
|
||||||
@@ -865,11 +869,10 @@ class IndexRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun evalValue(scope: Scope): Obj {
|
override suspend fun evalValue(scope: Scope): Obj {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
val base = target.evalValue(scope)
|
||||||
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
|
|
||||||
if (base == ObjNull && isOptional) return ObjNull
|
if (base == ObjNull && isOptional) return ObjNull
|
||||||
val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value
|
val idx = index.evalValue(scope)
|
||||||
if (fastRval) {
|
if (PerfFlags.RVAL_FASTPATH) {
|
||||||
// Fast list[int] path
|
// Fast list[int] path
|
||||||
if (base is ObjList && idx is ObjInt) {
|
if (base is ObjList && idx is ObjInt) {
|
||||||
val i = idx.toInt()
|
val i = idx.toInt()
|
||||||
@@ -1018,9 +1021,8 @@ class CallRef(
|
|||||||
private val isOptionalInvoke: Boolean,
|
private val isOptionalInvoke: Boolean,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
|
||||||
val usePool = PerfFlags.SCOPE_POOL
|
val usePool = PerfFlags.SCOPE_POOL
|
||||||
val callee = if (fastRval) target.evalValue(scope) else target.get(scope).value
|
val callee = target.evalValue(scope)
|
||||||
if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly
|
if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly
|
||||||
val callArgs = args.toArguments(scope, tailBlock)
|
val callArgs = args.toArguments(scope, tailBlock)
|
||||||
val result: Obj = if (usePool) {
|
val result: Obj = if (usePool) {
|
||||||
@@ -1108,10 +1110,9 @@ class MethodCallRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val fastRval = PerfFlags.RVAL_FASTPATH
|
|
||||||
val methodPic = PerfFlags.METHOD_PIC
|
val methodPic = PerfFlags.METHOD_PIC
|
||||||
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
val picCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||||
val base = if (fastRval) receiver.evalValue(scope) else receiver.get(scope).value
|
val base = receiver.evalValue(scope)
|
||||||
if (base == ObjNull && isOptional) return ObjNull.asReadonly
|
if (base == ObjNull && isOptional) return ObjNull.asReadonly
|
||||||
val callArgs = args.toArguments(scope, tailBlock)
|
val callArgs = args.toArguments(scope, tailBlock)
|
||||||
if (methodPic) {
|
if (methodPic) {
|
||||||
@@ -1169,18 +1170,28 @@ class MethodCallRef(
|
|||||||
when (base) {
|
when (base) {
|
||||||
is ObjInstance -> {
|
is ObjInstance -> {
|
||||||
// Prefer resolved class member to avoid per-call lookup on hit
|
// Prefer resolved class member to avoid per-call lookup on hit
|
||||||
val member = base.objClass.getInstanceMemberOrNull(name)
|
// BUT only if it's NOT a root object member (which can be shadowed by extensions)
|
||||||
if (member != null) {
|
var hierarchyMember: ObjRecord? = null
|
||||||
val visibility = member.visibility
|
for (cls in base.objClass.mro) {
|
||||||
val callable = member.value
|
if (cls.className == "Obj") break
|
||||||
|
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||||
|
if (rec != null && !rec.isAbstract && rec.type != ObjRecord.Type.Field) {
|
||||||
|
hierarchyMember = rec
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hierarchyMember != null) {
|
||||||
|
val visibility = hierarchyMember.visibility
|
||||||
|
val callable = hierarchyMember.value
|
||||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
||||||
val inst = obj as ObjInstance
|
val inst = obj as ObjInstance
|
||||||
if (!visibility.isPublic)
|
if (!visibility.isPublic && !canAccessMember(visibility, hierarchyMember.declaringClass ?: inst.objClass, sc.currentClassCtx))
|
||||||
sc.raiseError(ObjAccessException(sc, "can't invoke non-public method $name"))
|
sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name"))
|
||||||
callable.invoke(inst.instanceScope, inst, a)
|
callable.invoke(inst.instanceScope, inst, a)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to name-based lookup per call (uncommon)
|
// Fallback to name-based lookup per call (handles extensions and root members)
|
||||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
|
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1214,7 +1225,14 @@ class MethodCallRef(
|
|||||||
/**
|
/**
|
||||||
* Reference to a local/visible variable by name (Phase A: scope lookup).
|
* Reference to a local/visible variable by name (Phase A: scope lookup).
|
||||||
*/
|
*/
|
||||||
class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
||||||
|
override fun forEachVariable(block: (String) -> Unit) {
|
||||||
|
block(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forEachVariableWithPos(block: (String, Pos) -> Unit) {
|
||||||
|
block(name, atPos)
|
||||||
|
}
|
||||||
// Per-frame slot cache to avoid repeated name lookups
|
// Per-frame slot cache to avoid repeated name lookups
|
||||||
private var cachedFrameId: Long = 0L
|
private var cachedFrameId: Long = 0L
|
||||||
private var cachedSlot: Int = -1
|
private var cachedSlot: Int = -1
|
||||||
@@ -1231,7 +1249,6 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
// 1) Try fast slot/local
|
|
||||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||||
scope.getSlotIndexOf(name)?.let {
|
scope.getSlotIndexOf(name)?.let {
|
||||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicHit++
|
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicHit++
|
||||||
@@ -1266,10 +1283,15 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) {
|
if (slot >= 0) {
|
||||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
val rec = scope.getSlotRecord(slot)
|
||||||
if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++
|
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
|
// Not visible via slot, fallback to other lookups
|
||||||
|
} else {
|
||||||
|
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||||
|
if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++
|
||||||
|
}
|
||||||
|
return rec
|
||||||
}
|
}
|
||||||
return scope.getSlotRecord(slot)
|
|
||||||
}
|
}
|
||||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||||
// 2) Fallback name in scope or field on `this`
|
// 2) Fallback name in scope or field on `this`
|
||||||
@@ -1297,21 +1319,21 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
override suspend fun evalValue(scope: Scope): Obj {
|
override suspend fun evalValue(scope: Scope): Obj {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||||
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
|
scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) }
|
||||||
// fallback to current-scope object or field on `this`
|
// fallback to current-scope object or field on `this`
|
||||||
scope[name]?.let { return it.value }
|
scope[name]?.let { return scope.resolve(it, name) }
|
||||||
run {
|
run {
|
||||||
var s: Scope? = scope
|
var s: Scope? = scope
|
||||||
val visited = HashSet<Long>(4)
|
val visited = HashSet<Long>(4)
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
if (!visited.add(s.frameId)) break
|
if (!visited.add(s.frameId)) break
|
||||||
if (s is ClosureScope) {
|
if (s is ClosureScope) {
|
||||||
s.closureScope.chainLookupWithMembers(name)?.let { return it.value }
|
s.closureScope.chainLookupWithMembers(name)?.let { return s.resolve(it, name) }
|
||||||
}
|
}
|
||||||
s = s.parent
|
s = s.parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.chainLookupIgnoreClosure(name)?.let { return it.value }
|
scope.chainLookupIgnoreClosure(name)?.let { return scope.resolve(it, name) }
|
||||||
return try {
|
return try {
|
||||||
scope.thisObj.readField(scope, name).value
|
scope.thisObj.readField(scope, name).value
|
||||||
} catch (e: ExecutionError) {
|
} catch (e: ExecutionError) {
|
||||||
@@ -1321,23 +1343,31 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
}
|
}
|
||||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) return scope.getSlotRecord(slot).value
|
if (slot >= 0) {
|
||||||
|
val rec = scope.getSlotRecord(slot)
|
||||||
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
|
return scope.resolve(rec, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Fallback name in scope or field on `this`
|
// Fallback name in scope or field on `this`
|
||||||
scope[name]?.let { return it.value }
|
scope[name]?.let {
|
||||||
|
return scope.resolve(it, name)
|
||||||
|
}
|
||||||
run {
|
run {
|
||||||
var s: Scope? = scope
|
var s: Scope? = scope
|
||||||
val visited = HashSet<Long>(4)
|
val visited = HashSet<Long>(4)
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
if (!visited.add(s.frameId)) break
|
if (!visited.add(s.frameId)) break
|
||||||
if (s is ClosureScope) {
|
if (s is ClosureScope) {
|
||||||
s.closureScope.chainLookupWithMembers(name)?.let { return it.value }
|
s.closureScope.chainLookupWithMembers(name)?.let { return s.resolve(it, name) }
|
||||||
}
|
}
|
||||||
s = s.parent
|
s = s.parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.chainLookupIgnoreClosure(name)?.let { return it.value }
|
scope.chainLookupIgnoreClosure(name)?.let { return scope.resolve(it, name) }
|
||||||
return try {
|
return try {
|
||||||
scope.thisObj.readField(scope, name).value
|
val res = scope.thisObj.readField(scope, name).value
|
||||||
|
res
|
||||||
} catch (e: ExecutionError) {
|
} catch (e: ExecutionError) {
|
||||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||||
throw e
|
throw e
|
||||||
@@ -1349,13 +1379,11 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||||
scope.getSlotIndexOf(name)?.let {
|
scope.getSlotIndexOf(name)?.let {
|
||||||
val rec = scope.getSlotRecord(it)
|
val rec = scope.getSlotRecord(it)
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
scope.assign(rec, name, newValue)
|
||||||
rec.value = newValue
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scope[name]?.let { stored ->
|
scope[name]?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
@@ -1365,8 +1393,7 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
if (!visited.add(s.frameId)) break
|
if (!visited.add(s.frameId)) break
|
||||||
if (s is ClosureScope) {
|
if (s is ClosureScope) {
|
||||||
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
s.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1374,8 +1401,7 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Fallback: write to field on `this`
|
// Fallback: write to field on `this`
|
||||||
@@ -1385,13 +1411,13 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
|
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) {
|
if (slot >= 0) {
|
||||||
val rec = scope.getSlotRecord(slot)
|
val rec = scope.getSlotRecord(slot)
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
rec.value = newValue
|
scope.assign(rec, name, newValue)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scope[name]?.let { stored ->
|
scope[name]?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
@@ -1401,8 +1427,7 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
if (!visited.add(s.frameId)) break
|
if (!visited.add(s.frameId)) break
|
||||||
if (s is ClosureScope) {
|
if (s is ClosureScope) {
|
||||||
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
s.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1410,8 +1435,7 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scope.thisObj.writeField(scope, name, newValue)
|
scope.thisObj.writeField(scope, name, newValue)
|
||||||
@@ -1429,19 +1453,29 @@ class BoundLocalVarRef(
|
|||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
return scope.getSlotRecord(slot)
|
val rec = scope.getSlotRecord(slot)
|
||||||
|
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||||
|
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
|
||||||
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun evalValue(scope: Scope): Obj {
|
override suspend fun evalValue(scope: Scope): Obj {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
return scope.getSlotRecord(slot).value
|
val rec = scope.getSlotRecord(slot)
|
||||||
|
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||||
|
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
|
||||||
|
// We might not have the name in BoundLocalVarRef, but let's try to find it or use a placeholder
|
||||||
|
// Actually BoundLocalVarRef is mostly used for parameters which are not delegated yet.
|
||||||
|
// But for consistency:
|
||||||
|
return scope.resolve(rec, "local")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
val rec = scope.getSlotRecord(slot)
|
val rec = scope.getSlotRecord(slot)
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||||
rec.value = newValue
|
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
|
||||||
|
scope.assign(rec, "local", newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1450,9 +1484,12 @@ class BoundLocalVarRef(
|
|||||||
* It resolves the slot once per frame and never falls back to global/module lookup.
|
* It resolves the slot once per frame and never falls back to global/module lookup.
|
||||||
*/
|
*/
|
||||||
class FastLocalVarRef(
|
class FastLocalVarRef(
|
||||||
private val name: String,
|
val name: String,
|
||||||
private val atPos: Pos,
|
private val atPos: Pos,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
|
override fun forEachVariable(block: (String) -> Unit) {
|
||||||
|
block(name)
|
||||||
|
}
|
||||||
// Cache the exact scope frame that owns the slot, not just the current frame
|
// Cache the exact scope frame that owns the slot, not just the current frame
|
||||||
private var cachedOwnerScope: Scope? = null
|
private var cachedOwnerScope: Scope? = null
|
||||||
private var cachedOwnerFrameId: Long = 0L
|
private var cachedOwnerFrameId: Long = 0L
|
||||||
@@ -1500,10 +1537,13 @@ class FastLocalVarRef(
|
|||||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot >= 0 && actualOwner != null) {
|
if (slot >= 0 && actualOwner != null) {
|
||||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
val rec = actualOwner.getSlotRecord(slot)
|
||||||
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
|
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||||
|
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
|
||||||
|
}
|
||||||
|
return rec
|
||||||
}
|
}
|
||||||
return actualOwner.getSlotRecord(slot)
|
|
||||||
}
|
}
|
||||||
// Try per-frame local binding maps in the ancestry first (locals declared in frames)
|
// Try per-frame local binding maps in the ancestry first (locals declared in frames)
|
||||||
run {
|
run {
|
||||||
@@ -1540,13 +1580,20 @@ class FastLocalVarRef(
|
|||||||
val ownerValid = isOwnerValidFor(scope)
|
val ownerValid = isOwnerValidFor(scope)
|
||||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot >= 0 && actualOwner != null) return actualOwner.getSlotRecord(slot).value
|
if (slot >= 0 && actualOwner != null) {
|
||||||
|
val rec = actualOwner.getSlotRecord(slot)
|
||||||
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
|
return actualOwner.resolve(rec, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Try per-frame local binding maps in the ancestry first
|
// Try per-frame local binding maps in the ancestry first
|
||||||
run {
|
run {
|
||||||
var s: Scope? = scope
|
var s: Scope? = scope
|
||||||
var guard = 0
|
var guard = 0
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
s.localBindings[name]?.let { return it.value }
|
s.localBindings[name]?.let {
|
||||||
|
return s.resolve(it, name)
|
||||||
|
}
|
||||||
val next = s.parent
|
val next = s.parent
|
||||||
if (next === s) break
|
if (next === s) break
|
||||||
s = next
|
s = next
|
||||||
@@ -1558,7 +1605,9 @@ class FastLocalVarRef(
|
|||||||
var s: Scope? = scope
|
var s: Scope? = scope
|
||||||
var guard = 0
|
var guard = 0
|
||||||
while (s != null) {
|
while (s != null) {
|
||||||
s.objects[name]?.let { return it.value }
|
s.objects[name]?.let {
|
||||||
|
return s.resolve(it, name)
|
||||||
|
}
|
||||||
val next = s.parent
|
val next = s.parent
|
||||||
if (next === s) break
|
if (next === s) break
|
||||||
s = next
|
s = next
|
||||||
@@ -1566,7 +1615,9 @@ class FastLocalVarRef(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback to standard name lookup (locals or closure chain)
|
// Fallback to standard name lookup (locals or closure chain)
|
||||||
scope[name]?.let { return it.value }
|
scope[name]?.let {
|
||||||
|
return scope.resolve(it, name)
|
||||||
|
}
|
||||||
return scope.thisObj.readField(scope, name).value
|
return scope.thisObj.readField(scope, name).value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1577,9 +1628,10 @@ class FastLocalVarRef(
|
|||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot >= 0 && actualOwner != null) {
|
if (slot >= 0 && actualOwner != null) {
|
||||||
val rec = actualOwner.getSlotRecord(slot)
|
val rec = actualOwner.getSlotRecord(slot)
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||||
rec.value = newValue
|
scope.assign(rec, name, newValue)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Try per-frame local binding maps in the ancestry first
|
// Try per-frame local binding maps in the ancestry first
|
||||||
run {
|
run {
|
||||||
@@ -1588,8 +1640,7 @@ class FastLocalVarRef(
|
|||||||
while (s != null) {
|
while (s != null) {
|
||||||
val rec = s.localBindings[name]
|
val rec = s.localBindings[name]
|
||||||
if (rec != null) {
|
if (rec != null) {
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
s.assign(rec, name, newValue)
|
||||||
rec.value = newValue
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val next = s.parent
|
val next = s.parent
|
||||||
@@ -1600,8 +1651,7 @@ class FastLocalVarRef(
|
|||||||
}
|
}
|
||||||
// Fallback to standard name lookup
|
// Fallback to standard name lookup
|
||||||
scope[name]?.let { stored ->
|
scope[name]?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
scope.assign(stored, name, newValue)
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scope.thisObj.writeField(scope, name, newValue)
|
scope.thisObj.writeField(scope, name, newValue)
|
||||||
@@ -1610,6 +1660,24 @@ class FastLocalVarRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
||||||
|
override fun forEachVariable(block: (String) -> Unit) {
|
||||||
|
for (e in entries) {
|
||||||
|
when (e) {
|
||||||
|
is ListEntry.Element -> e.ref.forEachVariable(block)
|
||||||
|
is ListEntry.Spread -> e.ref.forEachVariable(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forEachVariableWithPos(block: (String, Pos) -> Unit) {
|
||||||
|
for (e in entries) {
|
||||||
|
when (e) {
|
||||||
|
is ListEntry.Element -> e.ref.forEachVariableWithPos(block)
|
||||||
|
is ListEntry.Spread -> e.ref.forEachVariableWithPos(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
// Heuristic capacity hint: count element entries; spreads handled opportunistically
|
// Heuristic capacity hint: count element entries; spreads handled opportunistically
|
||||||
val elemCount = entries.count { it is ListEntry.Element }
|
val elemCount = entries.count { it is ListEntry.Element }
|
||||||
@@ -1617,11 +1685,11 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
|||||||
for (e in entries) {
|
for (e in entries) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is ListEntry.Element -> {
|
is ListEntry.Element -> {
|
||||||
val v = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
val v = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.evalValue(scope)
|
||||||
list += v
|
list += v
|
||||||
}
|
}
|
||||||
is ListEntry.Spread -> {
|
is ListEntry.Spread -> {
|
||||||
val elements = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
val elements = e.ref.evalValue(scope)
|
||||||
when (elements) {
|
when (elements) {
|
||||||
is ObjList -> {
|
is ObjList -> {
|
||||||
// Grow underlying array once when possible
|
// Grow underlying array once when possible
|
||||||
@@ -1633,7 +1701,54 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ObjList(list).asReadonly
|
return ObjList(list).asMutable
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||||
|
val sourceList = (newValue as? ObjList)?.list
|
||||||
|
?: throw ScriptError(pos, "destructuring assignment requires a list on the right side")
|
||||||
|
|
||||||
|
val ellipsisIdx = entries.indexOfFirst { it is ListEntry.Spread }
|
||||||
|
if (entries.count { it is ListEntry.Spread } > 1) {
|
||||||
|
throw ScriptError(pos, "destructuring pattern can have only one splat")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ellipsisIdx < 0) {
|
||||||
|
if (sourceList.size < entries.size)
|
||||||
|
throw ScriptError(pos, "too few elements for destructuring")
|
||||||
|
for (i in entries.indices) {
|
||||||
|
val entry = entries[i]
|
||||||
|
if (entry is ListEntry.Element) {
|
||||||
|
entry.ref.setAt(pos, scope, sourceList[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val headCount = ellipsisIdx
|
||||||
|
val tailCount = entries.size - ellipsisIdx - 1
|
||||||
|
if (sourceList.size < headCount + tailCount)
|
||||||
|
throw ScriptError(pos, "too few elements for destructuring")
|
||||||
|
|
||||||
|
// head
|
||||||
|
for (i in 0 until headCount) {
|
||||||
|
val entry = entries[i]
|
||||||
|
if (entry is ListEntry.Element) {
|
||||||
|
entry.ref.setAt(pos, scope, sourceList[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tail
|
||||||
|
for (i in 0 until tailCount) {
|
||||||
|
val entry = entries[entries.size - 1 - i]
|
||||||
|
if (entry is ListEntry.Element) {
|
||||||
|
entry.ref.setAt(pos, scope, sourceList[sourceList.size - 1 - i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ellipsis
|
||||||
|
val spreadEntry = entries[ellipsisIdx] as ListEntry.Spread
|
||||||
|
val spreadList = sourceList.subList(headCount, sourceList.size - tailCount)
|
||||||
|
spreadEntry.ref.setAt(pos, scope, ObjList(spreadList.toMutableList()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1650,16 +1765,14 @@ class MapLiteralRef(private val entries: List<MapLiteralEntry>) : ObjRef {
|
|||||||
for (e in entries) {
|
for (e in entries) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is MapLiteralEntry.Named -> {
|
is MapLiteralEntry.Named -> {
|
||||||
val v = if (PerfFlags.RVAL_FASTPATH) e.value.evalValue(scope) else e.value.get(scope).value
|
val v = e.value.evalValue(scope)
|
||||||
result.map[ObjString(e.key)] = v
|
result.map[ObjString(e.key)] = v
|
||||||
}
|
}
|
||||||
is MapLiteralEntry.Spread -> {
|
is MapLiteralEntry.Spread -> {
|
||||||
val m = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
val m = e.ref.evalValue(scope)
|
||||||
if (m !is ObjMap) scope.raiseIllegalArgument("spread element in map literal must be a Map")
|
if (m !is ObjMap) scope.raiseIllegalArgument("spread element in map literal must be a Map")
|
||||||
// Enforce string keys for map literals
|
|
||||||
for ((k, v) in m.map) {
|
for ((k, v) in m.map) {
|
||||||
val sKey = k as? ObjString ?: scope.raiseIllegalArgument("spread map must have string keys; got $k")
|
result.map[k] = v
|
||||||
result.map[sKey] = v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1677,8 +1790,8 @@ class RangeRef(
|
|||||||
private val isEndInclusive: Boolean
|
private val isEndInclusive: Boolean
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val l = left?.let { if (PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
|
val l = left?.evalValue(scope) ?: ObjNull
|
||||||
val r = right?.let { if (PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
|
val r = right?.evalValue(scope) ?: ObjNull
|
||||||
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
|
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1690,11 +1803,17 @@ class AssignRef(
|
|||||||
private val atPos: Pos,
|
private val atPos: Pos,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val v = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
|
val v = value.evalValue(scope)
|
||||||
val rec = target.get(scope)
|
// For properties, we should not call get() on target because it invokes the getter.
|
||||||
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
|
// Instead, we call setAt directly.
|
||||||
if (rec.value.assign(scope, v) == null) {
|
if (target is FieldRef || target is IndexRef || target is LocalVarRef || target is FastLocalVarRef || target is BoundLocalVarRef) {
|
||||||
target.setAt(atPos, scope, v)
|
target.setAt(atPos, scope, v)
|
||||||
|
} else {
|
||||||
|
val rec = target.get(scope)
|
||||||
|
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
|
||||||
|
if (rec.value.assign(scope, v) == null) {
|
||||||
|
target.setAt(atPos, scope, v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return v.asReadonly
|
return v.asReadonly
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class ObjRegexMatch(val match: MatchResult) : Obj() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toString(scope: Scope,calledFromLyng: Boolean): ObjString {
|
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||||
return ObjString("RegexMath(${objRange.toString(scope)},${objGroups.toString(scope)})")
|
return ObjString("RegexMath(${objRange.toString(scope)},${objGroups.toString(scope)})")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,12 @@ class ObjRingBuffer(val capacity: Int) : Obj() {
|
|||||||
returns = type("lyng.Void"),
|
returns = type("lyng.Void"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
) { thisAs<ObjRingBuffer>().apply { buffer.add(requireOnlyArg<Obj>()) } }
|
) { thisAs<ObjRingBuffer>().apply { buffer.add(requireOnlyArg<Obj>()) } }
|
||||||
|
addFnDoc(
|
||||||
|
name = "first",
|
||||||
|
doc = "Return the oldest element in the buffer.",
|
||||||
|
returns = type("lyng.Any"),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) { thisAs<ObjRingBuffer>().buffer.first() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,12 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
|
|||||||
return set.contains(other)
|
return set.contains(other)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
|
||||||
|
for (item in set) {
|
||||||
|
if (!callback(item)) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||||
return ObjSet(
|
return ObjSet(
|
||||||
if (other is ObjSet)
|
if (other is ObjSet)
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
returns = type("lyng.Int"),
|
returns = type("lyng.Int"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
) {
|
) {
|
||||||
ObjInt(
|
ObjInt.of(
|
||||||
thisAs<ObjString>().value.toLongOrNull()
|
thisAs<ObjString>().value.toLongOrNull()
|
||||||
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
||||||
)
|
)
|
||||||
@@ -159,7 +159,7 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
}
|
}
|
||||||
addConstDoc(
|
addConstDoc(
|
||||||
name = "length",
|
name = "length",
|
||||||
value = statement { ObjInt(thisAs<ObjString>().value.length.toLong()) },
|
value = statement { ObjInt.of(thisAs<ObjString>().value.length.toLong()) },
|
||||||
doc = "Number of UTF-16 code units in this string.",
|
doc = "Number of UTF-16 code units in this string.",
|
||||||
type = type("lyng.Int"),
|
type = type("lyng.Int"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
@@ -216,6 +216,14 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
) {
|
) {
|
||||||
thisAs<ObjString>().value.lowercase().let(::ObjString)
|
thisAs<ObjString>().value.lowercase().let(::ObjString)
|
||||||
}
|
}
|
||||||
|
addFnDoc(
|
||||||
|
name = "lowercase",
|
||||||
|
doc = "Lowercase version of this string (default locale).",
|
||||||
|
returns = type("lyng.String"),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
|
thisAs<ObjString>().value.lowercase().let(::ObjString)
|
||||||
|
}
|
||||||
addFnDoc(
|
addFnDoc(
|
||||||
name = "upper",
|
name = "upper",
|
||||||
doc = "Uppercase version of this string (default locale).",
|
doc = "Uppercase version of this string (default locale).",
|
||||||
@@ -224,6 +232,14 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
) {
|
) {
|
||||||
thisAs<ObjString>().value.uppercase().let(::ObjString)
|
thisAs<ObjString>().value.uppercase().let(::ObjString)
|
||||||
}
|
}
|
||||||
|
addFnDoc(
|
||||||
|
name = "uppercase",
|
||||||
|
doc = "Uppercase version of this string (default locale).",
|
||||||
|
returns = type("lyng.String"),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
|
thisAs<ObjString>().value.uppercase().let(::ObjString)
|
||||||
|
}
|
||||||
addFnDoc(
|
addFnDoc(
|
||||||
name = "characters",
|
name = "characters",
|
||||||
doc = "List of characters of this string.",
|
doc = "List of characters of this string.",
|
||||||
@@ -253,14 +269,14 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
doc = "Alias for length: the number of characters (code units) in this string.",
|
doc = "Alias for length: the number of characters (code units) in this string.",
|
||||||
returns = type("lyng.Int"),
|
returns = type("lyng.Int"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
) { ObjInt(thisAs<ObjString>().value.length.toLong()) }
|
) { ObjInt.of(thisAs<ObjString>().value.length.toLong()) }
|
||||||
addFnDoc(
|
addFnDoc(
|
||||||
name = "toReal",
|
name = "toReal",
|
||||||
doc = "Parse this string as a real number (floating point).",
|
doc = "Parse this string as a real number (floating point).",
|
||||||
returns = type("lyng.Real"),
|
returns = type("lyng.Real"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
) {
|
) {
|
||||||
ObjReal(thisAs<ObjString>().value.toDouble())
|
ObjReal.of(thisAs<ObjString>().value.toDouble())
|
||||||
}
|
}
|
||||||
addFnDoc(
|
addFnDoc(
|
||||||
name = "trim",
|
name = "trim",
|
||||||
|
|||||||
@@ -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,8 +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
|
||||||
throw UnsupportedOperationException("not comparable")
|
return -3
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
|||||||
@@ -83,15 +83,16 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe
|
|||||||
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
|
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
|
||||||
it
|
it
|
||||||
} ?: run {
|
} ?: run {
|
||||||
val fallback = runCatching { scope.eval(className.value) }.getOrNull()
|
// Use Scope API that mirrors compiler-emitted ObjRef chain for qualified identifiers
|
||||||
if (fallback != null) {
|
val evaluated = scope.resolveQualifiedIdentifier(className.value)
|
||||||
println("Fallback to eval successful")
|
if (evaluated !is ObjClass)
|
||||||
fallback as ObjClass
|
scope.raiseClassCastError("Expected obj class but got ${evaluated::class.simpleName}")
|
||||||
}
|
evaluated
|
||||||
else scope.raiseSymbolNotFound("can't deserialize: not found type $className")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper moved to Scope as resolveQualifiedIdentifier
|
||||||
|
|
||||||
suspend fun decodeAnyList(scope: Scope, fixedSize: Int? = null): MutableList<Obj> {
|
suspend fun decodeAnyList(scope: Scope, fixedSize: Int? = null): MutableList<Obj> {
|
||||||
return if (bin.getBit() == 1) {
|
return if (bin.getBit() == 1) {
|
||||||
// homogenous
|
// homogenous
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
package net.sergeych.tools
|
package net.sergeych.tools
|
||||||
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
inline fun bm(text: String="", f: ()->Unit) {
|
inline fun bm(text: String="", f: ()->Unit) {
|
||||||
val start = Clock.System.now()
|
val start = Clock.System.now()
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -49,10 +49,10 @@ class BindingHighlightTest {
|
|||||||
|
|
||||||
val binding = Binder.bind(text, mini!!)
|
val binding = Binder.bind(text, mini!!)
|
||||||
|
|
||||||
// Find the top-level symbol for counter and ensure it is mutable (Var)
|
// Find the top-level symbol for counter and ensure it is mutable (Variable)
|
||||||
val sym = binding.symbols.firstOrNull { it.name == "counter" }
|
val sym = binding.symbols.firstOrNull { it.name == "counter" }
|
||||||
assertNotNull(sym, "Top-level var 'counter' must be registered as a symbol")
|
assertNotNull(sym, "Top-level var 'counter' must be registered as a symbol")
|
||||||
assertEquals(SymbolKind.Var, sym.kind, "'counter' declared with var should be SymbolKind.Var")
|
assertEquals(SymbolKind.Variable, sym.kind, "'counter' declared with var should be SymbolKind.Variable")
|
||||||
|
|
||||||
// Declaration position
|
// Declaration position
|
||||||
val declRange = sym.declStart to sym.declEnd
|
val declRange = sym.declStart to sym.declEnd
|
||||||
@@ -82,7 +82,7 @@ class BindingHighlightTest {
|
|||||||
|
|
||||||
val sym = binding.symbols.firstOrNull { it.name == "answer" }
|
val sym = binding.symbols.firstOrNull { it.name == "answer" }
|
||||||
assertNotNull(sym, "Top-level val 'answer' must be registered as a symbol")
|
assertNotNull(sym, "Top-level val 'answer' must be registered as a symbol")
|
||||||
assertEquals(SymbolKind.Val, sym.kind, "'answer' declared with val should be SymbolKind.Val")
|
assertEquals(SymbolKind.Value, sym.kind, "'answer' declared with val should be SymbolKind.Value")
|
||||||
|
|
||||||
val declRange = sym.declStart to sym.declEnd
|
val declRange = sym.declStart to sym.declEnd
|
||||||
val refs = binding.references.filter { it.symbolId == sym.id && (it.start to it.end) != declRange }
|
val refs = binding.references.filter { it.symbolId == sym.id && (it.start to it.end) != declRange }
|
||||||
@@ -119,7 +119,7 @@ class BindingHighlightTest {
|
|||||||
// Ensure we registered the local var/val symbol for `name`
|
// Ensure we registered the local var/val symbol for `name`
|
||||||
val nameSym = binding.symbols.firstOrNull { it.name == "name" }
|
val nameSym = binding.symbols.firstOrNull { it.name == "name" }
|
||||||
assertNotNull(nameSym, "Local variable 'name' should be registered as a symbol")
|
assertNotNull(nameSym, "Local variable 'name' should be registered as a symbol")
|
||||||
assertEquals(SymbolKind.Var, nameSym.kind, "'name' is declared with var and must be SymbolKind.Var")
|
assertEquals(SymbolKind.Variable, nameSym.kind, "'name' is declared with var and must be SymbolKind.Variable")
|
||||||
|
|
||||||
// Ensure there is at least one usage reference to `name` (not just the declaration)
|
// Ensure there is at least one usage reference to `name` (not just the declaration)
|
||||||
val nameRefs = binding.references.filter { it.symbolId == nameSym.id }
|
val nameRefs = binding.references.filter { it.symbolId == nameSym.id }
|
||||||
@@ -165,7 +165,7 @@ class BindingHighlightTest {
|
|||||||
|
|
||||||
val binding = Binder.bind(text, mini!!)
|
val binding = Binder.bind(text, mini!!)
|
||||||
|
|
||||||
val nameSym = binding.symbols.firstOrNull { it.name == "name" && (it.kind == SymbolKind.Var || it.kind == SymbolKind.Val) }
|
val nameSym = binding.symbols.firstOrNull { it.name == "name" && (it.kind == SymbolKind.Variable || it.kind == SymbolKind.Value) }
|
||||||
assertNotNull(nameSym, "Local variable 'name' should be registered as a symbol")
|
assertNotNull(nameSym, "Local variable 'name' should be registered as a symbol")
|
||||||
|
|
||||||
// Find the specific usage inside string-literal invocation: "%s is directory"(name)
|
// Find the specific usage inside string-literal invocation: "%s is directory"(name)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -49,7 +49,7 @@ class BindingTest {
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
// Expect at least one Param symbol "a" and one Val symbol "x"
|
// Expect at least one Parameter symbol "a" and one Value symbol "x"
|
||||||
val aIds = snap.symbols.filter { it.name == "a" }.map { it.id }
|
val aIds = snap.symbols.filter { it.name == "a" }.map { it.id }
|
||||||
val xIds = snap.symbols.filter { it.name == "x" }.map { it.id }
|
val xIds = snap.symbols.filter { it.name == "x" }.map { it.id }
|
||||||
assertTrue(aIds.isNotEmpty())
|
assertTrue(aIds.isNotEmpty())
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ class MIC3MroTest {
|
|||||||
eval(
|
eval(
|
||||||
"""
|
"""
|
||||||
class A() { fun common() { "A" } }
|
class A() { fun common() { "A" } }
|
||||||
class B() : A() { fun common() { "B" } }
|
class B() : A() { override fun common() { "B" } }
|
||||||
class C() : A() { fun common() { "C" } }
|
class C() : A() { override fun common() { "C" } }
|
||||||
class D() : B(), C()
|
class D() : B(), C()
|
||||||
|
|
||||||
val d = D()
|
val d = D()
|
||||||
|
|||||||
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user