Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5144a720ba | ||
|
|
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 | ||
|
|
9e11519608 | ||
|
|
a2d26fc777 | ||
|
|
dd1a1544c6 | ||
|
|
fba44622e5 | ||
|
|
2737aaa14e | ||
|
|
bce88ced43 |
No files matched your search
@@ -17,3 +17,6 @@ xcuserdata
|
||||
/sample_texts/1.txt.gz
|
||||
/kotlin-js-store/wasm/yarn.lock
|
||||
/distributables
|
||||
.output*.txt
|
||||
debug.log
|
||||
/build.log
|
||||
@@ -0,0 +1,28 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Tests in 'lyng.lynglib.jvmTest'" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value=":lynglib:cleanJvmTest" />
|
||||
<option value=":lynglib:jvmTest" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>true</RunAsTest>
|
||||
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
||||
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -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">
|
||||
<configuration default="false" name="lyng:site [jsBrowserDevelopmentRun]" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
@@ -17,8 +34,11 @@
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<GradleProfilingDisabled>true</GradleProfilingDisabled>
|
||||
<GradleCoverageDisabled>true</GradleCoverageDisabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
### Unreleased
|
||||
|
||||
- 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`.
|
||||
|
||||
- Docs: Scopes and Closures guidance
|
||||
- New page: `docs/scopes_and_closures.md` detailing `ClosureScope` resolution order, recursion‑safe helpers (`chainLookupIgnoreClosure`, `chainLookupWithMembers`, `baseGetIgnoreClosure`), cycle prevention, and capturing lexical environments for callbacks (`snapshotForClosure`).
|
||||
- Updated: `docs/advanced_topics.md` (link to the new page), `docs/parallelism.md` (closures in `launch`/`flow`), `docs/OOP.md` (visibility from closures with preserved `currentClassCtx`), `docs/exceptions_handling.md` (compatibility alias `SymbolNotFound`).
|
||||
|
||||
@@ -4,11 +4,15 @@ Please visit the project homepage: [https://lynglang.com](https://lynglang.com)
|
||||
|
||||
- simple, compact, intuitive and elegant modern code:
|
||||
|
||||
```
|
||||
```lyng
|
||||
class Point(x, 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) {
|
||||
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)
|
||||
- [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)
|
||||
- [Formatter (core + CLI + IDE)](docs/formatter.md)
|
||||
- [Books directory](docs)
|
||||
@@ -184,6 +190,7 @@ Ready features:
|
||||
- [x] better stack reporting
|
||||
- [x] regular exceptions + extended `when`
|
||||
- [x] multiple inheritance for user classes
|
||||
- [x] class properties (accessors)
|
||||
|
||||
## plan: towards v1.5 Enhancing
|
||||
|
||||
|
||||
+12
-2
@@ -82,7 +82,7 @@ function updateIdeaPluginDownloadLink() {
|
||||
# default target settings
|
||||
case "com" in
|
||||
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
|
||||
ROOT=/bigstore/sergeych_pub/lyng # directory to rsync to
|
||||
;;
|
||||
@@ -98,10 +98,20 @@ esac
|
||||
|
||||
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
|
||||
refreshTextmateZip
|
||||
updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link"
|
||||
|
||||
./gradlew site:clean site:jsBrowserDistribution
|
||||
./gradlew site:jsBrowserDistribution
|
||||
BUILD_RC=$?
|
||||
|
||||
# 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");
|
||||
* 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)"
|
||||
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")
|
||||
}
|
||||
+41198
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -12,7 +12,7 @@ Is a [Iterable] with known `size`, a finite [Iterable]:
|
||||
(1)
|
||||
: `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
|
||||
[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.
|
||||
+102
-22
@@ -40,13 +40,13 @@ available, for example
|
||||
## joinToString
|
||||
|
||||
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.
|
||||
- `suffix` is inserted between items when there are more than one.
|
||||
- `transform` of specified is applied to each element, otherwise its `toString()` method is used.
|
||||
- `separator` is inserted between items when there are more than one.
|
||||
- `transformer` of specified is applied to each element, otherwise its `toString()` method is used.
|
||||
|
||||
Here is the sample:
|
||||
|
||||
@@ -55,7 +55,7 @@ Here is the sample:
|
||||
assertEquals( (1..3).joinToString { it * 10 }, "10 20 30")
|
||||
>>> void
|
||||
|
||||
## `sum` and `sumBy`
|
||||
## `sum` and `sumOf`
|
||||
|
||||
These, again, does the thing:
|
||||
|
||||
@@ -68,6 +68,80 @@ These, again, does the thing:
|
||||
|
||||
>>> void
|
||||
|
||||
## map, filter and their variations
|
||||
|
||||
Used to transform or filter the whole iterable stream:
|
||||
|
||||
val source = [1,2,3,4]
|
||||
|
||||
// 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 } )
|
||||
|
||||
// filterNotNull: skip all null elements:
|
||||
assertEquals([1, 2, 4], [1, 2, null, 4].filterNotNull())
|
||||
|
||||
>>> 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:
|
||||
|
||||
| fun/method | description |
|
||||
@@ -75,52 +149,56 @@ These, again, does the thing:
|
||||
| toList() | create a list from iterable |
|
||||
| toSet() | create a set from iterable |
|
||||
| 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 |
|
||||
| forEach(f) | call f for each element |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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) |
|
||||
| last | last element (1) |
|
||||
| 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 |
|
||||
| dropLast(n) | return new [Iterable] without last n elements |
|
||||
| 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 |
|
||||
| sortedWith(comparator) | sort using a comparator that compares elements (1) |
|
||||
| sortedBy(predicate) | sort by comparing results of the predicate function |
|
||||
| joinToString(s,t) | convert iterable to string, see (2) |
|
||||
| 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)
|
||||
: throws `NoSuchElementException` if there is no such element
|
||||
:: throws `NoSuchElementException` if there is no such element
|
||||
|
||||
(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.
|
||||
|
||||
(3)
|
||||
: 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
|
||||
:: sum of empty collection is `null`
|
||||
|
||||
## Abstract methods:
|
||||
|
||||
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:
|
||||
|
||||
@@ -134,6 +212,8 @@ Creates a list by iterating to the end. So, the Iterator should be finite to be
|
||||
|
||||
[List]: List.md
|
||||
|
||||
[Flow]: parallelism.md#flow
|
||||
|
||||
[Range]: Range.md
|
||||
|
||||
[Set]: Set.md
|
||||
|
||||
@@ -23,6 +23,8 @@ must throw `ObjIterationFinishedError`.
|
||||
|
||||
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:
|
||||
|
||||
- [List], [Range]
|
||||
|
||||
+30
-1
@@ -92,6 +92,34 @@ Open end ranges remove head and tail elements:
|
||||
assert( [1, 2, 3] !== [1, 2, 3])
|
||||
>>> 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
|
||||
|
||||
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 |
|
||||
| `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | 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)
|
||||
: optimized implementation that override `Array` one
|
||||
|
||||
+2
-2
@@ -172,7 +172,7 @@ Maps and entries can also be merged with `+` and `+=`:
|
||||
|
||||
Notes:
|
||||
- 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.
|
||||
- When you need computed or non-string keys, use the constructor form `Map(...)` or build entries with `=>` and then merge.
|
||||
- Spreads inside map literals and `+`/`+=` merges allow any objects as keys.
|
||||
- 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)
|
||||
+163
-6
@@ -42,6 +42,92 @@ 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
|
||||
example above.
|
||||
|
||||
## 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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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.
|
||||
|
||||
## 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
|
||||
calculate the magnitude. Length is called
|
||||
|
||||
@@ -53,8 +139,20 @@ statements discussed later, there could be default values, ellipsis, etc.
|
||||
class Point(x=0,y=0)
|
||||
val p = Point()
|
||||
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
|
||||
|
||||
Note that unlike **Kotlin**, which uses `=` for named arguments, Lyng uses `:` to avoid ambiguity with assignment expressions.
|
||||
|
||||
## Methods
|
||||
|
||||
Functions defined inside a class body are methods, and unless declared
|
||||
@@ -393,9 +491,11 @@ As usual, private statics are not accessible from the outside:
|
||||
|
||||
# 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
|
||||
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:
|
||||
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.
|
||||
|
||||
## 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() {
|
||||
when(this) {
|
||||
@@ -420,10 +520,67 @@ as an integer. In this case we _extend_ class `Object`, as it is the parent clas
|
||||
assert( ! "5.2".isInteger() )
|
||||
>>> 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,
|
||||
as they are modifying the type, not the context.
|
||||
## Extension properties
|
||||
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
val String.isLong = length > 10
|
||||
|
||||
val s = "Hello, world!"
|
||||
assert(s.isLong)
|
||||
```
|
||||
|
||||
### Properties with accessors
|
||||
|
||||
For more complex logic, use `get()` and `set()` blocks:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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
|
||||
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# 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.
|
||||
|
||||
+34
-14
@@ -75,6 +75,13 @@ destructuring arrays when calling functions and lambdas:
|
||||
getFirstAndLast( ...(1..10) ) // see "splats" section below
|
||||
>>> [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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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"))
|
||||
assertEquals(["a", "bar", "c"], test("a", c: "c"))
|
||||
val a = 1
|
||||
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.
|
||||
- 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.
|
||||
- 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)
|
||||
|
||||
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
|
||||
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)
|
||||
assertEquals(["A?","B!","c","D!"], r)
|
||||
>>> void
|
||||
```
|
||||
|
||||
Constraints:
|
||||
|
||||
@@ -155,3 +174,4 @@ If a call is immediately followed by a block `{ ... }`, it is treated as an extr
|
||||
|
||||
|
||||
[tutorial]: tutorial.md
|
||||
[List]: List.md
|
||||
+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") { ... }`.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
### 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 ...`.
|
||||
|
||||
@@ -212,7 +267,7 @@ val s = scope.eval("s").toKotlin(scope) // -> 144
|
||||
|
||||
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))`.
|
||||
- `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()
|
||||
```
|
||||
|
||||
### 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`).
|
||||
- Use `toKotlin(scope)` to get Kotlin values back. Collections convert to Kotlin collections recursively.
|
||||
|
||||
@@ -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 ;)
|
||||
+82
-7
@@ -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)
|
||||
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
|
||||
- [math in Lyng](math.md), [the `when` statement](when.md)
|
||||
- [Testing and Assertions](Testing.md)
|
||||
- [time](time.md) and [parallelism](parallelism.md)
|
||||
- [parallelism] - multithreaded code, coroutines, etc.
|
||||
- Some class
|
||||
@@ -106,6 +107,41 @@ Assignemnt is an expression that changes its lvalue and return assigned value:
|
||||
>>> 11
|
||||
>>> 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
|
||||
to use parentheses as assignment operation insofar is left-associated and will not
|
||||
allow chained assignments (we might fix it later). Use parentheses insofar:
|
||||
@@ -311,8 +347,6 @@ Reference quality and object equality example:
|
||||
assert( null == null) // singletons
|
||||
assert( null === null)
|
||||
// but, for non-singletons:
|
||||
assert( 5 == 5)
|
||||
assert( 5 !== 5)
|
||||
assert( "foo" !== "foo" )
|
||||
>>> void
|
||||
|
||||
@@ -329,7 +363,7 @@ will be thrown:
|
||||
// WRONG! Exception will be thrown at next line:
|
||||
foo + "bar"
|
||||
|
||||
Correct pattern is:
|
||||
The correct pattern is:
|
||||
|
||||
foo = "foo"
|
||||
// now is OK:
|
||||
@@ -403,6 +437,19 @@ It is possible to define also vararg using ellipsis:
|
||||
|
||||
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
|
||||
|
||||
Each __block has an isolated context that can be accessed from closures__. For example:
|
||||
@@ -478,6 +525,8 @@ one could be with ellipsis that means "the rest pf arguments as List":
|
||||
|
||||
### Using lambda as the parameter
|
||||
|
||||
See also: [Testing and Assertions](Testing.md)
|
||||
|
||||
// note that fun returns its last calculated value,
|
||||
// in our case, result after in-place addition:
|
||||
fun mapValues(iterable, transform) {
|
||||
@@ -1379,9 +1428,9 @@ Part match:
|
||||
Typical set of String functions includes:
|
||||
|
||||
| fun/prop | description / notes |
|
||||
|--------------------|------------------------------------------------------------|
|
||||
| lower() | change case to unicode upper |
|
||||
| upper() | change case to unicode lower |
|
||||
|----------------------|------------------------------------------------------------|
|
||||
| lower(), lowercase() | change case to unicode upper |
|
||||
| upper(), uppercase() | change case to unicode lower |
|
||||
| trim() | trim space chars from both ends |
|
||||
| startsWith(prefix) | true if starts with a prefix |
|
||||
| endsWith(prefix) | true if ends with a prefix |
|
||||
@@ -1466,6 +1515,8 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
|
||||
|
||||
[List]: List.md
|
||||
|
||||
[Testing]: Testing.md
|
||||
|
||||
[Iterable]: Iterable.md
|
||||
|
||||
[Iterator]: Iterator.md
|
||||
@@ -1557,4 +1608,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.
|
||||
- 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).
|
||||
@@ -76,8 +76,8 @@
|
||||
},
|
||||
"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]*" } ] },
|
||||
"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" } } } ] },
|
||||
"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" } ] },
|
||||
"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)\\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|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|π)" } ] },
|
||||
"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": "[!?]" } ] },
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[versions]
|
||||
agp = "8.5.2"
|
||||
clikt = "5.0.3"
|
||||
kotlin = "2.2.21"
|
||||
kotlin = "2.3.0"
|
||||
android-minSdk = "24"
|
||||
android-compileSdk = "34"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
mp_bintools = "0.1.12"
|
||||
mp_bintools = "0.3.2"
|
||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||
okioVersion = "3.10.2"
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,7 +21,7 @@ plugins {
|
||||
}
|
||||
|
||||
group = "net.sergeych.lyng"
|
||||
version = "0.0.3-SNAPSHOT"
|
||||
version = "0.0.5-SNAPSHOT"
|
||||
|
||||
kotlin {
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -117,6 +117,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
d.name,
|
||||
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 -> {
|
||||
addTypeSegments(d.returnType)
|
||||
d.params.forEach { addTypeSegments(it.type) }
|
||||
addTypeSegments(d.receiver)
|
||||
}
|
||||
is MiniValDecl -> {
|
||||
addTypeSegments(d.type)
|
||||
addTypeSegments(d.receiver)
|
||||
}
|
||||
is MiniValDecl -> addTypeSegments(d.type)
|
||||
is MiniClassDecl -> {
|
||||
d.ctorFields.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) {
|
||||
SymbolKind.Function -> LyngHighlighterColors.FUNCTION
|
||||
SymbolKind.Class, SymbolKind.Enum -> LyngHighlighterColors.TYPE
|
||||
SymbolKind.Param -> LyngHighlighterColors.PARAMETER
|
||||
SymbolKind.Val -> LyngHighlighterColors.VALUE
|
||||
SymbolKind.Var -> LyngHighlighterColors.VARIABLE
|
||||
SymbolKind.Parameter -> LyngHighlighterColors.PARAMETER
|
||||
SymbolKind.Value -> LyngHighlighterColors.VALUE
|
||||
SymbolKind.Variable -> LyngHighlighterColors.VARIABLE
|
||||
}
|
||||
|
||||
// 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.
|
||||
* 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.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.patterns.PlatformPatterns
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.util.ProcessingContext
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
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.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.miniast.*
|
||||
|
||||
@@ -48,6 +63,12 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
StdlibDocsBootstrap.ensure()
|
||||
val file: PsiFile = parameters.originalFile
|
||||
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
|
||||
val settings = LyngFormatterSettings.getInstance(file.project)
|
||||
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
|
||||
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
|
||||
val engineItems = try {
|
||||
@@ -104,13 +126,13 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
// Try inferring return/receiver class around the dot
|
||||
val inferred =
|
||||
// Prefer MiniAst-based inference (return type from member call or receiver type)
|
||||
guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported)
|
||||
?: guessReceiverClassViaMini(mini, text, memberDotPos, imported)
|
||||
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported, binding)
|
||||
?: DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDotPos, imported, binding)
|
||||
?:
|
||||
guessReturnClassFromMemberCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
||||
?: guessReceiverClass(text, memberDotPos, imported)
|
||||
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
||||
|
||||
if (inferred != null) {
|
||||
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)
|
||||
Kind.Class_ -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Class)
|
||||
Kind.Enum -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Enum)
|
||||
Kind.Value -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Field)
|
||||
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
||||
@@ -160,18 +184,18 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
add("lyng.stdlib")
|
||||
}.toList()
|
||||
val inferredClass =
|
||||
guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported)
|
||||
?: guessReceiverClassViaMini(mini, text, memberDotPos, imported)
|
||||
?: guessReturnClassFromMemberCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
||||
?: guessReceiverClass(text, memberDotPos, imported)
|
||||
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported, binding)
|
||||
?: DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDotPos, imported, binding)
|
||||
?: DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
||||
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}")
|
||||
for (name in ext) {
|
||||
if (existing.contains(name)) continue
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name)
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini)
|
||||
if (resolved != null) {
|
||||
when (val member = resolved.second) {
|
||||
is MiniMemberFunDecl -> {
|
||||
@@ -192,6 +216,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
emit(builder)
|
||||
existing.add(name)
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
} else {
|
||||
// Fallback: emit simple method name without detailed types
|
||||
@@ -214,12 +239,12 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
add("lyng.stdlib")
|
||||
}.toList()
|
||||
val inferred =
|
||||
guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported)
|
||||
?: guessReceiverClassViaMini(mini, text, memberDotPos, imported)
|
||||
?: guessReturnClassFromMemberCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
||||
?: guessReceiverClass(text, memberDotPos, imported)
|
||||
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported, binding)
|
||||
?: DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDotPos, imported, binding)
|
||||
?: DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
||||
if (inferred != null) {
|
||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Enrichment inferred class='$inferred' — offering its members")
|
||||
offerMembers(emit, imported, inferred, sourceText = text, mini = mini)
|
||||
@@ -249,7 +274,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
.withIcon(kindIcon)
|
||||
.withTypeText(typeOf(d.type), true)
|
||||
}
|
||||
else -> LookupElementBuilder.create(name)
|
||||
is MiniEnumDecl -> LookupElementBuilder.create(name)
|
||||
.withIcon(AllIcons.Nodes.Enum)
|
||||
}
|
||||
emit(builder)
|
||||
}
|
||||
@@ -276,9 +302,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
, sourceText: String,
|
||||
mini: MiniScript? = null
|
||||
) {
|
||||
// Ensure modules are seeded in the registry (triggers lazy stdlib build too)
|
||||
for (m in imported) BuiltinDocRegistry.docsForModule(m)
|
||||
val classes = DocLookupUtils.aggregateClasses(imported)
|
||||
val classes = DocLookupUtils.aggregateClasses(imported, mini)
|
||||
if (DEBUG_COMPLETION) {
|
||||
val keys = classes.keys.joinToString(", ")
|
||||
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 (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}")
|
||||
for ((name, sig) in scanned) {
|
||||
when (sig.kind) {
|
||||
@@ -329,6 +353,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
when (m) {
|
||||
is MiniMemberFunDecl -> if (!m.isStatic) continue
|
||||
is MiniMemberValDecl -> if (!m.isStatic) continue
|
||||
is MiniInitDecl -> continue
|
||||
}
|
||||
}
|
||||
val list = target.getOrPut(m.name) { mutableListOf() }
|
||||
@@ -401,9 +426,10 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
.firstOrNull { it.type != null } ?: rep
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(icon)
|
||||
.withTypeText(typeOf((chosen as MiniMemberValDecl).type), true)
|
||||
.withTypeText(typeOf(chosen.type), true)
|
||||
emit(builder)
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +480,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
emit(builder)
|
||||
already.add(name)
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
} else {
|
||||
// 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 {
|
||||
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}")
|
||||
for (name in ext) {
|
||||
if (already.contains(name)) continue
|
||||
@@ -504,6 +531,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
already.add(name)
|
||||
continue
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
}
|
||||
// Fallback: emit without detailed types if we couldn't resolve
|
||||
@@ -519,371 +547,6 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
|
||||
// --- 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) {
|
||||
val src = mini.range.start.source
|
||||
// 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)
|
||||
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_]*)*)")
|
||||
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()) {
|
||||
|
||||
+172
-66
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -69,59 +69,164 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
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}")
|
||||
|
||||
// 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()
|
||||
// Use lenient import provider so unresolved imports (e.g., lyng.io.fs) don't break docs
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
val src = Source("<ide>", text)
|
||||
var mini: MiniScript? = try {
|
||||
val mini = try {
|
||||
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||
sink.build()
|
||||
} 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 failed: ${t.message}")
|
||||
null
|
||||
}
|
||||
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)))
|
||||
}
|
||||
if (DEBUG_LOG) log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini produced partial AST: ${t.message}")
|
||||
sink.build()
|
||||
} ?: MiniScript(MiniRange(Pos(src, 1, 1), Pos(src, 1, 1)))
|
||||
val source = src
|
||||
|
||||
// Try resolve to: function param at position, function/class/val declaration at position
|
||||
// 1) Check declarations whose name range contains offset
|
||||
if (haveMini) for (d in mini.declarations) {
|
||||
val s = source.offsetOf(d.nameStart)
|
||||
val e = (s + d.name.length).coerceAtMost(text.length)
|
||||
if (offset in s until e) {
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched decl '${d.name}' kind=${d::class.simpleName}")
|
||||
// 1) Use unified declaration detection
|
||||
DocLookupUtils.findDeclarationAt(mini, offset, ident)?.let { (name, kind) ->
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched declaration '$name' kind=$kind")
|
||||
// Find the actual declaration object to render
|
||||
for (d in mini.declarations) {
|
||||
if (d.name == name && source.offsetOf(d.nameStart) <= offset && source.offsetOf(d.nameStart) + d.name.length > offset) {
|
||||
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
|
||||
}
|
||||
// 2) Check parameters of functions
|
||||
if (haveMini) for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||
}
|
||||
}
|
||||
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
|
||||
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||
for (p in fn.params) {
|
||||
val s = source.offsetOf(p.nameStart)
|
||||
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}'")
|
||||
if (p.name == name && source.offsetOf(p.nameStart) <= offset && source.offsetOf(p.nameStart) + p.name.length > offset) {
|
||||
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 {
|
||||
val dotPos = TextCtx.findDotLeft(text, idRange.startOffset)
|
||||
?: TextCtx.findDotLeft(text, offset)
|
||||
if (dotPos != null) {
|
||||
// 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()
|
||||
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"
|
||||
val importedModules = DocLookupUtils.canonicalImportedModules(mini, text)
|
||||
|
||||
// Try literal and call-based receiver inference around the dot
|
||||
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
@@ -154,7 +259,27 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
} 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 (className != null) {
|
||||
@@ -163,6 +288,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
}
|
||||
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
|
||||
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}")
|
||||
return renderDeclDoc(it)
|
||||
}
|
||||
|
||||
// 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")
|
||||
var importedModules = if (haveMini) DocLookupUtils.canonicalImportedModules(mini) else emptyList()
|
||||
// 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
|
||||
}
|
||||
}
|
||||
var importedModules = DocLookupUtils.canonicalImportedModules(mini, text)
|
||||
// Always include stdlib as a fallback context
|
||||
if (!importedModules.contains("lyng.stdlib")) importedModules = importedModules + "lyng.stdlib"
|
||||
// 4a) try top-level decls
|
||||
@@ -204,8 +323,9 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
// Also allow values/consts
|
||||
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<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
|
||||
if (ident == "println" || ident == "print") {
|
||||
@@ -224,6 +344,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -242,6 +363,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -254,6 +376,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,6 +391,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc("String", m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc("String", m)
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,6 +401,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,25 +412,6 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
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 fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded }
|
||||
@@ -347,15 +453,15 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val title = when (d) {
|
||||
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
|
||||
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)}"
|
||||
else -> d.name
|
||||
}
|
||||
// Show full detailed documentation, not just the summary
|
||||
val raw = d.doc?.raw
|
||||
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
||||
val sb = StringBuilder()
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -376,7 +482,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
||||
val sb = StringBuilder()
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -389,7 +495,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
|
||||
val sb = StringBuilder()
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -35,6 +35,8 @@ import com.intellij.psi.codeStyle.CodeStyleManager
|
||||
import net.sergeych.lyng.format.LyngFormatConfig
|
||||
import net.sergeych.lyng.format.LyngFormatter
|
||||
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 {
|
||||
private val log = Logger.getInstance(LyngEnterHandler::class.java)
|
||||
@@ -80,10 +82,22 @@ class LyngEnterHandler : EnterHandlerDelegate {
|
||||
val trimmed = prevText.trimStart()
|
||||
// consider only code part before // comment
|
||||
val code = trimmed.substringBefore("//").trim()
|
||||
if (code == "}") {
|
||||
// Previously we reindented the enclosed block on Enter after a lone '}'.
|
||||
// Per new behavior, this action is now bound to typing '}' instead.
|
||||
// Keep Enter flow limited to indenting the new line only.
|
||||
if (code == "}" || code == "*/") {
|
||||
// Adjust indent for the previous line if it's a block or comment closer
|
||||
val prevStart = doc.getLineStartOffset(prevLine)
|
||||
CodeStyleManager.getInstance(project).adjustLineIndent(file, prevStart)
|
||||
|
||||
// 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
|
||||
@@ -159,35 +173,6 @@ class LyngEnterHandler : EnterHandlerDelegate {
|
||||
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 =
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,14 +32,16 @@ import net.sergeych.lyng.format.LyngFormatConfig
|
||||
import net.sergeych.lyng.format.LyngFormatter
|
||||
import net.sergeych.lyng.idea.LyngLanguage
|
||||
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() {
|
||||
private val log = Logger.getInstance(LyngTypedHandler::class.java)
|
||||
|
||||
override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
|
||||
if (file.language != LyngLanguage) return Result.CONTINUE
|
||||
if (c != '}') return Result.CONTINUE
|
||||
|
||||
if (c == '}') {
|
||||
val doc = editor.document
|
||||
PsiDocumentManager.getInstance(project).commitDocument(doc)
|
||||
|
||||
@@ -58,6 +60,27 @@ class LyngTypedHandler : TypedHandlerDelegate() {
|
||||
val lineStart = doc.getLineStartOffset(line)
|
||||
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
||||
}
|
||||
} 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)
|
||||
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result.CONTINUE
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,9 +24,8 @@ import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.codeStyle.CommonCodeStyleSettings.IndentOptions
|
||||
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.util.FormattingUtils
|
||||
|
||||
/**
|
||||
* Lightweight indentation provider for Lyng.
|
||||
@@ -45,8 +44,7 @@ class LyngLineIndentProvider : LineIndentProvider {
|
||||
val options = CodeStyle.getIndentOptions(project, doc)
|
||||
|
||||
val line = doc.getLineNumberSafe(offset)
|
||||
val indent = computeDesiredIndentFromCore(doc, line, options)
|
||||
return indent
|
||||
return FormattingUtils.computeDesiredIndent(project, doc, line)
|
||||
}
|
||||
|
||||
override fun isSuitableFor(language: Language?): Boolean = language == null || language == LyngLanguage
|
||||
@@ -79,25 +77,4 @@ class LyngLineIndentProvider : LineIndentProvider {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,8 @@ class LyngLexer : LexerBase() {
|
||||
private val keywords = setOf(
|
||||
"fun", "val", "var", "class", "type", "import", "as",
|
||||
"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) {
|
||||
|
||||
+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");
|
||||
~ 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: -->
|
||||
<!-- <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>
|
||||
|
||||
<actions/>
|
||||
|
||||
@@ -38,7 +38,7 @@ kotlin {
|
||||
publishLibraryVariants("release")
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
iosX64()
|
||||
@@ -71,11 +71,7 @@ kotlin {
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
||||
// languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
// Correct opt-in markers for coroutines
|
||||
// languageSettings.optIn("kotlinx.coroutines.DelicateCoroutinesApi")
|
||||
// languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
|
||||
// languageSettings.optIn("kotlinx.coroutines.FlowPreview")
|
||||
languageSettings.optIn("kotlin.time.ExperimentalTime")
|
||||
}
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
@@ -115,8 +111,8 @@ android {
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
lint {
|
||||
// Prevent Android Lint from failing the build due to Kotlin toolchain
|
||||
|
||||
@@ -163,7 +163,7 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
|
||||
fsGuard {
|
||||
val self = this.thisObj as ObjPath
|
||||
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
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
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
|
||||
|
||||
@@ -51,7 +51,7 @@ kotlin {
|
||||
publishLibraryVariants("release")
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
iosX64()
|
||||
@@ -89,16 +89,16 @@ kotlin {
|
||||
languageSettings.optIn("kotlinx.coroutines.DelicateCoroutinesApi")
|
||||
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
|
||||
languageSettings.optIn("kotlinx.coroutines.FlowPreview")
|
||||
languageSettings.optIn("kotlin.time.ExperimentalTime")
|
||||
}
|
||||
|
||||
val commonMain by getting {
|
||||
kotlin.srcDir("$buildDir/generated/buildConfig/commonMain/kotlin")
|
||||
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
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(libs.mp.bintools)
|
||||
api("net.sergeych:mp_stools:1.5.2")
|
||||
}
|
||||
}
|
||||
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 ----
|
||||
// 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");
|
||||
* 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,
|
||||
defaultAccessType: AccessType = AccessType.Var,
|
||||
defaultVisibility: Visibility = Visibility.Public,
|
||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
|
||||
) {
|
||||
fun assign(a: Item, value: Obj) {
|
||||
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
|
||||
value.byValueCopy(),
|
||||
a.visibility ?: defaultVisibility,
|
||||
recordType = ObjRecord.Type.Argument)
|
||||
recordType = ObjRecord.Type.Argument,
|
||||
declaringClass = declaringClass)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
val value = if (hp < callArgs.size) callArgs[hp++]
|
||||
else a.defaultValue?.execute(scope)
|
||||
?: scope.raiseIllegalArgument("too few arguments for the call")
|
||||
?: scope.raiseIllegalArgument("too few arguments for the call (${arguments.list})")
|
||||
assign(a, value)
|
||||
}
|
||||
i++
|
||||
|
||||
@@ -58,17 +58,25 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
|
||||
?.instanceScope
|
||||
?.objects
|
||||
?.get(name)
|
||||
?.let { return it }
|
||||
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, 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 rec
|
||||
}
|
||||
|
||||
// 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
|
||||
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
findExtension(callScope.thisObj.objClass, name)?.let { return it }
|
||||
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
|
||||
}
|
||||
|
||||
// 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
|
||||
if (name.startsWith("__")) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import ObjEnumClass
|
||||
import net.sergeych.lyng.Compiler.Companion.compile
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
@@ -674,12 +673,19 @@ class Compiler(
|
||||
Token.Type.RBRACKET -> return entries
|
||||
Token.Type.ELLIPSIS -> {
|
||||
parseExpressionLevel()?.let { entries += ListEntry.Spread(it) }
|
||||
?: throw ScriptError(t.pos, "spread element must have an expression")
|
||||
}
|
||||
|
||||
else -> {
|
||||
cc.previous()
|
||||
parseExpressionLevel()?.let { entries += ListEntry.Element(it) }
|
||||
?: throw ScriptError(t.pos, "invalid list literal: expecting expression")
|
||||
parseExpressionLevel()?.let { expr ->
|
||||
if (cc.current().type == Token.Type.ELLIPSIS) {
|
||||
cc.next()
|
||||
entries += ListEntry.Spread(expr)
|
||||
} else {
|
||||
entries += ListEntry.Element(expr)
|
||||
}
|
||||
} ?: throw ScriptError(t.pos, "invalid list literal: expecting expression")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1029,6 +1035,12 @@ class Compiler(
|
||||
if (t2.type == Token.Type.COLON) {
|
||||
// name: expr
|
||||
val name = t1.value
|
||||
// Check for shorthand: name: (comma or rparen)
|
||||
val next = cc.peekNextNonWhitespace()
|
||||
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
|
||||
val localVar = LocalVarRef(name, t1.pos)
|
||||
return ParsedArgument(statement(t1.pos) { localVar.get(it).value }, t1.pos, isSplat = false, name = name)
|
||||
}
|
||||
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
|
||||
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
|
||||
}
|
||||
@@ -1044,7 +1056,7 @@ class Compiler(
|
||||
}
|
||||
|
||||
Token.Type.ELLIPSIS -> {
|
||||
parseStatement()?.let { args += ParsedArgument(it, t.pos, isSplat = true) }
|
||||
parseExpression()?.let { args += ParsedArgument(it, t.pos, isSplat = true) }
|
||||
?: throw ScriptError(t.pos, "Expecting arguments list")
|
||||
}
|
||||
|
||||
@@ -1095,6 +1107,12 @@ class Compiler(
|
||||
val t2 = cc.next()
|
||||
if (t2.type == Token.Type.COLON) {
|
||||
val name = t1.value
|
||||
// Check for shorthand: name: (comma or rparen)
|
||||
val next = cc.peekNextNonWhitespace()
|
||||
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
|
||||
val localVar = LocalVarRef(name, t1.pos)
|
||||
return ParsedArgument(statement(t1.pos) { localVar.get(it).value }, t1.pos, isSplat = false, name = name)
|
||||
}
|
||||
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
|
||||
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
|
||||
}
|
||||
@@ -1110,7 +1128,7 @@ class Compiler(
|
||||
}
|
||||
|
||||
Token.Type.ELLIPSIS -> {
|
||||
parseStatement()?.let { args += ParsedArgument(it, t.pos, isSplat = true) }
|
||||
parseExpression()?.let { args += ParsedArgument(it, t.pos, isSplat = true) }
|
||||
?: throw ScriptError(t.pos, "Expecting arguments list")
|
||||
}
|
||||
|
||||
@@ -1216,12 +1234,12 @@ class Compiler(
|
||||
return when (t.type) {
|
||||
Token.Type.INT, Token.Type.HEX -> {
|
||||
val n = t.value.replace("_", "").toLong(if (t.type == Token.Type.HEX) 16 else 10)
|
||||
if (isPlus) ObjInt(n) else ObjInt(-n)
|
||||
if (isPlus) ObjInt.of(n) else ObjInt.of(-n)
|
||||
}
|
||||
|
||||
Token.Type.REAL -> {
|
||||
val d = t.value.toDouble()
|
||||
if (isPlus) ObjReal(d) else ObjReal(-d)
|
||||
if (isPlus) ObjReal.of(d) else ObjReal.of(-d)
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -1353,6 +1371,23 @@ class Compiler(
|
||||
parseClassDeclaration()
|
||||
}
|
||||
|
||||
"init" -> {
|
||||
if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
|
||||
val block = parseBlock()
|
||||
lastParsedBlockRange?.let { range ->
|
||||
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
||||
}
|
||||
val initStmt = statement(id.pos) { scp ->
|
||||
block.execute(scp)
|
||||
ObjVoid
|
||||
}
|
||||
statement {
|
||||
currentClassCtx?.instanceInitializers?.add(initStmt)
|
||||
ObjVoid
|
||||
}
|
||||
} else null
|
||||
}
|
||||
|
||||
"enum" -> {
|
||||
pendingDeclStart = id.pos
|
||||
pendingDeclDoc = consumePendingDoc()
|
||||
@@ -1714,6 +1749,9 @@ class Compiler(
|
||||
}
|
||||
|
||||
private fun parseEnumDeclaration(): Statement {
|
||||
val startPos = pendingDeclStart ?: cc.currentPos()
|
||||
val doc = pendingDeclDoc ?: consumePendingDoc()
|
||||
pendingDeclDoc = null
|
||||
val nameToken = cc.requireToken(Token.Type.ID)
|
||||
// so far only simplest enums:
|
||||
val names = mutableListOf<String>()
|
||||
@@ -1741,6 +1779,16 @@ class Compiler(
|
||||
}
|
||||
} while (true)
|
||||
|
||||
miniSink?.onEnumDecl(
|
||||
MiniEnumDecl(
|
||||
range = MiniRange(startPos, cc.currentPos()),
|
||||
name = nameToken.value,
|
||||
entries = names,
|
||||
doc = doc,
|
||||
nameStart = nameToken.pos
|
||||
)
|
||||
)
|
||||
|
||||
return statement {
|
||||
ObjEnumClass.createSimpleEnum(nameToken.value, names).also {
|
||||
addItem(nameToken.value, false, it, recordType = ObjRecord.Type.Enum)
|
||||
@@ -1754,7 +1802,7 @@ class Compiler(
|
||||
val constructorArgsDeclaration =
|
||||
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
|
||||
parseArgsDeclaration(isClassDeclaration = true)
|
||||
else null
|
||||
else ArgsDeclaration(emptyList(), Token.Type.RPAREN)
|
||||
|
||||
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
|
||||
throw ScriptError(
|
||||
@@ -1791,7 +1839,9 @@ class Compiler(
|
||||
if (next.type == Token.Type.LBRACE) {
|
||||
// parse body
|
||||
val bodyStart = next.pos
|
||||
val st = parseScript()
|
||||
val st = withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) {
|
||||
parseScript()
|
||||
}
|
||||
val rbTok = cc.next()
|
||||
if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in class body")
|
||||
classBodyRange = MiniRange(bodyStart, rbTok.pos)
|
||||
@@ -1841,7 +1891,7 @@ class Compiler(
|
||||
// create class
|
||||
val className = nameToken.value
|
||||
|
||||
// @Suppress("UNUSED_VARIABLE") val defaultAccess = if (isStruct) AccessType.Var else AccessType.Initialization
|
||||
// @Suppress("UNUSED_VARIABLE") val defaultAccess = if (isStruct) AccessType.Variable else AccessType.Initialization
|
||||
// @Suppress("UNUSED_VARIABLE") val defaultVisibility = Visibility.Public
|
||||
|
||||
// create instance constructor
|
||||
@@ -1918,7 +1968,7 @@ class Compiler(
|
||||
while (cc.hasPrevious() && cnt < maxDepth) {
|
||||
val t = cc.previous()
|
||||
cnt++
|
||||
if (t.type == Token.Type.LABEL) {
|
||||
if (t.type == Token.Type.LABEL || t.type == Token.Type.ATLABEL) {
|
||||
found = t.value
|
||||
break
|
||||
}
|
||||
@@ -1949,7 +1999,8 @@ class Compiler(
|
||||
val namesForLoop = (currentLocalNames?.toSet() ?: emptySet()) + tVar.value
|
||||
val (canBreak, body, elseStatement) = withLocalNames(namesForLoop) {
|
||||
val loopParsed = cc.parseLoop {
|
||||
parseStatement() ?: throw ScriptError(start, "Bad for statement: expected loop body")
|
||||
if (cc.current().type == Token.Type.LBRACE) parseBlock()
|
||||
else parseStatement() ?: throw ScriptError(start, "Bad for statement: expected loop body")
|
||||
}
|
||||
// possible else clause
|
||||
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||
@@ -2001,7 +2052,7 @@ class Compiler(
|
||||
var breakCaught = false
|
||||
|
||||
if (size > 0) {
|
||||
var current = runCatching { sourceObj.getAt(forContext, ObjInt(0)) }
|
||||
var current = runCatching { sourceObj.getAt(forContext, ObjInt.of(0)) }
|
||||
.getOrElse {
|
||||
throw ScriptError(
|
||||
tOp.pos,
|
||||
@@ -2012,7 +2063,6 @@ class Compiler(
|
||||
var index = 0
|
||||
while (true) {
|
||||
loopSO.value = current
|
||||
if (canBreak) {
|
||||
try {
|
||||
result = body.execute(forContext)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
@@ -2026,9 +2076,8 @@ class Compiler(
|
||||
} else
|
||||
throw lbe
|
||||
}
|
||||
} else result = body.execute(forContext)
|
||||
if (++index >= size) break
|
||||
current = sourceObj.getAt(forContext, ObjInt(index.toLong()))
|
||||
current = sourceObj.getAt(forContext, ObjInt.of(index.toLong()))
|
||||
}
|
||||
}
|
||||
if (!breakCaught && elseStatement != null) {
|
||||
@@ -2048,11 +2097,9 @@ class Compiler(
|
||||
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
|
||||
): Obj {
|
||||
var result: Obj = ObjVoid
|
||||
val iVar = ObjInt(0)
|
||||
loopVar.value = iVar
|
||||
if (catchBreak) {
|
||||
for (i in start..<end) {
|
||||
iVar.value = i//.toLong()
|
||||
loopVar.value = ObjInt.of(i)
|
||||
try {
|
||||
result = body.execute(forScope)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
@@ -2065,7 +2112,7 @@ class Compiler(
|
||||
}
|
||||
} else {
|
||||
for (i in start..<end) {
|
||||
iVar.value = i
|
||||
loopVar.value = ObjInt.of(i)
|
||||
result = body.execute(forScope)
|
||||
}
|
||||
}
|
||||
@@ -2077,57 +2124,50 @@ class Compiler(
|
||||
body: Statement, elseStatement: Statement?, label: String?,
|
||||
catchBreak: Boolean
|
||||
): Obj {
|
||||
val iterObj = sourceObj.invokeInstanceMethod(forScope, "iterator")
|
||||
var result: Obj = ObjVoid
|
||||
var completedNaturally = false
|
||||
var breakCaught = false
|
||||
sourceObj.enumerate(forScope) { item ->
|
||||
loopVar.value = item
|
||||
if (catchBreak) {
|
||||
try {
|
||||
while (iterObj.invokeInstanceMethod(forScope, "hasNext").toBool()) {
|
||||
if (catchBreak)
|
||||
try {
|
||||
loopVar.value = iterObj.invokeInstanceMethod(forScope, "next")
|
||||
result = body.execute(forScope)
|
||||
true
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
if (lbe.doContinue) continue
|
||||
// premature finish, will trigger cancel in finally
|
||||
return lbe.result
|
||||
if (lbe.doContinue) true
|
||||
else {
|
||||
result = lbe.result
|
||||
breakCaught = true
|
||||
false
|
||||
}
|
||||
} else
|
||||
throw lbe
|
||||
}
|
||||
else {
|
||||
loopVar.value = iterObj.invokeInstanceMethod(forScope, "next")
|
||||
} else {
|
||||
result = body.execute(forScope)
|
||||
true
|
||||
}
|
||||
}
|
||||
completedNaturally = true
|
||||
return elseStatement?.execute(forScope) ?: result
|
||||
} finally {
|
||||
if (!completedNaturally) {
|
||||
// Best-effort cancellation on premature termination
|
||||
runCatching {
|
||||
iterObj.invokeInstanceMethod(forScope, "cancelIteration") { ObjVoid }
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (!breakCaught && elseStatement != null) {
|
||||
elseStatement.execute(forScope)
|
||||
} else result
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
private suspend fun parseDoWhileStatement(): Statement {
|
||||
val label = getLabel()?.also { cc.labels += it }
|
||||
val (breakFound, body) = cc.parseLoop {
|
||||
parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad while statement: expected statement")
|
||||
val (canBreak, body) = cc.parseLoop {
|
||||
parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad do-while statement: expected body statement")
|
||||
}
|
||||
label?.also { cc.labels -= it }
|
||||
|
||||
cc.skipTokens(Token.Type.NEWLINE)
|
||||
cc.skipWsTokens()
|
||||
val tWhile = cc.next()
|
||||
if (tWhile.type != Token.Type.ID || tWhile.value != "while")
|
||||
throw ScriptError(tWhile.pos, "Expected 'while' after do body")
|
||||
|
||||
val t = cc.next()
|
||||
if (t.type != Token.Type.ID && t.value != "while")
|
||||
cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here")
|
||||
|
||||
val conditionStart = ensureLparen()
|
||||
val condition =
|
||||
parseExpression() ?: throw ScriptError(conditionStart, "Bad while statement: expected expression")
|
||||
ensureLparen()
|
||||
val condition = parseExpression() ?: throw ScriptError(cc.currentPos(), "Expected condition after 'while'")
|
||||
ensureRparen()
|
||||
|
||||
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||
@@ -2138,13 +2178,11 @@ class Compiler(
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
return statement(body.pos) {
|
||||
var wasBroken = false
|
||||
var result: Obj = ObjVoid
|
||||
lateinit var doScope: Scope
|
||||
while (true) {
|
||||
doScope = it.createChildScope().apply { skipScopeCreation = true }
|
||||
val doScope = it.createChildScope().apply { skipScopeCreation = true }
|
||||
try {
|
||||
result = body.execute(doScope)
|
||||
} catch (e: LoopBreakContinueException) {
|
||||
@@ -2156,11 +2194,12 @@ class Compiler(
|
||||
}
|
||||
// for continue: just fall through to condition check below
|
||||
} else {
|
||||
// Not our label, let outer loops handle it
|
||||
throw e
|
||||
}
|
||||
}
|
||||
if (!condition.execute(doScope).toBool()) break
|
||||
if (!condition.execute(doScope).toBool()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
|
||||
result
|
||||
@@ -2174,7 +2213,10 @@ class Compiler(
|
||||
parseExpression() ?: throw ScriptError(start, "Bad while statement: expected expression")
|
||||
ensureRparen()
|
||||
|
||||
val body = parseStatement() ?: throw ScriptError(start, "Bad while statement: expected statement")
|
||||
val (canBreak, body) = cc.parseLoop {
|
||||
if (cc.current().type == Token.Type.LBRACE) parseBlock()
|
||||
else parseStatement() ?: throw ScriptError(start, "Bad while statement: expected statement")
|
||||
}
|
||||
label?.also { cc.labels -= it }
|
||||
|
||||
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||
@@ -2188,10 +2230,10 @@ class Compiler(
|
||||
var result: Obj = ObjVoid
|
||||
var wasBroken = false
|
||||
while (condition.execute(it).toBool()) {
|
||||
val loopScope = it.createChildScope()
|
||||
if (canBreak) {
|
||||
try {
|
||||
// we don't need to create new context here: if body is a block,
|
||||
// parse block will do it, otherwise single statement doesn't need it:
|
||||
result = body.execute(it)
|
||||
result = body.execute(loopScope)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
if (lbe.doContinue) continue
|
||||
@@ -2203,6 +2245,8 @@ class Compiler(
|
||||
} else
|
||||
throw lbe
|
||||
}
|
||||
} else
|
||||
result = body.execute(loopScope)
|
||||
}
|
||||
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
|
||||
result
|
||||
@@ -2227,7 +2271,12 @@ class Compiler(
|
||||
cc.previous()
|
||||
val resultExpr = if (t.pos.line == start.line && (!t.isComment &&
|
||||
t.type != Token.Type.SEMICOLON &&
|
||||
t.type != Token.Type.NEWLINE)
|
||||
t.type != Token.Type.NEWLINE &&
|
||||
t.type != Token.Type.RBRACE &&
|
||||
t.type != Token.Type.RPAREN &&
|
||||
t.type != Token.Type.RBRACKET &&
|
||||
t.type != Token.Type.COMMA &&
|
||||
t.type != Token.Type.EOF)
|
||||
) {
|
||||
// we have something on this line, could be expression
|
||||
parseStatement()
|
||||
@@ -2329,6 +2378,7 @@ class Compiler(
|
||||
throw ScriptError(t.pos, "Expected identifier after 'fun'")
|
||||
else t.value
|
||||
var nameStartPos: Pos = t.pos
|
||||
var receiverMini: MiniTypeRef? = null
|
||||
|
||||
val annotation = lastAnnotation
|
||||
val parentContext = codeContexts.last()
|
||||
@@ -2337,6 +2387,12 @@ class Compiler(
|
||||
// Is extension?
|
||||
if (t.type == Token.Type.DOT) {
|
||||
extTypeName = name
|
||||
val receiverEnd = Pos(start.source, start.line, start.column + name.length)
|
||||
receiverMini = MiniTypeName(
|
||||
range = MiniRange(start, receiverEnd),
|
||||
segments = listOf(MiniTypeName.Segment(name, MiniRange(start, receiverEnd))),
|
||||
nullable = false
|
||||
)
|
||||
t = cc.next()
|
||||
if (t.type != Token.Type.ID)
|
||||
throw ScriptError(t.pos, "illegal extension format: expected function name")
|
||||
@@ -2380,7 +2436,8 @@ class Compiler(
|
||||
returnType = returnTypeMini,
|
||||
body = null,
|
||||
doc = declDocLocal,
|
||||
nameStart = nameStartPos
|
||||
nameStart = nameStartPos,
|
||||
receiver = receiverMini
|
||||
)
|
||||
miniSink?.onFunDecl(node)
|
||||
pendingDeclDoc = null
|
||||
@@ -2396,7 +2453,20 @@ class Compiler(
|
||||
val fnStatements = if (isExtern)
|
||||
statement { raiseError("extern function not provided: $name") }
|
||||
else
|
||||
withLocalNames(paramNames) { parseBlock() }
|
||||
withLocalNames(paramNames) {
|
||||
val next = cc.peekNextNonWhitespace()
|
||||
if (next.type == Token.Type.ASSIGN) {
|
||||
cc.skipWsTokens()
|
||||
cc.next() // consume '='
|
||||
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected function body expression")
|
||||
// Shorthand function returns the expression value
|
||||
statement(expr.pos) { scope ->
|
||||
expr.execute(scope)
|
||||
}
|
||||
} else {
|
||||
parseBlock()
|
||||
}
|
||||
}
|
||||
// Capture and pop the local declarations count for this function
|
||||
val fnLocalDecls = localDeclCountStack.removeLastOrNull() ?: 0
|
||||
|
||||
@@ -2436,7 +2506,7 @@ class Compiler(
|
||||
// class extension method
|
||||
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
|
||||
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
|
||||
type.addFn(name, isOpen = true, visibility = visibility) {
|
||||
val stmt = statement {
|
||||
// ObjInstance has a fixed instance scope, so we need to build a closure
|
||||
(thisObj as? ObjInstance)?.let { i ->
|
||||
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
|
||||
@@ -2444,6 +2514,7 @@ class Compiler(
|
||||
// other classes can create one-time scope for this rare case:
|
||||
?: annotatedFnBody.execute(thisObj.autoInstanceScope(this))
|
||||
}
|
||||
context.addExtension(type, name, ObjRecord(stmt, isMutable = false, visibility = visibility, declaringClass = null))
|
||||
}
|
||||
// regular function/method
|
||||
?: run {
|
||||
@@ -2532,45 +2603,139 @@ class Compiler(
|
||||
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
|
||||
isStatic: Boolean = false
|
||||
): Statement {
|
||||
val nextToken = cc.next()
|
||||
val start = nextToken.pos
|
||||
|
||||
if (nextToken.type == Token.Type.LBRACKET) {
|
||||
// Destructuring
|
||||
if (isStatic) throw ScriptError(start, "static destructuring is not supported")
|
||||
|
||||
val entries = parseArrayLiteral()
|
||||
val pattern = ListLiteralRef(entries)
|
||||
|
||||
// Register all names in the pattern
|
||||
pattern.forEachVariableWithPos { name, namePos ->
|
||||
declareLocalName(name)
|
||||
val declRange = MiniRange(namePos, namePos)
|
||||
val node = MiniValDecl(
|
||||
range = declRange,
|
||||
name = name,
|
||||
mutable = isMutable,
|
||||
type = null,
|
||||
initRange = null,
|
||||
doc = pendingDeclDoc,
|
||||
nameStart = namePos
|
||||
)
|
||||
miniSink?.onValDecl(node)
|
||||
}
|
||||
pendingDeclDoc = null
|
||||
|
||||
val eqToken = cc.next()
|
||||
if (eqToken.type != Token.Type.ASSIGN)
|
||||
throw ScriptError(eqToken.pos, "destructuring declaration must be initialized")
|
||||
|
||||
val initialExpression = parseStatement(true)
|
||||
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
|
||||
|
||||
val names = mutableListOf<String>()
|
||||
pattern.forEachVariable { names.add(it) }
|
||||
|
||||
return statement(start) { context ->
|
||||
val value = initialExpression.execute(context)
|
||||
for (name in names) {
|
||||
context.addItem(name, true, ObjVoid, visibility)
|
||||
}
|
||||
pattern.setAt(start, context, value)
|
||||
if (!isMutable) {
|
||||
for (name in names) {
|
||||
val rec = context.objects[name]!!
|
||||
val immutableRec = rec.copy(isMutable = false)
|
||||
context.objects[name] = immutableRec
|
||||
context.localBindings[name] = immutableRec
|
||||
context.updateSlotFor(name, immutableRec)
|
||||
}
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
if (nextToken.type != Token.Type.ID)
|
||||
throw ScriptError(nextToken.pos, "Expected identifier or [ here")
|
||||
var name = nextToken.value
|
||||
var extTypeName: String? = null
|
||||
var nameStartPos: Pos = nextToken.pos
|
||||
var receiverMini: MiniTypeRef? = null
|
||||
|
||||
if (cc.peekNextNonWhitespace().type == Token.Type.DOT) {
|
||||
cc.skipWsTokens()
|
||||
cc.next() // consume dot
|
||||
extTypeName = name
|
||||
val receiverEnd = Pos(nextToken.pos.source, nextToken.pos.line, nextToken.pos.column + name.length)
|
||||
receiverMini = MiniTypeName(
|
||||
range = MiniRange(nextToken.pos, receiverEnd),
|
||||
segments = listOf(MiniTypeName.Segment(name, MiniRange(nextToken.pos, receiverEnd))),
|
||||
nullable = false
|
||||
)
|
||||
val nameToken = cc.next()
|
||||
val start = nameToken.pos
|
||||
if (nameToken.type != Token.Type.ID)
|
||||
throw ScriptError(nameToken.pos, "Expected identifier here")
|
||||
val name = nameToken.value
|
||||
throw ScriptError(nameToken.pos, "Expected identifier after dot in extension declaration")
|
||||
name = nameToken.value
|
||||
nameStartPos = nameToken.pos
|
||||
}
|
||||
|
||||
// Optional explicit type annotation
|
||||
val varTypeMini: MiniTypeRef? = if (cc.current().type == Token.Type.COLON) {
|
||||
parseTypeDeclarationWithMini().second
|
||||
} else null
|
||||
|
||||
val markBeforeEq = cc.savePos()
|
||||
cc.skipWsTokens()
|
||||
val eqToken = cc.next()
|
||||
var setNull = false
|
||||
var isProperty = false
|
||||
|
||||
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name
|
||||
|
||||
if (declaringClassNameCaptured != null || extTypeName != null) {
|
||||
val mark = cc.savePos()
|
||||
cc.restorePos(markBeforeEq)
|
||||
cc.skipWsTokens()
|
||||
val next = cc.peekNextNonWhitespace()
|
||||
if (next.isId("get") || next.isId("set")) {
|
||||
isProperty = true
|
||||
cc.restorePos(markBeforeEq)
|
||||
cc.skipWsTokens()
|
||||
} else {
|
||||
cc.restorePos(mark)
|
||||
}
|
||||
}
|
||||
|
||||
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
|
||||
if (!isStatic) declareLocalName(name)
|
||||
|
||||
val isDelegate = if (eqToken.isId("by")) {
|
||||
val isDelegate = if (!isProperty && eqToken.isId("by")) {
|
||||
true
|
||||
} else {
|
||||
if (eqToken.type != Token.Type.ASSIGN) {
|
||||
if (!isMutable)
|
||||
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
|
||||
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
|
||||
throw ScriptError(start, "val must be initialized")
|
||||
else {
|
||||
cc.previous()
|
||||
cc.restorePos(markBeforeEq)
|
||||
cc.skipWsTokens()
|
||||
setNull = true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
val initialExpression = if (setNull) null
|
||||
val initialExpression = if (setNull || isProperty) null
|
||||
else parseStatement(true)
|
||||
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
|
||||
|
||||
// Emit MiniValDecl for this declaration (before execution wiring), attach doc if any
|
||||
run {
|
||||
val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos())
|
||||
val initR = if (setNull) null else MiniRange(eqToken.pos, cc.currentPos())
|
||||
val initR = if (setNull || isProperty) null else MiniRange(eqToken.pos, cc.currentPos())
|
||||
val node = MiniValDecl(
|
||||
range = declRange,
|
||||
name = name,
|
||||
@@ -2578,7 +2743,8 @@ class Compiler(
|
||||
type = varTypeMini,
|
||||
initRange = initR,
|
||||
doc = pendingDeclDoc,
|
||||
nameStart = nameToken.pos
|
||||
nameStart = nameStartPos,
|
||||
receiver = receiverMini
|
||||
)
|
||||
miniSink?.onValDecl(node)
|
||||
pendingDeclDoc = null
|
||||
@@ -2599,17 +2765,95 @@ class Compiler(
|
||||
return NopStatement
|
||||
}
|
||||
|
||||
// Determine declaring class (if inside class body) at compile time, capture it in the closure
|
||||
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name
|
||||
// Check for accessors if it is a class member
|
||||
var getter: Statement? = null
|
||||
var setter: Statement? = null
|
||||
if (declaringClassNameCaptured != null || extTypeName != null) {
|
||||
while (true) {
|
||||
val t = cc.peekNextNonWhitespace()
|
||||
if (t.isId("get")) {
|
||||
cc.skipWsTokens()
|
||||
cc.next() // consume 'get'
|
||||
cc.requireToken(Token.Type.LPAREN)
|
||||
cc.requireToken(Token.Type.RPAREN)
|
||||
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||
cc.skipWsTokens()
|
||||
parseBlock()
|
||||
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
|
||||
cc.skipWsTokens()
|
||||
cc.next() // consume '='
|
||||
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected getter expression")
|
||||
(expr as? Statement) ?: statement(expr.pos) { s -> expr.execute(s) }
|
||||
} else {
|
||||
throw ScriptError(cc.current().pos, "Expected { or = after get()")
|
||||
}
|
||||
} else if (t.isId("set")) {
|
||||
cc.skipWsTokens()
|
||||
cc.next() // consume 'set'
|
||||
cc.requireToken(Token.Type.LPAREN)
|
||||
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||
cc.requireToken(Token.Type.RPAREN)
|
||||
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||
cc.skipWsTokens()
|
||||
val body = parseBlock()
|
||||
statement(body.pos) { scope ->
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||
body.execute(scope)
|
||||
}
|
||||
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
|
||||
cc.skipWsTokens()
|
||||
cc.next() // consume '='
|
||||
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected setter expression")
|
||||
val st = (expr as? Statement) ?: statement(expr.pos) { s -> expr.execute(s) }
|
||||
statement(st.pos) { scope ->
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||
st.execute(scope)
|
||||
}
|
||||
} else {
|
||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||
}
|
||||
} else break
|
||||
}
|
||||
if (getter != null || setter != null) {
|
||||
if (isMutable) {
|
||||
if (getter == null || setter == null) {
|
||||
throw ScriptError(start, "var property must have both get() and set()")
|
||||
}
|
||||
} else {
|
||||
if (setter != null)
|
||||
throw ScriptError(start, "val property cannot have a setter (name: $name)")
|
||||
if (getter == null)
|
||||
throw ScriptError(start, "val property with accessors must have a getter (name: $name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statement(nameToken.pos) { context ->
|
||||
return statement(start) { context ->
|
||||
if (extTypeName != null) {
|
||||
val prop = if (getter != null || setter != null) {
|
||||
ObjProperty(name, getter, setter)
|
||||
} else {
|
||||
// Simple val extension with initializer
|
||||
val initExpr = initialExpression ?: throw ScriptError(start, "Extension val must be initialized")
|
||||
ObjProperty(name, statement(initExpr.pos) { scp -> initExpr.execute(scp) }, null)
|
||||
}
|
||||
|
||||
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
|
||||
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
|
||||
|
||||
context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, declaringClass = null, type = ObjRecord.Type.Property))
|
||||
|
||||
return@statement prop
|
||||
}
|
||||
// In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions
|
||||
// Do NOT infer declaring class from runtime thisObj here; only the compile-time captured
|
||||
// ClassBody qualifies for class-field storage. Otherwise, this is a plain local.
|
||||
val declaringClassName = declaringClassNameCaptured
|
||||
if (declaringClassName == null) {
|
||||
if (context.containsLocal(name))
|
||||
throw ScriptError(nameToken.pos, "Variable $name is already defined")
|
||||
throw ScriptError(start, "Variable $name is already defined")
|
||||
}
|
||||
|
||||
// Register the local name so subsequent identifiers can be emitted as fast locals
|
||||
@@ -2617,23 +2861,32 @@ class Compiler(
|
||||
|
||||
if (isDelegate) {
|
||||
TODO()
|
||||
// println("initial expr = $initialExpression")
|
||||
// val initValue =
|
||||
// (initialExpression?.execute(context.copy(Arguments(ObjString(name)))) as? Statement)
|
||||
// ?.execute(context.copy(Arguments(ObjString(name))))
|
||||
// ?: context.raiseError("delegate initialization required")
|
||||
// println("delegate init: $initValue")
|
||||
// if (!initValue.isInstanceOf(ObjArray))
|
||||
// context.raiseIllegalArgument("delegate initialized must be an array")
|
||||
// val s = initValue.getAt(context, 1)
|
||||
// val setter = if (s == ObjNull) statement { raiseNotImplemented("setter is not provided") }
|
||||
// else (s as? Statement) ?: context.raiseClassCastError("setter must be a callable")
|
||||
// ObjDelegate(
|
||||
// (initValue.getAt(context, 0) as? Statement)
|
||||
// ?: context.raiseClassCastError("getter must be a callable"), setter
|
||||
// ).also {
|
||||
// context.addItem(name, isMutable, it, visibility, recordType = ObjRecord.Type.Field)
|
||||
// }
|
||||
} else if (getter != null || setter != null) {
|
||||
val declaringClassName = declaringClassNameCaptured!!
|
||||
val storageName = "$declaringClassName::$name"
|
||||
val prop = ObjProperty(name, getter, setter)
|
||||
|
||||
// If we are in class scope now (defining instance field), defer initialization to instance time
|
||||
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
|
||||
if (isClassScope) {
|
||||
val cls = context.thisObj as ObjClass
|
||||
// Register the property
|
||||
cls.instanceInitializers += statement(start) { scp ->
|
||||
scp.addItem(
|
||||
storageName,
|
||||
isMutable,
|
||||
prop,
|
||||
visibility,
|
||||
recordType = ObjRecord.Type.Property
|
||||
)
|
||||
ObjVoid
|
||||
}
|
||||
ObjVoid
|
||||
} else {
|
||||
// We are in instance scope already: perform initialization immediately
|
||||
context.addItem(storageName, isMutable, prop, visibility, recordType = ObjRecord.Type.Property)
|
||||
prop
|
||||
}
|
||||
} else {
|
||||
if (declaringClassName != null && !isStatic) {
|
||||
val storageName = "$declaringClassName::$name"
|
||||
@@ -2642,7 +2895,7 @@ class Compiler(
|
||||
if (isClassScope) {
|
||||
val cls = context.thisObj as ObjClass
|
||||
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
|
||||
val initStmt = statement(nameToken.pos) { scp ->
|
||||
val initStmt = statement(start) { scp ->
|
||||
val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: ObjNull
|
||||
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
|
||||
scp.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||
@@ -2762,22 +3015,22 @@ class Compiler(
|
||||
|
||||
// Arithmetic for ints only (keep semantics simple at compile time)
|
||||
BinOp.PLUS -> when {
|
||||
a is ObjInt && b is ObjInt -> ObjInt(a.value + b.value)
|
||||
a is ObjInt && b is ObjInt -> ObjInt.of(a.value + b.value)
|
||||
a is ObjString && b is ObjString -> ObjString(a.value + b.value)
|
||||
else -> null
|
||||
}
|
||||
|
||||
BinOp.MINUS -> if (a is ObjInt && b is ObjInt) ObjInt(a.value - b.value) else null
|
||||
BinOp.STAR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value * b.value) else null
|
||||
BinOp.SLASH -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt(a.value / b.value) else null
|
||||
BinOp.PERCENT -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt(a.value % b.value) else null
|
||||
BinOp.MINUS -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value - b.value) else null
|
||||
BinOp.STAR -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value * b.value) else null
|
||||
BinOp.SLASH -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt.of(a.value / b.value) else null
|
||||
BinOp.PERCENT -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt.of(a.value % b.value) else null
|
||||
|
||||
// Bitwise for ints
|
||||
BinOp.BAND -> if (a is ObjInt && b is ObjInt) ObjInt(a.value and b.value) else null
|
||||
BinOp.BXOR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value xor b.value) else null
|
||||
BinOp.BOR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value or b.value) else null
|
||||
BinOp.SHL -> if (a is ObjInt && b is ObjInt) ObjInt(a.value shl (b.value.toInt() and 63)) else null
|
||||
BinOp.SHR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value shr (b.value.toInt() and 63)) else null
|
||||
BinOp.BAND -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value and b.value) else null
|
||||
BinOp.BXOR -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value xor b.value) else null
|
||||
BinOp.BOR -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value or b.value) else null
|
||||
BinOp.SHL -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value shl (b.value.toInt() and 63)) else null
|
||||
BinOp.SHR -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value shr (b.value.toInt() and 63)) else null
|
||||
|
||||
// Non-folded / side-effecting or type-dependent ops
|
||||
BinOp.EQARROW, BinOp.REF_EQ, BinOp.REF_NEQ, BinOp.MATCH, BinOp.NOTMATCH,
|
||||
@@ -2790,12 +3043,12 @@ class Compiler(
|
||||
return when (op) {
|
||||
UnaryOp.NOT -> if (a is ObjBool) if (!a.value) ObjTrue else ObjFalse else null
|
||||
UnaryOp.NEGATE -> when (a) {
|
||||
is ObjInt -> ObjInt(-a.value)
|
||||
is ObjReal -> ObjReal(-a.value)
|
||||
is ObjInt -> ObjInt.of(-a.value)
|
||||
is ObjReal -> ObjReal.of(-a.value)
|
||||
else -> null
|
||||
}
|
||||
|
||||
UnaryOp.BITNOT -> if (a is ObjInt) ObjInt(a.value.inv()) else null
|
||||
UnaryOp.BITNOT -> if (a is ObjInt) ObjInt.of(a.value.inv()) else null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,12 @@ class CompilerContext(val tokens: List<Token>) {
|
||||
var loopLevel = 0
|
||||
|
||||
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
|
||||
if (++loopLevel == 0) breakFound = false
|
||||
val oldBreakFound = breakFound
|
||||
breakFound = false
|
||||
val result = f()
|
||||
return Pair(breakFound, result).also {
|
||||
--loopLevel
|
||||
}
|
||||
val currentBreakFound = breakFound
|
||||
breakFound = oldBreakFound || currentBreakFound
|
||||
return Pair(currentBreakFound, result)
|
||||
}
|
||||
|
||||
var currentIndex = 0
|
||||
@@ -108,7 +109,6 @@ class CompilerContext(val tokens: List<Token>) {
|
||||
val t = next()
|
||||
return if (t.type != tokenType) {
|
||||
if (!isOptional) {
|
||||
println("unexpected: $t (needed $tokenType)")
|
||||
throw ScriptError(t.pos, errorMessage)
|
||||
} else {
|
||||
previous()
|
||||
|
||||
@@ -45,22 +45,39 @@ class ModuleScope(
|
||||
if (record.visibility.isPublic) {
|
||||
val newName = symbols?.let { ss: Map<String, String> ->
|
||||
ss[symbol]
|
||||
?.also { symbolsToImport!!.remove(it) }
|
||||
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached")
|
||||
} ?: symbol
|
||||
?.also { symbolsToImport!!.remove(symbol) }
|
||||
?: return@let null
|
||||
} ?: if (symbols == null) symbol else null
|
||||
|
||||
if (newName != null) {
|
||||
val existing = scope.objects[newName]
|
||||
if (existing != null) {
|
||||
if (existing.importedFrom != record.importedFrom)
|
||||
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
|
||||
// already imported
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// when importing records, we keep track of its package (not otherwise needed)
|
||||
if (record.importedFrom == null) record.importedFrom = this
|
||||
scope.objects[newName] = record
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for ((cls, map) in this.extensions) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!symbolsToImport.isNullOrEmpty())
|
||||
scope.raiseSymbolNotFound("symbols $packageName.{$symbolsToImport} are.were not found")
|
||||
}
|
||||
|
||||
@@ -62,6 +62,30 @@ open class Scope(
|
||||
*/
|
||||
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. */
|
||||
private fun ensureNoCycle(candidateParent: Scope?) {
|
||||
if (candidateParent == null) return
|
||||
@@ -82,10 +106,17 @@ open class Scope(
|
||||
* 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.
|
||||
*/
|
||||
private fun tryGetLocalRecord(s: Scope, name: String): ObjRecord? {
|
||||
s.objects[name]?.let { return it }
|
||||
s.localBindings[name]?.let { return it }
|
||||
s.getSlotIndexOf(name)?.let { return s.getSlotRecord(it) }
|
||||
private fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
|
||||
s.objects[name]?.let { rec ->
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -95,7 +126,7 @@ open class Scope(
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
@@ -110,17 +141,22 @@ open class Scope(
|
||||
*/
|
||||
internal fun baseGetIgnoreClosure(name: String): ObjRecord? {
|
||||
// 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
|
||||
var s = parent
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
// 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)) rec else null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,13 +166,18 @@ open class Scope(
|
||||
* This completely avoids invoking overridden `get` implementations, preventing
|
||||
* 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
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
tryGetLocalRecord(s, name, caller)?.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)) return rec
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
@@ -152,6 +193,10 @@ open class Scope(
|
||||
// copy locals and bindings
|
||||
snap.objects.putAll(this.objects)
|
||||
snap.localBindings.putAll(this.localBindings)
|
||||
// copy extensions
|
||||
for ((cls, map) in extensions) {
|
||||
snap.extensions[cls] = map.toMutableMap()
|
||||
}
|
||||
// copy slots map preserving indices
|
||||
if (this.slotCount() > 0) {
|
||||
var i = 0
|
||||
@@ -273,13 +318,19 @@ open class Scope(
|
||||
if (name == "this") thisObj.asReadonly
|
||||
else {
|
||||
// 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)
|
||||
?: localBindings[name]
|
||||
?: localBindings[name]?.let { rec ->
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
|
||||
}
|
||||
// Walk up ancestry
|
||||
?: parent?.get(name)
|
||||
// Finally, fallback to class members on thisObj
|
||||
?: thisObj.objClass.getInstanceMemberOrNull(name)
|
||||
?: thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -297,6 +348,10 @@ open class Scope(
|
||||
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.
|
||||
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||
@@ -313,6 +368,7 @@ open class Scope(
|
||||
slots.clear()
|
||||
nameToSlot.clear()
|
||||
localBindings.clear()
|
||||
extensions.clear()
|
||||
// Now safe to validate and re-parent
|
||||
ensureNoCycle(parent)
|
||||
this.parent = parent
|
||||
@@ -396,9 +452,10 @@ open class Scope(
|
||||
isMutable: Boolean,
|
||||
value: Obj,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||
recordType: ObjRecord.Type = ObjRecord.Type.Other,
|
||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
|
||||
): ObjRecord {
|
||||
val rec = ObjRecord(value, isMutable, visibility, declaringClass = currentClassCtx, type = recordType)
|
||||
val rec = ObjRecord(value, isMutable, visibility, declaringClass = declaringClass, type = recordType)
|
||||
objects[name] = rec
|
||||
// Index this binding within the current frame to help resolve locals across suspension
|
||||
localBindings[name] = rec
|
||||
@@ -415,9 +472,12 @@ open class Scope(
|
||||
callScope.allocateSlotFor(name, rec)
|
||||
}
|
||||
}
|
||||
// Map to a slot for fast local access (if not already mapped)
|
||||
if (getSlotIndexOf(name) == null) {
|
||||
// Map to a slot for fast local access (ensure consistency)
|
||||
val idx = getSlotIndexOf(name)
|
||||
if (idx == null) {
|
||||
allocateSlotFor(name, rec)
|
||||
} else {
|
||||
slots[idx] = rec
|
||||
}
|
||||
return rec
|
||||
}
|
||||
@@ -428,13 +488,13 @@ open class Scope(
|
||||
}
|
||||
|
||||
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Scope.() -> Unit) {
|
||||
addFn<ObjVoid>(*names) {
|
||||
addFn(*names) {
|
||||
fn(this)
|
||||
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() {
|
||||
override val pos: Pos = Pos.builtIn
|
||||
|
||||
@@ -516,6 +576,23 @@ open class Scope(
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -173,53 +173,98 @@ class Script(
|
||||
val a = requiredArg<Obj>(0)
|
||||
val b = requiredArg<Obj>(1)
|
||||
if (a.compareTo(this, b) != 0)
|
||||
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"))
|
||||
raiseError(
|
||||
ObjAssertionFailedException(
|
||||
this,
|
||||
"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"
|
||||
)
|
||||
)
|
||||
}
|
||||
// alias used in tests
|
||||
addVoidFn("assertEqual") {
|
||||
val a = requiredArg<Obj>(0)
|
||||
val b = requiredArg<Obj>(1)
|
||||
if (a.compareTo(this, b) != 0)
|
||||
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"))
|
||||
raiseError(
|
||||
ObjAssertionFailedException(
|
||||
this,
|
||||
"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"
|
||||
)
|
||||
)
|
||||
}
|
||||
addVoidFn("assertNotEquals") {
|
||||
val a = requiredArg<Obj>(0)
|
||||
val b = requiredArg<Obj>(1)
|
||||
if (a.compareTo(this, b) == 0)
|
||||
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} != ${b.inspect(this)}"))
|
||||
raiseError(
|
||||
ObjAssertionFailedException(
|
||||
this,
|
||||
"Assertion failed: ${a.inspect(this)} != ${b.inspect(this)}"
|
||||
)
|
||||
)
|
||||
}
|
||||
addFn("assertThrows") {
|
||||
val code = requireOnlyArg<Statement>()
|
||||
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 {
|
||||
code.execute(this)
|
||||
null
|
||||
}
|
||||
catch( e: ExecutionError ) {
|
||||
} catch (e: ExecutionError) {
|
||||
e.errorObject
|
||||
}
|
||||
catch (_: ScriptError) {
|
||||
} catch (_: ScriptError) {
|
||||
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 $result")
|
||||
if (result.exceptionClass != expectedClass) {
|
||||
raiseError("Expected $expectedClass, got ${result.exceptionClass}")
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
addFn("dynamic") {
|
||||
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") {
|
||||
val condition = requiredArg<ObjBool>(0)
|
||||
if (!condition.value) {
|
||||
val message = args.list.getOrNull(1)?.toString() ?: "requirement not met"
|
||||
raiseIllegalArgument(message)
|
||||
var message = args.list.getOrNull(1)
|
||||
if( message is Statement ) message = message.execute(this)
|
||||
raiseIllegalArgument(message?.toString() ?: "requirement not met")
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
addFn("check") {
|
||||
val condition = requiredArg<ObjBool>(0)
|
||||
if (!condition.value) {
|
||||
val message = args.list.getOrNull(1)?.toString() ?: "check failed"
|
||||
raiseIllegalState(message)
|
||||
var message = args.list.getOrNull(1)
|
||||
if( message is Statement ) message = message.execute(this)
|
||||
raiseIllegalState(message?.toString() ?: "check failed")
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
@@ -257,6 +302,7 @@ class Script(
|
||||
addConst("Deferred", ObjDeferred.type)
|
||||
addConst("CompletableDeferred", ObjCompletableDeferred.type)
|
||||
addConst("Mutex", ObjMutex.type)
|
||||
addConst("Flow", ObjFlow.type)
|
||||
|
||||
addConst("Regex", ObjRegex.type)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ enum class 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 {
|
||||
return when (visibility) {
|
||||
val res = when (visibility) {
|
||||
Visibility.Public -> true
|
||||
Visibility.Private -> (decl != null && caller === decl)
|
||||
Visibility.Protected -> when {
|
||||
@@ -36,4 +36,5 @@ fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass
|
||||
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");
|
||||
* 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.SimpleLyngHighlighter
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.miniast.MiniClassDecl
|
||||
import net.sergeych.lyng.miniast.MiniFunDecl
|
||||
import net.sergeych.lyng.miniast.MiniScript
|
||||
import net.sergeych.lyng.miniast.MiniValDecl
|
||||
import net.sergeych.lyng.miniast.*
|
||||
|
||||
enum class SymbolKind { Class, Enum, Function, Val, Var, Param }
|
||||
enum class SymbolKind { Class, Enum, Function, Value, Variable, Parameter }
|
||||
|
||||
data class Symbol(
|
||||
val id: Int,
|
||||
@@ -111,7 +108,7 @@ object Binder {
|
||||
for (cf in d.ctorFields) {
|
||||
val fs = source.offsetOf(cf.nameStart)
|
||||
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
|
||||
@@ -138,7 +135,7 @@ object Binder {
|
||||
for (p in d.params) {
|
||||
val ps = source.offsetOf(p.nameStart)
|
||||
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)
|
||||
fnScope.locals += paramSym.id
|
||||
symbols += paramSym
|
||||
@@ -147,7 +144,7 @@ object Binder {
|
||||
}
|
||||
is MiniValDecl -> {
|
||||
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)
|
||||
if (ownerClass != null) {
|
||||
// class field
|
||||
@@ -160,6 +157,12 @@ object Binder {
|
||||
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 +194,7 @@ object Binder {
|
||||
.maxByOrNull { it.rangeEnd - it.rangeStart }
|
||||
if (containerFn != null) {
|
||||
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)
|
||||
symbols += localSym
|
||||
containerFn.locals += localSym.id
|
||||
@@ -231,7 +234,7 @@ object Binder {
|
||||
val inFn = functions.asSequence()
|
||||
.filter { it.rangeEnd > it.rangeStart && nameStart >= it.rangeStart && nameStart <= it.rangeEnd }
|
||||
.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) {
|
||||
val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = inFn.id)
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,11 +34,18 @@ object LyngFormatter {
|
||||
var parenBalance = 0
|
||||
var bracketBalance = 0
|
||||
var prevBracketContinuation = false
|
||||
var inBlockComment = false
|
||||
// We don't keep per-"[" base alignment; continuation rules define alignment.
|
||||
|
||||
fun codePart(s: String): String {
|
||||
val idx = s.indexOf("//")
|
||||
return if (idx >= 0) s.substring(0, idx) else s
|
||||
fun updateBalances(ch: Char) {
|
||||
when (ch) {
|
||||
'{' -> blockLevel++
|
||||
'}' -> if (blockLevel > 0) blockLevel--
|
||||
'(' -> parenBalance++
|
||||
')' -> if (parenBalance > 0) parenBalance--
|
||||
'[' -> bracketBalance++
|
||||
']' -> if (bracketBalance > 0) bracketBalance--
|
||||
}
|
||||
}
|
||||
|
||||
fun indentOf(level: Int, continuation: Int): String =
|
||||
@@ -57,13 +64,20 @@ object LyngFormatter {
|
||||
}
|
||||
|
||||
for ((i, rawLine) in lines.withIndex()) {
|
||||
val line = rawLine
|
||||
val code = codePart(line)
|
||||
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
|
||||
val code = parts.filter { it.type == PartType.Code }.joinToString("") { it.text }
|
||||
val trimmedStart = code.dropWhile { it == ' ' || it == '\t' }
|
||||
val trimmedLine = rawLine.trim()
|
||||
|
||||
// Compute effective indent level for this line
|
||||
var effectiveLevel = blockLevel
|
||||
if (trimmedStart.startsWith("}")) effectiveLevel = (effectiveLevel - 1).coerceAtLeast(0)
|
||||
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,
|
||||
// because the preceding '}' has already reduced [blockLevel] appropriately.
|
||||
|
||||
@@ -74,15 +88,7 @@ object LyngFormatter {
|
||||
if (applyAwaiting) effectiveLevel += 1
|
||||
|
||||
val firstChar = trimmedStart.firstOrNull()
|
||||
// Do not apply continuation on a line that starts with a closer ')' or ']'
|
||||
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 ')'.
|
||||
// While inside parentheses, continuation applies scaled by nesting level
|
||||
val parenContLevels = if (parenBalance > 0 && firstChar != ')') parenBalance else 0
|
||||
val continuation = when {
|
||||
// One-shot continuation when previous line ended with '[' to align first element
|
||||
@@ -91,6 +97,7 @@ object LyngFormatter {
|
||||
bracketBalance > 0 && firstChar != ']' -> config.continuationIndentSize
|
||||
// While inside parentheses, continuation applies scaled by nesting level
|
||||
parenContLevels > 0 -> config.continuationIndentSize * parenContLevels
|
||||
trimmedStart.startsWith(".") -> config.indentSize
|
||||
else -> 0
|
||||
}
|
||||
|
||||
@@ -100,8 +107,8 @@ object LyngFormatter {
|
||||
}
|
||||
|
||||
// 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 }
|
||||
var content = line.substring(contentStart)
|
||||
val contentStart = rawLine.indexOfFirst { it != ' ' && it != '\t' }.let { if (it < 0) rawLine.length else it }
|
||||
var content = rawLine.substring(contentStart)
|
||||
// Collapse spaces right after an opening '[' to avoid "[ 1"; make it "[1"
|
||||
if (content.startsWith("[")) {
|
||||
content = "[" + content.drop(1).trimStart()
|
||||
@@ -115,11 +122,7 @@ object LyngFormatter {
|
||||
}
|
||||
// Determine base indent using structural level and continuation only (spaces only)
|
||||
val indentString = indentOf(effectiveLevel, continuation)
|
||||
if (content.isEmpty()) {
|
||||
// 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 {
|
||||
if (content.isNotEmpty()) {
|
||||
sb.append(indentString).append(content)
|
||||
}
|
||||
|
||||
@@ -127,14 +130,12 @@ object LyngFormatter {
|
||||
if (i < lines.lastIndex) sb.append('\n')
|
||||
|
||||
// Update balances using this line's code content
|
||||
for (ch in code) when (ch) {
|
||||
'{' -> blockLevel++
|
||||
'}' -> if (blockLevel > 0) blockLevel--
|
||||
'(' -> parenBalance++
|
||||
')' -> if (parenBalance > 0) parenBalance--
|
||||
'[' -> bracketBalance++
|
||||
']' -> if (bracketBalance > 0) bracketBalance--
|
||||
for (part in parts) {
|
||||
if (part.type == PartType.Code) {
|
||||
for (ch in part.text) updateBalances(ch)
|
||||
}
|
||||
}
|
||||
inBlockComment = nextInBlockComment
|
||||
|
||||
// Update awaitingSingleIndent based on current line
|
||||
if (applyAwaiting && trimmedStart.isNotEmpty()) {
|
||||
@@ -149,70 +150,41 @@ object LyngFormatter {
|
||||
}
|
||||
|
||||
// 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("[")
|
||||
// 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) {
|
||||
// One-shot continuation for the very next line
|
||||
prevBracketContinuation = true
|
||||
} 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
|
||||
}
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/** Full format. Currently performs indentation only; spacing/wrapping can be added later. */
|
||||
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
||||
// Phase 1: indentation
|
||||
val indented = reindent(text, config)
|
||||
if (!config.applySpacing && !config.applyWrapping) return indented
|
||||
|
||||
// Phase 2: minimal, safe spacing (PSI-free). Skip block comments completely and
|
||||
// only apply spacing to the part before '//' on each line.
|
||||
// Phase 2: minimal, safe spacing (PSI-free).
|
||||
val lines = indented.split('\n')
|
||||
val out = StringBuilder(indented.length)
|
||||
var inBlockComment = false
|
||||
for ((i, rawLine) in lines.withIndex()) {
|
||||
var line = rawLine
|
||||
if (config.applySpacing) {
|
||||
if (inBlockComment) {
|
||||
// Pass-through until we see the end of the block comment on some line
|
||||
val end = line.indexOf("*/")
|
||||
if (end >= 0) {
|
||||
inBlockComment = false
|
||||
}
|
||||
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
|
||||
val sb = StringBuilder()
|
||||
for (part in parts) {
|
||||
if (part.type == PartType.Code) {
|
||||
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 {
|
||||
// Normal code line: respect single-line comments
|
||||
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)
|
||||
}
|
||||
sb.append(part.text)
|
||||
}
|
||||
}
|
||||
line = sb.toString()
|
||||
inBlockComment = nextInBlockComment
|
||||
}
|
||||
out.append(line.trimEnd())
|
||||
if (i < lines.lastIndex) out.append('\n')
|
||||
@@ -369,7 +341,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
|
||||
// Ensure space before '(' for control-flow keywords
|
||||
s = s.replace(Regex("\\b(if|for|while)\\("), "$1 (")
|
||||
@@ -408,15 +453,16 @@ private fun applyMinimalSpacing(code: String): String {
|
||||
s = s.replace(Regex(", \\)"), ",)")
|
||||
s = s.replace(Regex(", \\]"), ",]")
|
||||
// Equality/boolean operators: ensure spaces around
|
||||
s = s.replace(Regex("\\s*(==|!=|<=|>=|&&|\\|\\|)\\s*"), " $1 ")
|
||||
// Assignment '=' (not part of '==', '!=', '<=', '>=', '=>'): collapse whitespace to single spaces around '='
|
||||
s = s.replace(Regex("(?<![=!<>])\\s*=\\s*(?![=>])"), " = ")
|
||||
// Multiply/Divide/Mod as binary: 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 '='
|
||||
s = s.replace(Regex("(?<![=!<>+\\-*/])\\s*=\\s*(?![=>])"), " = ")
|
||||
// Multiply/Divide/Mod as binary: spaces around. Avoid '*=', '/='
|
||||
s = s.replace(Regex("(?<![*])\\s*([*/%])\\s*(?![=])"), " $1 ")
|
||||
// Addition as binary: spaces around '+'. We try not to break '++' and '+=' here.
|
||||
s = s.replace(Regex("(?<![+])\\s*\\+\\s*(?![+=])"), " + ")
|
||||
// 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)
|
||||
s = s.replace(Regex("(?<!:)\\s*:(?!:)\\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");
|
||||
* 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`).
|
||||
* We do a lightweight regex scan like: `fun ClassName.methodName(` and collect distinct names.
|
||||
* List names of extension members defined for [className] in the stdlib text (`root.lyng`).
|
||||
* 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 out = LinkedHashSet<String>()
|
||||
// Match lines like: fun String.trim(...)
|
||||
val re = Regex("(?m)^\\s*fun\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\(")
|
||||
// Match lines like: fun String.trim(...) or val Int.isEven = ...
|
||||
val re = Regex("^\\s*(?:fun|val|var)\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\b", RegexOption.MULTILINE)
|
||||
re.findAll(src).forEach { m ->
|
||||
val name = m.groupValues.getOrNull(1)?.trim()
|
||||
if (!name.isNullOrEmpty()) out.add(name)
|
||||
}
|
||||
return out.toList()
|
||||
}
|
||||
|
||||
@Deprecated("Use extensionMemberNamesFor", ReplaceWith("extensionMemberNamesFor(className)"))
|
||||
fun extensionMethodNamesFor(className: String): List<String> = extensionMemberNamesFor(className)
|
||||
}
|
||||
|
||||
// ---------------- Builders ----------------
|
||||
@@ -367,8 +371,8 @@ private object StdlibInlineDocIndex {
|
||||
else -> {
|
||||
// Non-comment, non-blank: try to match a declaration just after comments
|
||||
if (buf.isNotEmpty()) {
|
||||
// fun Class.name( ... )
|
||||
val mExt = Regex("^fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(line)
|
||||
// fun/val/var Class.name( ... )
|
||||
val mExt = Regex("^(?:fun|val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
|
||||
if (mExt != null) {
|
||||
val (cls, name) = mExt.destructured
|
||||
flushTo(Key.Method(cls, 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.")
|
||||
}
|
||||
|
||||
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.") {
|
||||
// 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"))
|
||||
|
||||
@@ -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.
|
||||
* Mirrors the IntelliJ MVP logic: MiniAst + BuiltinDocRegistry + lenient imports.
|
||||
@@ -16,7 +33,7 @@ data class CompletionItem(
|
||||
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.
|
||||
@@ -45,43 +62,33 @@ object CompletionEngineLight {
|
||||
StdlibDocsBootstrap.ensure()
|
||||
val prefix = prefixAt(text, caret)
|
||||
val mini = buildMiniAst(text)
|
||||
// Build imported modules as a UNION of MiniAst-derived and textual extraction, always including stdlib
|
||||
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 imported: List<String> = DocLookupUtils.canonicalImportedModules(mini ?: return emptyList(), text)
|
||||
|
||||
val cap = 200
|
||||
val out = ArrayList<CompletionItem>(64)
|
||||
|
||||
// Member context detection: dot immediately before caret or before current word start
|
||||
val word = wordRangeAt(text, caret)
|
||||
val memberDot = findDotLeft(text, word?.first ?: caret)
|
||||
val word = DocLookupUtils.wordRangeAt(text, caret)
|
||||
val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret)
|
||||
if (memberDot != null) {
|
||||
// 0) Try chained member call return type inference
|
||||
guessReturnClassFromMemberCallBefore(text, memberDot, imported)?.let { cls ->
|
||||
offerMembersAdd(out, prefix, imported, cls)
|
||||
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
|
||||
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||
return out
|
||||
}
|
||||
// 0a) Top-level call before dot
|
||||
guessReturnClassFromTopLevelCallBefore(text, memberDot, imported)?.let { cls ->
|
||||
offerMembersAdd(out, prefix, imported, cls)
|
||||
DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDot, imported, mini)?.let { cls ->
|
||||
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||
return out
|
||||
}
|
||||
// 0b) Across-known-callees (Iterable/Iterator/List preference)
|
||||
guessReturnClassAcrossKnownCallees(text, memberDot, imported)?.let { cls ->
|
||||
offerMembersAdd(out, prefix, imported, cls)
|
||||
DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDot, imported, mini)?.let { cls ->
|
||||
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||
return out
|
||||
}
|
||||
// 1) Receiver inference fallback
|
||||
guessReceiverClass(text, memberDot, imported)?.let { cls ->
|
||||
offerMembersAdd(out, prefix, imported, cls)
|
||||
(DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
|
||||
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||
return out
|
||||
}
|
||||
// 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 funs = decls.filterIsInstance<MiniFunDecl>().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() }
|
||||
funs.forEach { offerDeclAdd(out, prefix, it) }
|
||||
classes.forEach { offerDeclAdd(out, prefix, it) }
|
||||
enums.forEach { offerDeclAdd(out, prefix, it) }
|
||||
vals.forEach { offerDeclAdd(out, prefix, it) }
|
||||
}
|
||||
|
||||
@@ -109,9 +118,11 @@ object CompletionEngineLight {
|
||||
val decls = BuiltinDocRegistry.docsForModule(mod)
|
||||
val funs = decls.filterIsInstance<MiniFunDecl>().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() }
|
||||
funs.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++ } }
|
||||
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)))
|
||||
}
|
||||
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)))
|
||||
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) {
|
||||
val classes = DocLookupUtils.aggregateClasses(imported)
|
||||
private fun offerMembersAdd(out: MutableList<CompletionItem>, prefix: String, imported: List<String>, className: String, mini: MiniScript? = null) {
|
||||
val classes = DocLookupUtils.aggregateClasses(imported, mini)
|
||||
val visited = mutableSetOf<String>()
|
||||
val directMap = 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))
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,10 +203,10 @@ object CompletionEngineLight {
|
||||
emitGroup(directMap)
|
||||
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 {
|
||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className)
|
||||
val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
|
||||
for (name in ext) {
|
||||
if (already.contains(name)) continue
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
|
||||
@@ -210,6 +223,7 @@ object CompletionEngineLight {
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
already.add(name)
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
} else {
|
||||
// Fallback: emit simple method name without detailed types
|
||||
@@ -223,167 +237,16 @@ object CompletionEngineLight {
|
||||
|
||||
// --- Inference helpers (text-only, PSI-free) ---
|
||||
|
||||
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>): String? {
|
||||
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 {
|
||||
private suspend fun buildMiniAst(text: String): MiniScript? {
|
||||
val sink = MiniAstBuilder()
|
||||
return try {
|
||||
val src = Source("<engine>", text)
|
||||
val provider = LenientImportProvider.create()
|
||||
Compiler.compileWithMini(src, provider, sink)
|
||||
sink.build()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
sink.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun typeOf(t: MiniTypeRef?): String = when (t) {
|
||||
@@ -404,42 +267,8 @@ object CompletionEngineLight {
|
||||
private fun prefixAt(text: String, offset: Int): String {
|
||||
val off = offset.coerceIn(0, text.length)
|
||||
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
|
||||
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.
|
||||
*/
|
||||
package net.sergeych.lyng.miniast
|
||||
|
||||
import net.sergeych.lyng.binding.BindingSnapshot
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
|
||||
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.
|
||||
* Heuristics:
|
||||
* - 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.".
|
||||
* - 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 result = LinkedHashSet<String>()
|
||||
for (name in raw) {
|
||||
val canon = if (name.startsWith("lyng.")) name else "lyng.$name"
|
||||
result.add(canon)
|
||||
}
|
||||
|
||||
if (result.isEmpty() && sourceText != null) {
|
||||
result.addAll(extractImportsFromText(sourceText))
|
||||
}
|
||||
|
||||
// Always make stdlib available as a fallback context for common types,
|
||||
// even when there are no explicit imports in the file
|
||||
result.add("lyng.stdlib")
|
||||
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.
|
||||
val buckets = LinkedHashMap<String, MutableList<MiniClassDecl>>()
|
||||
for (mod in importedModules) {
|
||||
@@ -32,6 +178,23 @@ object DocLookupUtils {
|
||||
for (cls in docs.filterIsInstance<MiniClassDecl>()) {
|
||||
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 {
|
||||
@@ -72,8 +235,8 @@ object DocLookupUtils {
|
||||
return result
|
||||
}
|
||||
|
||||
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String): Pair<String, MiniMemberDecl>? {
|
||||
val classes = aggregateClasses(importedModules)
|
||||
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
|
||||
val classes = aggregateClasses(importedModules, localMini)
|
||||
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? {
|
||||
val cls = classes[name] ?: return null
|
||||
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
||||
@@ -86,12 +249,12 @@ object DocLookupUtils {
|
||||
return dfs(className, mutableSetOf())
|
||||
}
|
||||
|
||||
fun findMemberAcrossClasses(importedModules: List<String>, member: String): Pair<String, MiniMemberDecl>? {
|
||||
val classes = aggregateClasses(importedModules)
|
||||
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
|
||||
val classes = aggregateClasses(importedModules, localMini)
|
||||
// Preferred order for ambiguous common ops
|
||||
val preference = listOf("Iterable", "Iterator", "List")
|
||||
for (name in preference) {
|
||||
resolveMemberWithInheritance(importedModules, name, member)?.let { return it }
|
||||
resolveMemberWithInheritance(importedModules, name, member, localMini)?.let { return it }
|
||||
}
|
||||
for ((name, cls) in classes) {
|
||||
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 `(`.
|
||||
* 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)
|
||||
// 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--
|
||||
if (i < 0 || text[i] != ')') return null
|
||||
// Walk back to matching '(' accounting nested parentheses
|
||||
@@ -134,9 +293,396 @@ object DocLookupUtils {
|
||||
if (start >= end) return null
|
||||
val name = text.substring(start, end)
|
||||
// Validate against imported classes
|
||||
val classes = aggregateClasses(importedModules)
|
||||
val classes = aggregateClasses(importedModules, localMini)
|
||||
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
|
||||
val funRe = Regex("^\\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
|
||||
val valRe = Regex("^\\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
|
||||
) {
|
||||
// Register runtime method
|
||||
addFn(name, isOpen, visibility, code)
|
||||
addFn(name, isOpen, visibility, code = code)
|
||||
// Register docs for the member under this class
|
||||
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -109,7 +109,8 @@ data class MiniFunDecl(
|
||||
val returnType: MiniTypeRef?,
|
||||
val body: MiniBlock?,
|
||||
override val doc: MiniDoc?,
|
||||
override val nameStart: Pos
|
||||
override val nameStart: Pos,
|
||||
val receiver: MiniTypeRef? = null
|
||||
) : MiniDecl
|
||||
|
||||
data class MiniValDecl(
|
||||
@@ -119,7 +120,8 @@ data class MiniValDecl(
|
||||
val type: MiniTypeRef?,
|
||||
val initRange: MiniRange?,
|
||||
override val doc: MiniDoc?,
|
||||
override val nameStart: Pos
|
||||
override val nameStart: Pos,
|
||||
val receiver: MiniTypeRef? = null
|
||||
) : MiniDecl
|
||||
|
||||
data class MiniClassDecl(
|
||||
@@ -135,6 +137,14 @@ data class MiniClassDecl(
|
||||
val members: List<MiniMemberDecl> = emptyList()
|
||||
) : 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(
|
||||
val name: String,
|
||||
val mutable: Boolean,
|
||||
@@ -183,6 +193,15 @@ data class MiniMemberValDecl(
|
||||
override val isStatic: Boolean = false,
|
||||
) : 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.
|
||||
interface MiniAstSink {
|
||||
fun onScriptStart(start: Pos) {}
|
||||
@@ -193,7 +212,9 @@ interface MiniAstSink {
|
||||
fun onImport(node: MiniImport) {}
|
||||
fun onFunDecl(node: MiniFunDecl) {}
|
||||
fun onValDecl(node: MiniValDecl) {}
|
||||
fun onInitDecl(node: MiniInitDecl) {}
|
||||
fun onClassDecl(node: MiniClassDecl) {}
|
||||
fun onEnumDecl(node: MiniEnumDecl) {}
|
||||
|
||||
fun onBlock(node: MiniBlock) {}
|
||||
fun onIdentifier(node: MiniIdentifier) {}
|
||||
@@ -263,6 +284,12 @@ class MiniAstBuilder : MiniAstSink {
|
||||
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) {
|
||||
blocks.addLast(node)
|
||||
}
|
||||
|
||||
@@ -83,15 +83,17 @@ open class Obj {
|
||||
scope: Scope,
|
||||
name: String,
|
||||
args: Arguments = Arguments.EMPTY,
|
||||
onNotFoundResult: (() -> Obj?)? = null
|
||||
onNotFoundResult: (suspend () -> Obj?)? = null
|
||||
): Obj {
|
||||
val rec = objClass.getInstanceMemberOrNull(name)
|
||||
// 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) {
|
||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
val decl = rec.declaringClass ?: cls
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||
// Propagate declaring class as current class context during method execution
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||
val saved = scope.currentClassCtx
|
||||
scope.currentClassCtx = decl
|
||||
try {
|
||||
@@ -100,6 +102,33 @@ open class Obj {
|
||||
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(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||
val saved = scope.currentClassCtx
|
||||
scope.currentClassCtx = decl
|
||||
try {
|
||||
return rec.value.invoke(scope, this, args)
|
||||
} finally {
|
||||
scope.currentClassCtx = saved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return onNotFoundResult?.invoke()
|
||||
?: scope.raiseError(
|
||||
"no such member: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}. " +
|
||||
@@ -127,6 +156,40 @@ open class Obj {
|
||||
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:
|
||||
*
|
||||
@@ -143,13 +206,17 @@ open class Obj {
|
||||
* but it is used to avoid endless recursion in [Obj.toString] base implementation
|
||||
*/
|
||||
open suspend fun toString(scope: Scope, calledFromLyng: Boolean = false): ObjString {
|
||||
return if (this is ObjString) this
|
||||
else if( !calledFromLyng ) {
|
||||
invokeInstanceMethod(scope, "toString") {
|
||||
ObjString(this.toString())
|
||||
if (this is ObjString) return this
|
||||
return if (!calledFromLyng) {
|
||||
invokeInstanceMethod(scope, "toString", Arguments.EMPTY) {
|
||||
defaultToString(scope)
|
||||
} 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.
|
||||
@@ -228,7 +295,7 @@ open class Obj {
|
||||
|
||||
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
|
||||
@@ -275,34 +342,83 @@ open class Obj {
|
||||
// suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
|
||||
|
||||
open suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||
// could be property or class field:
|
||||
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError(
|
||||
// 1. Hierarchy members (excluding root fallback)
|
||||
for (cls in objClass.mro) {
|
||||
if (cls.className == "Obj") break
|
||||
cls.members[name]?.let { return resolveRecord(scope, it, name, it.declaringClass) }
|
||||
cls.classScope?.objects?.get(name)?.let { return resolveRecord(scope, it, name, it.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)}"
|
||||
)
|
||||
val decl = obj.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
}
|
||||
|
||||
protected suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
|
||||
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(ObjAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||
return when (val value = obj.value) {
|
||||
is Statement -> {
|
||||
ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
|
||||
}
|
||||
// could be writable property naturally
|
||||
// null -> ObjNull.asReadonly
|
||||
else -> obj
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||
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
|
||||
field = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||
if (field != null) 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)}"
|
||||
)
|
||||
val decl = field.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
|
||||
val decl = field.declaringClass
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(field.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||
if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
||||
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 {
|
||||
@@ -440,8 +556,8 @@ open class Obj {
|
||||
is Obj -> obj
|
||||
is Double -> ObjReal(obj)
|
||||
is Float -> ObjReal(obj.toDouble())
|
||||
is Int -> ObjInt(obj.toLong())
|
||||
is Long -> ObjInt(obj)
|
||||
is Int -> ObjInt.of(obj.toLong())
|
||||
is Long -> ObjInt.of(obj)
|
||||
is String -> ObjString(obj)
|
||||
is CharSequence -> ObjString(obj.toString())
|
||||
is Boolean -> ObjBool(obj)
|
||||
|
||||
@@ -53,6 +53,12 @@ class ObjChar(val value: Char): Obj() {
|
||||
returns = type("lyng.Int"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) { 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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -175,7 +175,11 @@ open class ObjClass(
|
||||
}
|
||||
|
||||
/** 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 base + rootObjectType
|
||||
}
|
||||
|
||||
/** Parents in C3 order (no self). */
|
||||
val mroParents: List<ObjClass> by lazy { mro.drop(1) }
|
||||
@@ -200,12 +204,23 @@ open class ObjClass(
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
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)
|
||||
// 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)
|
||||
// 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.currentClassCtx = null
|
||||
// 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
|
||||
// 1) members-defined methods
|
||||
@@ -223,18 +238,42 @@ open class ObjClass(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Constructor chaining MVP: initialize base classes left-to-right, then this class.
|
||||
// Ensure each ancestor is initialized at most once (diamond-safe).
|
||||
val visited = hashSetOf<ObjClass>()
|
||||
return instance
|
||||
}
|
||||
|
||||
suspend fun initClass(c: ObjClass, argsForThis: Arguments?, isRoot: Boolean = false) {
|
||||
/**
|
||||
* Run initializers and optionally constructors for the given [instance].
|
||||
* Handles Multiple Inheritance correctly (diamond-safe).
|
||||
*/
|
||||
internal suspend fun initializeInstance(
|
||||
instance: ObjInstance,
|
||||
args: Arguments?,
|
||||
runConstructors: Boolean
|
||||
) {
|
||||
val visited = hashSetOf<ObjClass>()
|
||||
initClassInternal(instance, visited, this, args, isRoot = true, runConstructors = runConstructors)
|
||||
}
|
||||
|
||||
private suspend fun initClassInternal(
|
||||
instance: ObjInstance,
|
||||
visited: MutableSet<ObjClass>,
|
||||
c: ObjClass,
|
||||
argsForThis: Arguments?,
|
||||
@Suppress("UNUSED_PARAMETER") isRoot: Boolean = false,
|
||||
runConstructors: Boolean = true
|
||||
) {
|
||||
if (!visited.add(c)) return
|
||||
// For the most-derived class, bind its constructor params BEFORE parents so base arg thunks can see them
|
||||
if (isRoot) {
|
||||
|
||||
// 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)
|
||||
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) {
|
||||
@@ -242,12 +281,12 @@ open class ObjClass(
|
||||
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 (e.g., second pass before ctor)
|
||||
// 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)
|
||||
@@ -255,8 +294,25 @@ open class ObjClass(
|
||||
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)
|
||||
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
|
||||
@@ -270,28 +326,13 @@ open class ObjClass(
|
||||
}
|
||||
}
|
||||
// Then run this class' constructor, if any
|
||||
if (runConstructors) {
|
||||
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)
|
||||
val execScope =
|
||||
instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
|
||||
ctor.execute(execScope)
|
||||
}
|
||||
}
|
||||
|
||||
initClass(this, instance.instanceScope.args, isRoot = true)
|
||||
return instance
|
||||
}
|
||||
|
||||
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
|
||||
@@ -304,14 +345,15 @@ open class ObjClass(
|
||||
initialValue: Obj,
|
||||
isMutable: Boolean = false,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
pos: Pos = Pos.builtIn
|
||||
pos: Pos = Pos.builtIn,
|
||||
declaringClass: ObjClass? = this
|
||||
) {
|
||||
// Allow overriding ancestors: only prevent redefinition if THIS class already defines an immutable member
|
||||
val existingInSelf = members[name]
|
||||
if (existingInSelf != null && existingInSelf.isMutable == false)
|
||||
throw ScriptError(pos, "$name is already defined in $objClass")
|
||||
// Install/override in this class
|
||||
members[name] = ObjRecord(initialValue, isMutable, visibility, declaringClass = this)
|
||||
members[name] = ObjRecord(initialValue, isMutable, visibility, declaringClass = declaringClass)
|
||||
// Structural change: bump layout version for PIC invalidation
|
||||
layoutVersion += 1
|
||||
}
|
||||
@@ -341,13 +383,29 @@ open class ObjClass(
|
||||
name: String,
|
||||
isOpen: Boolean = false,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
declaringClass: ObjClass? = this,
|
||||
code: suspend Scope.() -> Obj
|
||||
) {
|
||||
val stmt = statement { code() }
|
||||
createField(name, stmt, isOpen, visibility)
|
||||
createField(name, stmt, isOpen, visibility, Pos.builtIn, declaringClass)
|
||||
}
|
||||
|
||||
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,
|
||||
declaringClass: ObjClass? = this
|
||||
) {
|
||||
val g = getter?.let { statement { it() } }
|
||||
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
|
||||
val prop = ObjProperty(name, g, s)
|
||||
members[name] = ObjRecord(prop, false, visibility, declaringClass, type = ObjRecord.Type.Property)
|
||||
layoutVersion += 1
|
||||
}
|
||||
|
||||
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
|
||||
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
|
||||
createClassField(name, statement { code() }, isOpen)
|
||||
@@ -415,7 +473,7 @@ open class ObjClass(
|
||||
|
||||
override suspend fun invokeInstanceMethod(
|
||||
scope: Scope, name: String, args: Arguments,
|
||||
onNotFoundResult: (() -> Obj?)?
|
||||
onNotFoundResult: (suspend () -> Obj?)?
|
||||
): Obj {
|
||||
return classScope?.objects?.get(name)?.value?.invoke(scope, this, args)
|
||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
|
||||
@@ -81,7 +81,7 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
|
||||
scope: Scope,
|
||||
name: String,
|
||||
args: Arguments,
|
||||
onNotFoundResult: (() -> Obj?)?
|
||||
onNotFoundResult: (suspend () -> Obj?)?
|
||||
): Obj {
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
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
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -18,7 +35,6 @@
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
|
||||
@@ -71,7 +71,7 @@ open class ObjException(
|
||||
)
|
||||
} else {
|
||||
// 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
|
||||
@@ -89,7 +89,7 @@ open class ObjException(
|
||||
|
||||
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)
|
||||
?: ObjString("(unknown)")
|
||||
return ObjString("${objClass.className}: $message at $at")
|
||||
|
||||
@@ -32,24 +32,25 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
|
||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||
// Direct (unmangled) lookup first
|
||||
instanceScope[name]?.let {
|
||||
val decl = it.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
// 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.
|
||||
instanceScope[name]?.let { rec ->
|
||||
val decl = rec.declaringClass
|
||||
// Allow unconditional access when accessing through `this` of the same instance
|
||||
if (scope.thisObj === this) return it
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 // do not default to objClass for outsiders
|
||||
if (!canAccessMember(it.visibility, decl, caller))
|
||||
// BUT only if we are in the class context (not extension)
|
||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
scope,
|
||||
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
return it
|
||||
}
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
val prop = rec.value as ObjProperty
|
||||
return rec.copy(value = prop.callGetter(scope, this, decl))
|
||||
}
|
||||
return rec
|
||||
}
|
||||
// Try MI-mangled lookup along linearization (C3 MRO): ClassName::name
|
||||
val cls = objClass
|
||||
@@ -70,9 +71,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||
}
|
||||
if (scope.thisObj === this) return rec
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 // do not default to objClass for outsiders
|
||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
@@ -80,6 +80,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
"can't access field $name (declared in ${declaring?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
}
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
val prop = rec.value as ObjProperty
|
||||
return rec.copy(value = prop.callGetter(scope, this, declaring))
|
||||
}
|
||||
return rec
|
||||
}
|
||||
// Fall back to methods/properties on class
|
||||
@@ -89,18 +94,20 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||
// Direct (unmangled) first
|
||||
instanceScope[name]?.let { f ->
|
||||
val decl = f.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
if (scope.thisObj === this) {
|
||||
// direct self-assignment allowed; enforce mutability below
|
||||
} else {
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 // do not default to objClass for outsiders
|
||||
val decl = f.declaringClass
|
||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(f.visibility, decl, caller))
|
||||
ObjIllegalAssignmentException(
|
||||
scope,
|
||||
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
||||
).raise()
|
||||
}
|
||||
if (f.type == ObjRecord.Type.Property) {
|
||||
val prop = f.value as ObjProperty
|
||||
prop.callSetter(scope, this, newValue, decl)
|
||||
return
|
||||
}
|
||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
if (f.value.assign(scope, newValue) == null)
|
||||
f.value = newValue
|
||||
@@ -122,15 +129,19 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||
}
|
||||
if (scope.thisObj !== this) {
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 // do not default to objClass for outsiders
|
||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
||||
ObjIllegalAssignmentException(
|
||||
scope,
|
||||
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
|
||||
).raise()
|
||||
}
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
val prop = rec.value as ObjProperty
|
||||
prop.callSetter(scope, this, newValue, declaring)
|
||||
return
|
||||
}
|
||||
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
if (rec.value.assign(scope, newValue) == null)
|
||||
rec.value = newValue
|
||||
@@ -141,12 +152,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
|
||||
override suspend fun invokeInstanceMethod(
|
||||
scope: Scope, name: String, args: Arguments,
|
||||
onNotFoundResult: (() -> Obj?)?
|
||||
onNotFoundResult: (suspend () -> Obj?)?
|
||||
): Obj =
|
||||
instanceScope[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
|
||||
val decl = rec.declaringClass
|
||||
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
@@ -154,25 +164,17 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
// execute with lexical class context propagated to declaring class
|
||||
val saved = instanceScope.currentClassCtx
|
||||
instanceScope.currentClassCtx = decl
|
||||
try {
|
||||
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
|
||||
val decl = rec.declaringClass
|
||||
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
@@ -180,13 +182,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
val saved = instanceScope.currentClassCtx
|
||||
instanceScope.currentClassCtx = decl
|
||||
try {
|
||||
rec.value.invoke(instanceScope, this, args)
|
||||
} finally {
|
||||
instanceScope.currentClassCtx = saved
|
||||
}
|
||||
}
|
||||
}
|
||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
@@ -197,9 +193,20 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
!it.key.contains("::") && it.value.visibility.isPublic && it.value.type.serializable
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
|
||||
return "${objClass.className}($fields)"
|
||||
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||
return ObjString(buildString {
|
||||
append("${objClass.className}(")
|
||||
var first = true
|
||||
for ((name, value) in publicFields) {
|
||||
if (first) first = false else append(",")
|
||||
append("$name=${value.value.toString(scope)}")
|
||||
}
|
||||
append(")")
|
||||
})
|
||||
}
|
||||
|
||||
override suspend fun inspect(scope: Scope): String {
|
||||
return toString(scope).value
|
||||
}
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
@@ -210,10 +217,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
|
||||
// using objlist allow for some optimizations:
|
||||
val params = meta.params.map { readField(scope, it.name).value }
|
||||
println("serializing $objClass with params: $params")
|
||||
encoder.encodeAnyList(scope, params)
|
||||
val vars = serializingVars.values.map { it.value }
|
||||
println("encoding vars: $vars")
|
||||
if (vars.isNotEmpty<Obj>()) {
|
||||
encoder.encodeAnyList(scope, vars)
|
||||
}
|
||||
@@ -248,7 +253,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
instanceScope.objects.filter {
|
||||
it.value.type.serializable &&
|
||||
it.value.type == ObjRecord.Type.Field &&
|
||||
it.value.isMutable }
|
||||
it.value.isMutable
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) {
|
||||
@@ -381,7 +387,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
||||
scope: Scope,
|
||||
name: String,
|
||||
args: Arguments,
|
||||
onNotFoundResult: (() -> Obj?)?
|
||||
onNotFoundResult: (suspend () -> Obj?)?
|
||||
): Obj {
|
||||
// Qualified method dispatch must start from the specified ancestor, not from the instance scope.
|
||||
memberFromAncestor(name)?.let { rec ->
|
||||
|
||||
@@ -33,7 +33,9 @@ class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(na
|
||||
if (args.size > actualSize)
|
||||
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
|
||||
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)
|
||||
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
|
||||
}
|
||||
@@ -41,7 +43,7 @@ class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(na
|
||||
|
||||
init {
|
||||
addFn("toString", true) {
|
||||
ObjString(thisObj.toString())
|
||||
thisObj.toString(this, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
|
||||
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.JsonPrimitive
|
||||
import net.sergeych.lyng.Scope
|
||||
@@ -28,6 +24,10 @@ import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonSettings
|
||||
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() {
|
||||
override val objClass: ObjClass get() = type
|
||||
|
||||
@@ -24,36 +24,32 @@ import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
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 doubleValue get() = value.toDouble()
|
||||
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 {
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
override suspend fun getAndIncrement(scope: Scope): Obj {
|
||||
ensureNotConst(scope)
|
||||
return ObjInt(value).also { value++ }
|
||||
return this
|
||||
}
|
||||
|
||||
override suspend fun getAndDecrement(scope: Scope): Obj {
|
||||
ensureNotConst(scope)
|
||||
return ObjInt(value).also { value-- }
|
||||
return this
|
||||
}
|
||||
|
||||
override suspend fun incrementAndGet(scope: Scope): Obj {
|
||||
ensureNotConst(scope)
|
||||
return ObjInt(++value)
|
||||
return of(value + 1)
|
||||
}
|
||||
|
||||
override suspend fun decrementAndGet(scope: Scope): Obj {
|
||||
ensureNotConst(scope)
|
||||
return ObjInt(--value)
|
||||
return of(value - 1)
|
||||
}
|
||||
|
||||
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 =
|
||||
if (other is ObjInt)
|
||||
ObjInt(this.value + other.value)
|
||||
of(this.value + other.value)
|
||||
else
|
||||
ObjReal(this.doubleValue + other.toDouble())
|
||||
ObjReal.of(this.doubleValue + other.toDouble())
|
||||
|
||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt)
|
||||
ObjInt(this.value - other.value)
|
||||
of(this.value - other.value)
|
||||
else
|
||||
ObjReal(this.doubleValue - other.toDouble())
|
||||
ObjReal.of(this.doubleValue - other.toDouble())
|
||||
|
||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt) {
|
||||
ObjInt(this.value * other.value)
|
||||
} else ObjReal(this.value * other.toDouble())
|
||||
of(this.value * other.value)
|
||||
} else ObjReal.of(this.value * other.toDouble())
|
||||
|
||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt)
|
||||
ObjInt(this.value / other.value)
|
||||
else ObjReal(this.value / other.toDouble())
|
||||
of(this.value / other.value)
|
||||
else ObjReal.of(this.value / other.toDouble())
|
||||
|
||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt)
|
||||
ObjInt(this.value % other.value)
|
||||
else ObjReal(this.value.toDouble() % other.toDouble())
|
||||
of(this.value % other.value)
|
||||
else ObjReal.of(this.value.toDouble() % other.toDouble())
|
||||
|
||||
/**
|
||||
* We are by-value type ([byValueCopy] is implemented) so we can do in-place
|
||||
* assignment
|
||||
* Numbers are now immutable, so we can't do in-place assignment.
|
||||
*/
|
||||
override suspend fun assign(scope: Scope, other: Obj): Obj? {
|
||||
return if (!isConst && other is ObjInt) {
|
||||
value = other.value
|
||||
this
|
||||
} else null
|
||||
}
|
||||
override suspend fun assign(scope: Scope, other: Obj): Obj? = null
|
||||
|
||||
override suspend fun toKotlin(scope: Scope): Any {
|
||||
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 {
|
||||
return ObjInt(-value)
|
||||
return of(-value)
|
||||
}
|
||||
|
||||
// Bitwise operations
|
||||
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}")
|
||||
|
||||
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}")
|
||||
|
||||
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}")
|
||||
|
||||
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}")
|
||||
|
||||
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}")
|
||||
|
||||
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) {
|
||||
0L -> LynonType.Int0
|
||||
@@ -168,20 +158,32 @@ class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Zero = ObjInt(0, true)
|
||||
val One = ObjInt(1, true)
|
||||
private val cache = Array(256) { ObjInt((it - 128).toLong(), 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") {
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||
when (lynonType) {
|
||||
null -> ObjInt(decoder.unpackSigned())
|
||||
null -> of(decoder.unpackSigned())
|
||||
LynonType.Int0 -> Zero
|
||||
LynonType.IntPositive -> ObjInt(decoder.unpackUnsigned().toLong())
|
||||
LynonType.IntNegative -> ObjInt(-decoder.unpackUnsigned().toLong())
|
||||
LynonType.IntSigned -> ObjInt(decoder.unpackSigned())
|
||||
LynonType.IntPositive -> of(decoder.unpackUnsigned().toLong())
|
||||
LynonType.IntNegative -> of(-decoder.unpackUnsigned().toLong())
|
||||
LynonType.IntSigned -> of(decoder.unpackSigned())
|
||||
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"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
val result = ObjMap()
|
||||
val result = mutableMapOf<Obj, Obj>()
|
||||
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(
|
||||
@@ -158,6 +161,23 @@ val ObjIterable by lazy {
|
||||
ObjList(result)
|
||||
}
|
||||
|
||||
addFnDoc(
|
||||
name = "mapNotNull",
|
||||
doc = "Transform elements by applying the given lambda unless it returns null.",
|
||||
params = listOf(ParamDoc("transform")),
|
||||
returns = type("lyng.List"),
|
||||
isOpen = true,
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
val fn = requiredArg<Statement>(0)
|
||||
val result = mutableListOf<Obj>()
|
||||
thisObj.toFlow(this).collect {
|
||||
val transformed = fn.call(this, it)
|
||||
if( transformed != ObjNull) result += transformed
|
||||
}
|
||||
ObjList(result)
|
||||
}
|
||||
|
||||
addFnDoc(
|
||||
name = "take",
|
||||
doc = "Take the first N elements and return them as a list.",
|
||||
|
||||
@@ -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 ->
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
|
||||
for (item in list) {
|
||||
if (!callback(item)) break
|
||||
}
|
||||
}
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type
|
||||
|
||||
@@ -198,6 +204,18 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
return JsonArray(list.map { it.toJson(scope) })
|
||||
}
|
||||
|
||||
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||
return ObjString(buildString {
|
||||
append("[")
|
||||
var first = true
|
||||
for (v in list) {
|
||||
if (first) first = false else append(",")
|
||||
append(v.toString(scope).value)
|
||||
}
|
||||
append("]")
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = object : ObjClass("List", ObjArray) {
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||
|
||||
@@ -52,8 +52,8 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
|
||||
else -> scope.raiseIndexOutOfBounds()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$key=>$value"
|
||||
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||
return ObjString("(${key.toString(scope).value} => ${value.toString(scope).value})")
|
||||
}
|
||||
|
||||
override val objClass = type
|
||||
@@ -124,7 +124,7 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
||||
return -1
|
||||
}
|
||||
|
||||
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
||||
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||
val reusult = buildString {
|
||||
append("Map(")
|
||||
var first = true
|
||||
@@ -174,18 +174,21 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
||||
val map = mutableMapOf<Obj, Obj>()
|
||||
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 {
|
||||
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
|
||||
}
|
||||
@@ -296,13 +299,11 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
||||
is ObjMap -> {
|
||||
// Rightmost wins: copy all entries from `other` over existing ones
|
||||
for ((k, v) in other.map) {
|
||||
val key = k as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got $k")
|
||||
map[key] = v
|
||||
map[k] = v
|
||||
}
|
||||
}
|
||||
is ObjMapEntry -> {
|
||||
val key = other.key as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got ${other.key}")
|
||||
map[key] = other.value
|
||||
map[other.key] = other.value
|
||||
}
|
||||
is ObjList -> {
|
||||
// Treat as list of map entries
|
||||
@@ -311,8 +312,7 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
||||
is ObjMapEntry -> 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[key] = entry.value
|
||||
map[entry.key] = entry.value
|
||||
}
|
||||
}
|
||||
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 suspend fun toString(scope: Scope,calledFromLyng: Boolean): ObjString {
|
||||
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||
val result = StringBuilder()
|
||||
result.append("${start?.inspect(scope) ?: '∞'} ..")
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return (other as? ObjRange)?.let {
|
||||
if( start == other.start && end == other.end ) 0 else -1
|
||||
|
||||
@@ -32,14 +32,14 @@ import kotlin.math.floor
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
override val longValue: Long by lazy { floor(value).toLong() }
|
||||
override val doubleValue: Double by lazy { value }
|
||||
override val toObjInt: ObjInt by lazy { ObjInt(longValue) }
|
||||
override val toObjReal: ObjReal by lazy { ObjReal(value) }
|
||||
override val longValue: Long get() = floor(value).toLong()
|
||||
override val doubleValue: Double get() = value
|
||||
override val toObjInt: ObjInt get() = ObjInt.of(longValue)
|
||||
override val toObjReal: ObjReal get() = this
|
||||
|
||||
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 {
|
||||
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 =
|
||||
ObjReal(this.value + other.toDouble())
|
||||
of(this.value + other.toDouble())
|
||||
|
||||
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 =
|
||||
ObjReal(this.value * other.toDouble())
|
||||
of(this.value * other.toDouble())
|
||||
|
||||
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 =
|
||||
ObjReal(this.value % other.toDouble())
|
||||
of(this.value % other.toDouble())
|
||||
|
||||
/**
|
||||
* Returns unboxed Double value
|
||||
@@ -96,7 +96,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
}
|
||||
|
||||
override suspend fun negate(scope: Scope): Obj {
|
||||
return ObjReal(-value)
|
||||
return of(-value)
|
||||
}
|
||||
|
||||
override suspend fun lynonType(): LynonType = LynonType.Real
|
||||
@@ -110,9 +110,18 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
}
|
||||
|
||||
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") {
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||
ObjReal(decoder.unpackDouble())
|
||||
of(decoder.unpackDouble())
|
||||
}.apply {
|
||||
// roundToInt: number rounded to the nearest integer
|
||||
addConstDoc(
|
||||
@@ -130,7 +139,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
returns = type("lyng.Int"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
ObjInt(thisAs<ObjReal>().value.toLong())
|
||||
ObjInt.of(thisAs<ObjReal>().value.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ data class ObjRecord(
|
||||
@Suppress("unused")
|
||||
Class,
|
||||
Enum,
|
||||
Property,
|
||||
Other;
|
||||
|
||||
val isArgument get() = this == Argument
|
||||
|
||||
@@ -27,6 +27,7 @@ import net.sergeych.lyng.*
|
||||
*/
|
||||
sealed interface ObjRef {
|
||||
suspend fun get(scope: Scope): 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.
|
||||
@@ -35,6 +36,20 @@ sealed interface ObjRef {
|
||||
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
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. */
|
||||
@@ -117,16 +132,16 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
|
||||
val av = a.value
|
||||
val bv = b.value
|
||||
val r: Obj? = when (op) {
|
||||
BinOp.PLUS -> ObjInt(av + bv)
|
||||
BinOp.MINUS -> ObjInt(av - bv)
|
||||
BinOp.STAR -> ObjInt(av * bv)
|
||||
BinOp.SLASH -> if (bv != 0L) ObjInt(av / bv) else null
|
||||
BinOp.PERCENT -> if (bv != 0L) ObjInt(av % bv) else null
|
||||
BinOp.BAND -> ObjInt(av and bv)
|
||||
BinOp.BXOR -> ObjInt(av xor bv)
|
||||
BinOp.BOR -> ObjInt(av or bv)
|
||||
BinOp.SHL -> ObjInt(av shl (bv.toInt() and 63))
|
||||
BinOp.SHR -> ObjInt(av shr (bv.toInt() and 63))
|
||||
BinOp.PLUS -> ObjInt.of(av + bv)
|
||||
BinOp.MINUS -> ObjInt.of(av - bv)
|
||||
BinOp.STAR -> ObjInt.of(av * bv)
|
||||
BinOp.SLASH -> if (bv != 0L) ObjInt.of(av / bv) else null
|
||||
BinOp.PERCENT -> if (bv != 0L) ObjInt.of(av % bv) else null
|
||||
BinOp.BAND -> ObjInt.of(av and bv)
|
||||
BinOp.BXOR -> ObjInt.of(av xor bv)
|
||||
BinOp.BOR -> ObjInt.of(av or bv)
|
||||
BinOp.SHL -> ObjInt.of(av shl (bv.toInt() and 63))
|
||||
BinOp.SHR -> ObjInt.of(av shr (bv.toInt() and 63))
|
||||
BinOp.EQ -> if (av == bv) ObjTrue else ObjFalse
|
||||
BinOp.NEQ -> if (av != bv) ObjTrue else ObjFalse
|
||||
BinOp.LT -> if (av < bv) ObjTrue else ObjFalse
|
||||
@@ -201,11 +216,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 bd: Double = if (b is ObjInt) b.doubleValue else (b as ObjReal).value
|
||||
val rNum: Obj? = when (op) {
|
||||
BinOp.PLUS -> ObjReal(ad + bd)
|
||||
BinOp.MINUS -> ObjReal(ad - bd)
|
||||
BinOp.STAR -> ObjReal(ad * bd)
|
||||
BinOp.SLASH -> ObjReal(ad / bd)
|
||||
BinOp.PERCENT -> ObjReal(ad % bd)
|
||||
BinOp.PLUS -> ObjReal.of(ad + bd)
|
||||
BinOp.MINUS -> ObjReal.of(ad - bd)
|
||||
BinOp.STAR -> ObjReal.of(ad * bd)
|
||||
BinOp.SLASH -> ObjReal.of(ad / bd)
|
||||
BinOp.PERCENT -> ObjReal.of(ad % bd)
|
||||
BinOp.LT -> if (ad < bd) ObjTrue else ObjFalse
|
||||
BinOp.LTE -> if (ad <= bd) ObjTrue else ObjFalse
|
||||
BinOp.GT -> if (ad > bd) ObjTrue else ObjFalse
|
||||
@@ -360,22 +375,12 @@ class IncDecRef(
|
||||
if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value")
|
||||
val v = rec.value
|
||||
val one = ObjInt.One
|
||||
return if (v.isConst) {
|
||||
// Mirror existing semantics in Compiler for const values
|
||||
// We now treat numbers as immutable and always perform write-back via setAt.
|
||||
// This avoids issues where literals are shared and mutated in-place.
|
||||
// For post-inc: return ORIGINAL value; for pre-inc: return NEW value.
|
||||
val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one)
|
||||
// write back
|
||||
target.setAt(atPos, scope, result)
|
||||
// For post-inc: previous code returned NEW value; for pre-inc: returned ORIGINAL value
|
||||
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
|
||||
}
|
||||
return (if (isPost) v else result).asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,6 +625,9 @@ class FieldRef(
|
||||
val (k, v) = receiverKeyAndVersion(base)
|
||||
val rec = tRecord
|
||||
if (rec != null && tKey == k && tVer == v && tFrameId == scope.frameId) {
|
||||
// If it is a property, we must go through writeField (slow path for now)
|
||||
// or handle it here.
|
||||
if (rec.type != ObjRecord.Type.Property) {
|
||||
// visibility/mutability checks
|
||||
if (!rec.isMutable) scope.raiseError(ObjIllegalAssignmentException(scope, "can't reassign val $name"))
|
||||
if (!rec.visibility.isPublic)
|
||||
@@ -628,6 +636,7 @@ class FieldRef(
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fieldPic) {
|
||||
val (key, ver) = receiverKeyAndVersion(base)
|
||||
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) {
|
||||
@@ -1169,18 +1178,25 @@ class MethodCallRef(
|
||||
when (base) {
|
||||
is ObjInstance -> {
|
||||
// Prefer resolved class member to avoid per-call lookup on hit
|
||||
val member = base.objClass.getInstanceMemberOrNull(name)
|
||||
if (member != null) {
|
||||
val visibility = member.visibility
|
||||
val callable = member.value
|
||||
// BUT only if it's NOT a root object member (which can be shadowed by extensions)
|
||||
var hierarchyMember: ObjRecord? = null
|
||||
for (cls in base.objClass.mro) {
|
||||
if (cls.className == "Obj") break
|
||||
hierarchyMember = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||
if (hierarchyMember != null) break
|
||||
}
|
||||
|
||||
if (hierarchyMember != null) {
|
||||
val visibility = hierarchyMember.visibility
|
||||
val callable = hierarchyMember.value
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
||||
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"))
|
||||
callable.invoke(inst.instanceScope, inst, a)
|
||||
}
|
||||
} 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) }
|
||||
}
|
||||
}
|
||||
@@ -1214,7 +1230,14 @@ class MethodCallRef(
|
||||
/**
|
||||
* 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
|
||||
private var cachedFrameId: Long = 0L
|
||||
private var cachedSlot: Int = -1
|
||||
@@ -1231,7 +1254,6 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
scope.pos = atPos
|
||||
// 1) Try fast slot/local
|
||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||
scope.getSlotIndexOf(name)?.let {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicHit++
|
||||
@@ -1266,10 +1288,15 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||
if (slot >= 0) {
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
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 scope.getSlotRecord(slot)
|
||||
return rec
|
||||
}
|
||||
}
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||
// 2) Fallback name in scope or field on `this`
|
||||
@@ -1321,7 +1348,11 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
}
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
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 rec.value
|
||||
}
|
||||
// Fallback name in scope or field on `this`
|
||||
scope[name]?.let { return it.value }
|
||||
run {
|
||||
@@ -1385,10 +1416,12 @@ 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)
|
||||
if (slot >= 0) {
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
return
|
||||
}
|
||||
}
|
||||
scope[name]?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
@@ -1429,17 +1462,25 @@ class BoundLocalVarRef(
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
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(ObjAccessException(scope, "private field access"))
|
||||
return rec
|
||||
}
|
||||
|
||||
override suspend fun evalValue(scope: Scope): Obj {
|
||||
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(ObjAccessException(scope, "private field access"))
|
||||
return rec.value
|
||||
}
|
||||
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
scope.pos = atPos
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||
scope.raiseError(ObjAccessException(scope, "private field access"))
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
}
|
||||
@@ -1450,9 +1491,12 @@ class BoundLocalVarRef(
|
||||
* It resolves the slot once per frame and never falls back to global/module lookup.
|
||||
*/
|
||||
class FastLocalVarRef(
|
||||
private val name: String,
|
||||
val name: String,
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
override fun forEachVariable(block: (String) -> Unit) {
|
||||
block(name)
|
||||
}
|
||||
// Cache the exact scope frame that owns the slot, not just the current frame
|
||||
private var cachedOwnerScope: Scope? = null
|
||||
private var cachedOwnerFrameId: Long = 0L
|
||||
@@ -1500,10 +1544,13 @@ class FastLocalVarRef(
|
||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||
val actualOwner = cachedOwnerScope
|
||||
if (slot >= 0 && actualOwner != null) {
|
||||
val rec = actualOwner.getSlotRecord(slot)
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
|
||||
}
|
||||
return actualOwner.getSlotRecord(slot)
|
||||
return rec
|
||||
}
|
||||
}
|
||||
// Try per-frame local binding maps in the ancestry first (locals declared in frames)
|
||||
run {
|
||||
@@ -1540,7 +1587,11 @@ class FastLocalVarRef(
|
||||
val ownerValid = isOwnerValidFor(scope)
|
||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||
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 rec.value
|
||||
}
|
||||
// Try per-frame local binding maps in the ancestry first
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
@@ -1577,10 +1628,12 @@ class FastLocalVarRef(
|
||||
val actualOwner = cachedOwnerScope
|
||||
if (slot >= 0 && actualOwner != null) {
|
||||
val rec = actualOwner.getSlotRecord(slot)
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
return
|
||||
}
|
||||
}
|
||||
// Try per-frame local binding maps in the ancestry first
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
@@ -1610,6 +1663,24 @@ class FastLocalVarRef(
|
||||
}
|
||||
|
||||
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 {
|
||||
// Heuristic capacity hint: count element entries; spreads handled opportunistically
|
||||
val elemCount = entries.count { it is ListEntry.Element }
|
||||
@@ -1625,7 +1696,7 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
||||
when (elements) {
|
||||
is ObjList -> {
|
||||
// Grow underlying array once when possible
|
||||
if (list is ArrayList) list.ensureCapacity(list.size + elements.list.size)
|
||||
list.ensureCapacity(list.size + elements.list.size)
|
||||
list.addAll(elements.list)
|
||||
}
|
||||
else -> scope.raiseError("Spread element must be list")
|
||||
@@ -1633,7 +1704,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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1656,10 +1774,8 @@ class MapLiteralRef(private val entries: List<MapLiteralEntry>) : ObjRef {
|
||||
is MapLiteralEntry.Spread -> {
|
||||
val m = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
||||
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) {
|
||||
val sKey = k as? ObjString ?: scope.raiseIllegalArgument("spread map must have string keys; got $k")
|
||||
result.map[sKey] = v
|
||||
result.map[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1691,11 +1807,17 @@ class AssignRef(
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val v = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
|
||||
// For properties, we should not call get() on target because it invokes the getter.
|
||||
// Instead, we call setAt directly.
|
||||
if (target is FieldRef || target is IndexRef || target is LocalVarRef || target is FastLocalVarRef || target is BoundLocalVarRef) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)})")
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,12 @@ class ObjRingBuffer(val capacity: Int) : Obj() {
|
||||
returns = type("lyng.Void"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) { 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return ObjSet(
|
||||
if (other is ObjSet)
|
||||
|
||||
@@ -134,7 +134,7 @@ data class ObjString(val value: String) : Obj() {
|
||||
returns = type("lyng.Int"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
ObjInt(
|
||||
ObjInt.of(
|
||||
thisAs<ObjString>().value.toLongOrNull()
|
||||
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
||||
)
|
||||
@@ -159,7 +159,7 @@ data class ObjString(val value: String) : Obj() {
|
||||
}
|
||||
addConstDoc(
|
||||
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.",
|
||||
type = type("lyng.Int"),
|
||||
moduleName = "lyng.stdlib"
|
||||
@@ -216,6 +216,14 @@ data class ObjString(val value: String) : Obj() {
|
||||
) {
|
||||
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(
|
||||
name = "upper",
|
||||
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)
|
||||
}
|
||||
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(
|
||||
name = "characters",
|
||||
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.",
|
||||
returns = type("lyng.Int"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) { ObjInt(thisAs<ObjString>().value.length.toLong()) }
|
||||
) { ObjInt.of(thisAs<ObjString>().value.length.toLong()) }
|
||||
addFnDoc(
|
||||
name = "toReal",
|
||||
doc = "Parse this string as a real number (floating point).",
|
||||
returns = type("lyng.Real"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
ObjReal(thisAs<ObjString>().value.toDouble())
|
||||
ObjReal.of(thisAs<ObjString>().value.toDouble())
|
||||
}
|
||||
addFnDoc(
|
||||
name = "trim",
|
||||
|
||||
@@ -47,7 +47,8 @@ abstract class Statement(
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if( other == ObjNull || other == ObjVoid ) return 1
|
||||
throw UnsupportedOperationException("not comparable")
|
||||
if( other === this ) return 0
|
||||
return -1
|
||||
}
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
|
||||
@@ -82,8 +82,16 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe
|
||||
if (it !is ObjClass)
|
||||
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
|
||||
it
|
||||
} ?: scope.raiseSymbolNotFound("can't deserialize: not found type $className")
|
||||
} ?: run {
|
||||
// Use Scope API that mirrors compiler-emitted ObjRef chain for qualified identifiers
|
||||
val evaluated = scope.resolveQualifiedIdentifier(className.value)
|
||||
if (evaluated !is ObjClass)
|
||||
scope.raiseClassCastError("Expected obj class but got ${evaluated::class.simpleName}")
|
||||
evaluated
|
||||
}
|
||||
}
|
||||
|
||||
// helper moved to Scope as resolveQualifiedIdentifier
|
||||
|
||||
suspend fun decodeAnyList(scope: Scope, fixedSize: Int? = null): MutableList<Obj> {
|
||||
return if (bin.getBit() == 1) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package net.sergeych.tools
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
|
||||
inline fun bm(text: String="", f: ()->Unit) {
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -49,10 +49,10 @@ class BindingHighlightTest {
|
||||
|
||||
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" }
|
||||
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
|
||||
val declRange = sym.declStart to sym.declEnd
|
||||
@@ -82,7 +82,7 @@ class BindingHighlightTest {
|
||||
|
||||
val sym = binding.symbols.firstOrNull { it.name == "answer" }
|
||||
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 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`
|
||||
val nameSym = binding.symbols.firstOrNull { it.name == "name" }
|
||||
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)
|
||||
val nameRefs = binding.references.filter { it.symbolId == nameSym.id }
|
||||
@@ -165,7 +165,7 @@ class BindingHighlightTest {
|
||||
|
||||
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")
|
||||
|
||||
// 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");
|
||||
* 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 xIds = snap.symbols.filter { it.name == "x" }.map { it.id }
|
||||
assertTrue(aIds.isNotEmpty())
|
||||
|
||||
@@ -24,7 +24,6 @@ import net.sergeych.lyng.ExecutionError
|
||||
import net.sergeych.lyng.ScriptError
|
||||
import net.sergeych.lyng.eval
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class MapLiteralTest {
|
||||
@@ -119,21 +118,22 @@ class MapLiteralTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun spreadNonStringKeysIsRuntimeError() = runTest {
|
||||
assertFailsWith<ExecutionError> {
|
||||
eval("""{ ...Map(1 => "x") }""")
|
||||
}
|
||||
fun spreadNonStringKeysIsAllowed() = runTest {
|
||||
eval("""
|
||||
val m = { ...Map(1 => "x") }
|
||||
assertEquals("x", m[1])
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mergeNonStringKeyIsRuntimeError() = runTest {
|
||||
assertFailsWith<ExecutionError> {
|
||||
fun mergeNonStringKeyIsAllowed() = runTest {
|
||||
eval("""
|
||||
val e = (1 => "x")
|
||||
{ "a": 1 } + e
|
||||
val m = { "a": 1 } + e
|
||||
assertEquals(1, m["a"])
|
||||
assertEquals("x", m[1])
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shorthandUndefinedIdIsError() = runTest {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -131,4 +131,78 @@ class MiniAstTest {
|
||||
// Bases captured as plain names for now
|
||||
assertEquals(listOf("Base1", "Base2"), cd.bases)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun miniAst_captures_enum_entries_and_doc() = runTest {
|
||||
val code = """
|
||||
/** Enum E docs */
|
||||
enum E {
|
||||
A,
|
||||
B,
|
||||
C
|
||||
}
|
||||
"""
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
val ed = mini!!.declarations.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == "E" }
|
||||
assertNotNull(ed)
|
||||
assertNotNull(ed.doc)
|
||||
assertTrue(ed.doc!!.raw.contains("Enum E docs"))
|
||||
assertEquals(listOf("A", "B", "C"), ed.entries)
|
||||
assertEquals("E", ed.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enum_to_synthetic_class_members() = runTest {
|
||||
val code = """
|
||||
enum MyEnum { V1, V2 }
|
||||
"""
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
|
||||
// I'll check via aggregateClasses by mocking the registry or just checking it includes Enum base.
|
||||
val stdlib = BuiltinDocRegistry.docsForModule("lyng.stdlib")
|
||||
val enumBase = stdlib.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "Enum" }
|
||||
assertNotNull(enumBase, "Enum base class should be in stdlib")
|
||||
assertTrue(enumBase.members.any { it.name == "name" })
|
||||
assertTrue(enumBase.members.any { it.name == "ordinal" })
|
||||
|
||||
// Check if aggregateClasses handles enums from local MiniScript
|
||||
val classes = DocLookupUtils.aggregateClasses(listOf("lyng.stdlib"), mini)
|
||||
val myEnum = classes["MyEnum"]
|
||||
assertNotNull(myEnum, "Local enum should be aggregated as a class")
|
||||
assertEquals(listOf("Enum"), myEnum.bases)
|
||||
assertTrue(myEnum.members.any { it.name == "entries" }, "Should have entries")
|
||||
assertTrue(myEnum.members.any { it.name == "valueOf" }, "Should have valueOf")
|
||||
assertTrue(myEnum.members.any { it.name == "V1" }, "Should have V1")
|
||||
assertTrue(myEnum.members.any { it.name == "V2" }, "Should have V2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun complete_enum_members() = runTest {
|
||||
val code = """
|
||||
enum MyEnum { V1, V2 }
|
||||
val x = MyEnum.V1.<caret>
|
||||
"""
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val names = items.map { it.name }.toSet()
|
||||
assertTrue(names.contains("name"), "Should contain name from Enum base")
|
||||
assertTrue(names.contains("ordinal"), "Should contain ordinal from Enum base")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun complete_enum_class_members() = runTest {
|
||||
val code = """
|
||||
enum MyEnum { V1, V2 }
|
||||
val x = MyEnum.<caret>
|
||||
"""
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val names = items.map { it.name }.toSet()
|
||||
assertTrue(names.contains("entries"), "Should contain entries")
|
||||
assertTrue(names.contains("valueOf"), "Should contain valueOf")
|
||||
assertTrue(names.contains("V1"), "Should contain V1")
|
||||
assertTrue(names.contains("V2"), "Should contain V2")
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,19 @@
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.eval
|
||||
import net.sergeych.lyng.obj.ObjInstance
|
||||
import net.sergeych.lyng.obj.ObjList
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
|
||||
class OOTest {
|
||||
@Test
|
||||
fun testClassProps() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
import lyng.time
|
||||
|
||||
class Point(x,y) {
|
||||
@@ -35,11 +41,14 @@ class OOTest {
|
||||
assertEquals(Point(0,0), Point.origin)
|
||||
assertEquals(Point(1,2), Point.center)
|
||||
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClassMethods() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
import lyng.time
|
||||
|
||||
class Point(x,y) {
|
||||
@@ -58,12 +67,14 @@ class OOTest {
|
||||
assertEquals(null, Point.getData() )
|
||||
Point.setData("foo")
|
||||
assertEquals( "foo!", Point.getData() )
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDynamicGet() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
val accessor = dynamic {
|
||||
get { name ->
|
||||
if( name == "foo" ) "bar" else null
|
||||
@@ -74,12 +85,14 @@ class OOTest {
|
||||
assertEquals("bar", accessor.foo)
|
||||
assertEquals(null, accessor.bar)
|
||||
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDelegateSet() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
var setValueForBar = null
|
||||
val accessor = dynamic {
|
||||
get { name ->
|
||||
@@ -104,12 +117,14 @@ class OOTest {
|
||||
assertThrows {
|
||||
accessor.bad = "!23"
|
||||
}
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDynamicIndexAccess() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
val store = Map()
|
||||
val accessor = dynamic {
|
||||
get { name ->
|
||||
@@ -124,23 +139,27 @@ class OOTest {
|
||||
accessor["foo"] = "bar"
|
||||
assertEquals("bar", accessor["foo"])
|
||||
assertEquals("bar", accessor.foo)
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultilineConstructor() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
class Point(
|
||||
x,
|
||||
y
|
||||
)
|
||||
assertEquals(Point(1,2), Point(1,2) )
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDynamicClass() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
|
||||
fun getContract(contractName) {
|
||||
dynamic {
|
||||
@@ -150,14 +169,16 @@ class OOTest {
|
||||
}
|
||||
}
|
||||
getContract("foo").bar
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDynamicClassReturn2() = runTest {
|
||||
// todo: should work without extra parenthesis
|
||||
// see below
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
|
||||
fun getContract(contractName) {
|
||||
println("1")
|
||||
@@ -184,6 +205,195 @@ class OOTest {
|
||||
assertEquals(6, x(1,2,3))
|
||||
// v HERE v
|
||||
assertEquals(15, cc.foo.bar(10,2,3))
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClassInitialization() = runTest {
|
||||
eval(
|
||||
"""
|
||||
var countInstances = 0
|
||||
class Point(val x: Int, val y: Int) {
|
||||
println("Class initializer is called 1")
|
||||
var magnitude
|
||||
|
||||
/*
|
||||
init {} section optionally provide initialization code that is called on each instance creation.
|
||||
it should have the same access to this.* and constructor parameters as any other member
|
||||
function.
|
||||
*/
|
||||
init {
|
||||
countInstances++
|
||||
magnitude = Math.sqrt(x*x + y*y)
|
||||
}
|
||||
}
|
||||
|
||||
val p = Point(1, 2)
|
||||
assertEquals(1, countInstances)
|
||||
assertEquals(p, Point(1,2) )
|
||||
assertEquals(2, countInstances)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMIInitialization() = runTest {
|
||||
eval(
|
||||
"""
|
||||
var order = []
|
||||
class A {
|
||||
init { order.add("A") }
|
||||
}
|
||||
class B : A {
|
||||
init { order.add("B") }
|
||||
}
|
||||
class C {
|
||||
init { order.add("C") }
|
||||
}
|
||||
class D : B, C {
|
||||
init { order.add("D") }
|
||||
}
|
||||
D()
|
||||
assertEquals(["A", "B", "C", "D"], order)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMIDiamondInitialization() = runTest {
|
||||
eval(
|
||||
"""
|
||||
var order = []
|
||||
class A {
|
||||
init { order.add("A") }
|
||||
}
|
||||
class B : A {
|
||||
init { order.add("B") }
|
||||
}
|
||||
class C : A {
|
||||
init { order.add("C") }
|
||||
}
|
||||
class D : B, C {
|
||||
init { order.add("D") }
|
||||
}
|
||||
D()
|
||||
assertEquals(["A", "B", "C", "D"], order)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInitBlockInDeserialization() = runTest {
|
||||
eval(
|
||||
"""
|
||||
import lyng.serialization
|
||||
var count = 0
|
||||
class A {
|
||||
init { count++ }
|
||||
}
|
||||
val a1 = A()
|
||||
val coded = Lynon.encode(a1)
|
||||
val a2 = Lynon.decode(coded)
|
||||
assertEquals(2, count)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDefaultCompare() = runTest {
|
||||
eval(
|
||||
"""
|
||||
class Point(val x: Int, val y: Int)
|
||||
|
||||
assertEquals(Point(1,2), Point(1,2) )
|
||||
assert( Point(1,2) != Point(2,1) )
|
||||
assert( Point(1,2) == Point(1,2) )
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConstructorCallsWithNamedParams() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val list = scope.eval(
|
||||
"""
|
||||
import lyng.time
|
||||
|
||||
class BarRequest(
|
||||
id,
|
||||
vaultId, userAddress, isDepositRequest, grossWeight, fineness, notes="",
|
||||
createdAt = Instant.now().truncateToSecond(),
|
||||
updatedAt = Instant.now().truncateToSecond()
|
||||
) {
|
||||
// unrelated for comparison
|
||||
static val stateNames = [1, 2, 3]
|
||||
|
||||
val cell = cached { Cell[id] }
|
||||
}
|
||||
assertEquals( 5,5.toInt())
|
||||
val b1 = BarRequest(1, "v1", "u1", true, 1000, 999)
|
||||
val b2 = BarRequest(1, "v1", "u1", true, 1000, 999, createdAt: b1.createdAt, updatedAt: b1.updatedAt)
|
||||
assertEquals(b1, b2)
|
||||
assertEquals( 0, b1 <=> b2)
|
||||
[b1, b2]
|
||||
""".trimIndent()
|
||||
) as ObjList
|
||||
val b1 = list.list[0] as ObjInstance
|
||||
val b2 = list.list[1] as ObjInstance
|
||||
assertEquals(0, b1.compareTo(scope, b2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPropAsExtension() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval("""
|
||||
class A(x) {
|
||||
private val privateVal = 100
|
||||
val p1 get() = x + 1
|
||||
}
|
||||
assertEquals(2, A(1).p1)
|
||||
|
||||
fun A.f() = x + 5
|
||||
assertEquals(7, A(2).f())
|
||||
|
||||
// The same, we should be able to add member values to a class;
|
||||
// notice it should access to the class public instance members,
|
||||
// somewhat like it is declared in the class body
|
||||
val A.simple = x + 3
|
||||
|
||||
assertEquals(5, A(2).simple)
|
||||
|
||||
// it should also work with properties:
|
||||
val A.p10 get() = x * 10
|
||||
assertEquals(20, A(2).p10)
|
||||
""".trimIndent())
|
||||
|
||||
// important is that such extensions should not be able to access private members
|
||||
// and thus remove privateness:
|
||||
assertFails {
|
||||
scope.eval("val A.exportPrivateVal = privateVal; A(1).exportPrivateVal")
|
||||
}
|
||||
assertFails {
|
||||
scope.eval("val A.exportPrivateValProp get() = privateVal; A(1).exportPrivateValProp")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtensionsAreScopeIsolated() = runTest {
|
||||
val scope1 = Script.newScope()
|
||||
scope1.eval("""
|
||||
val String.totalDigits get() {
|
||||
// notice using `this`:
|
||||
this.characters.filter{ it.isDigit() }.size()
|
||||
}
|
||||
assertEquals(2, "answer is 42".totalDigits)
|
||||
""")
|
||||
val scope2 = Script.newScope()
|
||||
scope2.eval("""
|
||||
// in scope2 we didn't override `totalDigits` extension:
|
||||
assertThrows { "answer is 42".totalDigits }
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.eval
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ScopePoolingRegressionTest {
|
||||
|
||||
@Test
|
||||
fun testPooledScopeInstance() = runTest {
|
||||
val saved = PerfFlags.SCOPE_POOL
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
try {
|
||||
val result = eval("""
|
||||
class A {
|
||||
fun test() {
|
||||
// println is a global function
|
||||
println("Calling println from A")
|
||||
"method ok"
|
||||
}
|
||||
}
|
||||
|
||||
// Use a transient scope (lambda) to create the instance
|
||||
val creator = { A() }
|
||||
val a = creator()
|
||||
|
||||
// Re-use the pool to ensure the scope used above is reset/repurposed
|
||||
val other = { 1 + 2 }
|
||||
other()
|
||||
other()
|
||||
|
||||
// Now call method on 'a'. It should still find global 'println'
|
||||
a.test()
|
||||
""".trimIndent())
|
||||
assertEquals("method ok", result.toString())
|
||||
} finally {
|
||||
PerfFlags.SCOPE_POOL = saved
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,6 @@ import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -36,7 +34,9 @@ import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
||||
import net.sergeych.mp_tools.globalDefer
|
||||
import net.sergeych.tools.bm
|
||||
import kotlin.test.*
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
@@ -2318,6 +2318,7 @@ class ScriptTest {
|
||||
@Test
|
||||
fun doWhileValuesLabelTest() = runTest {
|
||||
withTimeout(5.seconds) {
|
||||
try {
|
||||
eval(
|
||||
"""
|
||||
var count = 0
|
||||
@@ -2341,6 +2342,9 @@ class ScriptTest {
|
||||
assertEquals( 4, count3)
|
||||
""".trimIndent()
|
||||
)
|
||||
} catch (e: ExecutionError) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3020,6 +3024,37 @@ class ScriptTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMapWithNonStringKeys() = runTest {
|
||||
eval(
|
||||
"""
|
||||
val map = Map( 1 => "one", 2 => "two" )
|
||||
assertEquals( "one", map[1] )
|
||||
assertEquals( "two", map[2] )
|
||||
assertEquals( null, map[3] )
|
||||
map[3] = "three"
|
||||
assertEquals( "three", map[3] )
|
||||
map += (4 => "four")
|
||||
assertEquals( "four", map[4] )
|
||||
|
||||
// Test toMap()
|
||||
val map2 = [1 => "a", 2 => "b"].toMap()
|
||||
assertEquals("a", map2[1])
|
||||
assertEquals("b", map2[2])
|
||||
|
||||
// Test Map constructor with mixed entries and arrays
|
||||
val map3 = Map( 1 => "a", [2, "b"] )
|
||||
assertEquals("a", map3[1])
|
||||
assertEquals("b", map3[2])
|
||||
|
||||
// Test plus
|
||||
val map4 = map3 + (3 => "c")
|
||||
assertEquals("c", map4[3])
|
||||
assertEquals("a", map4[1])
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBuffer() = runTest {
|
||||
eval(
|
||||
@@ -3919,10 +3954,12 @@ class ScriptTest {
|
||||
@Test
|
||||
fun testJsonTime() = runTest {
|
||||
val now = Clock.System.now()
|
||||
val x = eval("""
|
||||
val x = eval(
|
||||
"""
|
||||
import lyng.time
|
||||
Instant.now().truncateToSecond()
|
||||
""".trimIndent()).decodeSerializable<Instant>()
|
||||
""".trimIndent()
|
||||
).decodeSerializable<Instant>()
|
||||
println(x)
|
||||
assertIs<Instant>(x)
|
||||
assertTrue((now - x).absoluteValue < 2.seconds)
|
||||
@@ -4043,10 +4080,12 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun deserializeMapWithJsonTest() = runTest {
|
||||
val x = eval("""
|
||||
val x = eval(
|
||||
"""
|
||||
import lyng.serialization
|
||||
{ value: 1, inner: { "foo": 1, "bar": 2 }}
|
||||
""".trimIndent()).decodeSerializable<TestJson2>()
|
||||
""".trimIndent()
|
||||
).decodeSerializable<TestJson2>()
|
||||
assertEquals(TestJson2(1, mapOf("foo" to 1, "bar" to 2)), x)
|
||||
}
|
||||
|
||||
@@ -4055,42 +4094,56 @@ class ScriptTest {
|
||||
val value: Int,
|
||||
val inner: JsonObject
|
||||
)
|
||||
|
||||
@Test
|
||||
fun deserializeAnyMapWithJsonTest() = runTest {
|
||||
val x = eval("""
|
||||
val x = eval(
|
||||
"""
|
||||
import lyng.serialization
|
||||
{ value: 12, inner: { "foo": 1, "bar": "two" }}
|
||||
""".trimIndent()).decodeSerializable<TestJson3>()
|
||||
assertEquals(TestJson3(12, JsonObject(mapOf("foo" to JsonPrimitive(1), "bar" to Json.encodeToJsonElement("two")))), x)
|
||||
""".trimIndent()
|
||||
).decodeSerializable<TestJson3>()
|
||||
assertEquals(
|
||||
TestJson3(
|
||||
12,
|
||||
JsonObject(mapOf("foo" to JsonPrimitive(1), "bar" to Json.encodeToJsonElement("two")))
|
||||
), x
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class TestEnum {
|
||||
One, Two
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TestJson4(val value: TestEnum)
|
||||
|
||||
@Test
|
||||
fun deserializeEnumJsonTest() = runTest {
|
||||
val x = eval("""
|
||||
val x = eval(
|
||||
"""
|
||||
import lyng.serialization
|
||||
enum TestEnum { One, Two }
|
||||
{ value: TestEnum.One }
|
||||
""".trimIndent()).decodeSerializable<TestJson4>()
|
||||
""".trimIndent()
|
||||
).decodeSerializable<TestJson4>()
|
||||
assertEquals(TestJson4(TestEnum.One), x)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStringLast() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
assertEquals('t', "assert".last())
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLogicalNot() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
val vf = false
|
||||
fun f() { false }
|
||||
assert( !false )
|
||||
@@ -4127,7 +4180,8 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testHangOnPrintlnInMethods() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
class T(someList) {
|
||||
fun f() {
|
||||
val x = [...someList]
|
||||
@@ -4135,12 +4189,14 @@ class ScriptTest {
|
||||
}
|
||||
}
|
||||
T([1,2]).f()
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHangOnNonexistingMethod() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
class T(someList) {
|
||||
fun f() {
|
||||
nonExistingMethod()
|
||||
@@ -4156,12 +4212,14 @@ class ScriptTest {
|
||||
println(t::class)
|
||||
// ok
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUsingClassConstructorVars() = runTest {
|
||||
val r = eval("""
|
||||
val r = eval(
|
||||
"""
|
||||
import lyng.time
|
||||
|
||||
class Request {
|
||||
@@ -4185,7 +4243,8 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
test()
|
||||
""".trimIndent()).toJson()
|
||||
""".trimIndent()
|
||||
).toJson()
|
||||
println(r)
|
||||
}
|
||||
|
||||
@@ -4193,7 +4252,8 @@ class ScriptTest {
|
||||
fun testScopeShortCircuit() = runTest() {
|
||||
val baseScope = Script.newScope()
|
||||
|
||||
baseScope.eval("""
|
||||
baseScope.eval(
|
||||
"""
|
||||
val exports = Map()
|
||||
fun Export(name,f) {
|
||||
exports[name] = f
|
||||
@@ -4204,7 +4264,8 @@ class ScriptTest {
|
||||
|
||||
val exports: MutableMap<Obj, Obj> = (baseScope.eval("exports") as ObjMap).map
|
||||
|
||||
baseScope.eval("""
|
||||
baseScope.eval(
|
||||
"""
|
||||
class A(val a) {
|
||||
fun methodA() {
|
||||
a + 1
|
||||
@@ -4221,18 +4282,29 @@ class ScriptTest {
|
||||
fun exportedFunction(x) {
|
||||
someFunction(x)
|
||||
}
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
// Calling from the script is ok:
|
||||
val instanceScope = baseScope.createChildScope()
|
||||
instanceScope.eval("""
|
||||
instanceScope.eval(
|
||||
"""
|
||||
val a1 = a0 + 1
|
||||
""".trimIndent())
|
||||
assertEquals( ObjInt(2), instanceScope.eval("""
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(
|
||||
ObjInt(2), instanceScope.eval(
|
||||
"""
|
||||
exportedFunction(1)
|
||||
"""))
|
||||
assertEquals( ObjInt(103), instanceScope.eval("""
|
||||
"""
|
||||
)
|
||||
)
|
||||
assertEquals(
|
||||
ObjInt(103), instanceScope.eval(
|
||||
"""
|
||||
exportedFunction(a1 + 1)
|
||||
"""))
|
||||
"""
|
||||
)
|
||||
)
|
||||
val dummyThis = Obj()
|
||||
// but we should be able to call it directly
|
||||
val otherScope = baseScope.createChildScope()
|
||||
@@ -4241,4 +4313,223 @@ class ScriptTest {
|
||||
assertEquals(51, r.toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFirstInEnum() = runTest {
|
||||
eval(
|
||||
"""
|
||||
enum E {
|
||||
one, two, three
|
||||
}
|
||||
println(E.entries)
|
||||
assertEquals( E.two, E.entries.findFirst {
|
||||
println(it.name)
|
||||
it.name in ["aaa", "two"]
|
||||
} )
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAutoSplatArgs() = runTest {
|
||||
eval(
|
||||
"""
|
||||
fun tf(x, y, z) {
|
||||
"x=%s, y=%s, z=%s"(x,y,z)
|
||||
}
|
||||
assertEquals(tf(1, 2, 3), "x=1, y=2, z=3")
|
||||
val a = { x: 3, y: 4, z: 5 }
|
||||
assertEquals(tf(...a), "x=3, y=4, z=5")
|
||||
assertEquals(tf(...{ x: 3, y: 4, z: 50 }), "x=3, y=4, z=50")
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCached() = runTest {
|
||||
eval(
|
||||
"""
|
||||
var counter = 0
|
||||
val f = cached { ++counter }
|
||||
|
||||
assertEquals(1,f())
|
||||
assertEquals(1, counter)
|
||||
assertEquals(1,f())
|
||||
assertEquals(1, counter)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCustomToStringBug() = runTest {
|
||||
eval(
|
||||
"""
|
||||
class A(x,y)
|
||||
class B(x,y) {
|
||||
fun toString() {
|
||||
"B(%d,%d)"(x,y)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("B(1,2)", B(1,2).toString())
|
||||
assertEquals("A(x=1,y=2)", A(1,2).toString())
|
||||
|
||||
// now tricky part: this _should_ cakk custom toString()
|
||||
assertEquals(":B(1,2)", ":" + B(1,2).toString())
|
||||
// and this must be exactly same:
|
||||
assertEquals(":B(1,2)", ":" + B(1,2))
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testDestructuringAssignment() = runTest {
|
||||
eval(
|
||||
"""
|
||||
val abc = [1, 2, 3]
|
||||
// plain:
|
||||
val [a, b, c] = abc
|
||||
assertEquals( 1, a )
|
||||
assertEquals( 2, b )
|
||||
assertEquals( 3, c )
|
||||
// with splats, receiving into list:
|
||||
val [ab..., c1] = abc
|
||||
assertEquals( [1, 2], ab )
|
||||
assertEquals( 3, c1 )
|
||||
// also in the end
|
||||
val [a1, rest...] = abc
|
||||
assertEquals( 1, a1 )
|
||||
assertEquals( [2, 3], rest )
|
||||
// and in the middle
|
||||
val [a_mid, middle..., e0, e1] = [ 1, 2, 3, 4, 5, 6]
|
||||
assertEquals( [2, 3, 4], middle )
|
||||
assertEquals( 5, e0 )
|
||||
assertEquals( 6, e1 )
|
||||
assertEquals( 1, a_mid )
|
||||
|
||||
// nested destructuring:
|
||||
val [n1, [n2, n3...], n4] = [1, [2, 3, 4], 5]
|
||||
assertEquals(1, n1)
|
||||
assertEquals(2, n2)
|
||||
assertEquals([3, 4], n3)
|
||||
assertEquals(5, n4)
|
||||
|
||||
// also it could be used to reassign vars:
|
||||
var x = 5
|
||||
var y = 10
|
||||
|
||||
[x, y] = [y, x]
|
||||
assertEquals( 10, x )
|
||||
assertEquals( 5, y )
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProperlyReportExceptionPos() = runTest {
|
||||
var x = assertFailsWith<ExecutionError> {
|
||||
eval(
|
||||
"""
|
||||
val tmp = 11
|
||||
|
||||
if( tmp < 100 ) {
|
||||
throw "success"
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
println(x)
|
||||
assertEquals(3, x.pos.line)
|
||||
assertContains(x.message!!, "throw \"success\"")
|
||||
|
||||
// comments shoudl not change reporting:
|
||||
x = assertFailsWith<ExecutionError> {
|
||||
eval(
|
||||
"""
|
||||
val tmp = 11
|
||||
/*
|
||||
This should not change position:
|
||||
*/
|
||||
fun test() {
|
||||
throw "success"
|
||||
}
|
||||
test()
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
println(x)
|
||||
assertEquals(5, x.pos.line)
|
||||
assertContains(x.message!!, "throw \"success\"")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClassAndFunAutoNamedArgs() = runTest {
|
||||
// Shorthand for named arguments: name: is equivalent to name: name.
|
||||
// This is consistent with map literal shorthand in Lyng.
|
||||
eval(
|
||||
"""
|
||||
fun test(a, b, c) {
|
||||
"%s-%s-%s"(a,b,c)
|
||||
}
|
||||
|
||||
val a = 1
|
||||
val b = 2
|
||||
val c = 3
|
||||
|
||||
// Basic usage:
|
||||
assertEquals( "1-2-3", test(a:, b:, c:) )
|
||||
assertEquals( "1-2-3", test(c:, b:, a:) )
|
||||
|
||||
// Class constructors also support it:
|
||||
class Point(x, y) {
|
||||
val r = "x:%s, y:%s"(x, y)
|
||||
}
|
||||
val x = 10
|
||||
val y = 20
|
||||
assertEquals( "x:10, y:20", Point(x:, y:).r )
|
||||
assertEquals( "x:10, y:20", Point(y:, x:).r )
|
||||
|
||||
// Mixed with positional arguments:
|
||||
assertEquals( "0-2-3", test(0, b:, c:) )
|
||||
|
||||
// Mixed with regular named arguments:
|
||||
assertEquals( "1-99-3", test(a:, b: 99, c:) )
|
||||
|
||||
// Integration with splats (spread arguments):
|
||||
val args = { b:, c: } // map literal shorthand
|
||||
assertEquals( "1-2-3", test(a:, ...args) )
|
||||
|
||||
// Default values:
|
||||
fun sum(a, b=10, c=100) { a + b + c }
|
||||
assertEquals( 111, sum(a:) )
|
||||
assertEquals( 103, sum(a:, b:) )
|
||||
|
||||
// Complex scenario with multiple splats and shorthands:
|
||||
val p1 = 1
|
||||
val p2 = 2
|
||||
val more = { c: 3, d: 4 }
|
||||
fun quad(a, b, c, d) { a + b + c + d }
|
||||
assertEquals( 10, quad(a: p1, b: p2, ...more) )
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFunMiniDeclaration() = runTest {
|
||||
eval("""
|
||||
class T(x) {
|
||||
fun method() = x + 1
|
||||
}
|
||||
fun median(a,b) = (a+b)/2
|
||||
|
||||
assertEquals(11, T(10).method())
|
||||
assertEquals(2, median(1,3))
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -91,4 +91,26 @@ class StdlibTest {
|
||||
assertEquals([1,2], (1..8).dropLast(6).toList() )
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFlattenAndFilter() = runTest {
|
||||
eval("""
|
||||
assertEquals([1,2,3,4,5,6], [1,3,5].map { [it, it+1] }.flatten() )
|
||||
assertEquals([1,3,5], [null,1,null, 3,5].filterNotNull().toList())
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFlatMap() = runTest {
|
||||
eval("""
|
||||
assertEquals([1,2,3,4,5,6], [1,3,5].flatMap { [it,it+1] }.toList() )
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCount() = runTest {
|
||||
eval("""
|
||||
assertEquals(5, (1..10).toList().count { it % 2 == 1 } )
|
||||
""")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
|
||||
class PropsTest {
|
||||
|
||||
@Test
|
||||
fun propsProposal() = runTest {
|
||||
eval("""
|
||||
|
||||
class WithProps {
|
||||
|
||||
// readonly property without declared type:
|
||||
val readonlyProp
|
||||
get() {
|
||||
"readonly foo"
|
||||
}
|
||||
|
||||
val readonlyWithType: Int get() { 42 }
|
||||
|
||||
private var field = 0
|
||||
private var field2 = ""
|
||||
|
||||
// with type declaration
|
||||
var propName: Int
|
||||
get() {
|
||||
field * 10
|
||||
}
|
||||
set(value) {
|
||||
field = value
|
||||
}
|
||||
|
||||
// or without
|
||||
var propNameWithoutType
|
||||
get() {
|
||||
"/"+ field2 + "/"
|
||||
}
|
||||
set(value) {
|
||||
field2 = value
|
||||
}
|
||||
}
|
||||
|
||||
val w = WithProps()
|
||||
assertEquals("readonly foo", w.readonlyProp)
|
||||
assertEquals(42, w.readonlyWithType)
|
||||
|
||||
w.propNameWithoutType = "foo"
|
||||
assertEquals("/foo/", w.propNameWithoutType)
|
||||
|
||||
w.propName = 123
|
||||
assertEquals(1230, w.propName)
|
||||
|
||||
class Shorthand {
|
||||
private var _p = 0
|
||||
var p: Int
|
||||
get() = _p * 2
|
||||
set(v) = _p = v
|
||||
}
|
||||
val s = Shorthand()
|
||||
s.p = 21
|
||||
assertEquals(42, s.p)
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -331,6 +331,25 @@ class LyngFormatterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiLineChainCalls() {
|
||||
val src = """
|
||||
somevar.where
|
||||
.isGreen()
|
||||
.isWarm()
|
||||
""".trimIndent()
|
||||
|
||||
val expected = """
|
||||
somevar.where
|
||||
.isGreen()
|
||||
.isWarm()
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 8)
|
||||
val out = LyngFormatter.reindent(src, cfg)
|
||||
assertEquals(expected, out)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun chainedCalls_and_MemberAccess() {
|
||||
val src = """
|
||||
@@ -413,6 +432,55 @@ class LyngFormatterTest {
|
||||
assertEquals(expected, LyngFormatter.format(out, cfg))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multilineCommentIndentation() {
|
||||
val src = """
|
||||
/*
|
||||
This is
|
||||
a comment
|
||||
*/
|
||||
fun f() {
|
||||
/*
|
||||
Inner
|
||||
comment
|
||||
*/
|
||||
val x = 1 /* end of line
|
||||
comment */
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val expected = """
|
||||
/*
|
||||
This is
|
||||
a comment
|
||||
*/
|
||||
fun f() {
|
||||
/*
|
||||
Inner
|
||||
comment
|
||||
*/
|
||||
val x = 1 /* end of line
|
||||
comment */
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(indentSize = 4)
|
||||
val out = LyngFormatter.reindent(src, cfg)
|
||||
assertEquals(expected, out)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multicharacterOperators_spacing() {
|
||||
val operators = listOf("+=", "-=", "*=", "/=", "=>", "==", "!=", "<=", ">=", "&&", "||", "->")
|
||||
for (op in operators) {
|
||||
val src = "a${op}b"
|
||||
val expected = "a $op b"
|
||||
val cfg = LyngFormatConfig(applySpacing = true)
|
||||
val out = LyngFormatter.format(src, cfg)
|
||||
assertEquals(expected, out, "Failed for operator $op")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun propertyBasedIdempotencyMedium() {
|
||||
// Compose slightly longer random snippets out of templates
|
||||
@@ -608,4 +676,58 @@ class LyngFormatterTest {
|
||||
// Idempotent
|
||||
assertEquals(expected, LyngFormatter.reindent(out, cfg))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stringLiterals_areNotNormalized() {
|
||||
val src = """
|
||||
val s = "a=b"
|
||||
val s2 = "a + b"
|
||||
val s3 = "a - b"
|
||||
val s4 = "a,b"
|
||||
val s5 = "if(x){}"
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(applySpacing = true)
|
||||
val formatted = LyngFormatter.format(src, cfg)
|
||||
|
||||
assertEquals(src, formatted, "String literals should not be changed by space normalization")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mixedCodeAndStrings_normalization() {
|
||||
val src = "val x=1+\"a+b\"+2"
|
||||
val expected = "val x = 1 + \"a+b\" + 2"
|
||||
|
||||
val cfg = LyngFormatConfig(applySpacing = true)
|
||||
val formatted = LyngFormatter.format(src, cfg)
|
||||
|
||||
assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reindent_ignoresBracesInStrings() {
|
||||
val src = """
|
||||
fun test() {
|
||||
val s = "{"
|
||||
val s2 = "}"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(indentSize = 4)
|
||||
val formatted = LyngFormatter.reindent(src, cfg)
|
||||
|
||||
// If it fails, s2 will be indented differently
|
||||
assertEquals(src, formatted, "Braces in strings should not affect indentation")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun blockComments_spacing_robustness() {
|
||||
val src = "val x=1/*a=b*/+2"
|
||||
val expected = "val x = 1/*a=b*/ + 2"
|
||||
|
||||
val cfg = LyngFormatConfig(applySpacing = true)
|
||||
val formatted = LyngFormatter.format(src, cfg)
|
||||
|
||||
assertEquals(expected, formatted)
|
||||
}
|
||||
}
|
||||
@@ -18,15 +18,15 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
actual object PerfDefaults {
|
||||
actual val LOCAL_SLOT_PIC: Boolean = true
|
||||
actual val EMIT_FAST_LOCAL_REFS: Boolean = true
|
||||
actual val LOCAL_SLOT_PIC: Boolean = false
|
||||
actual val EMIT_FAST_LOCAL_REFS: Boolean = false
|
||||
|
||||
actual val ARG_BUILDER: Boolean = true
|
||||
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
|
||||
actual val SCOPE_POOL: Boolean = true
|
||||
actual val ARG_BUILDER: Boolean = false
|
||||
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false
|
||||
actual val SCOPE_POOL: Boolean = false
|
||||
|
||||
actual val FIELD_PIC: Boolean = true
|
||||
actual val METHOD_PIC: Boolean = true
|
||||
actual val FIELD_PIC: Boolean = false
|
||||
actual val METHOD_PIC: Boolean = false
|
||||
actual val FIELD_PIC_SIZE_4: Boolean = false
|
||||
actual val METHOD_PIC_SIZE_4: Boolean = false
|
||||
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
|
||||
@@ -35,8 +35,8 @@ actual object PerfDefaults {
|
||||
|
||||
actual val PIC_DEBUG_COUNTERS: Boolean = false
|
||||
|
||||
actual val PRIMITIVE_FASTOPS: Boolean = true
|
||||
actual val RVAL_FASTPATH: Boolean = true
|
||||
actual val PRIMITIVE_FASTOPS: Boolean = false
|
||||
actual val RVAL_FASTPATH: Boolean = false
|
||||
|
||||
// Regex caching (JVM-first): enabled by default on JVM
|
||||
actual val REGEX_CACHE: Boolean = true
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.Clock
|
||||
import net.sergeych.lyng.Scope
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.test.Test
|
||||
import kotlin.time.Clock
|
||||
|
||||
suspend fun executeSampleTests(fileName: String) {
|
||||
val sample = withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -15,18 +15,53 @@ fun cached(builder) {
|
||||
value
|
||||
}
|
||||
}
|
||||
/* Filter elements of this iterable using the provided predicate. */
|
||||
fun Iterable.filter(predicate) {
|
||||
/* Filter elements of this iterable using the provided predicate and provide a flow
|
||||
of results. Coudl be used to map infinte flows, etc.
|
||||
*/
|
||||
fun Iterable.filterFlow(predicate): Flow {
|
||||
val list = this
|
||||
flow {
|
||||
for( item in list ) {
|
||||
if( predicate(item) ) {ln
|
||||
if( predicate(item) ) {
|
||||
emit(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Filter this iterable and return List of elements
|
||||
*/
|
||||
fun Iterable.filter(predicate) {
|
||||
val result = []
|
||||
for( item in this ) if( predicate(item) ) result.add(item)
|
||||
result
|
||||
}
|
||||
|
||||
/*
|
||||
Count all items in this iterable for which predicate return true
|
||||
*/
|
||||
fun Iterable.count(predicate): Int {
|
||||
var hits = 0
|
||||
this.forEach {
|
||||
if( predicate(it) ) hits++
|
||||
}
|
||||
hits
|
||||
}
|
||||
/*
|
||||
filter out all null elements from this collection (Iterable); flow of
|
||||
non-null elements is returned
|
||||
*/
|
||||
fun Iterable.filterFlowNotNull(): Flow {
|
||||
filterFlow { it != null }
|
||||
}
|
||||
|
||||
/* Filter non-null elements and collect them into a List
|
||||
*/
|
||||
fun Iterable.filterNotNull(): List {
|
||||
filter { it != null }
|
||||
}
|
||||
|
||||
/* Skip the first N elements of this iterable. */
|
||||
fun Iterable.drop(n) {
|
||||
var cnt = 0
|
||||
@@ -34,14 +69,38 @@ fun Iterable.drop(n) {
|
||||
}
|
||||
|
||||
/* Return the first element or throw if the iterable is empty. */
|
||||
fun Iterable.first() {
|
||||
val Iterable.first get() {
|
||||
val i = iterator()
|
||||
if( !i.hasNext() ) throw NoSuchElementException()
|
||||
i.next().also { i.cancelIteration() }
|
||||
}
|
||||
|
||||
/*
|
||||
Return the first element that matches the predicate or throws
|
||||
NuSuchElementException
|
||||
*/
|
||||
fun Iterable.findFirst(predicate) {
|
||||
for( x in this ) {
|
||||
if( predicate(x) )
|
||||
break x
|
||||
}
|
||||
else throw NoSuchElementException()
|
||||
}
|
||||
|
||||
/*
|
||||
return the first element matching the predicate or null
|
||||
*/
|
||||
fun Iterable.findFirstOrNull(predicate) {
|
||||
for( x in this ) {
|
||||
if( predicate(x) )
|
||||
break x
|
||||
}
|
||||
else null
|
||||
}
|
||||
|
||||
|
||||
/* Return the last element or throw if the iterable is empty. */
|
||||
fun Iterable.last() {
|
||||
val Iterable.last get() {
|
||||
var found = false
|
||||
var element = null
|
||||
for( i in this ) {
|
||||
@@ -72,13 +131,13 @@ fun Iterable.takeLast(n) {
|
||||
buffer
|
||||
}
|
||||
|
||||
/* Join elements into a string with a separator (prefix parameter) and optional transformer. */
|
||||
fun Iterable.joinToString(prefix=" ", transformer=null) {
|
||||
/* Join elements into a string with a separator (separator parameter) and optional transformer. */
|
||||
fun Iterable.joinToString(separator=" ", transformer=null) {
|
||||
var result = null
|
||||
for( part in this ) {
|
||||
val transformed = transformer?(part)?.toString() ?: part.toString()
|
||||
if( result == null ) result = transformed
|
||||
else result += prefix + transformed
|
||||
else result += separator + transformed
|
||||
}
|
||||
result ?: ""
|
||||
}
|
||||
@@ -155,6 +214,26 @@ fun Iterable.shuffled() {
|
||||
toList().apply { shuffle() }
|
||||
}
|
||||
|
||||
/*
|
||||
Returns a single list of all elements from all collections in the given collection.
|
||||
@return List
|
||||
*/
|
||||
fun Iterable.flatten() {
|
||||
val result = []
|
||||
forEach { i ->
|
||||
i.forEach { result.add(it) }
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/*
|
||||
Returns a single list of all elements yielded from results of transform function being
|
||||
invoked on each element of original collection.
|
||||
*/
|
||||
fun Iterable.flatMap(transform): List {
|
||||
map(transform).flatten()
|
||||
}
|
||||
|
||||
/* Return string representation like [a,b,c]. */
|
||||
fun List.toString() {
|
||||
"[" + joinToString(",") + "]"
|
||||
@@ -186,12 +265,14 @@ class StackTraceEntry(
|
||||
/* Print this exception and its stack trace to standard output. */
|
||||
fun Exception.printStackTrace() {
|
||||
println(this)
|
||||
var lastEntry = null
|
||||
for( entry in stackTrace() ) {
|
||||
println("\tat "+entry)
|
||||
if( lastEntry == null || lastEntry !is StackTraceEntry || lastEntry.line != entry.line )
|
||||
println("\tat "+entry.toString())
|
||||
lastEntry = entry
|
||||
}
|
||||
}
|
||||
|
||||
/* Compile this string into a regular expression. */
|
||||
fun String.re(): Regex { Regex(this) }
|
||||
|
||||
val String.re get() = Regex(this)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -366,6 +366,7 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
|
||||
when (d) {
|
||||
is MiniFunDecl -> putName(d.nameStart, d.name, "hl-fn")
|
||||
is MiniClassDecl -> putName(d.nameStart, d.name, "hl-class")
|
||||
is net.sergeych.lyng.miniast.MiniEnumDecl -> putName(d.nameStart, d.name, "hl-class")
|
||||
is net.sergeych.lyng.miniast.MiniValDecl -> putName(d.nameStart, d.name, if (d.mutable) "hl-var" else "hl-val")
|
||||
}
|
||||
}
|
||||
@@ -404,6 +405,7 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
|
||||
}
|
||||
is net.sergeych.lyng.miniast.MiniValDecl -> addTypeSegments(d.type)
|
||||
is MiniClassDecl -> {}
|
||||
is net.sergeych.lyng.miniast.MiniEnumDecl -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +413,7 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
|
||||
try {
|
||||
if (mini != null) {
|
||||
val binding = Binder.bind(text, mini)
|
||||
val symbolsById = binding.symbols.associateBy { it.id }
|
||||
// Map decl ranges to avoid overriding declarations
|
||||
val declKeys = HashSet<Pair<Int, Int>>()
|
||||
for (sym in binding.symbols) {
|
||||
@@ -419,15 +422,15 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
|
||||
fun classForKind(k: SymbolKind): String? = when (k) {
|
||||
SymbolKind.Function -> "hl-fn"
|
||||
SymbolKind.Class, SymbolKind.Enum -> "hl-class"
|
||||
SymbolKind.Param -> "hl-param"
|
||||
SymbolKind.Val -> "hl-val"
|
||||
SymbolKind.Var -> "hl-var"
|
||||
SymbolKind.Parameter -> "hl-param"
|
||||
SymbolKind.Value -> "hl-val"
|
||||
SymbolKind.Variable -> "hl-var"
|
||||
}
|
||||
for (ref in binding.references) {
|
||||
val key = ref.start to ref.end
|
||||
if (declKeys.contains(key)) continue
|
||||
if (!overrides.containsKey(key)) {
|
||||
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||
val sym = symbolsById[ref.symbolId]
|
||||
val cls = sym?.let { classForKind(it.kind) }
|
||||
if (cls != null) overrides[key] = cls
|
||||
}
|
||||
@@ -646,14 +649,14 @@ private fun detectDeclarationAndParamOverrides(text: String): Map<Pair<Int, Int>
|
||||
i = p
|
||||
continue
|
||||
}
|
||||
// Generic function call site: ident followed by '(' (after optional spaces)
|
||||
// Generic function call site: ident followed by '(' or '=' (after optional spaces)
|
||||
readIdent(i)?.let { (name, endIdx) ->
|
||||
val startIdx = i
|
||||
// Avoid keywords; allow member calls too (a.b()) by not checking preceding char
|
||||
if (name !in kw) {
|
||||
var q = skipWs(endIdx)
|
||||
if (q < n && text[q] == '(') {
|
||||
// Mark as function identifier at call site
|
||||
if (q < n && (text[q] == '(' || text[q] == '=')) {
|
||||
// Mark as function identifier at call site or shorthand declaration
|
||||
result[startIdx to endIdx] = "hl-fn"
|
||||
i = endIdx
|
||||
return@let
|
||||
|
||||
@@ -181,7 +181,7 @@ fun DocsPage(
|
||||
scheduled = false
|
||||
try {
|
||||
val heads = toc.mapNotNull { id -> el.querySelector("#${id.id}") as? HTMLHeadingElement }
|
||||
val tops = heads.map { it.getBoundingClientRect().top + window.scrollY }
|
||||
val tops = heads.map { it.getBoundingClientRect().top }
|
||||
val offset = (updateNavbarOffsetVar() + 16).toDouble()
|
||||
val idx = activeIndexForTops(tops, offset)
|
||||
setActiveTocId(if (idx in toc.indices) toc[idx].id else null)
|
||||
|
||||
@@ -36,7 +36,7 @@ fun HomePage() {
|
||||
// Benefits pills
|
||||
listOf(
|
||||
"Clean, familiar syntax",
|
||||
"Immutable-first collections",
|
||||
"both FP and OOP",
|
||||
"Batteries-included standard library",
|
||||
"Embeddable and testable"
|
||||
).forEach { b ->
|
||||
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user