Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65963ce537 | ||
|
|
9d222be924 | ||
|
|
f00631cd88 | ||
|
|
b8d6ff01a6 | ||
|
|
ba725fc9ed | ||
|
|
5d4d548275 | ||
|
|
7e289382ed | ||
|
|
c854b06683 | ||
|
|
e8b630715a | ||
|
|
3481a718b1 | ||
|
|
2696f1546d | ||
|
|
f45fa7f7a0 | ||
|
|
ead2f7168e | ||
|
|
2743511b62 | ||
|
|
8431ab4f96 |
No files matched your search
@@ -1,54 +0,0 @@
|
|||||||
# This workflow uses actions that are not certified by GitHub.
|
|
||||||
# They are provided by a third-party and are governed by
|
|
||||||
# separate terms of service, privacy policy, and support
|
|
||||||
# documentation.
|
|
||||||
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
|
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
|
|
||||||
|
|
||||||
name: Java CI with Gradle
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- target: iosSimulatorArm64Test
|
|
||||||
os: macos-latest
|
|
||||||
- target: jvmTest
|
|
||||||
os: ubuntu-latest
|
|
||||||
- target: linuxX64Test
|
|
||||||
os: ubuntu-latest
|
|
||||||
- target: testDebugUnitTest
|
|
||||||
os: ubuntu-latest
|
|
||||||
- target: testReleaseUnitTest
|
|
||||||
os: ubuntu-latest
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Validate Gradle Wrapper
|
|
||||||
uses: gradle/actions/wrapper-validation@v3
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.konan
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/.lock') }}
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
- name: Build with Gradle
|
|
||||||
uses: gradle/gradle-build-action@ce999babab2de1c4b649dc15f0ee67e6246c994f
|
|
||||||
with:
|
|
||||||
arguments: ${{ matrix.target }}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
name: Publish
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [released, prereleased]
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
name: Release build and publish
|
|
||||||
runs-on: macOS-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Set up JDK 21
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: 21
|
|
||||||
- name: Publish to MavenCentral
|
|
||||||
run: ./gradlew publishToMavenCentral --no-configuration-cache
|
|
||||||
env:
|
|
||||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
|
|
||||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
|
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }}
|
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }}
|
|
||||||
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<!--
|
|
||||||
~ 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="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>
|
|
||||||
<DebugAllEnabled>false</DebugAllEnabled>
|
|
||||||
<RunAsTest>true</RunAsTest>
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
@@ -134,7 +134,19 @@ Designed to add scripting to kotlin multiplatform application in easy and effici
|
|||||||
|
|
||||||
# Language Roadmap
|
# Language Roadmap
|
||||||
|
|
||||||
## v1.0.0
|
```mermaid
|
||||||
|
flowchart
|
||||||
|
v0([the idea])
|
||||||
|
v1([v1: make it happen])
|
||||||
|
v2([v1.5: testmake it fasted and rich featured])
|
||||||
|
v3([v2: ideal script language])
|
||||||
|
v0 --code MVP--> v1
|
||||||
|
v1 --refine and extend--> v2
|
||||||
|
v2 -- optimize --> v3
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## v1.0.0 "Make it happen"
|
||||||
|
|
||||||
Planned autumn 2025. Complete dynamic language with sufficient standard library:
|
Planned autumn 2025. Complete dynamic language with sufficient standard library:
|
||||||
|
|
||||||
@@ -162,23 +174,28 @@ Ready features:
|
|||||||
|
|
||||||
### Under way:
|
### Under way:
|
||||||
|
|
||||||
- [ ] regular exceptions
|
- [ ] regular exceptions + extended `when`
|
||||||
- [ ] multiple inheritance for user classes
|
- [ ] multiple inheritance for user classes
|
||||||
- [ ] site with integrated interpreter to give a try
|
- [ ] site with integrated interpreter to give a try
|
||||||
- [ ] kotlin part public API good docs, integration focused
|
- [ ] kotlin part public API good docs, integration focused
|
||||||
|
|
||||||
## v1.1+
|
## plan: v1.0 - v1.5 "Rich and stable"
|
||||||
|
|
||||||
|
Estimated spring of 2026
|
||||||
|
|
||||||
Planned features.
|
Planned features.
|
||||||
|
|
||||||
- [ ] type specifications
|
- [ ] type specifications
|
||||||
|
- [ ] language server or compose-based lyng-aware editor
|
||||||
- [ ] source docs and maybe lyng.md to a standard
|
- [ ] source docs and maybe lyng.md to a standard
|
||||||
- [ ] metadata first class access from lyng
|
- [ ] metadata first class access from lyng
|
||||||
|
|
||||||
Further
|
## After 1.5 "Ideal scripting"
|
||||||
|
|
||||||
|
Estimated winter 2027
|
||||||
|
|
||||||
- [ ] client with GUI support based on compose multiplatform somehow
|
- [ ] client with GUI support based on compose multiplatform somehow
|
||||||
- [ ] notebook - style workbooks with graphs, formulae, etc.
|
- [ ] notebook - style workbooks with graphs, formulae, etc.
|
||||||
- [ ] language server or compose-based lyng-aware editor
|
- [ ] aggressive optimizations
|
||||||
|
|
||||||
[parallelism]: docs/parallelism.md
|
[parallelism]: docs/parallelism.md
|
||||||
+50
-3
@@ -266,7 +266,7 @@ The same we can provide writable dynamic fields (var-type), adding set method:
|
|||||||
// constant field
|
// constant field
|
||||||
"foo" -> "bar"
|
"foo" -> "bar"
|
||||||
// mutable field
|
// mutable field
|
||||||
"bar" -> setValueForBar
|
"bar" -> storedValueForBar
|
||||||
|
|
||||||
else -> throw SymbolNotFoundException()
|
else -> throw SymbolNotFoundException()
|
||||||
}
|
}
|
||||||
@@ -275,8 +275,8 @@ The same we can provide writable dynamic fields (var-type), adding set method:
|
|||||||
// only 'bar' is mutable:
|
// only 'bar' is mutable:
|
||||||
if( name == "bar" )
|
if( name == "bar" )
|
||||||
storedValueForBar = value
|
storedValueForBar = value
|
||||||
// the rest is immotable. consider throw also
|
// the rest is immotable. consider throw also
|
||||||
// SymbolNotFoundException when needed.
|
// SymbolNotFoundException when needed.
|
||||||
else throw IllegalAssignmentException("Can't assign "+name)
|
else throw IllegalAssignmentException("Can't assign "+name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,9 +289,56 @@ The same we can provide writable dynamic fields (var-type), adding set method:
|
|||||||
assertThrows {
|
assertThrows {
|
||||||
accessor.bad = "!23"
|
accessor.bad = "!23"
|
||||||
}
|
}
|
||||||
|
void
|
||||||
|
>>> void
|
||||||
|
|
||||||
Of course, you can return any object from dynamic fields; returning lambdas let create _dynamic methods_ - the callable method. It is very convenient to implement libraries with dynamic remote interfaces, etc.
|
Of course, you can return any object from dynamic fields; returning lambdas let create _dynamic methods_ - the callable method. It is very convenient to implement libraries with dynamic remote interfaces, etc.
|
||||||
|
|
||||||
|
### Dynamic indexers
|
||||||
|
|
||||||
|
Index access for dynamics is passed to the same getter and setter, so it is
|
||||||
|
generally the same:
|
||||||
|
|
||||||
|
var storedValue = "bar"
|
||||||
|
val x = dynamic {
|
||||||
|
get {
|
||||||
|
if( it == "foo" ) storedValue
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals("bar", x["foo"] )
|
||||||
|
assertEquals("bar", x.foo )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
And assigning them works the same. You can make it working
|
||||||
|
mimicking arrays, but remember, it is not Collection so
|
||||||
|
collection's sugar won't work with it:
|
||||||
|
|
||||||
|
var storedValue = "bar"
|
||||||
|
val x = dynamic {
|
||||||
|
get {
|
||||||
|
when(it) {
|
||||||
|
"size" -> 1
|
||||||
|
0 -> storedValue
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set { index, value ->
|
||||||
|
if( index == 0 ) storedValue = value
|
||||||
|
else throw "Illegal index: "+index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals("bar", x[0] )
|
||||||
|
assertEquals(1, x.size )
|
||||||
|
x[0] = "buzz"
|
||||||
|
assertThrows { x[1] = 1 }
|
||||||
|
assertEquals("buzz", storedValue)
|
||||||
|
assertEquals("buzz", x[0])
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
If you want dynamic to function like an array, create a [feature
|
||||||
|
request](https://gitea.sergeych.net/SergeychWorks/lyng/issues).
|
||||||
|
|
||||||
# Theory
|
# Theory
|
||||||
|
|
||||||
## Basic principles:
|
## Basic principles:
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Regular expressions
|
||||||
|
|
||||||
|
In lyng, you create regular expressions using class `Regex` or `String.re` methods:
|
||||||
|
|
||||||
|
assert( "\d*".re is Regex )
|
||||||
|
assert( Regex("\d*") is Regex )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
We plan to add slash syntax at some point.
|
||||||
|
|
||||||
|
To check that some string matches as whole to some regex:
|
||||||
|
|
||||||
|
assert( "123".matches("\d{3}".re) )
|
||||||
|
assert( !"123".matches("\d{4}".re) )
|
||||||
|
assert( !"1234".matches("\d".re) )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
To check that _part of the string_ matches some regular expession, use _match operator_ `=~` just like in Ruby, and its
|
||||||
|
counterpart, _not match_ operator `!~`:
|
||||||
|
|
||||||
|
assert( "abc123def" =~ "\d\d\d".re )
|
||||||
|
assert( "abc" !~ "\d\d\d".re )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
When you need to find groups, and more detailed match information, use `Regex.find`:
|
||||||
|
|
||||||
|
val result = Regex("abc(\d)(\d)(\d)").find( "bad456 good abc123")
|
||||||
|
assert( result != null )
|
||||||
|
assertEquals( 12 .. 17, result.range )
|
||||||
|
assertEquals( "abc123", result[0] )
|
||||||
|
assertEquals( "1", result[1] )
|
||||||
|
assertEquals( "2", result[2] )
|
||||||
|
assertEquals( "3", result[3] )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Note that the object `RegexMatch`, returned by [Regex.find], behaves much like in many other languages: it provides the
|
||||||
|
index range and groups matches as indexes.
|
||||||
|
|
||||||
|
Match operator actually also provides `RegexMatch` in `$~` reserved variable (borrowed from Ruby too):
|
||||||
|
|
||||||
|
assert( "bad456 good abc123" =~ "abc(\d)(\d)(\d)".re )
|
||||||
|
assertEquals( 12 .. 17, $~.range )
|
||||||
|
assertEquals( "abc123", $~[0] )
|
||||||
|
assertEquals( "1", $~[1] )
|
||||||
|
assertEquals( "2", $~[2] )
|
||||||
|
assertEquals( "3", $~[3] )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
This is often more readable than calling `find`.
|
||||||
|
|
||||||
|
Note that `=~` and `!~` operators against strings and regular expressions are commutative, e.g. regular expression and a
|
||||||
|
string can be either left or right operator, but not both:
|
||||||
|
|
||||||
|
assert( "abc" =~ "\wc".re )
|
||||||
|
assert( "abc" !~ "\w1c".re )
|
||||||
|
assert( "a\wc".re =~ "abcd" )
|
||||||
|
assert( "a[a-z]c".re !~ "a2cd" )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Also, string indexing is Regex-aware, and works like `Regex.find` (_not findall!_):
|
||||||
|
|
||||||
|
assert( "cd" == "abcdef"[ "c.".re ].value )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
|
||||||
|
# Regex class reference
|
||||||
|
|
||||||
|
| name | description | notes |
|
||||||
|
|--------------|-------------------------------------|-------|
|
||||||
|
| matches(str) | true if the whole `str` matches | |
|
||||||
|
| find(str) | find first match in `str` or null | (1) |
|
||||||
|
| findAll(str) | find all matches in `str` as [List] | (1) |
|
||||||
|
|
||||||
|
(1)
|
||||||
|
:: See `RegexMatch` class description below
|
||||||
|
|
||||||
|
# RegexMatch
|
||||||
|
|
||||||
|
| name | description | notes |
|
||||||
|
|-------|-------------------------------------------|-------|
|
||||||
|
| range | the [Range] of the match in source string | |
|
||||||
|
| value | the value that matches | |
|
||||||
|
| [n] | [List] of group matches | (1) |
|
||||||
|
|
||||||
|
(1)
|
||||||
|
:: the [0] element is always value, [1] is group 1 match of any, etc.
|
||||||
|
|
||||||
|
[List]: List.md
|
||||||
|
|
||||||
|
[Range]: Range.md
|
||||||
|
|
||||||
Whitespace-only changes.
@@ -0,0 +1,26 @@
|
|||||||
|
# String
|
||||||
|
|
||||||
|
# This document is for developer notes only
|
||||||
|
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
|
||||||
|
## Interpolation proposal
|
||||||
|
|
||||||
|
"""no $iterpolation"""
|
||||||
|
|
||||||
|
val inpterpolation1 = "foo"
|
||||||
|
"${interpolation1}"
|
||||||
|
>>> "foo"
|
||||||
|
|
||||||
|
"$interpolation2"
|
||||||
|
"no $$ interpolatino"
|
||||||
|
|
||||||
|
|
||||||
|
## Regexp vs div / ?
|
||||||
|
|
||||||
|
```EBNF
|
||||||
|
regex_literal = "/", { regchar }, "/", [ flag ]
|
||||||
|
foag = "i" | "n"....
|
||||||
|
regchar = x`x`
|
||||||
|
```
|
||||||
@@ -21,10 +21,10 @@ import lyng.time
|
|||||||
//
|
//
|
||||||
// After all optimizations it takes ~120ms.
|
// After all optimizations it takes ~120ms.
|
||||||
//
|
//
|
||||||
//for( r in 1..100 ) {
|
for( r in 1..100 ) {
|
||||||
// val start = Instant.now()
|
val start = Instant.now()
|
||||||
val found = naiveCountHappyNumbers()
|
val found = naiveCountHappyNumbers()
|
||||||
// println("Found happy numbers: %d time %s"(found, Instant.now() - start))
|
println("Found happy numbers: %d time %s"(found, Instant.now() - start))
|
||||||
assert( found == 55252 )
|
assert( found == 55252 )
|
||||||
// delay(0.01)
|
delay(0.1)
|
||||||
//}
|
}
|
||||||
+61
-3
@@ -275,6 +275,8 @@ Logical operation could be used the same
|
|||||||
| === | | Any | (2) |
|
| === | | Any | (2) |
|
||||||
| !== | | Any | (2) |
|
| !== | | Any | (2) |
|
||||||
| != | | Any | (1) |
|
| != | | Any | (1) |
|
||||||
|
| =~ | | | (3) |
|
||||||
|
| !~ | | | (3) |
|
||||||
| ++a, a++ | | Int | |
|
| ++a, a++ | | Int | |
|
||||||
| --a, a-- | | Int | |
|
| --a, a-- | | Int | |
|
||||||
|
|
||||||
@@ -286,6 +288,9 @@ Logical operation could be used the same
|
|||||||
singleton object, like `null`, are referentially equal too, while string different literals even being equal are most
|
singleton object, like `null`, are referentially equal too, while string different literals even being equal are most
|
||||||
likely referentially not equal
|
likely referentially not equal
|
||||||
|
|
||||||
|
(3)
|
||||||
|
: Implemented now in String and Regex as regular expression match and not match, see [Regex].
|
||||||
|
|
||||||
Reference quality and object equality example:
|
Reference quality and object equality example:
|
||||||
|
|
||||||
assert( null == null) // singletons
|
assert( null == null) // singletons
|
||||||
@@ -1184,6 +1189,30 @@ See also [math operations](math.md)
|
|||||||
|
|
||||||
The type for the character objects is `Char`.
|
The type for the character objects is `Char`.
|
||||||
|
|
||||||
|
### String literal escapes
|
||||||
|
|
||||||
|
| escape | ASCII value |
|
||||||
|
|--------|-----------------------|
|
||||||
|
| \n | 0x10, newline |
|
||||||
|
| \r | 0x13, carriage return |
|
||||||
|
| \t | 0x07, tabulation |
|
||||||
|
| \\ | \ slash character |
|
||||||
|
| \" | " double quote |
|
||||||
|
|
||||||
|
Other `\c` combinations, where c is any char except mentioned above, are left intact, e.g.:
|
||||||
|
|
||||||
|
val s = "\a"
|
||||||
|
assert(s[0] == '\')
|
||||||
|
assert(s[1] == 'a')
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
same as:
|
||||||
|
|
||||||
|
val s = "\\a"
|
||||||
|
assert(s[0] == '\')
|
||||||
|
assert(s[1] == 'a')
|
||||||
|
>>> void
|
||||||
|
|
||||||
### Char literal escapes
|
### Char literal escapes
|
||||||
|
|
||||||
Are the same as in string literals with little difference:
|
Are the same as in string literals with little difference:
|
||||||
@@ -1191,6 +1220,7 @@ Are the same as in string literals with little difference:
|
|||||||
| escape | ASCII value |
|
| escape | ASCII value |
|
||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
| \n | 0x10, newline |
|
| \n | 0x10, newline |
|
||||||
|
| \r | 0x13, carriage return |
|
||||||
| \t | 0x07, tabulation |
|
| \t | 0x07, tabulation |
|
||||||
| \\ | \ slash character |
|
| \\ | \ slash character |
|
||||||
| \' | ' apostrophe |
|
| \' | ' apostrophe |
|
||||||
@@ -1260,9 +1290,26 @@ Open-ended ranges could be used to get start and end too:
|
|||||||
assertEquals( "pult", "catapult"[ 4.. ])
|
assertEquals( "pult", "catapult"[ 4.. ])
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
|
||||||
### String operations
|
### String operations
|
||||||
|
|
||||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
||||||
|
[Regex] support for strings, see the link, for example, whole string match:
|
||||||
|
|
||||||
|
assert( !"123".matches( "\d\d".re ) )
|
||||||
|
assert( "123".matches( "\d\d\d".re ) )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Extraction:
|
||||||
|
|
||||||
|
"abcd42def"[ "\d+".re ].value
|
||||||
|
>>> "42"
|
||||||
|
|
||||||
|
Part match:
|
||||||
|
|
||||||
|
assert( "abc foo def" =~ "f[oO]+".re )
|
||||||
|
assert( "foo" == $~.value )
|
||||||
|
>>> void
|
||||||
|
|
||||||
Typical set of String functions includes:
|
Typical set of String functions includes:
|
||||||
|
|
||||||
@@ -1280,17 +1327,24 @@ Typical set of String functions includes:
|
|||||||
| size | size in characters like `length` because String is [Array] |
|
| size | size in characters like `length` because String is [Array] |
|
||||||
| (args...) | sprintf-like formatting, see [string formatting] |
|
| (args...) | sprintf-like formatting, see [string formatting] |
|
||||||
| [index] | character at index |
|
| [index] | character at index |
|
||||||
| [Range] | substring at range |
|
| [Range] | substring at range (2) |
|
||||||
|
| [Regex] | find first match of regex, like [Regex.find] (2) |
|
||||||
| s1 + s2 | concatenation |
|
| s1 + s2 | concatenation |
|
||||||
| s1 += s2 | self-modifying concatenation |
|
| s1 += s2 | self-modifying concatenation |
|
||||||
| toReal() | attempts to parse string as a Real value |
|
| toReal() | attempts to parse string as a Real value |
|
||||||
| toInt() | parse string to Int value |
|
| toInt() | parse string to Int value |
|
||||||
| characters() | create [List] of characters (1) |
|
| characters() | create [List] of characters (1) |
|
||||||
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
||||||
|
| matches(re) | matches the regular expression (2) |
|
||||||
|
| | |
|
||||||
|
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
: List is mutable therefore a new copy is created on each call.
|
: List is mutable therefore a new copy is created on each call.
|
||||||
|
|
||||||
|
(2)
|
||||||
|
: See [Regex]
|
||||||
|
|
||||||
### Literals
|
### Literals
|
||||||
|
|
||||||
String literal could be multiline:
|
String literal could be multiline:
|
||||||
@@ -1349,7 +1403,7 @@ See [math functions](math.md). Other general purpose functions are:
|
|||||||
|
|
||||||
[Range]: Range.md
|
[Range]: Range.md
|
||||||
|
|
||||||
[String]: String.md
|
[String]: development/String.md
|
||||||
|
|
||||||
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
|
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
|
||||||
|
|
||||||
@@ -1364,3 +1418,7 @@ See [math functions](math.md). Other general purpose functions are:
|
|||||||
[RingBuffer]: RingBuffer.md
|
[RingBuffer]: RingBuffer.md
|
||||||
|
|
||||||
[Collection]: Collection.md
|
[Collection]: Collection.md
|
||||||
|
|
||||||
|
[Array]: Array.md
|
||||||
|
|
||||||
|
[Regex]: Regex.md
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.5.2"
|
agp = "8.5.2"
|
||||||
clikt = "5.0.3"
|
clikt = "5.0.3"
|
||||||
kotlin = "2.1.21"
|
kotlin = "2.2.20"
|
||||||
android-minSdk = "24"
|
android-minSdk = "24"
|
||||||
android-compileSdk = "34"
|
android-compileSdk = "34"
|
||||||
kotlinx-coroutines = "1.9.0"
|
kotlinx-coroutines = "1.10.2"
|
||||||
mp_bintools = "0.1.12"
|
mp_bintools = "0.1.12"
|
||||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||||
okioVersion = "3.10.2"
|
okioVersion = "3.10.2"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.1.21"
|
kotlin("multiplatform") version "2.2.20"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.8.15-SNAPSHOT"
|
version = "0.9.3"
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
@@ -37,7 +37,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.androidLibrary)
|
alias(libs.plugins.androidLibrary)
|
||||||
// alias(libs.plugins.vanniktech.mavenPublish)
|
// alias(libs.plugins.vanniktech.mavenPublish)
|
||||||
kotlin("plugin.serialization") version "2.2.0"
|
kotlin("plugin.serialization") version "2.1.0"
|
||||||
id("com.codingfeline.buildkonfig") version "0.17.1"
|
id("com.codingfeline.buildkonfig") version "0.17.1"
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
@@ -62,10 +62,14 @@ kotlin {
|
|||||||
jvmTarget.set(JvmTarget.JVM_11)
|
jvmTarget.set(JvmTarget.JVM_11)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// iosX64()
|
iosX64()
|
||||||
// iosArm64()
|
iosArm64()
|
||||||
// iosSimulatorArm64()
|
iosSimulatorArm64()
|
||||||
|
macosX64()
|
||||||
|
macosArm64()
|
||||||
|
mingwX64()
|
||||||
linuxX64()
|
linuxX64()
|
||||||
|
linuxArm64()
|
||||||
js {
|
js {
|
||||||
browser()
|
browser()
|
||||||
nodejs()
|
nodejs()
|
||||||
@@ -79,9 +83,9 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
all {
|
all {
|
||||||
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||||
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
|
|
||||||
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
||||||
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
|
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
|
||||||
|
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
|
||||||
languageSettings.optIn("kotlinx.coroutines.flow.DelicateCoroutinesApi")
|
languageSettings.optIn("kotlinx.coroutines.flow.DelicateCoroutinesApi")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -796,7 +796,7 @@ class Compiler(
|
|||||||
val v = left.getter(context)
|
val v = left.getter(context)
|
||||||
if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly
|
if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly
|
||||||
v.value.callOn(
|
v.value.callOn(
|
||||||
context.copy(
|
context.createChildScope(
|
||||||
context.pos,
|
context.pos,
|
||||||
args.toArguments(context, detectedBlockArgument)
|
args.toArguments(context, detectedBlockArgument)
|
||||||
// Arguments(
|
// Arguments(
|
||||||
@@ -888,6 +888,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("SameParameterValue")
|
||||||
private fun parseNumber(isPlus: Boolean): Obj {
|
private fun parseNumber(isPlus: Boolean): Obj {
|
||||||
return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number")
|
return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number")
|
||||||
}
|
}
|
||||||
@@ -901,7 +902,7 @@ class Compiler(
|
|||||||
val args = extras?.let { required + it } ?: required
|
val args = extras?.let { required + it } ?: required
|
||||||
val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}")
|
val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}")
|
||||||
if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}")
|
if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}")
|
||||||
(fn.execute(scope.copy(Arguments(args))) as? Statement)
|
(fn.execute(scope.createChildScope(Arguments(args))) as? Statement)
|
||||||
?: scope.raiseClassCastError("function annotation must return callable")
|
?: scope.raiseClassCastError("function annotation must return callable")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1000,6 +1001,7 @@ class Compiler(
|
|||||||
// condition could be a value, in and is clauses:
|
// condition could be a value, in and is clauses:
|
||||||
// parse several conditions for one then clause
|
// parse several conditions for one then clause
|
||||||
|
|
||||||
|
|
||||||
// loop cases
|
// loop cases
|
||||||
outer@ while (true) {
|
outer@ while (true) {
|
||||||
|
|
||||||
@@ -1195,7 +1197,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exceptionObject != null) {
|
if (exceptionObject != null) {
|
||||||
val catchContext = this.copy(pos = cdata.catchVar.pos)
|
val catchContext = this.createChildScope(pos = cdata.catchVar.pos)
|
||||||
catchContext.addItem(cdata.catchVar.value, false, objException)
|
catchContext.addItem(cdata.catchVar.value, false, objException)
|
||||||
result = cdata.block.execute(catchContext)
|
result = cdata.block.execute(catchContext)
|
||||||
isCaught = true
|
isCaught = true
|
||||||
@@ -1311,7 +1313,7 @@ class Compiler(
|
|||||||
// accessors, constructor registration, etc.
|
// accessors, constructor registration, etc.
|
||||||
addItem(className, false, newClass)
|
addItem(className, false, newClass)
|
||||||
if (initScope.isNotEmpty()) {
|
if (initScope.isNotEmpty()) {
|
||||||
val classScope = copy(newThisObj = newClass)
|
val classScope = createChildScope(newThisObj = newClass)
|
||||||
newClass.classScope = classScope
|
newClass.classScope = classScope
|
||||||
for (s in initScope)
|
for (s in initScope)
|
||||||
s.execute(classScope)
|
s.execute(classScope)
|
||||||
@@ -1365,7 +1367,7 @@ class Compiler(
|
|||||||
|
|
||||||
|
|
||||||
return statement(body.pos) { cxt ->
|
return statement(body.pos) { cxt ->
|
||||||
val forContext = cxt.copy(start)
|
val forContext = cxt.createChildScope(start)
|
||||||
|
|
||||||
// loop var: StoredObject
|
// loop var: StoredObject
|
||||||
val loopSO = forContext.addItem(tVar.value, true, ObjNull)
|
val loopSO = forContext.addItem(tVar.value, true, ObjNull)
|
||||||
@@ -1376,9 +1378,16 @@ class Compiler(
|
|||||||
if (sourceObj is ObjRange && sourceObj.isIntRange) {
|
if (sourceObj is ObjRange && sourceObj.isIntRange) {
|
||||||
loopIntRange(
|
loopIntRange(
|
||||||
forContext,
|
forContext,
|
||||||
sourceObj.start!!.toInt(),
|
sourceObj.start!!.toLong(),
|
||||||
if (sourceObj.isEndInclusive) sourceObj.end!!.toInt() + 1 else sourceObj.end!!.toInt(),
|
if (sourceObj.isEndInclusive)
|
||||||
loopSO, body, elseStatement, label, canBreak
|
sourceObj.end!!.toLong() + 1
|
||||||
|
else
|
||||||
|
sourceObj.end!!.toLong(),
|
||||||
|
loopSO,
|
||||||
|
body,
|
||||||
|
elseStatement,
|
||||||
|
label,
|
||||||
|
canBreak
|
||||||
)
|
)
|
||||||
} else if (sourceObj.isInstanceOf(ObjIterable)) {
|
} else if (sourceObj.isInstanceOf(ObjIterable)) {
|
||||||
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
|
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
|
||||||
@@ -1439,7 +1448,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loopIntRange(
|
private suspend fun loopIntRange(
|
||||||
forScope: Scope, start: Int, end: Int, loopVar: ObjRecord,
|
forScope: Scope, start: Long, end: Long, loopVar: ObjRecord,
|
||||||
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
|
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
|
||||||
): Obj {
|
): Obj {
|
||||||
var result: Obj = ObjVoid
|
var result: Obj = ObjVoid
|
||||||
@@ -1447,7 +1456,7 @@ class Compiler(
|
|||||||
loopVar.value = iVar
|
loopVar.value = iVar
|
||||||
if (catchBreak) {
|
if (catchBreak) {
|
||||||
for (i in start..<end) {
|
for (i in start..<end) {
|
||||||
iVar.value = i.toLong()
|
iVar.value = i//.toLong()
|
||||||
try {
|
try {
|
||||||
result = body.execute(forScope)
|
result = body.execute(forScope)
|
||||||
} catch (lbe: LoopBreakContinueException) {
|
} catch (lbe: LoopBreakContinueException) {
|
||||||
@@ -1459,7 +1468,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (i in start.toLong()..<end.toLong()) {
|
for (i in start..<end) {
|
||||||
iVar.value = i
|
iVar.value = i
|
||||||
result = body.execute(forScope)
|
result = body.execute(forScope)
|
||||||
}
|
}
|
||||||
@@ -1527,7 +1536,7 @@ class Compiler(
|
|||||||
var result: Obj = ObjVoid
|
var result: Obj = ObjVoid
|
||||||
lateinit var doScope: Scope
|
lateinit var doScope: Scope
|
||||||
do {
|
do {
|
||||||
doScope = it.copy().apply { skipScopeCreation = true }
|
doScope = it.createChildScope().apply { skipScopeCreation = true }
|
||||||
try {
|
try {
|
||||||
result = body.execute(doScope)
|
result = body.execute(doScope)
|
||||||
} catch (e: LoopBreakContinueException) {
|
} catch (e: LoopBreakContinueException) {
|
||||||
@@ -1673,7 +1682,7 @@ class Compiler(
|
|||||||
|
|
||||||
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||||
// could be else block:
|
// could be else block:
|
||||||
val t2 = cc.next()
|
val t2 = cc.nextNonWhitespace()
|
||||||
|
|
||||||
// we generate different statements: optimization
|
// we generate different statements: optimization
|
||||||
return if (t2.type == Token.Type.ID && t2.value == "else") {
|
return if (t2.type == Token.Type.ID && t2.value == "else") {
|
||||||
@@ -1807,7 +1816,7 @@ class Compiler(
|
|||||||
val block = parseScript()
|
val block = parseScript()
|
||||||
return statement(startPos) {
|
return statement(startPos) {
|
||||||
// block run on inner context:
|
// block run on inner context:
|
||||||
block.execute(if (it.skipScopeCreation) it else it.copy(startPos))
|
block.execute(if (it.skipScopeCreation) it else it.createChildScope(startPos))
|
||||||
}.also {
|
}.also {
|
||||||
val t1 = cc.next()
|
val t1 = cc.next()
|
||||||
if (t1.type != Token.Type.RBRACE)
|
if (t1.type != Token.Type.RBRACE)
|
||||||
@@ -2000,6 +2009,8 @@ class Compiler(
|
|||||||
Operator.simple(Token.Type.NEQ, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
|
Operator.simple(Token.Type.NEQ, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
|
||||||
Operator.simple(Token.Type.REF_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
|
Operator.simple(Token.Type.REF_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
|
||||||
Operator.simple(Token.Type.REF_NEQ, lastPriority) { _, a, b -> ObjBool(a !== b) },
|
Operator.simple(Token.Type.REF_NEQ, lastPriority) { _, a, b -> ObjBool(a !== b) },
|
||||||
|
Operator.simple(Token.Type.MATCH, lastPriority) { s, a, b -> a.operatorMatch(s,b) },
|
||||||
|
Operator.simple(Token.Type.NOTMATCH, lastPriority) { s, a, b -> a.operatorNotMatch(s,b) },
|
||||||
// relational <=,... 5
|
// relational <=,... 5
|
||||||
Operator.simple(Token.Type.LTE, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
|
Operator.simple(Token.Type.LTE, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
|
||||||
Operator.simple(Token.Type.LT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) },
|
Operator.simple(Token.Type.LT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) },
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ class CompilerContext(val tokens: List<Token>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
inline fun addBreak() {
|
inline fun addBreak() {
|
||||||
breakFound = true
|
breakFound = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ package net.sergeych.lyng
|
|||||||
val digitsSet = ('0'..'9').toSet()
|
val digitsSet = ('0'..'9').toSet()
|
||||||
val digits = { d: Char -> d in digitsSet }
|
val digits = { d: Char -> d in digitsSet }
|
||||||
val hexDigits = digitsSet + ('a'..'f') + ('A'..'F')
|
val hexDigits = digitsSet + ('a'..'f') + ('A'..'F')
|
||||||
val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit() }
|
val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit() || d == '$' || d == '~' }
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val idFirstChars = { d: Char -> d.isLetter() || d == '_' }
|
val idFirstChars = { d: Char -> d.isLetter() || d == '_' || d == '$' }
|
||||||
|
|
||||||
fun parseLyng(source: Source): List<Token> {
|
fun parseLyng(source: Source): List<Token> {
|
||||||
val p = Parser(fromPos = source.startPos)
|
val p = Parser(fromPos = source.startPos)
|
||||||
@@ -67,13 +67,16 @@ private class Parser(fromPos: Pos) {
|
|||||||
pos.advance()
|
pos.advance()
|
||||||
Token("===", from, Token.Type.REF_EQ)
|
Token("===", from, Token.Type.REF_EQ)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Token("==", from, Token.Type.EQ)
|
else -> Token("==", from, Token.Type.EQ)
|
||||||
}
|
}
|
||||||
} else if( currentChar == '>' ) {
|
} else if (currentChar == '>') {
|
||||||
pos.advance()
|
pos.advance()
|
||||||
Token("=>", from, Token.Type.EQARROW)
|
Token("=>", from, Token.Type.EQARROW)
|
||||||
}
|
} else if (currentChar == '~') {
|
||||||
else
|
pos.advance()
|
||||||
|
Token("=~", from, Token.Type.MATCH)
|
||||||
|
} else
|
||||||
Token("=", from, Token.Type.ASSIGN)
|
Token("=", from, Token.Type.ASSIGN)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +230,9 @@ private class Parser(fromPos: Pos) {
|
|||||||
Token("!==", from, Token.Type.REF_NEQ)
|
Token("!==", from, Token.Type.REF_NEQ)
|
||||||
} else
|
} else
|
||||||
Token("!=", from, Token.Type.NEQ)
|
Token("!=", from, Token.Type.NEQ)
|
||||||
|
} else if (currentChar == '~') {
|
||||||
|
pos.advance()
|
||||||
|
Token("!~", from, Token.Type.NOTMATCH)
|
||||||
} else
|
} else
|
||||||
Token("!", from, Token.Type.NOT)
|
Token("!", from, Token.Type.NOT)
|
||||||
}
|
}
|
||||||
@@ -267,7 +273,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
|
|
||||||
in digitsSet -> {
|
in digitsSet -> {
|
||||||
pos.back()
|
pos.back()
|
||||||
decodeNumber(loadChars { it in digitsSet || it == '_'}, from)
|
decodeNumber(loadChars { it in digitsSet || it == '_' }, from)
|
||||||
}
|
}
|
||||||
|
|
||||||
'\'' -> {
|
'\'' -> {
|
||||||
@@ -291,7 +297,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
'?' -> {
|
'?' -> {
|
||||||
when(currentChar.also { pos.advance() }) {
|
when (currentChar.also { pos.advance() }) {
|
||||||
':' -> Token("??", from, Token.Type.ELVIS)
|
':' -> Token("??", from, Token.Type.ELVIS)
|
||||||
'?' -> Token("??", from, Token.Type.ELVIS)
|
'?' -> Token("??", from, Token.Type.ELVIS)
|
||||||
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
|
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
|
||||||
@@ -310,7 +316,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
// Labels processing is complicated!
|
// Labels processing is complicated!
|
||||||
// some@ statement: label 'some', ID 'statement'
|
// some@ statement: label 'some', ID 'statement'
|
||||||
// statement@some: ID 'statement', LABEL 'some'!
|
// statement@some: ID 'statement', LABEL 'some'!
|
||||||
if (ch.isLetter() || ch == '_') {
|
if (idNextChars(ch)) {
|
||||||
val text = ch + loadChars(idNextChars)
|
val text = ch + loadChars(idNextChars)
|
||||||
if (currentChar == '@') {
|
if (currentChar == '@') {
|
||||||
pos.advance()
|
pos.advance()
|
||||||
@@ -395,25 +401,24 @@ private class Parser(fromPos: Pos) {
|
|||||||
private fun fixMultilineStringLiteral(source: String): String {
|
private fun fixMultilineStringLiteral(source: String): String {
|
||||||
val sizes = mutableListOf<Int>()
|
val sizes = mutableListOf<Int>()
|
||||||
val lines = source.lines().toMutableList()
|
val lines = source.lines().toMutableList()
|
||||||
if( lines.size == 0 ) return ""
|
if (lines.size == 0) return ""
|
||||||
if( lines[0].isBlank() ) lines.removeFirst()
|
if (lines[0].isBlank()) lines.removeFirst()
|
||||||
if( lines.isEmpty()) return ""
|
if (lines.isEmpty()) return ""
|
||||||
if( lines.last().isBlank() ) lines.removeLast()
|
if (lines.last().isBlank()) lines.removeLast()
|
||||||
|
|
||||||
val normalized = lines.map { l ->
|
val normalized = lines.map { l ->
|
||||||
if( l.isBlank() ) {
|
if (l.isBlank()) {
|
||||||
sizes.add(-1)
|
sizes.add(-1)
|
||||||
""
|
""
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
val margin = leftMargin(l)
|
val margin = leftMargin(l)
|
||||||
sizes += margin
|
sizes += margin
|
||||||
" ".repeat(margin) + l.trim()
|
" ".repeat(margin) + l.trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonMargin = sizes.filter { it >= 0 }.min()
|
val commonMargin = sizes.filter { it >= 0 }.min()
|
||||||
val fixed = if( commonMargin < 1 ) lines else normalized.map {
|
val fixed = if (commonMargin < 1) lines else normalized.map {
|
||||||
if( it.isBlank() ) "" else it.drop(commonMargin)
|
if (it.isBlank()) "" else it.drop(commonMargin)
|
||||||
}
|
}
|
||||||
return fixed.joinToString("\n")
|
return fixed.joinToString("\n")
|
||||||
}
|
}
|
||||||
@@ -433,15 +438,34 @@ private class Parser(fromPos: Pos) {
|
|||||||
'\\' -> {
|
'\\' -> {
|
||||||
pos.advance() ?: raise("unterminated string")
|
pos.advance() ?: raise("unterminated string")
|
||||||
when (currentChar) {
|
when (currentChar) {
|
||||||
'n' -> {sb.append('\n'); pos.advance()}
|
'n' -> {
|
||||||
'r' -> {sb.append('\r'); pos.advance()}
|
sb.append('\n'); pos.advance()
|
||||||
't' -> {sb.append('\t'); pos.advance()}
|
}
|
||||||
'"' -> {sb.append('"'); pos.advance()}
|
|
||||||
else -> sb.append('\\').append(currentChar)
|
'r' -> {
|
||||||
|
sb.append('\r'); pos.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
't' -> {
|
||||||
|
sb.append('\t'); pos.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
'"' -> {
|
||||||
|
sb.append('"'); pos.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
'\\' -> {
|
||||||
|
sb.append('\\'); pos.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
sb.append('\\').append(currentChar)
|
||||||
|
pos.advance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
'\n', '\r'-> {
|
'\n', '\r' -> {
|
||||||
newlineDetected = true
|
newlineDetected = true
|
||||||
sb.append(currentChar)
|
sb.append(currentChar)
|
||||||
pos.advance()
|
pos.advance()
|
||||||
@@ -455,7 +479,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
}
|
}
|
||||||
pos.advance()
|
pos.advance()
|
||||||
|
|
||||||
val result = sb.toString().let { if( newlineDetected ) fixMultilineStringLiteral(it) else it }
|
val result = sb.toString().let { if (newlineDetected) fixMultilineStringLiteral(it) else it }
|
||||||
|
|
||||||
return Token(result, start, Token.Type.STRING)
|
return Token(result, start, Token.Type.STRING)
|
||||||
}
|
}
|
||||||
@@ -534,7 +558,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
// skip shebang
|
// skip shebang
|
||||||
if( pos.readFragment("#!") )
|
if (pos.readFragment("#!"))
|
||||||
loadToEndOfLine()
|
loadToEndOfLine()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ data class Pos(val source: Source, val line: Int, val column: Int) {
|
|||||||
else if( line > 0) Pos(source, line-1, source.lines[line-1].length - 1)
|
else if( line > 0) Pos(source, line-1, source.lines[line-1].length - 1)
|
||||||
else throw IllegalStateException("can't go back from line 0, column 0")
|
else throw IllegalStateException("can't go back from line 0, column 0")
|
||||||
|
|
||||||
val currentLine: String get() = if( end ) "EOF" else source.lines[line]
|
val currentLine: String get() =
|
||||||
|
if( end ) "EOF"
|
||||||
|
else if( line >= 0 ) source.lines[line] else "<no line information>"
|
||||||
|
|
||||||
val end: Boolean get() = line >= source.lines.size
|
val end: Boolean get() = line >= source.lines.size
|
||||||
|
|
||||||
|
|||||||
@@ -136,20 +136,29 @@ open class Scope(
|
|||||||
?: thisObj.objClass
|
?: thisObj.objClass
|
||||||
.getInstanceMemberOrNull(name)
|
.getInstanceMemberOrNull(name)
|
||||||
)
|
)
|
||||||
// ?.also {
|
|
||||||
// if( name == "predicate") {
|
|
||||||
// println("got predicate $it")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
/**
|
||||||
|
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
||||||
|
*/
|
||||||
|
fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||||
Scope(this, args, pos, newThisObj ?: thisObj)
|
Scope(this, args, pos, newThisObj ?: thisObj)
|
||||||
|
|
||||||
fun copy(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
/**
|
||||||
|
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
||||||
|
* The child scope inherits the current scope's properties such as position and the existing `thisObj` if no new `thisObj` is provided.
|
||||||
|
*
|
||||||
|
* @param args The arguments to associate with the child scope. Defaults to [Arguments.EMPTY].
|
||||||
|
* @param newThisObj The new `thisObj` to associate with the child scope. Defaults to the current scope's `thisObj` if not provided.
|
||||||
|
* @return A new instance of [Scope] initialized with the specified arguments and `thisObj`.
|
||||||
|
*/
|
||||||
|
fun createChildScope(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||||
Scope(this, args, pos, newThisObj ?: thisObj)
|
Scope(this, args, pos, newThisObj ?: thisObj)
|
||||||
|
|
||||||
fun copy() = Scope(this, args, pos, thisObj)
|
/**
|
||||||
|
* @return A child scope with the same arguments, position and [thisObj]
|
||||||
|
*/
|
||||||
|
fun createChildScope() = Scope(this, args, pos, thisObj)
|
||||||
|
|
||||||
fun addItem(
|
fun addItem(
|
||||||
name: String,
|
name: String,
|
||||||
@@ -245,8 +254,6 @@ open class Scope(
|
|||||||
p = p.parent
|
p = p.parent
|
||||||
}
|
}
|
||||||
println("--------------------")
|
println("--------------------")
|
||||||
ObjVoid
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun applyClosure(closure: Scope): Scope = ClosureScope(this, closure)
|
open fun applyClosure(closure: Scope): Scope = ClosureScope(this, closure)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import net.sergeych.lynon.ObjLynonClass
|
|||||||
import net.sergeych.mp_tools.globalDefer
|
import net.sergeych.mp_tools.globalDefer
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
|
@Suppress("TYPE_INTERSECTION_AS_REIFIED_WARNING")
|
||||||
class Script(
|
class Script(
|
||||||
override val pos: Pos,
|
override val pos: Pos,
|
||||||
private val statements: List<Statement> = emptyList(),
|
private val statements: List<Statement> = emptyList(),
|
||||||
@@ -182,7 +183,7 @@ class Script(
|
|||||||
catch( e: ExecutionError ) {
|
catch( e: ExecutionError ) {
|
||||||
e.errorObject
|
e.errorObject
|
||||||
}
|
}
|
||||||
catch (e: ScriptError) {
|
catch (_: ScriptError) {
|
||||||
ObjNull
|
ObjNull
|
||||||
}
|
}
|
||||||
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
|
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
|
||||||
@@ -241,6 +242,8 @@ class Script(
|
|||||||
addConst("CompletableDeferred", ObjCompletableDeferred.type)
|
addConst("CompletableDeferred", ObjCompletableDeferred.type)
|
||||||
addConst("Mutex", ObjMutex.type)
|
addConst("Mutex", ObjMutex.type)
|
||||||
|
|
||||||
|
addConst("Regex", ObjRegex.type)
|
||||||
|
|
||||||
addFn("launch") {
|
addFn("launch") {
|
||||||
val callable = requireOnlyArg<Statement>()
|
val callable = requireOnlyArg<Statement>()
|
||||||
ObjDeferred(globalDefer {
|
ObjDeferred(globalDefer {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable?
|
|||||||
$pos: Error: $errorMessage
|
$pos: Error: $errorMessage
|
||||||
|
|
||||||
${pos.currentLine}
|
${pos.currentLine}
|
||||||
${"-".repeat(pos.column)}^
|
${if( pos.column >= 0 ) "-".repeat(pos.column) + "^" else ""}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
cause
|
cause
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
enum class Type {
|
enum class Type {
|
||||||
ID, INT, REAL, HEX, STRING, CHAR,
|
ID, INT, REAL, HEX, STRING, STRING2, REGEX, CHAR,
|
||||||
LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA,
|
LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA,
|
||||||
SEMICOLON, COLON,
|
SEMICOLON, COLON,
|
||||||
PLUS, MINUS, STAR, SLASH, PERCENT,
|
PLUS, MINUS, STAR, SLASH, PERCENT,
|
||||||
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
||||||
PLUS2, MINUS2,
|
PLUS2, MINUS2,
|
||||||
IN, NOTIN, IS, NOTIS,
|
IN, NOTIN, IS, NOTIS,
|
||||||
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ,
|
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
|
||||||
SHUTTLE,
|
SHUTTLE,
|
||||||
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
||||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||||
|
|||||||
@@ -17,19 +17,12 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import net.sergeych.bintools.encodeToHex
|
|
||||||
import net.sergeych.lyng.*
|
import net.sergeych.lyng.*
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
import net.sergeych.mptools.CachedExpression
|
|
||||||
import net.sergeych.synctools.ProtectedOp
|
|
||||||
import net.sergeych.synctools.withLock
|
|
||||||
import kotlin.contracts.ExperimentalContracts
|
|
||||||
|
|
||||||
open class Obj {
|
open class Obj {
|
||||||
|
|
||||||
@@ -43,12 +36,12 @@ open class Obj {
|
|||||||
|
|
||||||
var isFrozen: Boolean = false
|
var isFrozen: Boolean = false
|
||||||
|
|
||||||
private val monitor = Mutex()
|
// private val monitor = Mutex()
|
||||||
|
|
||||||
// private val memberMutex = Mutex()
|
// private val memberMutex = Mutex()
|
||||||
internal var parentInstances: MutableList<Obj> = mutableListOf()
|
// internal var parentInstances: MutableList<Obj> = mutableListOf()
|
||||||
|
|
||||||
private val opInstances = ProtectedOp()
|
// private val opInstances = ProtectedOp()
|
||||||
|
|
||||||
open suspend fun inspect(scope: Scope): String = toString(scope).value
|
open suspend fun inspect(scope: Scope): String = toString(scope).value
|
||||||
|
|
||||||
@@ -104,7 +97,7 @@ open class Obj {
|
|||||||
// note that getInstanceMember traverses the hierarchy
|
// note that getInstanceMember traverses the hierarchy
|
||||||
objClass.getInstanceMember(scope.pos, name).value
|
objClass.getInstanceMember(scope.pos, name).value
|
||||||
|
|
||||||
fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value
|
// fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value
|
||||||
|
|
||||||
// methods that to override
|
// methods that to override
|
||||||
|
|
||||||
@@ -182,6 +175,14 @@ open class Obj {
|
|||||||
scope.raiseNotImplemented()
|
scope.raiseNotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
|
||||||
|
scope.raiseNotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun operatorNotMatch(scope: Scope, other: Obj): Obj {
|
||||||
|
return operatorMatch(scope,other).logicalNot(scope)
|
||||||
|
}
|
||||||
|
|
||||||
open suspend fun assign(scope: Scope, other: Obj): Obj? = null
|
open suspend fun assign(scope: Scope, other: Obj): Obj? = null
|
||||||
|
|
||||||
open fun getValue(scope: Scope) = this
|
open fun getValue(scope: Scope) = this
|
||||||
@@ -228,14 +229,14 @@ open class Obj {
|
|||||||
if (isFrozen) scope.raiseError("attempt to mutate frozen object")
|
if (isFrozen) scope.raiseError("attempt to mutate frozen object")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
|
// suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
|
||||||
|
|
||||||
open suspend fun readField(scope: Scope, name: String): ObjRecord {
|
open suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
// could be property or class field:
|
// could be property or class field:
|
||||||
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
|
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
|
||||||
return when (val value = obj.value) {
|
return when (val value = obj.value) {
|
||||||
is Statement -> {
|
is Statement -> {
|
||||||
ObjRecord(value.execute(scope.copy(scope.pos, newThisObj = this)), obj.isMutable)
|
ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
|
||||||
}
|
}
|
||||||
// could be writable property naturally
|
// could be writable property naturally
|
||||||
// null -> ObjNull.asReadonly
|
// null -> ObjNull.asReadonly
|
||||||
@@ -264,11 +265,11 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj =
|
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj =
|
||||||
callOn(scope.copy(scope.pos, args = args, newThisObj = thisObj))
|
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj))
|
||||||
|
|
||||||
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
|
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
|
||||||
callOn(
|
callOn(
|
||||||
scope.copy(
|
scope.createChildScope(
|
||||||
scope.pos,
|
scope.pos,
|
||||||
args = Arguments(args.toList()),
|
args = Arguments(args.toList()),
|
||||||
newThisObj = thisObj
|
newThisObj = thisObj
|
||||||
@@ -277,7 +278,7 @@ open class Obj {
|
|||||||
|
|
||||||
suspend fun invoke(scope: Scope, thisObj: Obj): Obj =
|
suspend fun invoke(scope: Scope, thisObj: Obj): Obj =
|
||||||
callOn(
|
callOn(
|
||||||
scope.copy(
|
scope.createChildScope(
|
||||||
scope.pos,
|
scope.pos,
|
||||||
args = Arguments.EMPTY,
|
args = Arguments.EMPTY,
|
||||||
newThisObj = thisObj
|
newThisObj = thisObj
|
||||||
@@ -285,7 +286,7 @@ open class Obj {
|
|||||||
)
|
)
|
||||||
|
|
||||||
suspend fun invoke(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
|
suspend fun invoke(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
|
||||||
callOn(scope.copy(atPos, args = args, newThisObj = thisObj))
|
callOn(scope.createChildScope(atPos, args = args, newThisObj = thisObj))
|
||||||
|
|
||||||
|
|
||||||
val asReadonly: ObjRecord by lazy { ObjRecord(this, false) }
|
val asReadonly: ObjRecord by lazy { ObjRecord(this, false) }
|
||||||
@@ -298,13 +299,25 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun autoInstanceScope(parent: Scope): Scope {
|
fun autoInstanceScope(parent: Scope): Scope {
|
||||||
val scope = parent.copy(newThisObj = this, args = parent.args)
|
val scope = parent.createChildScope(newThisObj = this, args = parent.args)
|
||||||
for (m in objClass.members) {
|
for (m in objClass.members) {
|
||||||
scope.objects[m.key] = m.value
|
scope.objects[m.key] = m.value
|
||||||
}
|
}
|
||||||
return scope
|
return scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified R: Obj> cast(scope: Scope): R {
|
||||||
|
castOrNull<R>()?.let { return it }
|
||||||
|
scope.raiseClassCastError("can't cast ${this::class.simpleName} to ${R::class.simpleName}")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified R: Obj> castOrNull(): R? {
|
||||||
|
(this as? R)?.let { return it }
|
||||||
|
// todo: check for subclasses
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val rootObjectType = ObjClass("Obj").apply {
|
val rootObjectType = ObjClass("Obj").apply {
|
||||||
@@ -319,7 +332,7 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
// utilities
|
// utilities
|
||||||
addFn("let") {
|
addFn("let") {
|
||||||
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
|
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
|
||||||
}
|
}
|
||||||
addFn("apply") {
|
addFn("apply") {
|
||||||
val body = args.firstAndOnly()
|
val body = args.firstAndOnly()
|
||||||
@@ -331,7 +344,7 @@ open class Obj {
|
|||||||
thisObj
|
thisObj
|
||||||
}
|
}
|
||||||
addFn("also") {
|
addFn("also") {
|
||||||
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
|
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
|
||||||
thisObj
|
thisObj
|
||||||
}
|
}
|
||||||
addFn("run") {
|
addFn("run") {
|
||||||
@@ -351,6 +364,7 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
inline fun from(obj: Any?): Obj {
|
inline fun from(obj: Any?): Obj {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return when (obj) {
|
return when (obj) {
|
||||||
@@ -500,195 +514,4 @@ data class ObjNamespace(val name: String) : Obj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* note on [getStackTrace]. If [useStackTrace] is not null, it is used instead. Otherwise, it is calculated
|
|
||||||
* from the current scope which is treated as exception scope. It is used to restore serialized
|
|
||||||
* exception with stack trace; the scope of the de-serialized exception is not valid
|
|
||||||
* for stack unwinding.
|
|
||||||
*/
|
|
||||||
open class ObjException(
|
|
||||||
val exceptionClass: ExceptionClass,
|
|
||||||
val scope: Scope,
|
|
||||||
val message: ObjString,
|
|
||||||
@Suppress("unused") val extraData: Obj = ObjNull,
|
|
||||||
val useStackTrace: ObjList? = null
|
|
||||||
) : Obj() {
|
|
||||||
constructor(name: String, scope: Scope, message: String) : this(
|
|
||||||
getOrCreateExceptionClass(name),
|
|
||||||
scope,
|
|
||||||
ObjString(message)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val cachedStackTrace = CachedExpression(initialValue = useStackTrace)
|
|
||||||
|
|
||||||
suspend fun getStackTrace(): ObjList {
|
|
||||||
return cachedStackTrace.get {
|
|
||||||
val result = ObjList()
|
|
||||||
val cls = scope.get("StackTraceEntry")!!.value as ObjClass
|
|
||||||
var s: Scope? = scope
|
|
||||||
var lastPos: Pos? = null
|
|
||||||
while (s != null) {
|
|
||||||
val pos = s.pos
|
|
||||||
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
|
||||||
result.list += cls.callWithArgs(
|
|
||||||
scope,
|
|
||||||
pos.source.objSourceName,
|
|
||||||
ObjInt(pos.line.toLong()),
|
|
||||||
ObjInt(pos.column.toLong()),
|
|
||||||
ObjString(pos.currentLine)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
s = s.parent
|
|
||||||
lastPos = pos
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(scope: Scope, message: String) : this(Root, scope, ObjString(message))
|
|
||||||
|
|
||||||
fun raise(): Nothing {
|
|
||||||
throw ExecutionError(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val objClass: ObjClass = exceptionClass
|
|
||||||
|
|
||||||
override suspend fun toString(scope: Scope,calledFromLyng: Boolean): ObjString {
|
|
||||||
val at = getStackTrace().list.firstOrNull()?.toString(scope)
|
|
||||||
?: ObjString("(unknown)")
|
|
||||||
return ObjString("${objClass.className}: $message at $at")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
|
||||||
encoder.encodeAny(scope, exceptionClass.classNameObj)
|
|
||||||
encoder.encodeAny(scope, message)
|
|
||||||
encoder.encodeAny(scope, extraData)
|
|
||||||
encoder.encodeAny(scope, getStackTrace())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
|
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
|
||||||
val message = scope.args.getOrNull(0)?.toString(scope) ?: ObjString(name)
|
|
||||||
return ObjException(this, scope, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
|
|
||||||
|
|
||||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
|
||||||
return try {
|
|
||||||
val lyngClass = decoder.decodeAnyAs<ObjString>(scope).value.let {
|
|
||||||
((scope[it] ?: scope.raiseIllegalArgument("Unknown exception class: $it"))
|
|
||||||
.value as? ExceptionClass)
|
|
||||||
?: scope.raiseIllegalArgument("Not an exception class: $it")
|
|
||||||
}
|
|
||||||
ObjException(
|
|
||||||
lyngClass,
|
|
||||||
scope,
|
|
||||||
decoder.decodeAnyAs<ObjString>(scope),
|
|
||||||
decoder.decodeAny(scope),
|
|
||||||
decoder.decodeAnyAs<ObjList>(scope)
|
|
||||||
)
|
|
||||||
} catch (e: ScriptError) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
scope.raiseIllegalArgument("Failed to deserialize exception: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val Root = ExceptionClass("Throwable").apply {
|
|
||||||
addConst("message", statement {
|
|
||||||
(thisObj as ObjException).message.toObj()
|
|
||||||
})
|
|
||||||
addFn("stackTrace") {
|
|
||||||
(thisObj as ObjException).getStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val op = ProtectedOp()
|
|
||||||
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
|
|
||||||
return op.withLock {
|
|
||||||
existingErrorClasses.getOrPut(name) {
|
|
||||||
ExceptionClass(name, Root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get [ObjClass] for error class by name if exists.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
fun getErrorClass(name: String): ObjClass? = op.withLock {
|
|
||||||
existingErrorClasses[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addExceptionsToContext(scope: Scope) {
|
|
||||||
scope.addConst("Exception", Root)
|
|
||||||
existingErrorClasses["Exception"] = Root
|
|
||||||
for (name in listOf(
|
|
||||||
"NullReferenceException",
|
|
||||||
"AssertionFailedException",
|
|
||||||
"ClassCastException",
|
|
||||||
"IndexOutOfBoundsException",
|
|
||||||
"IllegalArgumentException",
|
|
||||||
"NoSuchElementException",
|
|
||||||
"IllegalAssignmentException",
|
|
||||||
"SymbolNotDefinedException",
|
|
||||||
"IterationEndException",
|
|
||||||
"AccessException",
|
|
||||||
"UnknownException",
|
|
||||||
"NotFoundException"
|
|
||||||
)) {
|
|
||||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObjNullReferenceException(scope: Scope) : ObjException("NullReferenceException", scope, "object is null")
|
|
||||||
|
|
||||||
class ObjAssertionFailedException(scope: Scope, message: String) :
|
|
||||||
ObjException("AssertionFailedException", scope, message)
|
|
||||||
|
|
||||||
class ObjClassCastException(scope: Scope, message: String) : ObjException("ClassCastException", scope, message)
|
|
||||||
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
|
|
||||||
ObjException("IndexOutOfBoundsException", scope, message)
|
|
||||||
|
|
||||||
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
|
|
||||||
ObjException("IllegalArgumentException", scope, message)
|
|
||||||
|
|
||||||
class ObjIllegalStateException(scope: Scope, message: String = "illegal state") :
|
|
||||||
ObjException("IllegalStateException", scope, message)
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
|
|
||||||
ObjException("IllegalArgumentException", scope, message)
|
|
||||||
|
|
||||||
class ObjIllegalAssignmentException(scope: Scope, message: String = "illegal assignment") :
|
|
||||||
ObjException("NoSuchElementException", scope, message)
|
|
||||||
|
|
||||||
class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is not defined") :
|
|
||||||
ObjException("SymbolNotDefinedException", scope, message)
|
|
||||||
|
|
||||||
class ObjIterationFinishedException(scope: Scope) :
|
|
||||||
ObjException("IterationEndException", scope, "iteration finished")
|
|
||||||
|
|
||||||
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
|
|
||||||
ObjException("AccessException", scope, message)
|
|
||||||
|
|
||||||
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
|
|
||||||
ObjException("UnknownException", scope, message)
|
|
||||||
|
|
||||||
class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") :
|
|
||||||
ObjException("IllegalOperationException", scope, message)
|
|
||||||
|
|
||||||
class ObjNotFoundException(scope: Scope, message: String = "not found") :
|
|
||||||
ObjException("NotFoundException", scope, message)
|
|
||||||
@@ -35,12 +35,12 @@ class ObjBitBuffer(val bitArray: BitArray) : Obj() {
|
|||||||
}.apply {
|
}.apply {
|
||||||
addFn("toBuffer") {
|
addFn("toBuffer") {
|
||||||
requireNoArgs()
|
requireNoArgs()
|
||||||
ObjBuffer(thisAs<ObjBitBuffer>().bitArray.asUbyteArray())
|
ObjBuffer(thisAs<ObjBitBuffer>().bitArray.asUByteArray())
|
||||||
}
|
}
|
||||||
addFn("toDump") {
|
addFn("toDump") {
|
||||||
requireNoArgs()
|
requireNoArgs()
|
||||||
ObjString(
|
ObjString(
|
||||||
thisAs<ObjBitBuffer>().bitArray.asUbyteArray().toDump()
|
thisAs<ObjBitBuffer>().bitArray.asUByteArray().toDump()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
addFn("size") {
|
addFn("size") {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ open class ObjClass(
|
|||||||
|
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
val instance = ObjInstance(this)
|
val instance = ObjInstance(this)
|
||||||
instance.instanceScope = scope.copy(newThisObj = instance, args = scope.args)
|
instance.instanceScope = scope.createChildScope(newThisObj = instance, args = scope.args)
|
||||||
if (instanceConstructor != null) {
|
if (instanceConstructor != null) {
|
||||||
instanceConstructor!!.execute(instance.instanceScope)
|
instanceConstructor!!.execute(instance.instanceScope)
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ open class ObjClass(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
|
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
|
||||||
return callOn(scope.copy(Arguments(*plainArgs)))
|
return callOn(scope.createChildScope(Arguments(*plainArgs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,13 +47,20 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ObjDynamic : Obj() {
|
/**
|
||||||
|
* Object that delegates all its field access/invocation operations to a callback. It is used to implement dynamic
|
||||||
|
* objects using "dynamic" keyword.
|
||||||
|
*/
|
||||||
|
open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: Statement? = null) : Obj() {
|
||||||
|
|
||||||
internal var readCallback: Statement? = null
|
override val objClass: ObjClass = type
|
||||||
internal var writeCallback: Statement? = null
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use read callback to dynamically resolve the field name. Note that it does not work
|
||||||
|
* with method invocation which is implemented separately in [invokeInstanceMethod] below.
|
||||||
|
*/
|
||||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
return readCallback?.execute(scope.copy(Arguments(ObjString(name))))?.let {
|
return readCallback?.execute(scope.createChildScope(Arguments(ObjString(name))))?.let {
|
||||||
if (writeCallback != null)
|
if (writeCallback != null)
|
||||||
it.asMutable
|
it.asMutable
|
||||||
else
|
else
|
||||||
@@ -62,21 +69,53 @@ class ObjDynamic : Obj() {
|
|||||||
?: super.readField(scope, name)
|
?: super.readField(scope, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notice that invocation currently does not rely on [readField], which is a buffy moment to be reconsidered
|
||||||
|
* in the future, so we implement it separately:
|
||||||
|
*/
|
||||||
|
override suspend fun invokeInstanceMethod(
|
||||||
|
scope: Scope,
|
||||||
|
name: String,
|
||||||
|
args: Arguments,
|
||||||
|
onNotFoundResult: (() -> Obj?)?
|
||||||
|
): Obj {
|
||||||
|
val over = readCallback?.execute(
|
||||||
|
scope.createChildScope(
|
||||||
|
Arguments(ObjString(name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return over?.invoke(scope, scope.thisObj, args)
|
||||||
|
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
writeCallback?.execute(scope.copy(Arguments(ObjString(name), newValue)))
|
writeCallback?.execute(scope.createChildScope(Arguments(ObjString(name), newValue)))
|
||||||
?: super.writeField(scope, name, newValue)
|
?: super.writeField(scope, name, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||||
|
return readCallback?.execute(scope.createChildScope(Arguments(index)))
|
||||||
|
?: super.getAt(scope, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||||
|
writeCallback?.execute(scope.createChildScope(Arguments(index, newValue)))
|
||||||
|
?: super.putAt(scope, index, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
suspend fun create(scope: Scope, builder: Statement): ObjDynamic {
|
suspend fun create(scope: Scope, builder: Statement): ObjDynamic {
|
||||||
val delegate = ObjDynamic()
|
val delegate = ObjDynamic()
|
||||||
val context = ObjDynamicContext(delegate)
|
val context = ObjDynamicContext(delegate)
|
||||||
builder.execute(scope.copy(newThisObj = context))
|
builder.execute(scope.createChildScope(newThisObj = context))
|
||||||
return delegate
|
return delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
val type = object : ObjClass("Delegate") {}
|
val type = object : ObjClass("Delegate") {}.apply {
|
||||||
|
// addClassConst("IndexGetName", operatorGetName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
* 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.bintools.encodeToHex
|
||||||
|
import net.sergeych.lyng.*
|
||||||
|
import net.sergeych.lynon.LynonDecoder
|
||||||
|
import net.sergeych.lynon.LynonEncoder
|
||||||
|
import net.sergeych.lynon.LynonType
|
||||||
|
import net.sergeych.mptools.CachedExpression
|
||||||
|
import net.sergeych.synctools.ProtectedOp
|
||||||
|
import net.sergeych.synctools.withLock
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note on [getStackTrace]. If [useStackTrace] is not null, it is used instead. Otherwise, it is calculated
|
||||||
|
* from the current scope, which is treated as an exception scope. It is used to restore a serialized
|
||||||
|
* exception with stack trace; the scope of the deserialized exception is not valid
|
||||||
|
* for stack unwinding.
|
||||||
|
*/
|
||||||
|
open class ObjException(
|
||||||
|
val exceptionClass: ExceptionClass,
|
||||||
|
val scope: Scope,
|
||||||
|
val message: ObjString,
|
||||||
|
@Suppress("unused") val extraData: Obj = ObjNull,
|
||||||
|
val useStackTrace: ObjList? = null
|
||||||
|
) : Obj() {
|
||||||
|
constructor(name: String, scope: Scope, message: String) : this(
|
||||||
|
getOrCreateExceptionClass(name),
|
||||||
|
scope,
|
||||||
|
ObjString(message)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val cachedStackTrace = CachedExpression(initialValue = useStackTrace)
|
||||||
|
|
||||||
|
suspend fun getStackTrace(): ObjList {
|
||||||
|
return cachedStackTrace.get {
|
||||||
|
val result = ObjList()
|
||||||
|
val cls = scope.get("StackTraceEntry")!!.value as ObjClass
|
||||||
|
var s: Scope? = scope
|
||||||
|
var lastPos: Pos? = null
|
||||||
|
while (s != null) {
|
||||||
|
val pos = s.pos
|
||||||
|
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
||||||
|
result.list += cls.callWithArgs(
|
||||||
|
scope,
|
||||||
|
pos.source.objSourceName,
|
||||||
|
ObjInt(pos.line.toLong()),
|
||||||
|
ObjInt(pos.column.toLong()),
|
||||||
|
ObjString(pos.currentLine)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
s = s.parent
|
||||||
|
lastPos = pos
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(scope: Scope, message: String) : this(Root, scope, ObjString(message))
|
||||||
|
|
||||||
|
fun raise(): Nothing {
|
||||||
|
throw ExecutionError(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val objClass: ObjClass = exceptionClass
|
||||||
|
|
||||||
|
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
||||||
|
val at = getStackTrace().list.firstOrNull()?.toString(scope)
|
||||||
|
?: ObjString("(unknown)")
|
||||||
|
return ObjString("${objClass.className}: $message at $at")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||||
|
encoder.encodeAny(scope, exceptionClass.classNameObj)
|
||||||
|
encoder.encodeAny(scope, message)
|
||||||
|
encoder.encodeAny(scope, extraData)
|
||||||
|
encoder.encodeAny(scope, getStackTrace())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
val message = scope.args.getOrNull(0)?.toString(scope) ?: ObjString(name)
|
||||||
|
return ObjException(this, scope, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
|
||||||
|
|
||||||
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||||
|
return try {
|
||||||
|
val lyngClass = decoder.decodeAnyAs<ObjString>(scope).value.let {
|
||||||
|
((scope[it] ?: scope.raiseIllegalArgument("Unknown exception class: $it"))
|
||||||
|
.value as? ExceptionClass)
|
||||||
|
?: scope.raiseIllegalArgument("Not an exception class: $it")
|
||||||
|
}
|
||||||
|
ObjException(
|
||||||
|
lyngClass,
|
||||||
|
scope,
|
||||||
|
decoder.decodeAnyAs<ObjString>(scope),
|
||||||
|
decoder.decodeAny(scope),
|
||||||
|
decoder.decodeAnyAs<ObjList>(scope)
|
||||||
|
)
|
||||||
|
} catch (e: ScriptError) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
scope.raiseIllegalArgument("Failed to deserialize exception: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val Root = ExceptionClass("Exception").apply {
|
||||||
|
addConst("message", statement {
|
||||||
|
(thisObj as ObjException).message.toObj()
|
||||||
|
})
|
||||||
|
addFn("stackTrace") {
|
||||||
|
(thisObj as ObjException).getStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val op = ProtectedOp()
|
||||||
|
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
|
||||||
|
return op.withLock {
|
||||||
|
existingErrorClasses.getOrPut(name) {
|
||||||
|
ExceptionClass(name, Root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get [ObjClass] for error class by name if exists.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
fun getErrorClass(name: String): ObjClass? = op.withLock {
|
||||||
|
existingErrorClasses[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addExceptionsToContext(scope: Scope) {
|
||||||
|
scope.addConst("Exception", Root)
|
||||||
|
existingErrorClasses["Exception"] = Root
|
||||||
|
for (name in listOf(
|
||||||
|
"NullReferenceException",
|
||||||
|
"AssertionFailedException",
|
||||||
|
"ClassCastException",
|
||||||
|
"IndexOutOfBoundsException",
|
||||||
|
"IllegalArgumentException",
|
||||||
|
"NoSuchElementException",
|
||||||
|
"IllegalAssignmentException",
|
||||||
|
"SymbolNotDefinedException",
|
||||||
|
"IterationEndException",
|
||||||
|
"AccessException",
|
||||||
|
"UnknownException",
|
||||||
|
"NotFoundException"
|
||||||
|
)) {
|
||||||
|
scope.addConst(name, getOrCreateExceptionClass(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjNullReferenceException(scope: Scope) : ObjException("NullReferenceException", scope, "object is null")
|
||||||
|
|
||||||
|
class ObjAssertionFailedException(scope: Scope, message: String) :
|
||||||
|
ObjException("AssertionFailedException", scope, message)
|
||||||
|
|
||||||
|
class ObjClassCastException(scope: Scope, message: String) : ObjException("ClassCastException", scope, message)
|
||||||
|
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
|
||||||
|
ObjException("IndexOutOfBoundsException", scope, message)
|
||||||
|
|
||||||
|
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
|
||||||
|
ObjException("IllegalArgumentException", scope, message)
|
||||||
|
|
||||||
|
class ObjIllegalStateException(scope: Scope, message: String = "illegal state") :
|
||||||
|
ObjException("IllegalStateException", scope, message)
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
|
||||||
|
ObjException("IllegalArgumentException", scope, message)
|
||||||
|
|
||||||
|
class ObjIllegalAssignmentException(scope: Scope, message: String = "illegal assignment") :
|
||||||
|
ObjException("NoSuchElementException", scope, message)
|
||||||
|
|
||||||
|
class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is not defined") :
|
||||||
|
ObjException("SymbolNotDefinedException", scope, message)
|
||||||
|
|
||||||
|
class ObjIterationFinishedException(scope: Scope) :
|
||||||
|
ObjException("IterationEndException", scope, "iteration finished")
|
||||||
|
|
||||||
|
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
|
||||||
|
ObjException("AccessException", scope, message)
|
||||||
|
|
||||||
|
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
|
||||||
|
ObjException("UnknownException", scope, message)
|
||||||
|
|
||||||
|
class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") :
|
||||||
|
ObjException("IllegalOperationException", scope, message)
|
||||||
|
|
||||||
|
class ObjNotFoundException(scope: Scope, message: String = "not found") :
|
||||||
|
ObjException("NotFoundException", scope, message)
|
||||||
@@ -58,7 +58,7 @@ class ObjFlowBuilder(val output: SendChannel<Obj>) : Obj() {
|
|||||||
private fun createLyngFlowInput(scope: Scope, producer: Statement): ReceiveChannel<Obj> {
|
private fun createLyngFlowInput(scope: Scope, producer: Statement): ReceiveChannel<Obj> {
|
||||||
val channel = Channel<Obj>(Channel.RENDEZVOUS)
|
val channel = Channel<Obj>(Channel.RENDEZVOUS)
|
||||||
val builder = ObjFlowBuilder(channel)
|
val builder = ObjFlowBuilder(channel)
|
||||||
val builderScope = scope.copy(newThisObj = builder)
|
val builderScope = scope.createChildScope(newThisObj = builder)
|
||||||
globalLaunch {
|
globalLaunch {
|
||||||
try {
|
try {
|
||||||
producer.execute(builderScope)
|
producer.execute(builderScope)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ObjInstanceClass(val name: String) : ObjClass(name) {
|
|||||||
val actualSize = constructorMeta?.params?.size ?: 0
|
val actualSize = constructorMeta?.params?.size ?: 0
|
||||||
if (args.size > actualSize)
|
if (args.size > actualSize)
|
||||||
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
|
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
|
||||||
val newScope = scope.copy(args = Arguments(args))
|
val newScope = scope.createChildScope(args = Arguments(args))
|
||||||
return (callOn(newScope) as ObjInstance).apply {
|
return (callOn(newScope) as ObjInstance).apply {
|
||||||
deserializeStateVars(scope, decoder)
|
deserializeStateVars(scope, decoder)
|
||||||
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
|
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ val ObjIterable by lazy {
|
|||||||
val fn = requiredArg<Statement>(0)
|
val fn = requiredArg<Statement>(0)
|
||||||
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
||||||
val x = it.invokeInstanceMethod(this, "next")
|
val x = it.invokeInstanceMethod(this, "next")
|
||||||
fn.execute(this.copy(Arguments(listOf(x))))
|
fn.execute(this.createChildScope(Arguments(listOf(x))))
|
||||||
}
|
}
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* 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.Scope
|
||||||
|
|
||||||
|
class ObjRegex(val regex: Regex) : Obj() {
|
||||||
|
override val objClass = type
|
||||||
|
|
||||||
|
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
|
||||||
|
return regex.find(other.cast<ObjString>(scope).value)?.let {
|
||||||
|
scope.addConst("$~", ObjRegexMatch(it))
|
||||||
|
ObjTrue
|
||||||
|
} ?: ObjFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
fun find(s: ObjString): Obj =
|
||||||
|
regex.find(s.value)?.let { ObjRegexMatch(it) } ?: ObjNull
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type by lazy {
|
||||||
|
object : ObjClass("Regex") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
return ObjRegex(
|
||||||
|
scope.requireOnlyArg<ObjString>().value.toRegex()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addFn("matches") {
|
||||||
|
ObjBool(args.firstAndOnly().toString().matches(thisAs<ObjRegex>().regex))
|
||||||
|
}
|
||||||
|
addFn("find") {
|
||||||
|
thisAs<ObjRegex>().find(requireOnlyArg<ObjString>())
|
||||||
|
}
|
||||||
|
addFn("findAll") {
|
||||||
|
val s = requireOnlyArg<ObjString>().value
|
||||||
|
ObjList(thisAs<ObjRegex>().regex.findAll(s).map { ObjRegexMatch(it) }.toMutableList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjRegexMatch(val match: MatchResult) : Obj() {
|
||||||
|
override val objClass = type
|
||||||
|
|
||||||
|
val objGroups: ObjList by lazy {
|
||||||
|
ObjList(
|
||||||
|
match.groups.map { it?.let { ObjString(it.value) } ?: ObjNull }.toMutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val objValue by lazy { ObjString(match.value) }
|
||||||
|
|
||||||
|
val objRange: ObjRange by lazy {
|
||||||
|
val r = match.range
|
||||||
|
|
||||||
|
ObjRange(
|
||||||
|
ObjInt(r.first.toLong()),
|
||||||
|
ObjInt(r.last.toLong()),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun toString(scope: Scope,calledFromLyng: Boolean): ObjString {
|
||||||
|
return ObjString("RegexMath(${objRange.toString(scope)},${objGroups.toString(scope)})")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||||
|
return objGroups.getAt(scope, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
|
if( other === this) return 0
|
||||||
|
return -2
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type by lazy {
|
||||||
|
object : ObjClass("RegexMatch") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("RegexMatch can't be constructed directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addFn("groups") {
|
||||||
|
thisAs<ObjRegexMatch>().objGroups
|
||||||
|
}
|
||||||
|
addFn("value") {
|
||||||
|
thisAs<ObjRegexMatch>().objValue
|
||||||
|
}
|
||||||
|
addFn("range") {
|
||||||
|
thisAs<ObjRegexMatch>().objRange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,16 +56,23 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||||
if (index is ObjInt) return ObjChar(value[index.toInt()])
|
when (index) {
|
||||||
if (index is ObjRange) {
|
is ObjInt -> return ObjChar(value[index.toInt()])
|
||||||
val start = if (index.start == null || index.start.isNull) 0 else index.start.toInt()
|
is ObjRange -> {
|
||||||
val end = if (index.end == null || index.end.isNull) value.length else {
|
val start = if (index.start == null || index.start.isNull) 0 else index.start.toInt()
|
||||||
val e = index.end.toInt()
|
val end = if (index.end == null || index.end.isNull) value.length else {
|
||||||
if (index.isEndInclusive) e + 1 else e
|
val e = index.end.toInt()
|
||||||
|
if (index.isEndInclusive) e + 1 else e
|
||||||
|
}
|
||||||
|
return ObjString(value.substring(start, end))
|
||||||
}
|
}
|
||||||
return ObjString(value.substring(start, end))
|
|
||||||
|
is ObjRegex -> {
|
||||||
|
return index.find(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> scope.raiseIllegalArgument("String index must be Int, Regex or Range")
|
||||||
}
|
}
|
||||||
scope.raiseIllegalArgument("String index must be Int or Range")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -96,6 +103,11 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
return value == other.value
|
return value == other.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
|
||||||
|
val re = other.cast<ObjRegex>(scope)
|
||||||
|
return re.operatorMatch(scope, this)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun lynonType(): LynonType = LynonType.String
|
override suspend fun lynonType(): LynonType = LynonType.String
|
||||||
|
|
||||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||||
@@ -108,8 +120,9 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
ObjString(decoder.unpackBinaryData().decodeToString())
|
ObjString(decoder.unpackBinaryData().decodeToString())
|
||||||
}.apply {
|
}.apply {
|
||||||
addFn("toInt") {
|
addFn("toInt") {
|
||||||
ObjInt(thisAs<ObjString>().value.toLongOrNull()
|
ObjInt(
|
||||||
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
thisAs<ObjString>().value.toLongOrNull()
|
||||||
|
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
addFn("startsWith") {
|
addFn("startsWith") {
|
||||||
@@ -160,6 +173,22 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
addFn("trim") {
|
addFn("trim") {
|
||||||
thisAs<ObjString>().value.trim().let(::ObjString)
|
thisAs<ObjString>().value.trim().let(::ObjString)
|
||||||
}
|
}
|
||||||
|
addFn("matches") {
|
||||||
|
val s = requireOnlyArg<Obj>()
|
||||||
|
val self = thisAs<ObjString>().value
|
||||||
|
ObjBool(
|
||||||
|
when (s) {
|
||||||
|
is ObjRegex -> self.matches(s.regex)
|
||||||
|
is ObjString -> {
|
||||||
|
if (s.value == ".*") true
|
||||||
|
else self.matches(s.value.toRegex())
|
||||||
|
}
|
||||||
|
|
||||||
|
else ->
|
||||||
|
raiseIllegalArgument("can't match ${s.objClass.className}: required Regex or String")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ abstract class ImportProvider(
|
|||||||
newModuleAt(pos).also {
|
newModuleAt(pos).also {
|
||||||
it.eval("import lyng.stdlib\n")
|
it.eval("import lyng.stdlib\n")
|
||||||
}
|
}
|
||||||
}.copy()
|
}.createChildScope()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ abstract class Statement(
|
|||||||
val type = ObjClass("Callable")
|
val type = ObjClass("Callable")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.copy(args = Arguments(*args)))
|
suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.createChildScope(args = Arguments(*args)))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ fun Exception.printStackTrace() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.re() { Regex(this) }
|
||||||
|
|
||||||
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
@@ -17,8 +17,10 @@
|
|||||||
|
|
||||||
package net.sergeych.lynon
|
package net.sergeych.lynon
|
||||||
|
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
interface BitList {
|
interface BitList: Comparable<BitList> {
|
||||||
operator fun get(bitIndex: Long): Int
|
operator fun get(bitIndex: Long): Int
|
||||||
operator fun set(bitIndex: Long,value: Int)
|
operator fun set(bitIndex: Long,value: Int)
|
||||||
val size: Long
|
val size: Long
|
||||||
@@ -31,6 +33,22 @@ interface BitList {
|
|||||||
if( index < size) this@BitList[index++]
|
if( index < size) this@BitList[index++]
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: BitList): Int {
|
||||||
|
val m = min(size, other.size)
|
||||||
|
for( i in 0 ..< m) {
|
||||||
|
val a = this[i]
|
||||||
|
val b = other[i]
|
||||||
|
when {
|
||||||
|
a < b -> return -1
|
||||||
|
a > b -> return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if( size > other.size) return 1
|
||||||
|
if( size < other.size) return -1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bitListOf(vararg bits: Int): BitList {
|
fun bitListOf(vararg bits: Int): BitList {
|
||||||
|
|||||||
@@ -91,14 +91,15 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe
|
|||||||
val type = LynonType.entries[getBitsAsInt(4)]
|
val type = LynonType.entries[getBitsAsInt(4)]
|
||||||
val list = mutableListOf<Obj>()
|
val list = mutableListOf<Obj>()
|
||||||
val objClass = if (type == LynonType.Other)
|
val objClass = if (type == LynonType.Other)
|
||||||
decodeClassObj(scope).also { println("detected class obj: $it") }
|
decodeClassObj(scope)//.also { println("detected class obj: $it") }
|
||||||
else type.objClass
|
else type.objClass
|
||||||
val size = fixedSize ?: bin.unpackUnsigned().toInt()
|
val size = fixedSize ?: bin.unpackUnsigned().toInt()
|
||||||
println("detected homogenous list type $type, $size items")
|
// println("detected homogenous list type $type, $size items")
|
||||||
for (i in 0..<size) {
|
for (i in 0..<size) {
|
||||||
list += decodeObject(scope, objClass, type).also {
|
list += decodeObject(scope, objClass, type)
|
||||||
println("decoded: $it")
|
//.also {
|
||||||
}
|
// println("decoded: $it")
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
list
|
list
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,13 +17,18 @@
|
|||||||
|
|
||||||
package net.sergeych.lynon
|
package net.sergeych.lynon
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BitList implementation as fixed suze array of bits; indexing works exactly same as if
|
* BitList implementation as fixed suze array of bits; indexing works exactly same as if
|
||||||
* [MemoryBitInput] is used with [MemoryBitInput.getBit]. See [MemoryBitOutput] for
|
* [MemoryBitInput] is used with [MemoryBitInput.getBit]. See [MemoryBitOutput] for
|
||||||
* bits order and more information.
|
* bits order and more information.
|
||||||
|
*
|
||||||
|
* It is [BitList] - comparable, and provides valid [hashCode] and [equals], so it could
|
||||||
|
* also be used as a key in maps.
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
class BitArray(val bytes: UByteArray, val lastByteBits: Int) : BitList {
|
class BitArray(val bytes: UByteArray, val lastByteBits: Int) : BitList {
|
||||||
|
|
||||||
val bytesSize: Int get() = bytes.size
|
val bytesSize: Int get() = bytes.size
|
||||||
@@ -73,7 +78,21 @@ class BitArray(val bytes: UByteArray, val lastByteBits: Int) : BitList {
|
|||||||
fun asByteArray(): ByteArray = bytes.asByteArray()
|
fun asByteArray(): ByteArray = bytes.asByteArray()
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun asUbyteArray(): UByteArray = bytes
|
fun asUByteArray(): UByteArray = bytes
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return when(other) {
|
||||||
|
is BitArray ->
|
||||||
|
// important: size must match as trailing zero bits will generate false eq otherwise:
|
||||||
|
size == other.size && bytes contentEquals other.bytes
|
||||||
|
is BitList -> compareTo(other) == 0
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return bytes.contentHashCode()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
package net.sergeych.lynon
|
package net.sergeych.lynon
|
||||||
|
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hoq many tetrades needed to store the value. It is faster to use this function
|
* Hoq many tetrades needed to store the value. It is faster to use this function
|
||||||
* than to use sizeInBits
|
* than to use sizeInBits
|
||||||
@@ -35,7 +37,7 @@ fun sizeInTetrades(value: ULong): Int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many bits needed to store the value. Size for 0 is 1,
|
* Calculates ow many bits needed to store the value. Size for 0, for example, is 1.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun sizeInBits(value: ULong): Int {
|
fun sizeInBits(value: ULong): Int {
|
||||||
@@ -50,3 +52,20 @@ fun sizeInBits(value: ULong): Int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sizeInBits(value: Int): Int = sizeInBits(value.toULong())
|
fun sizeInBits(value: Int): Int = sizeInBits(value.toULong())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random BitArray of the specified size. Important: this is
|
||||||
|
* __not cryptographically secure__. Use random from [crypto2](https://gitea.sergeych.net/sergeych/crypto2),
|
||||||
|
* `randomInt()`, `randomUBytes()`, etc., for cryptographically secure random data.
|
||||||
|
*
|
||||||
|
* @param sizeInBits The size of the BitArray to generate in bits.
|
||||||
|
* @return A BitArray of the specified size filled with random bits.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
fun BitArray.Companion.random(sizeInBits: Int): BitArray {
|
||||||
|
val result = BitArray.withBitSize(sizeInBits.toLong())
|
||||||
|
for (i in 0..<sizeInBits) {
|
||||||
|
result[i.toLong()] = Random.nextInt() and 1
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
package net.sergeych.lynon
|
package net.sergeych.lynon
|
||||||
|
|
||||||
import net.sergeych.collections.SortedList
|
import net.sergeych.collections.SortedList
|
||||||
import net.sergeych.lynon.Huffman.Alphabet
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -285,7 +284,7 @@ object Huffman {
|
|||||||
|
|
||||||
fun <T>decompress(bin: BitInput,alphabet: Alphabet<T>): UByteArray {
|
fun <T>decompress(bin: BitInput,alphabet: Alphabet<T>): UByteArray {
|
||||||
val codes = deserializeCanonicCodes(bin, alphabet)
|
val codes = deserializeCanonicCodes(bin, alphabet)
|
||||||
return decompressUsingCodes(bin, codes, alphabet).asUbyteArray()
|
return decompressUsingCodes(bin, codes, alphabet).asUByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,9 @@ import net.sergeych.bintools.ByteChunk
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LZW lightweight pure kotlin compression.
|
* LZW lightweight pure kotlin compression. LZW works with but streams [BitInput] and [BitOutput]
|
||||||
|
* to be used effectively in Lynon or other bit-grained formats. To safely comress byte arresy and
|
||||||
|
* strings without boilerplate use [lzwCompress], [lzwDecompress], [lzwCompressUtf8] and [lzwDecompressUtf8].
|
||||||
*/
|
*/
|
||||||
object LZW {
|
object LZW {
|
||||||
|
|
||||||
@@ -30,8 +32,10 @@ object LZW {
|
|||||||
val MAX_DICT_SIZE = (STOP_CODE * 0.92).roundToInt()
|
val MAX_DICT_SIZE = (STOP_CODE * 0.92).roundToInt()
|
||||||
|
|
||||||
|
|
||||||
fun compress(input: ByteArray, bitOutput: BitOutput)
|
/**
|
||||||
= compress(input.asUByteArray(), bitOutput)
|
* Compress a byte array using LZW algorithm writing the result to [bitOutput]
|
||||||
|
*/
|
||||||
|
fun compress(input: ByteArray, bitOutput: BitOutput) = compress(input.asUByteArray(), bitOutput)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compresses the input string using LZW algorithm
|
* Compresses the input string using LZW algorithm
|
||||||
@@ -139,7 +143,42 @@ object LZW {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private operator fun ByteChunk.plus(byte: UByte): ByteChunk {
|
private operator fun ByteChunk.plus(byte: UByte): ByteChunk {
|
||||||
return ByteChunk(data + byte)
|
return ByteChunk(data + byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely compress binary byte data using the LZW algorithm. This can use up to one byte more space than
|
||||||
|
* with [LZW.compress] and [BitOutput] but you often need byte array operations. Source data size is also
|
||||||
|
* encoded to prevent file-bomb-like attacks. Note that content protection is not included (we assume
|
||||||
|
* LZW is robust).
|
||||||
|
* @param source the data to compress
|
||||||
|
*/
|
||||||
|
fun lzwCompress(source: UByteArray): UByteArray {
|
||||||
|
val out = MemoryBitOutput()
|
||||||
|
out.packUnsigned(source.size.toULong())
|
||||||
|
LZW.compress(source, out)
|
||||||
|
return out.toBitArray().asUByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely decompress data compressed with [lzwCompress], checking size. Contect checks are not implemented
|
||||||
|
* here as we assume LZW is robust.
|
||||||
|
* @param packed the compressed data
|
||||||
|
* @throws DecompressionException if something goes wrong, like size mismatch or bad compressed data
|
||||||
|
*/
|
||||||
|
fun lzwDecompress(packed: UByteArray): UByteArray {
|
||||||
|
val inp = MemoryBitInput(packed, 8)
|
||||||
|
val size = inp.unpackUnsigned()
|
||||||
|
return LZW.decompress(inp, size.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compress a text using utf-8 encoding and [lzwCompress]
|
||||||
|
*/
|
||||||
|
fun lzwCompressUtf8(text: String) = lzwCompress(text.encodeToByteArray().toUByteArray())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompress a text compressed with [lzwCompressUtf8]. See also [lzwDecompress]
|
||||||
|
*/
|
||||||
|
fun lzwDecompressUtf8(packed: UByteArray): String = lzwDecompress(packed).asByteArray().decodeToString()
|
||||||
@@ -56,7 +56,7 @@ object ObjLynonClass : ObjClass("Lynon") {
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
suspend fun lynonEncodeAny(scope: Scope, value: Obj): UByteArray =
|
suspend fun lynonEncodeAny(scope: Scope, value: Obj): UByteArray =
|
||||||
(ObjLynonClass.encodeAny(scope, value))
|
(ObjLynonClass.encodeAny(scope, value))
|
||||||
.bitArray.asUbyteArray()
|
.bitArray.asUByteArray()
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
suspend fun lynonDecodeAny(scope: Scope, encoded: UByteArray): Obj =
|
suspend fun lynonDecodeAny(scope: Scope, encoded: UByteArray): Obj =
|
||||||
|
|||||||
@@ -107,6 +107,26 @@ class OOTest {
|
|||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDynamicIndexAccess() = runTest {
|
||||||
|
eval("""
|
||||||
|
val store = Map()
|
||||||
|
val accessor = dynamic {
|
||||||
|
get { name ->
|
||||||
|
store[name]
|
||||||
|
}
|
||||||
|
set { name, value ->
|
||||||
|
store[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(null, accessor["foo"])
|
||||||
|
assertEquals(null, accessor.foo)
|
||||||
|
accessor["foo"] = "bar"
|
||||||
|
assertEquals("bar", accessor["foo"])
|
||||||
|
assertEquals("bar", accessor.foo)
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMultilineConstructor() = runTest {
|
fun testMultilineConstructor() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
@@ -117,4 +137,53 @@ class OOTest {
|
|||||||
assertEquals(Point(1,2), Point(1,2) )
|
assertEquals(Point(1,2), Point(1,2) )
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDynamicClass() = runTest {
|
||||||
|
eval("""
|
||||||
|
|
||||||
|
fun getContract(contractName) {
|
||||||
|
dynamic {
|
||||||
|
get { name ->
|
||||||
|
println("Call: %s.%s"(contractName,name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getContract("foo").bar
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDynamicClassReturn2() = runTest {
|
||||||
|
// todo: should work without extra parenthesis
|
||||||
|
// see below
|
||||||
|
eval("""
|
||||||
|
|
||||||
|
fun getContract(contractName) {
|
||||||
|
println("1")
|
||||||
|
dynamic {
|
||||||
|
get { name ->
|
||||||
|
println("innrer %s.%s"(contractName,name))
|
||||||
|
{ args... ->
|
||||||
|
if( name == "bar" ) args.sum() else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cc = dynamic {
|
||||||
|
get { name ->
|
||||||
|
println("Call cc %s"(name))
|
||||||
|
getContract(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val x = cc.foo.bar
|
||||||
|
println(x)
|
||||||
|
x(1,2,3)
|
||||||
|
assertEquals(6, x(1,2,3))
|
||||||
|
// v HERE v
|
||||||
|
assertEquals(15, cc.foo.bar(10,2,3))
|
||||||
|
""")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2242,6 +2242,45 @@ class ScriptTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseSpecialVars() {
|
||||||
|
val l = parseLyng("$~".toSource("test$~"))
|
||||||
|
println(l)
|
||||||
|
assertEquals(Token.Type.ID, l[0].type)
|
||||||
|
assertEquals("$~", l[0].value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMatchOperator() = runTest {
|
||||||
|
eval("""
|
||||||
|
assert( "abc123".matches(".*\d{3}") )
|
||||||
|
assert( ".*\d{3}".re =~ "abc123" )
|
||||||
|
assert( "abc123" =~ ".*\d{3}".re )
|
||||||
|
assert( "abc123" !~ ".*\d{4}".re )
|
||||||
|
|
||||||
|
"abc123" =~ ".*(\d)(\d)(\d)$".re
|
||||||
|
println($~)
|
||||||
|
assertEquals("1", $~[1])
|
||||||
|
assertEquals("2", $~[2])
|
||||||
|
assertEquals("3", $~[3])
|
||||||
|
assertEquals("abc123", $~[0])
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// fun testWhenMatch() = runTest {
|
||||||
|
// eval(
|
||||||
|
// """
|
||||||
|
// when("abc123") {
|
||||||
|
// ".*(\d)(\d)(\d)".re -> { x ->
|
||||||
|
// assertEquals("123", x[0])
|
||||||
|
// }
|
||||||
|
// else -> assert(false)
|
||||||
|
// }
|
||||||
|
// """.trimIndent()
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testWhenSample1() = runTest {
|
fun testWhenSample1() = runTest {
|
||||||
eval(
|
eval(
|
||||||
@@ -3131,6 +3170,34 @@ class ScriptTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExceptionSerializationPlain() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
import lyng.serialization
|
||||||
|
val x = [1,2,3]
|
||||||
|
assertEquals( "[1,2,3]", x.toString() )
|
||||||
|
try {
|
||||||
|
throw "test"
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
println(e.stackTrace)
|
||||||
|
e.printStackTrace()
|
||||||
|
val coded = Lynon.encode(e)
|
||||||
|
val decoded = Lynon.decode(coded)
|
||||||
|
assertEquals( e::class, decoded::class )
|
||||||
|
assertEquals( e.stackTrace, decoded.stackTrace )
|
||||||
|
assertEquals( e.message, decoded.message )
|
||||||
|
println("-------------------- e")
|
||||||
|
println(e.toString())
|
||||||
|
println("-------------------- dee")
|
||||||
|
println(decoded.toString())
|
||||||
|
assertEquals( e.toString(), decoded.toString() )
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testThisInClosure() = runTest {
|
fun testThisInClosure() = runTest {
|
||||||
eval(
|
eval(
|
||||||
@@ -3242,7 +3309,45 @@ class ScriptTest {
|
|||||||
result.insertAt(-i-1, x)
|
result.insertAt(-i-1, x)
|
||||||
}
|
}
|
||||||
assertEquals( src.sorted(), result )
|
assertEquals( src.sorted(), result )
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// fun testMinimumOptimization() = runTest {
|
||||||
|
// val x = Scope().eval(
|
||||||
|
// """
|
||||||
|
// fun naiveCountHappyNumbers() {
|
||||||
|
// var count = 0
|
||||||
|
// for( n1 in 0..9 )
|
||||||
|
// for( n2 in 0..9 )
|
||||||
|
// for( n3 in 0..9 )
|
||||||
|
// for( n4 in 0..9 )
|
||||||
|
// for( n5 in 0..9 )
|
||||||
|
// for( n6 in 0..9 )
|
||||||
|
// if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
|
||||||
|
// count
|
||||||
|
// }
|
||||||
|
// naiveCountHappyNumbers()
|
||||||
|
// """.trimIndent()
|
||||||
|
// ).toInt()
|
||||||
|
// assertEquals(55252, x)
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRegex1() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
assert( ! "123".re.matches("abs123def") )
|
||||||
|
assert( ".*123.*".re.matches("abs123def") )
|
||||||
|
// assertEquals( "123", "123".re.find("abs123def")?.value )
|
||||||
|
// assertEquals( "123", "[0-9]{3}".re.find("abs123def")?.value )
|
||||||
|
assertEquals( "123", "\d{3}".re.find("abs123def")?.value )
|
||||||
|
assertEquals( "123", "\\d{3}".re.find("abs123def")?.value )
|
||||||
|
assertEquals( [1,2,3], "\d".re.findAll("abs123def").map { it.value.toInt() } )
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -332,4 +332,8 @@ class BookTest {
|
|||||||
runDocTests("../docs/Array.md")
|
runDocTests("../docs/Array.md")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRegex() = runBlocking {
|
||||||
|
runDocTests("../docs/Regex.md")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -397,6 +397,13 @@ class LynonTests {
|
|||||||
assertEquals(original, decompressed)
|
assertEquals(original, decompressed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompressed() {
|
||||||
|
val compressed = lzwCompressUtf8(original)
|
||||||
|
println("${compressed.size/original.encodeToByteArray().size.toDouble()} compression ratio")
|
||||||
|
assertEquals(original, lzwDecompressUtf8(compressed))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testTinyBits() {
|
fun testTinyBits() {
|
||||||
var a0 = TinyBits()
|
var a0 = TinyBits()
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import net.sergeych.lyng.Source
|
|||||||
import net.sergeych.lyng.eval
|
import net.sergeych.lyng.eval
|
||||||
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
||||||
import net.sergeych.lyng.toSource
|
import net.sergeych.lyng.toSource
|
||||||
|
import net.sergeych.lynon.BitArray
|
||||||
|
import net.sergeych.lynon.BitList
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
class OtherTests {
|
class OtherTests {
|
||||||
@Test
|
@Test
|
||||||
@@ -67,4 +70,33 @@ class OtherTests {
|
|||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBitArrayEqAndHash() {
|
||||||
|
|
||||||
|
val b1 = BitArray.ofBits(1, 0, 1, 1)
|
||||||
|
val b11 = BitArray.ofBits(1, 0, 1, 1)
|
||||||
|
val b2 = BitArray.ofBits(1, 1, 1, 1)
|
||||||
|
val b3 = BitArray.ofBits(1, 0, 1, 1, 0)
|
||||||
|
|
||||||
|
assert( b3 > b1 )
|
||||||
|
assert( b2 > b1)
|
||||||
|
assert( b11.compareTo(b1) == 0)
|
||||||
|
|
||||||
|
assertEquals(b1, b11)
|
||||||
|
assertNotEquals(b1, b2)
|
||||||
|
assertNotEquals(b1, b3)
|
||||||
|
|
||||||
|
assert( b1.hashCode() == b11.hashCode() )
|
||||||
|
|
||||||
|
val x = mutableMapOf<BitList,String>()
|
||||||
|
x[b1] = "wrong"
|
||||||
|
x[b11] = "OK"
|
||||||
|
x[b2] = "other"
|
||||||
|
|
||||||
|
assertEquals("OK", x[b11])
|
||||||
|
assertEquals("OK", x[b1])
|
||||||
|
assertEquals("other", x[b2])
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user