Compare commits

...

51 Commits

Author SHA1 Message Date
a8067d0a6b fix #31 type records for simple types and extern keyword for methods/functions 2025-06-19 01:06:45 +04:00
75a6f20150 fix #31 type records for simple types and extern keyword for methods/functions 2025-06-17 18:42:37 +04:00
d969d6d572 fix #37 AppliedContext optimization 2025-06-17 09:49:05 +04:00
2d4c4d345d fix #30: let, apply, also. Fix in context combining for lambda calls. 2025-06-16 15:44:22 +04:00
f9416105ec fix #29 mapentries, map iterators, => operator and .toMap() 2025-06-16 02:14:53 +04:00
c002204420 fix #28 basic map supports (still no iterators) 2025-06-16 01:12:04 +04:00
a4448ab2ff fix #10 set
+collection functions (map, forEach, toList, toSet, isEmpty, etc,)
2025-06-15 18:01:44 +04:00
8a4363bd84 fix #26 lists redesigned for ranges, less chaotic and serpentic 2025-06-14 14:51:41 +04:00
19eae213ec fix #23 string formatting and manipulations 2025-06-14 11:53:18 +04:00
1db1f12be3 refs #25 backbone of multiplatform shell 2025-06-14 10:18:38 +04:00
dcde11d722 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	lyng/src/nativeMain/kotlin/Common.native.kt
2025-06-14 01:21:44 +04:00
83e79f47c7 lyng CLI: support for shebang, started shell KMP code 2025-06-14 01:20:26 +04:00
e0bb183929 lyng CLI: support for shebang, started shell KMP code 2025-06-14 01:20:00 +04:00
b961296425 fix #19 set of null-coalesce operators 2025-06-13 22:25:18 +04:00
bd2b6bf06e readme actualized 2025-06-13 21:20:45 +04:00
253480e32a readme actualized 2025-06-13 20:15:39 +04:00
eb8110cbf0 removed unnecessary 2025-06-13 19:12:51 +04:00
8c6a1979ed published to our maven 2025-06-13 19:11:28 +04:00
185aa4e0cf better docs/2 2025-06-13 18:40:14 +04:00
ef266b73a2 better docs/2 2025-06-13 18:03:51 +04:00
89427de5cd better docs 2025-06-13 18:02:34 +04:00
cfb2f7f128 more docs, fixed parsing of an empty string 2025-06-13 17:59:40 +04:00
be4f2c7f45 fix #14 when(value) with blows and whistles. Collection now is container (has Contains). greatly improved container properties of builtin classes. 2025-06-13 17:27:23 +04:00
7cc80e2433 fix #20 do-while tests for labels, values, else 2025-06-13 11:05:30 +04:00
dacdcd7faa ref #13 get rid pf tokens for private/protected, added open attribute parsing 2025-06-13 10:31:32 +04:00
59a76efdce fix #22 test throw from kotlin code 2025-06-13 07:51:42 +04:00
bb862e6cb5 refs #22 test for try-free 2025-06-13 01:52:44 +04:00
aea819b89a refs #22 docs reordered chapters 2025-06-13 01:45:33 +04:00
88974e0f2d refs #22 try without catch but finally 2025-06-13 01:44:13 +04:00
0981d8370e v0.5.1-SNAPSHOT: Exceptions Handling! 2025-06-13 01:36:21 +04:00
c4122e8243 refs #22 more docs 2025-06-13 01:32:40 +04:00
c3bf536bab refs #22 more docs, fixed EH bug
+shortcut throw "message"
2025-06-13 01:30:26 +04:00
5ed8b2f123 refs #22 @35m 2 shorter forms for catch block, fixed catch class instance type checks 2025-06-13 00:12:21 +04:00
6c71f0a2e6 fix #18 basic exceptions handling 2025-06-12 19:17:35 +04:00
95aae0b231 fix #21 fixed loop break with label behavior 2025-06-12 12:34:03 +04:00
b3f08b4cac fix #1 for-continue test 2025-06-12 12:23:50 +04:00
badeea9b28 0.4.0-SNAPSHOT: do-while, val/var returns initial value and accept any statement as initialization, etc. 2025-06-12 11:24:35 +04:00
a1267c4395 fix #17 do-while loops
+mode string functions
+val/var cold be initialized with statement and return assigned values
2025-06-12 11:22:27 +04:00
55fd3ea716 more docs 2025-06-11 14:06:19 +04:00
697bafdcee fix for gitea KaTex parser bug 2025-06-11 09:57:35 +03:00
28b83f9892 ref #16 better processing of the last-block argument
+sample with national-characters vars
2025-06-11 10:54:06 +04:00
194fc8aca6 fix #16 last-block argument now can be combined with defaults and ellipsis 2025-06-11 09:43:42 +04:00
382532e0e1 further Arguments optimizations 2025-06-11 09:12:07 +04:00
c0eba1ecf0 Arguments.Info optimized out 2025-06-11 09:06:01 +04:00
b253eed032 fixed 1e-6 type Real literal 2025-06-11 08:59:09 +04:00
d482401b15 ObjInstance / ObjClass optimization no more list of accessible fields! 2025-06-10 23:51:01 +04:00
88b355c40d ObjInstanceClass optimization 2025-06-10 23:14:44 +04:00
652e1d3af4 fix #15, default toString for a class
+default comparison for a class
!give up struct in favor of class (simpler)
2025-06-10 18:51:04 +04:00
2a93e6f7da fix #9 block argument {} call without () 2025-06-10 18:32:38 +04:00
20c81dbf2e fix #11 and private visibility for constructor params, fields and methods. 2025-06-10 12:07:07 +04:00
fffa3d31bb - rewritten/fixed buildconfig
- less noise
- cosmetics in CLI tool
2025-06-10 02:06:03 +04:00
77 changed files with 4341 additions and 1028 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ xcuserdata
*.gpg
.gigaide
/kotlin-js-store/yarn.lock
/test.lyng

View File

@ -9,6 +9,10 @@ class Point(x,y) {
fun dist() { sqrt(x*x + y*y) }
}
Point(3,4).dist() //< 5
fun swapEnds(first, args..., last, f) {
f( last, ...args, first)
}
```
- extremely simple Kotlin integration on any platform
@ -31,16 +35,42 @@ and it is multithreaded on platforms supporting it (automatically, no code chang
## Integration in Kotlin multiplatform
### Add library
### Add dependency to your project
TBD
```kotlin
// update to current please:
val lyngVersion = "0.6.1-SNAPSHOT"
repositories {
// ...
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
}
```
And add dependency to the proper place in your project, it could look like:
```kotlin
comminMain by getting {
dependencies {
// ...
implementation("net.sergeych:lynglib:$lyngVersion")
}
}
```
Now you can import lyng and use it:
### Execute script:
```kotlin
assertEquals("hello, world", eval("""
"hello, " + "world"
""").toString())
import net.sergeyh.lyng.*
// we need a coroutine to start, as Lyng
// is a coroutine based language, async topdown
runBlocking {
assert(5 == eval(""" 3*3 - 4 """).toInt())
eval(""" println("Hello, Lyng!") """)
}
```
### Exchanging information
@ -49,6 +79,8 @@ Script is executed over some `Context`. Create instance of the context,
add your specific vars and functions to it, an call over it:
```kotlin
import new.sergeych.lyng.*
// simple function
val context = Context().apply {
addFn("addArgs") {
@ -83,22 +115,51 @@ Designed to add scripting to kotlin multiplatform application in easy and effici
# Language
- dynamic
- async
- multithreaded (coroutines could be dispatched using threads on appropriate platforms, automatically)
- Javascript, WasmJS, native, JVM, android - batteries included.
- dynamic types in most elegant and concise way
- async, 100% coroutines, supports multiple cores where platofrm supports thread
- good for functional an object-oriented style
## By-stage
# Language Roadmap
Here are plans to develop it:
## v1.0.0
### First stage
Planned autumn 2025. Complete dynamic language with sufficient standard library:
Interpreted, precompiled into threaded code, actually. Dynamic types.
Ready features:
### Second stage
- [x] Language platform and independent command-line launcher
- [x] Integral types and user classes, variables and constants, functions
- [x] lambdas and closures, coroutines for all callables
- [x] while-else, do-while-else, for-else loops with break-continue returning values and labels support
- [x] ranges, lists, strings, interfaces: Iterable, Iterator, Collection, Array
- [x] when(value), if-then-else
- [x] exception handling: throw, try-catch-finally, exception classes.
- [x] multiplatform maven publication
- [x] documentation for the current state
Will add:
Under way:
- optimizations
- p-code serialization
- static typing
- [ ] maps, sets and sequences (flows?)
- [ ] regular exceptions
- [ ] modules
- [ ] string interpolation and more string tools
- [ ] multiple inheritance for user classes
- [ ] launch, deferred, coroutineScope, mutex, etc.
- [ ] site with integrated interpreter to give a try
- [ ] kotlin part public API good docs, integration focused
- [ ] better stack reporting
## v1.1+
Planned features.
- [ ] type specifications
- [ ] source docs and maybe lyng.md to a standard
- [ ] moacro-style kotlin integration or something else to simplify it
Further
- [ ] client with GUI support based on compose multiplatform somehow
- [ ] notebook - style workbooks with graphs, formulaes, etc.
- [ ] language server or compose-based lyng-aware editor

3
bin/lyng_test Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env lyng
println("Hello from lyng!")

13
docs/Collection.md Normal file
View File

@ -0,0 +1,13 @@
# Collection
Is a [Iterable] with known `size`, a finite [Iterable]:
class Collection : Iterable {
val size
}
See [List], [Set] and [Iterable]
[Iterable]: Iterable.md
[List]: List.md
[Set]: Set.md

View File

@ -1,13 +1,17 @@
# Iterable interface
The inteface which requires iterator to be implemented:
Iterable is a class that provides function that creates _the iterator_:
fun iterator(): Iterator
class Iterable {
abstract fun iterator()
}
Note that each call of `iterator()` must provide an independent iterator.
Iterator itself is a simple interface that should provide only to method:
interface Iterable {
fun hasNext(): Bool
class Iterator {
abstract fun hasNext(): Bool
fun next(): Obj
}
@ -15,19 +19,26 @@ Just remember at this stage typed declarations are not yet supported.
Having `Iterable` in base classes allows to use it in for loop. Also, each `Iterable` has some utility functions available:
## Instance methods
fun Iterable.toList(): List
fun Iterable.toSet(): Set
fun Iterable.indexOf(element): Int
fun Iterable.contains(element): Bool
fun Iterable.isEmpty(element): Bool
fun Iterable.forEach(block: (Any?)->Void ): Void
fun Iterable.map(block: (Any?)->Void ): List
## Abstract methods
fun iterator(): Iterator
## Instance methods
### toList()
Creates a list by iterating to the end. So, the Iterator should be finite to be used with it.
## Included in interfaces:
- Collection, Array, [List]
- [Collection], Array, [List]
## Implemented in classes:

View File

@ -20,11 +20,11 @@ indexing is zero-based, as in C/C++/Java/Kotlin, etc.
list[1]
>>> 20
Using negative indexes has a special meaning: _offset from the end of the list_:
There is a shortcut for the last:
val list = [10, 20, 30]
list[-1]
>>> 30
[list.last, list.lastIndex]
>>> [30, 2]
__Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too.
@ -38,7 +38,8 @@ You can concatenate lists or iterable objects:
## Appending
To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will concatenate [Iterable] objects instead of appending them. To append [Iterable] instance itself, use `list.add`:
To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will
concatenate [Iterable] objects instead of appending them. To append [Iterable] instance itself, use `list.add`:
var list = [1, 2]
val other = [3, 4]
@ -57,7 +58,28 @@ To append to lists, use `+=` with elements, lists and any [Iterable] instances,
>>> void
## Removing elements
List is mutable, so it is possible to remove its contents. To remove a single element
by index use:
assertEquals( [1,2,3].removeAt(1), [1,3] )
assertEquals( [1,2,3].removeAt(0), [2,3] )
assertEquals( [1,2,3].removeLast(), [1,2] )
>>> void
There is a way to remove a range (see [Range] for more on ranges):
assertEquals( [1, 4], [1,2,3,4].removeRange(1..2))
assertEquals( [1, 4], [1,2,3,4].removeRange(1..<3))
>>> void
Open end ranges remove head and tail elements:
assertEquals( [3, 4, 5], [1,2,3,4,5].removeRange(..1))
assertEquals( [3, 4, 5], [1,2,3,4,5].removeRange(..<2))
assertEquals( [1, 2], [1,2,3,4,5].removeRange( (2..) ))
>>> void
## Comparisons
@ -72,19 +94,44 @@ To append to lists, use `+=` with elements, lists and any [Iterable] instances,
## Members
| name | meaning | type |
|-----------------------------------|-------------------------------------|----------|
| `size` | current size | Int |
| `add(elements...)` | add one or more elements to the end | Any |
| `addAt(index,elements...)` | insert elements at position | Int, Any |
| `removeAt(index)` | remove element at position | Int |
| `removeRangeInclusive(start,end)` | remove range, inclusive (1) | Int, Int |
| | | |
| name | meaning | type |
|-------------------------------|---------------------------------------|-------------|
| `size` | current size | Int |
| `add(elements...)` | add one or more elements to the end | Any |
| `insertAt(index,elements...)` | insert elements at position | Int, Any |
| `removeAt(index)` | remove element at position | Int |
| `remove(from,toNonInclusive)` | remove range from (incl) to (nonincl) | Int, Int |
| `remove(Range)` | remove range | Range |
| `removeLast()` | remove last element | |
| `removeLast(n)` | remove n last elements | Int |
| `contains(element)` | check the element is in the list (1) | |
| `[index]` | get or set element at index | Int |
| `[Range]` | get slice of the array (copy) | Range |
| `+=` | append element(s) | List or Obj |
(1)
: end-inclisiveness allows to use negative indexes to, for exampe, remove several last elements, like `list.removeRangeInclusive(-2, -1)` will remove two last elements.
: optimized implementation that override `Array` one
(2)
: `+=` append either a single element, or all elements if the List or other Iterable
instance is appended. If you want to append an Iterable object itself, use `add` instead.
It inherits from [Iterable] too.
## Member inherited from Array
| name | meaning | type |
|------------------|--------------------------------|-------|
| `last` | last element (throws if empty) | |
| `lastOrNull` | last element or null | |
| `lastIndex` | | Int |
| `indices` | range of indexes | Range |
| `contains(item)` | test that item is in the list | |
(1)
: end-inclisiveness allows to use negative indexes to, for exampe, remove several last elements, like
`list.removeRangeInclusive(-2, -1)` will remove two last elements.
# Notes
Could be rewritten using array as a class but List as the interface
[Range]: Range.md
[Iterable]: Iterable.md

109
docs/Map.md Normal file
View File

@ -0,0 +1,109 @@
# Map
Map is a mutable collection of key-value pars, where keys are unique. Maps could be created with
constructor or `.toMap` methods. When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List].
Constructed map instance is of class `Map` and implements `Collection` (and therefore `Iterable`)
val map = Map( ["foo", 1], ["bar", "buzz"] )
assert(map is Map)
assert(map.size == 2)
assert(map is Iterable)
>>> void
Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of standard types are). You can access elements with indexing operator:
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
assert( map["bar"] == "buzz")
assert( map[42] == "answer" )
assertThrows { map["nonexistent"] }
assert( map.getOrNull(101) == null )
assert( map.getOrPut(911) { "nine-eleven" } == "nine-eleven" )
// now 91 entry is set:
assert( map[911] == "nine-eleven" )
map["foo"] = -1
assert( map["foo"] == -1)
>>> void
To remove item from the collection. use `remove`. It returns last removed item or null. Be careful if you
hold nulls in the map - this is not a recommended practice when using `remove` returned value. `clear()`
removes all.
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
assertEquals( 1, map.remove("foo") )
assert( map.getOrNull("foo") == null)
assert( map.size == 2 )
map.clear()
assert( map.size == 0 )
>>> void
Map implements [contains] method that checks _the presence of the key_ in the map:
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
assert( "foo" in map )
assert( "answer" !in map )
>>> void
To iterate maps it is convenient to use `keys` method that returns [Set] of keys (keys are unique:
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
for( k in map.keys ) println(map[k])
>>> 1
>>> buzz
>>> answer
>>> void
Or iterate its key-value pairs that are instances of [MapEntry] class:
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
for( entry in map ) {
println("map[%s] = %s"(entry.key, entry.value))
}
void
>>> map[foo] = 1
>>> map[bar] = buzz
>>> map[42] = answer
>>> void
There is a shortcut to use `MapEntry` to create maps: operator `=>` which creates `MapEntry`:
val entry = "answer" => 42
assert( entry is MapEntry )
>>> void
And you can use it to construct maps:
val map = Map( "foo" => 1, "bar" => 22)
assertEquals(1, map["foo"])
assertEquals(22, map["bar"])
>>> void
Or use `.toMap` on anything that implements [Iterable] and which elements implements [Array] with 2 elements size, for example, `MapEntry`:
val map = ["foo" => 1, "bar" => 22].toMap()
assert( map is Map )
assertEquals(1, map["foo"])
assertEquals(22, map["bar"])
>>> void
It is possible also to get values as [List] (values are not unique):
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
assertEquals(map.values, [1, "buzz", "answer"] )
>>> void
Map could be tested to be equal: when all it key-value pairs are equal, the map
is equal.
val m1 = Map(["foo", 1])
val m2 = Map(["foo", 1])
val m3 = Map(["foo", 2])
assert( m1 == m2 )
// but the references are different:
assert( m1 !== m2 )
// different maps:
assert( m1 != m3 )
>>> void
[Collection](Collection.md)

View File

@ -1,6 +1,168 @@
# OO implementation in Lyng
Basic principles:
## Declaration
The class clause looks like
class Point(x,y)
assert( Point is Class )
>>> void
It creates new `Class` with two fields. Here is the more practical sample:
class Point(x,y) {
fun length() { sqrt(x*x + y*y) }
}
val p = Point(3,4)
assert(p is Point)
assertEquals(5, p.length())
// we can access the fields:
assert( p.x == 3 )
assert( p.y == 4 )
// we can assign new values to fields:
p.x = 1
p.y = 1
assertEquals(sqrt(2), p.length())
>>> void
Let's see in details. The statement `class Point(x,y)` creates a class,
with two field, which are mutable and publicly visible.`(x,y)` here
is the [argument list], same as when defining a function. All together creates a class with
a _constructor_ that requires two parameters for fields. So when creating it with
`Point(10, 20)` we say _calling Point constructor_ with these parameters.
Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the
example above.
Class point has a _method_, or a _member function_ `length()` that uses its _fields_ `x` and `y` to
calculate the magnitude. Length is called
### default values in constructor
Constructor arguments are the same as function arguments except visibility
statements discussed later, there could be default values, ellipsis, etc.
class Point(x=0,y=0)
val p = Point()
assert( p.x == 0 && p.y == 0 )
>>> void
## Methods
Functions defined inside a class body are methods, and unless declared
`private` are available to be called from outside the class:
class Point(x,y) {
// public method declaration:
fun length() { sqrt(d2()) }
// private method:
private fun d2() {x*x + y*y}
}
val p = Point(3,4)
// private called from inside public: OK
assertEquals( 5, p.length() )
// but us not available directly
assertThrows { p.d2() }
void
>>> void
## fields and visibility
It is possible to add non-constructor fields:
class Point(x,y) {
fun length() { sqrt(x*x + y*y) }
// set at construction time:
val initialLength = length()
}
val p = Point(3,4)
p.x = 3
p.y = 0
assertEquals( 3, p.length() )
// but initial length could not be changed after as declard val:
assert( p.initialLength == 5 )
>>> void
### Mutable fields
Are declared with var
class Point(x,y) {
var isSpecial = false
}
val p = Point(0,0)
assert( p.isSpecial == false )
p.isSpecial = true
assert( p.isSpecial == true )
>>> void
### Private fields
Private fields are visible only _inside the class instance_:
class SecretCounter {
private var count = 0
fun increment() {
count++
void // hide counter
}
fun isEnough() {
count > 10
}
}
val c = SecretCounter()
assert( c.isEnough() == false )
assert( c.increment() == void )
for( i in 0..10 ) c.increment()
assert( c.isEnough() )
// but the count is not available outside:
assertThrows { c.count }
void
>>> void
It is possible to provide private constructor parameters so they can be
set at construction but not available outside the class:
class SecretCounter(private var count = 0) {
// ...
}
val c = SecretCounter(10)
assertThrows { c.count }
void
>>> void
## Default class methods
In many cases it is necessary to implement custom comparison and `toString`, still
each class is provided with default implementations:
- default toString outputs class name and its _public_ fields.
- default comparison compares compares all public fields in order of appearance.
For example, for our class Point:
class Point(x,y)
assert( Point(1,2) == Point(1,2) )
assert( Point(1,2) !== Point(1,2) )
assert( Point(1,2) != Point(1,3) )
assert( Point(1,2) < Point(2,2) )
assert( Point(1,2) < Point(1,3) )
Point(1,1+1)
>>> Point(x=1,y=2)
# Theory
## Basic principles:
- Everything is an instance of some class
- Every class except Obj has at least one parent
@ -71,41 +233,6 @@ Regular methods are called on instances as usual `instance.method()`. The method
2. parents method: no guarantee but we enumerate parents in order of appearance;
3. possible extension methods (scoped)
# Defining a new class
The class is a some data record with named fields and fixed order, in fact. To define a class,
just Provide a name and a record like this:
// creating new class with main constructor
// with all fields public and mutable:
struct Point(x,y)
assert( Point is Class )
// now we can create instance
val p1 = Point(3,4)
// is is of the newly created type:
assert( p1 is Point )
// we can read and write its fields:
assert( p1.x == 3 )
assert( p1.y == 4 )
p1.y++
assert( p1.y == 5 )
>>> void
Let's see in details. The statement `struct Point(x,y)` creates a struct, or public class,
with two field, which are mutable and publicly visible, because it is _struct_. `(x,y)` here
is the [argument list], same as when defining a function. All together creates a class with
a _constructor_ that requires two parameters for fields. So when creating it with
`Point(10, 20)` we say _calling Point constructor_ with these parameters.
Such declaration is identical to `class Point(var x,var y)` which does exactly the same.
TBD
[argument list](declaring_arguments.md)

94
docs/Set.md Normal file
View File

@ -0,0 +1,94 @@
# List built-in class
Mutable set of any objects: a group of different objects, no repetitions.
Sets are not ordered, order of appearance does not matter.
val set = Set(1,2,3, "foo")
assert( 1 in set )
assert( "foo" in set)
assert( "bar" !in set)
>>> void
## Set is collection and therefore [Iterable]:
assert( Set(1,2) is Set)
assert( Set(1,2) is Iterable)
assert( Set(1,2) is Collection)
>>> void
So it supports all methods from [Iterable]; set is not, though, an [Array] and has
no indexing. Use [set.toList] as needed.
## Set operations
// Union
assertEquals( Set(1,2,3,4), Set(3, 1) + Set(2, 4))
// intersection
assertEquals( Set(1,4), Set(3, 1, 4).intersect(Set(2, 4, 1)) )
// or simple
assertEquals( Set(1,4), Set(3, 1, 4) * Set(2, 4, 1) )
// To find collection elements not present in another collection, use the
// subtract() or `-`:
assertEquals( Set( 1, 2), Set(1, 2, 4, 3) - Set(3, 4))
>>> void
## Adding elements
var s = Set()
s += 1
assertEquals( Set(1), s)
s += [3, 3, 4]
assertEquals( Set(3, 4, 1), s)
>>> void
## Removing elements
List is mutable, so it is possible to remove its contents. To remove a single element
by index use:
var s = Set(1,2,3)
s.remove(2)
assertEquals( s, Set(1,3) )
s = Set(1,2,3)
s.remove(2,1)
assertEquals( s, Set(3) )
>>> void
Note that `remove` returns true if at least one element was actually removed and false
if the set has not been changed.
## Comparisons and inclusion
Sets are only equal when contains exactly same elements, order, as was said, is not significant:
assert( Set(1, 2) == Set(2, 1) )
assert( Set(1, 2, 2) == Set(2, 1) )
assert( Set(1, 3) != Set(2, 1) )
assert( 1 in Set(5,1))
assert( 10 !in Set(5,1))
>>> void
## Members
| name | meaning | type |
|---------------------|--------------------------------------|-------|
| `size` | current size | Int |
| `+=` | add one or more elements | Any |
| `+`, `union` | union sets | Any |
| `-`, `subtract` | subtract sets | Any |
| `*`, `intersect` | subtract sets | Any |
| `remove(items...)` | remove one or more items | Range |
| `contains(element)` | check the element is in the list (1) | |
(1)
: optimized implementation that override `Iterable` one
Also, it inherits methods from [Iterable].
[Range]: Range.md

0
docs/String.md Normal file
View File

View File

@ -86,4 +86,28 @@ Lambda functions remember their scopes, so it will work the same as previous:
val c = createLambda()
println(c)
>> 1
>> void
>> void
# Elements of functional programming
With ellipsis and splats you can create partial functions, manipulate
arguments list in almost arbitrary ways. For example:
// Swap first and last arguments for call
fun swap_args(first, others..., last, f) {
f(last, ...others, first)
}
fun glue(args...) {
var result = ""
for( a in args ) result += a
}
assertEquals(
"4231",
swap_args( 1, 2, 3, 4, glue)
)
>>> void
,

View File

@ -1,7 +1,7 @@
# Declaring arguments in Lyng
It is a common thing that occurs in many places in Lyng, function declarations,
lambdas, struct and class declarations.
lambdas and class declarations.
## Regular

View File

@ -39,22 +39,5 @@ We can just put the code into the module code:
## class initialization
class foo {
private static var instanceCounter = 0
val instanceId = instanceCounter
fun close() {
instanceCounter--
}
// instance initializatino could be as this:
if( instanceId > 100 )
throw Exception("Too many instances")
static {
// static, one-per-class initializer could be posted here
instanceCounter = 1
}
}
already done using `ObjInstance` class and instance-bound context with local
context stored in ObjInstance and class constructor statement in ObjClass.

145
docs/exceptions_handling.md Normal file
View File

@ -0,0 +1,145 @@
# Exceptions handling
Exceptions are widely used in modern programming languages, so
they are implemented also in Lyng and in the most complete way.
# Exception classes
Exceptions are throwing instances of some class that inherits `Exception`
across the code. Below is the list of built-in exceptions. Note that
only objects that inherit `Exception` can be thrown. For example:
assert( IllegalArgumentException() is Exception)
>>> void
# Try statement: catching exceptions
There general pattern is:
```
try_statement = try_clause, [catch_clause, ...], [finally_clause]
try_clause = "try", "{", statements, "}"
catch_clause = "catch", [(full_catch | shorter_catch)], "{", statements "}"
full_catch = "(", catch_var, ":", exception_class [, excetpion_class...], ")
shorter_catch = "(", catch_var, ")"
finally_clause = "{", statements, "}"
```
Let's in details.
## Full catch block:
val result = try {
throw IllegalArgumentException("the test")
}
catch( x: IndexOutOfBoundsException, IllegalArgumentException) {
x.message
}
catch(x: Exception) {
"bad"
}
assertEquals(result, "the test")
>>> void
Because our exception is listed in a first catch block, it is processed there.
The full form allow a single catch block to process exceptions with specified classes and bind actual caught object to
the given variable. This is most common and well known form, implemented like this or similar in many other languages,
like Kotlin, Java or C++.
## Shorter form
When you want to catch _all_ the exceptions, you should write `catch(e: Exception)`,
but it is somewhat redundant, so there is simpler variant:
val sample2 = try {
throw IllegalArgumentException("sample 2")
}
catch(x) {
x.message
}
assertEquals( sample2, "sample 2" )
>>> void
But well most likely you will find default variable `it`, like in Kotlin, more than enough
to catch all exceptions to, then you can write it even shorter:
val sample2 = try {
throw IllegalArgumentException("sample 3")
}
catch {
it.message
}
assertEquals( sample2, "sample 3" )
>>> void
You can even check the type of the `it` and create more convenient and sophisticated processing logic. Such approach is
used, for example, in Scala.
## finally block
If `finally` block present, it will be executed after body (until first exception)
and catch block, if any will match. finally statement is executed even if the
exception will be thrown and not caught locally. It does not alter try/catch block result:
try {
}
finally {
println("called finally")
}
>>> called finally
>>> void
- and yes, there could be try-finally block, no catching, but perform some guaranteed cleanup.
# Conveying data with exceptions
The simplest way is to provide exception string and `Exception` class:
try {
throw Exception("this is my exception")
}
catch {
it.message
}
>>> "this is my exception"
This way, in turn, can also be shortened, as it is overly popular:
try {
throw "this is my exception"
}
catch {
it.message
}
>>> "this is my exception"
The trick, though, works with strings only, and always provide `Exception` instances, which is good for debugging but
most often not enough.
# Custom error classes
_this functionality is not yet released_
# Standard exception classes
| class | notes |
|----------------------------|-------------------------------------------------------|
| Exception | root of al throwable objects |
| NullReferenceException | |
| AssertionFailedException | |
| ClassCastException | |
| IndexOutOfBoundsException | |
| IllegalArgumentException | |
| IllegalAssignmentException | assigning to val, etc. |
| SymbolNotDefinedException | |
| IterationEndException | attempt to read iterator past end, `hasNext == false` |
| AccessException | attempt to access private members or like |
| UnknownException | unexpected kotlin exception caught |
| | |

26
docs/samples/sum.lyng Normal file
View File

@ -0,0 +1,26 @@
/*
Calculate the limit of Sum( f(n) )
until it reaches asymptotic limit 0.00001% change
return null or found limit
*/
fun findSumLimit(f) {
var sum = 0.0
for( n in 1..1000000 ) {
val s0 = sum
sum += f(n)
if( abs(sum - s0) / abs(sum) < 1.0e-6 ) {
println("limit reached after "+n+" rounds")
break sum
}
n++
}
else {
println("limit not reached")
null
}
}
val limit = findSumLimit { n -> 1.0/n/n }
println("Result: "+limit)

View File

@ -0,0 +1,53 @@
# Пример расчета суммы ряда
Рассмотрим как можно посчитать предел суммы ряда на lyng. Для наивной реализации
представим что у нас есть функция рассчитывающая n-й член ряда. Тогда мы можем
считать сумму до тех пор, пока отклонение при расчете следующего члена не станет
меньше чем заданная погрешность:
fun сумма_ряда(x, погрешность=0.0001, f) {
var сумма = 0
for( n in 1..100000) {
val следующая_сумма = сумма + f(x, n)
if( n > 1 && abs(следующая_сумма - сумма) < погрешность )
break следующая_сумма
сумма = следующая_сумма
}
else null
}
Для проверки можно посчитать на хорошо известном ряду Меркатора
$$ \ln(1+x)=x-{\dfrac {x^{2}}{2}}+{\dfrac {x^{3}}{3}}-\cdots =\sum \limits _{n=0}^{\infty }{\dfrac {(-1)^{n}x^{n+1}}{(n+1)}}=\sum \limits _{n=1}^{\infty }{\dfrac {(-1)^{n-1}x^{n}}{n}} $$
Который в нашем случае для точки $ x = 1 $ можно записать так:
val x = сумма_ряда(1) { x, n ->
val sign = if( n % 2 == 1 ) 1 else -1
sign * pow(x, n) / n
}
Проверим:
assert( x - ln(2) < 0.001 )
В нашем примере есть изъян - погрешность вычисляется примитивно: `abs(следующая_сумма - сумма) < погрешность`, что совершенно неверно, если значения малы. Значительно более корректно вычислять погрешность, нормированную на диапазон сравниваемых величин:
fun погрешность(x0, x1) {
abs( x1 - x0 ) / (abs( x1 + x0 ) / 2.0)
}
// относительная погрешность одинакова и в разных диапазонах
assertEquals( погрешность(5,6), погрешность(0.005,0.006))
Теперь мы могли бы написать более корректное сравнение для вещественных
// расхождение не больше 1% или сколько укажете:
fun почти_равны(a,b,epsilon=0.01) {
погрешность(a,b) <= epsilon
}
assert( почти_равны( 0.0005, 0.000501 ) )
Во многих случаях вычисление $n+1$ члена значительно проще cчитается от предыдущего члена, в нашем случае это можно было бы записать через итератор, что мы вскоре добавим.
(продолжение следует)

View File

@ -12,11 +12,10 @@ In other word, the code usually works as expected when you see it. So, nothing u
__Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md)
- Some class references: [List], [Real], [Range], [Iterable], [Iterator]
- Some samples: [combinatorics](samples/combinatorics.lyng.md)
See [samples folder](samples)
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator]
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
# Expressions
@ -119,6 +118,109 @@ These operators return rvalue, unmodifiable.
## Assignment return r-value!
Naturally, assignment returns its value:
var x
x = 11
>>> 11
rvalue means you cant assign the result if the assignment
var x
assertThrows { (x = 11) = 5 }
void
>>> void
This also prevents chain assignments so use parentheses:
var x
var y
x = (y = 1)
>>> 1
## Nullability
When the value is `null`, it might throws `NullReferenceException`, the name is somewhat a tradition. To avoid it
one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null,
the operation won't be performed and the result will be null. Here is the difference:
val ref = null
assertThrows { ref.field }
assertThrows { ref.method() }
assertThrows { ref.array[1] }
assertThrows { ref[1] }
assertThrows { ref() }
assert( ref?.field == null )
assert( ref?.method() == null )
assert( ref?.array?[1] == null )
assert( ref?[1] == null )
assert( ref?() == null )
>>> void
There is also "elvis operator", null-coalesce infix operator '?:' that returns rvalue if lvalue is `null`:
null ?: "nothing"
>>> "nothing"
## Utility functions
The following functions simplify nullable values processing and
allow to improve code look and readability. There are borrowed from Kotlin:
### let
`value.let {}` passes to the block value as the single parameter (by default it is assigned to `it`) and return block's returned value. It is useful dealing with null or to
get a snapshot of some externally varying value, or with `?.` to process nullable value in a safe manner:
// this state is changed from parallel processes
class GlobalState(nullableParam)
val state = GlobalState(null)
fun sample() {
state.nullableParam?.let { "it's not null: "+it} ?: "it's null"
}
assertEquals(sample(), "it's null")
state.nullableParam = 5
assertEquals(sample(), "it's not null: 5")
>>> void
This is the same as:
fun sample() {
val it = state.nullableParam
if( it != null ) "it's not null: "+it else "it's null"
}
The important is that nullableParam got a local copy that can't be changed from any
parallel thread/coroutine. Remember: Lyng _is __not__ a single-threaded language_.
## Also
Much like let, but it does not alter returned value:
assert( "test".also { println( it + "!") } == "test" )
>>> test!
>>> void
While it is not altering return value, the source object could be changed:
class Point(x,y)
val p = Point(1,2).also { it.x++ }
assertEquals(p.x, 2)
>>> void
## apply
It works much like `also`, but is executed in the context of the source object:
class Point(x,y)
// see the difference: apply changes this to newly created Point:
val p = Point(1,2).apply { x++; y++ }
assertEquals(p, Point(2,3))
>>> void
## Math
It is rather simple, like everywhere else:
@ -470,64 +572,97 @@ To add elements to the list:
assert( x == [1, 2, 3, "the", "end"])
>>> void
Self-modifying concatenation by `+=` also works:
Self-modifying concatenation by `+=` also works (also with single elements):
val x = [1, 2]
x += [3, 4]
assert( x == [1, 2, 3, 4])
x += 5
assert( x == [1, 2, 3, 4, 5])
>>> void
You can insert elements at any position using `addAt`:
You can insert elements at any position using `insertAt`:
val x = [1,2,3]
x.addAt(1, "foo", "bar")
x.insertAt(1, "foo", "bar")
assert( x == [1, "foo", "bar", 2, 3])
>>> void
Using splat arguments can simplify inserting list in list:
val x = [1, 2, 3]
x.addAt( 1, ...[0,100,0])
x.insertAt( 1, ...[0,100,0])
x
>>> [1, 0, 100, 0, 2, 3]
Using negative indexes can insert elements as offset from the end, for example:
val x = [1,2,3]
x.addAt(-1, 10)
x
>>> [1, 2, 10, 3]
Note that to add to the end you still need to use `add` or positive index of the after-last element:
val x = [1,2,3]
x.addAt(3, 10)
x.insertAt(3, 10)
x
>>> [1, 2, 3, 10]
but it is much simpler, and we recommend to use '+='
val x = [1,2,3]
x += 10
>>> [1, 2, 3, 10]
## Removing list items
val x = [1, 2, 3, 4, 5]
x.removeAt(2)
assert( x == [1, 2, 4, 5])
// or remove range (start inclusive, end exclusive):
x.removeRangeInclusive(1,2)
x.removeRange(1..2)
assert( x == [1, 5])
>>> void
Again, you can use negative indexes. For example, removing last elements like:
There is a shortcut to remove the last elements:
val x = [1, 2, 3, 4, 5]
// remove last:
x.removeAt(-1)
x.removeLast()
assert( x == [1, 2, 3, 4])
// remove 2 last:
x.removeRangeInclusive(-2,-1)
assert( x == [1, 2])
x.removeLast(2)
assertEquals( [1, 2], x)
>>> void
You can get ranges to extract a portion from a list:
val list = [1, 2, 3, 4, 5]
assertEquals( [1,2,3], list[..2])
assertEquals( [1,2,], list[..<2])
assertEquals( [4,5], list[3..])
assertEquals( [2,3], list[1..2])
assertEquals( [2,3], list[1..<3])
>>> void
# Sets
Set are unordered collection of unique elements, see [Set]. Sets are [Iterable] but have no indexing access.
assertEquals( Set(3, 2, 1), Set( 1, 2, 3))
assert( 5 !in Set(1, 2, 6) )
>>> void
Please see [Set] for detailed description.
# Maps
Maps are unordered collection of key-value pairs, where keys are unique. See [Map] for details. Map also
are [Iterable]:
val m = Map( "foo" => 77, "bar" => "buzz" )
assertEquals( m["foo"], 77 )
>>> void
Please see [Map] reference for detailed description on using Maps.
# Flow control operators
## if-then-else
@ -553,18 +688,101 @@ Or, more neat:
>>> just 3
>>> void
## When
It is very much like the kotlin's:
fun type(x) {
when(x) {
in 'a'..'z', in 'A'..'Z' -> "letter"
in '0'..'9' -> "digit"
'$' -> "dollar"
"EUR" -> "crap"
in ['@', '#', '^'] -> "punctuation1"
in "*&.," -> "punctuation2"
else -> "unknown"
}
}
assertEquals("digit", type('3'))
assertEquals("dollar", type('$'))
assertEquals("crap", type("EUR"))
>>> void
Notice, several conditions can be grouped with a comma.
Also, you can check the type too:
fun type(x) {
when(x) {
"42", 42 -> "answer to the great question"
is Real, is Int -> "number"
is String -> {
for( d in x ) {
if( d !in '0'..'9' )
break "unknown"
}
else "number"
}
}
}
assertEquals("number", type(5))
assertEquals("number", type("153"))
assertEquals("number", type(π/2))
assertEquals("unknown", type("12%"))
assertEquals("answer to the great question", type(42))
assertEquals("answer to the great question", type("42"))
>>> void
### supported when conditions:
#### Contains:
You can thest that _when expression_ is _contained_, or not contained, in some object using `in container` and `!in container`. The container is any object that provides `contains` method, otherwise the runtime exception will be thrown.
Typical builtin types that are containers (e.g. support `conain`):
| class | notes |
|------------|--------------------------------------------|
| Collection | contains an element (1) |
| Array | faster maybe that Collection's |
| List | faster than Array's |
| String | character in string or substring in string |
| Range | object is included in the range (2) |
(1)
: Iterable is not the container as it can be infinite
(2)
: Depending on the inclusivity and open/closed range parameters. BE careful here: String range is allowed, but it is usually not what you expect of it:
assert( "more" in "a".."z") // string range ok
assert( 'x' !in "a".."z") // char in string range: probably error
assert( 'x' in 'a'..'z') // character range: ok
assert( "x" !in 'a'..'z') // string in character range: could be error
>>> void
So we recommend not to mix characters and string ranges; use `ch in str` that works
as expected:
"foo" in "foobar"
>>> true
and also character inclusion:
'o' in "foobar"
>>> true
## while
Regular pre-condition while loop, as expression, loop returns it's last line result:
Regular pre-condition while loop, as expression, loop returns the last expression as everything else:
var count = 0
while( count < 5 ) {
count++
count * 10
}
>>> 50
val result = while( count < 5 ) count++
result
>>> 4
We can break as usual:
Notice it _is 4 because when count became 5, the loop body was not executed!_.
We can break while as usual:
var count = 0
while( count < 5 ) {
@ -683,6 +901,36 @@ So the returned value, as seen from diagram could be one of:
- value returned from the `else` clause, of the loop was not broken
- value returned from the last execution of loop body, if there was no `break` and no `else` clause.
## do-while loops
There works exactly as while loops but the body is executed prior to checking the while condition:
var i = 0
do { i++ } while( i < 1 )
i
>>> 1
The important feature of the do-while loop is that the condition expression is
evaluated on the body scope, e.g., variables, intruduced in the loop body are
available in the condition:
do {
var continueLoop = false
"OK"
} while( continueLoop )
>>> "OK"
This is sometimes convenient when condition is complex and has to be calculated inside the loop body. Notice the value returning by the loop:
fun readLine() { "done: result" }
val result = do {
val line = readLine()
} while( !line.startsWith("done:") )
result.drop(6)
>>> "result"
Suppose readLine() here reads some stream of lines.
## For loops
@ -724,6 +972,62 @@ We can use labels too:
assert( search(["hello", "world"], 'z') == null)
>>> void
# Exception handling
Very much like in Kotlin. Try block returns its body block result, if no exception was cauht, or the result from the catch block that caught the exception:
var error = "not caught"
var finallyCaught = false
val result = try {
throw IllegalArgumentException()
"invalid"
}
catch(nd: SymbolNotDefinedException) {
error = "bad catch"
}
catch(x: IllegalArgumentException) {
error = "no error"
"OK"
}
finally {
// finally does not affect returned value
"too bad"
}
assertEquals( "no error", error)
assertEquals( "OK", result)
>>> void
There is shorter form of catch block when you want to catch any exception:
var caught = null
try {
throw IllegalArgumentException()
}
catch(t) { // same as catch(t: Exception), but simpler
caught = t
}
assert( caught is IllegalArgumentException )
>>> void
And even shortest, for the Lying lang tradition, missing var is `it`:
var caught = null
try {
throw IllegalArgumentException()
}
catch { // same as catch(it: Exception), but simpler
caught = it
}
assert( caught is IllegalArgumentException )
>>> void
It is possible to catch several exceptions in the same block too, use
`catch( varName: ExceptionClass1, ExceptionClass2)`, etc, use short form of throw and
many more.
- see [exception handling](exceptions_handling.md) for detailed exceptions tutorial and reference.
# Self-assignments in expression
There are auto-increments and auto-decrements:
@ -792,7 +1096,7 @@ See [Ranges](Range.md) for detailed documentation on it.
// single line comment
var result = null // here we will store the result
>>> void
>>> null
# Integral data types
@ -838,10 +1142,84 @@ Are the same as in string literals with little difference:
## String details
Strings are arrays of Unicode characters. It can be indexed, and indexing will
return a valid Unicode character at position. No utf hassle:
"Парашют"[5]
>>> 'ю'
Its length is, of course, in characters:
"разум".length
>>> 5
And supports growing set of kotlin-borrowed operations, see below, for example:
assertEquals("Hell", "Hello".dropLast(1))
>>> void
To format a string use sprintf-style modifiers like:
val a = "hello"
val b = 11
assertEquals( "hello:11", "%s:%d"(a, b) )
assertEquals( " hello: 11", "%6s:%6d"(a, b) )
assertEquals( "hello :11 ", "%-6s:%-6d"(a, b) )
>>> void
List of format specifiers closely resembles C sprintf() one. See [format specifiers](https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary), this is doe using [mp_stools kotlin multiplatform library](https://github.com/sergeych/mp_stools). Currently supported Lyng types are `String`, `Int`, `Real`, `Bool`, the rest are displayed using their `toString()` representation.
This list will be extended.
To get the substring use:
assertEquals("pult", "catapult".takeLast(4))
assertEquals("cat", "catapult".take(3))
assertEquals("cat", "catapult".dropLast(5))
assertEquals("pult", "catapult".drop(4))
>>> void
And to get a portion you can slice it with range as the index:
assertEquals( "tap", "catapult"[ 2 .. 4 ])
assertEquals( "tap", "catapult"[ 2 ..< 5 ])
>>> void
Open-ended ranges could be used to get start and end too:
assertEquals( "cat", "catapult"[ ..< 3 ])
assertEquals( "cat", "catapult"[ .. 2 ])
assertEquals( "pult", "catapult"[ 4.. ])
>>> void
### String operations
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
Typical set of String functions includes:
| fun/prop | description / notes |
|--------------------|------------------------------------------------------------|
| lower() | change case to unicode upper |
| upper() | change case to unicode lower |
| startsWith(prefix) | true if starts with a prefix |
| endsWith(prefix) | true if ends with a prefix |
| take(n) | get a new string from up to n first characters |
| takeLast(n) | get a new string from up to n last characters |
| drop(n) | get a new string dropping n first chars, or empty string |
| dropLast(n) | get a new string dropping n last chars, or empty string |
| size | size in characters like `length` because String is [Array] |
| (args...) | sprintf-like formatting, see [string formatting] |
| [index] | character at index |
| [Range] | substring at range |
| s1 + s2 | concatenation |
| s1 += s2 | self-modifying concatenation |
### Literals
String literal could be multiline:
@ -872,5 +1250,7 @@ See [math functions](math.md). Other general purpose functions are:
[Iterator]: Iterator.md
[Real]: Real.md
[Range]: Range.md
[String]: String.md
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
[Set]: Set.md
[Map]: Map.md

View File

@ -1,11 +0,0 @@
count = 0
for n1 in range(10):
for n2 in range(10):
for n3 in range(10):
for n4 in range(10):
for n5 in range(10):
for n6 in range(10):
if n1 + n2 + n3 == n4 + n5 + n6:
count += 1
print(count)

View File

@ -1,54 +0,0 @@
package net.sergeych.lyng
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
suspend fun Collection<ParsedArgument>.toArguments(context: Context): Arguments {
val list = mutableListOf<Arguments.Info>()
for (x in this) {
val value = x.value.execute(context)
if (x.isSplat) {
when {
value is ObjList -> {
for (subitem in value.list) list.add(Arguments.Info(subitem, x.pos))
}
value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(context, "toList") as ObjList).list
i.forEach { list.add(Arguments.Info(it, x.pos)) }
}
else -> context.raiseClassCastError("expected list of objects for splat argument")
}
} else
list.add(Arguments.Info(value, x.pos))
}
return Arguments(list)
}
data class Arguments(val list: List<Info>) : Iterable<Obj> {
data class Info(val value: Obj, val pos: Pos)
val size by list::size
operator fun get(index: Int): Obj = list[index].value
val values: List<Obj> by lazy { list.map { it.value } }
fun firstAndOnly(): Obj {
if (list.size != 1) throw IllegalArgumentException("Expected one argument, got ${list.size}")
return list.first().value
}
companion object {
val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.map { Info(it, Pos.UNKNOWN) })
}
override fun iterator(): Iterator<Obj> {
return list.map { it.value }.iterator()
}
}

View File

@ -1,153 +0,0 @@
package net.sergeych.lyng
val ObjClassType by lazy { ObjClass("Class") }
class ObjClass(
val className: String,
vararg val parents: ObjClass,
) : Obj() {
var instanceConstructor: Statement? = null
val allParentsSet: Set<ObjClass> = parents.flatMap {
listOf(it) + it.allParentsSet
}.toSet()
override val objClass: ObjClass by lazy { ObjClassType }
// members: fields most often
private val members = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className
override suspend fun compareTo(context: Context, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(context: Context): Obj {
println("callOn $this constructing....")
println("on context: $context")
val instance = ObjInstance(this)
instance.instanceContext = context.copy(newThisObj = instance,args = context.args)
if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceContext)
}
return instance
}
fun defaultInstance(): Obj = object : Obj() {
override val objClass: ObjClass = this@ObjClass
}
fun createField(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Compiler.Visibility = Compiler.Visibility.Public,
pos: Pos = Pos.builtIn
) {
if (name in members || allParentsSet.any { name in it.members } == true)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Context.() -> Obj) {
createField(name, statement { code() }, isOpen)
}
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
/**
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
*/
fun getInstanceMemberOrNull(name: String): ObjRecord? {
members[name]?.let { return it }
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } }
return null
}
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
}
/**
* Abstract class that must provide `iterator` method that returns [ObjIterator] instance.
*/
val ObjIterable by lazy {
ObjClass("Iterable").apply {
addFn("toList") {
val result = mutableListOf<Obj>()
val iterator = thisObj.invokeInstanceMethod(this, "iterator")
while (iterator.invokeInstanceMethod(this, "hasNext").toBool())
result += iterator.invokeInstanceMethod(this, "next")
// val next = iterator.getMemberOrNull("next")!!
// val hasNext = iterator.getMemberOrNull("hasNext")!!
// while( hasNext.invoke(this, iterator).toBool() )
// result += next.invoke(this, iterator)
ObjList(result)
}
}
}
/**
* Collection is an iterator with `size`]
*/
val ObjCollection by lazy {
val i: ObjClass = ObjIterable
ObjClass("Collection", i)
}
val ObjIterator by lazy { ObjClass("Iterator") }
class ObjArrayIterator(val array: Obj) : Obj() {
override val objClass: ObjClass by lazy { type }
private var nextIndex = 0
private var lastIndex = 0
suspend fun init(context: Context) {
nextIndex = 0
lastIndex = array.invokeInstanceMethod(context, "size").toInt()
ObjVoid
}
companion object {
val type by lazy {
ObjClass("ArrayIterator", ObjIterator).apply {
addFn("next") {
val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) {
self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj())
} else raiseError(ObjIterationFinishedError(this))
}
addFn("hasNext") {
val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) ObjTrue else ObjFalse
}
}
}
}
}
val ObjArray by lazy {
/**
* Array abstract class is a [ObjCollection] with `getAt` method.
*/
ObjClass("Array", ObjCollection).apply {
// we can create iterators using size/getat:
addFn("iterator") {
ObjArrayIterator(thisObj).also { it.init(this) }
}
addFn("isample") { "ok".toObj() }
}
}

View File

@ -1,29 +0,0 @@
package net.sergeych.lyng
class ObjInstance(override val objClass: ObjClass) : Obj() {
internal val publicFields = mutableSetOf<String>()
internal val protectedFields = mutableSetOf<String>()
internal lateinit var instanceContext: Context
override suspend fun readField(context: Context, name: String): ObjRecord {
return if( name in publicFields ) instanceContext[name]!!
else super.readField(context, name)
}
override suspend fun writeField(context: Context, name: String, newValue: Obj) {
if( name in publicFields ) {
val f = instanceContext[name]!!
if( !f.isMutable ) ObjIllegalAssignmentError(context, "can't reassign val $name").raise()
if( f.value.assign(context, newValue) == null)
f.value = newValue
}
else super.writeField(context, name, newValue)
}
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj {
if( name in publicFields ) return instanceContext[name]!!.value.invoke(context, this, args)
return super.invokeInstanceMethod(context, name, args)
}
}

View File

@ -1,137 +0,0 @@
package net.sergeych.lyng
class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
init {
for (p in objClass.parents)
parentInstances.add(p.defaultInstance())
}
override fun toString(): String = "[${
list.joinToString(separator = ", ") { it.inspect() }
}]"
fun normalize(context: Context, index: Int, allowisEndInclusive: Boolean = false): Int {
val i = if (index < 0) list.size + index else index
if (allowisEndInclusive && i == list.size) return i
if (i !in list.indices) context.raiseError("index $index out of bounds for size ${list.size}")
return i
}
override suspend fun getAt(context: Context, index: Int): Obj {
val i = normalize(context, index)
return list[i]
}
override suspend fun putAt(context: Context, index: Int, newValue: Obj) {
val i = normalize(context, index)
list[i] = newValue
}
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjList) return -2
val mySize = list.size
val otherSize = other.list.size
val commonSize = minOf(mySize, otherSize)
for (i in 0..<commonSize) {
if (list[i].compareTo(context, other.list[i]) != 0) {
return list[i].compareTo(context, other.list[i])
}
}
// equal so far, longer is greater:
return when {
mySize < otherSize -> -1
mySize > otherSize -> 1
else -> 0
}
}
override suspend fun plus(context: Context, other: Obj): Obj =
when {
other is ObjList ->
ObjList((list + other.list).toMutableList())
other.isInstanceOf(ObjIterable) -> {
val l = other.callMethod<ObjList>(context, "toList")
ObjList((list + l.list).toMutableList())
}
else ->
context.raiseError("'+': can't concatenate $this with $other")
}
override suspend fun plusAssign(context: Context, other: Obj): Obj {
// optimization
if (other is ObjList) {
list += other.list
return this
}
if (other.isInstanceOf(ObjIterable)) {
val otherList = other.invokeInstanceMethod(context, "toList") as ObjList
list += otherList.list
} else
list += other
return this
}
override val objClass: ObjClass
get() = type
companion object {
val type = ObjClass("List", ObjArray).apply {
createField("size",
statement {
(thisObj as ObjList).list.size.toObj()
}
)
addFn("getAt") {
requireExactCount(1)
thisAs<ObjList>().getAt(this, requiredArg<ObjInt>(0).value.toInt())
}
addFn("putAt") {
requireExactCount(2)
val newValue = args[1]
thisAs<ObjList>().putAt(this, requiredArg<ObjInt>(0).value.toInt(), newValue)
newValue
}
createField("add",
statement {
val l = thisAs<ObjList>().list
for (a in args) l.add(a)
ObjVoid
}
)
createField("addAt",
statement {
if (args.size < 2) raiseError("addAt takes 2+ arguments")
val l = thisAs<ObjList>()
var index = l.normalize(
this, requiredArg<ObjInt>(0).value.toInt(),
allowisEndInclusive = true
)
for (i in 1..<args.size) l.list.add(index++, args[i])
ObjVoid
}
)
addFn("removeAt") {
val self = thisAs<ObjList>()
val start = self.normalize(this, requiredArg<ObjInt>(0).value.toInt())
if (args.size == 2) {
val end = requireOnlyArg<ObjInt>().value.toInt()
self.list.subList(start, self.normalize(this, end)).clear()
} else
self.list.removeAt(start)
self
}
addFn("removeRangeInclusive") {
val self = thisAs<ObjList>()
val start = self.normalize(this, requiredArg<ObjInt>(0).value.toInt())
val end = self.normalize(this, requiredArg<ObjInt>(1).value.toInt()) + 1
self.list.subList(start, end).clear()
self
}
}
}
}

View File

@ -1,46 +0,0 @@
package net.sergeych.lyng
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("string")
data class ObjString(val value: String) : Obj() {
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjString) return -2
return this.value.compareTo(other.value)
}
override fun toString(): String = value
override val asStr: ObjString by lazy { this }
override fun inspect(): String {
return "\"$value\""
}
override val objClass: ObjClass
get() = type
override suspend fun plus(context: Context, other: Obj): Obj {
return ObjString(value + other.asStr.value)
}
override suspend fun getAt(context: Context, index: Int): Obj {
return ObjChar(value[index])
}
companion object {
val type = ObjClass("String").apply {
addConst("startsWith",
statement {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
})
addConst("length",
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) }
)
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
}
}
}

View File

@ -1,13 +0,0 @@
package net.sergeych.lyng
class Symbols(
unitType: UnitType,
val name: String,
val x: TypeDecl
) {
enum class UnitType {
Module, Function, Lambda
}
}

View File

@ -1,21 +0,0 @@
package net.sergeych.lyng
sealed class TypeDecl {
// ??
data class Fn(val argTypes: List<ArgsDeclaration.Item>, val retType: TypeDecl) : TypeDecl()
object Obj : TypeDecl()
}
/*
To use in the compiler, we need symbol information when:
- declaring a class: the only way to export its public/protected symbols is to know it in compiler time
- importing a module: actually, we cam try to do it in a more efficient way.
Importing module:
The moudule is efficiently a statement, that initializes it with all its symbols modifying some context.
The thing is, we need only
*/

View File

@ -1,5 +0,0 @@
package net.sergeych.lyng
import net.sergeych.lyng.buildconfig.BuildConfig
val LyngVersion = BuildConfig.VERSION

View File

@ -29,11 +29,11 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":library"))
implementation(kotlin("stdlib-common"))
implementation(project(":lynglib"))
implementation(libs.okio)
implementation(libs.clikt)
implementation(kotlin("stdlib-common"))
// optional support for rendering markdown in help messages
// implementation(libs.clikt.markdown)
}
@ -42,9 +42,15 @@ kotlin {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation(libs.kotlinx.coroutines.core)
implementation(libs.okio.fakefilesystem)
}
}
// val nativeMain by getting {
// dependencies {
// implementation(kotlin("stdlib-common"))
// }
// }
val linuxX64Main by getting {
}

View File

@ -17,14 +17,31 @@ import okio.use
expect fun exit(code: Int)
expect class ShellCommandExecutor {
fun executeCommand(command: String): CommandResult
companion object {
fun create(): ShellCommandExecutor
}
}
data class CommandResult(
val exitCode: Int,
val output: String,
val error: String
)
val baseContext = Context().apply {
addFn("exit") {
exit(requireOnlyArg<ObjInt>().toInt())
ObjVoid
}
// ObjString.type.addFn("shell") {
//
// }
}
class LyngCLI(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
override val printHelpOnEmptyArgs = true
@ -90,11 +107,16 @@ class LyngCLI(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
}
suspend fun executeFile(fileName: String) {
val text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
fileSource.buffer().use { bs ->
bs.readUtf8()
}
}
if( text.startsWith("#!") ) {
// skip shebang
val pos = text.indexOf('\n')
text = text.substring(pos + 1)
}
processErrors {
Compiler().compile(Source(fileName, text)).execute(baseContext)
}

View File

@ -0,0 +1,23 @@
@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
package net.sergeych
// Alternative implementation for native targets
actual class ShellCommandExecutor() {
actual fun executeCommand(command: String): CommandResult {
val process = ProcessBuilder("/bin/sh", "-c", command).start()
val exitCode = process.waitFor()
val output = process.inputStream.bufferedReader().readText()
val error = process.errorStream.bufferedReader().readText()
return CommandResult(
exitCode = exitCode,
output = output.trim(),
error = error.trim()
)
}
actual companion object {
actual fun create(): ShellCommandExecutor = ShellCommandExecutor()
}
}

View File

@ -2,8 +2,8 @@ package net.sergeych.lyng_cli
import com.github.ajalt.clikt.core.main
import kotlinx.coroutines.runBlocking
import net.sergeych.LyngCLI
import net.sergeych.Lyng
fun main(args: Array<String>) {
LyngCLI({ runBlocking { it() } }).main(args)
Lyng({ runBlocking { it() } }).main(args)
}

View File

@ -1,7 +1,7 @@
import com.github.ajalt.clikt.core.main
import kotlinx.coroutines.runBlocking
import net.sergeych.LyngCLI
import net.sergeych.Lyng
fun main(args: Array<String>) {
LyngCLI( { runBlocking { it() } }).main(args)
Lyng( { runBlocking { it() } }).main(args)
}

View File

@ -1,7 +1,48 @@
@file:OptIn(ExperimentalForeignApi::class, ExperimentalForeignApi::class, ExperimentalForeignApi::class)
package net.sergeych
import kotlinx.cinterop.*
import platform.posix.fgets
import platform.posix.pclose
import platform.posix.popen
import kotlin.system.exitProcess
actual class ShellCommandExecutor() {
actual fun executeCommand(command: String): CommandResult {
val outputBuilder = StringBuilder()
val errorBuilder = StringBuilder()
val fp = popen(command, "r") ?: return CommandResult(
exitCode = -1,
output = "",
error = "Failed to execute command"
)
val buffer = ByteArray(4096)
while (true) {
val bytesRead = buffer.usePinned { pinned ->
fgets(pinned.addressOf(0), buffer.size.convert(), fp)
}
if (bytesRead == null) break
outputBuilder.append(bytesRead.toKString())
}
val status = pclose(fp)
val exitCode = if (status == 0) 0 else 1
return CommandResult(
exitCode = exitCode,
output = outputBuilder.toString().trim(),
error = errorBuilder.toString().trim()
)
}
actual companion object {
actual fun create(): ShellCommandExecutor = ShellCommandExecutor()
}
}
actual fun exit(code: Int) {
exitProcess(code)
}

View File

@ -1,17 +1,40 @@
import com.vanniktech.maven.publish.SonatypeHost
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "0.6.8-SNAPSHOT"
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0")
classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:latest_version")
}
}
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.vanniktech.mavenPublish)
// alias(libs.plugins.vanniktech.mavenPublish)
kotlin("plugin.serialization") version "2.1.20"
id("com.codingfeline.buildkonfig") version "0.17.1"
`maven-publish`
}
group = "net.sergeych"
version = "0.2.0-SNAPSHOT"
buildkonfig {
packageName = "net.sergeych.lyng"
// objectName = "YourAwesomeConfig"
// exposeObjectWithName = "YourAwesomePublicConfig"
defaultConfigs {
buildConfigField(STRING, "bcprovider", "codingfeline")
buildConfigField(STRING, "version", version.toString())
}
}
kotlin {
jvm()
@ -39,6 +62,7 @@ kotlin {
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
}
@ -77,73 +101,54 @@ dependencies {
implementation(libs.firebase.crashlytics.buildtools)
}
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
signAllPublications()
coordinates(group.toString(), "library", version.toString())
pom {
name = "Lyng language"
description = "Kotlin-bound scripting loanguage"
inceptionYear = "2025"
// url = "https://sergeych.net"
licenses {
license {
name = "XXX"
url = "YYY"
distribution = "ZZZ"
publishing {
val mavenToken by lazy {
File("${System.getProperty("user.home")}/.gitea_token").readText()
}
repositories {
maven {
credentials(HttpHeaderCredentials::class) {
name = "Authorization"
value = mavenToken
}
}
developers {
developer {
id = "XXX"
name = "YYY"
url = "ZZZ"
url = uri("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
authentication {
create("Authorization", HttpHeaderAuthentication::class)
}
}
scm {
url = "XXX"
connection = "YYY"
developerConnection = "ZZZ"
}
}
}
val projectVersion by project.extra(provider {
// Compute value lazily
(version as String)
})
val generateBuildConfig by tasks.registering {
// Declare outputs safely
val outputDir = layout.buildDirectory.dir("generated/buildConfig/commonMain/kotlin")
outputs.dir(outputDir)
val version = projectVersion.get()
// Inputs: Version is tracked as an input
inputs.property("version", version)
doLast {
val packageName = "net.sergeych.lyng.buildconfig"
val packagePath = packageName.replace('.', '/')
val buildConfigFile = outputDir.get().file("$packagePath/BuildConfig.kt").asFile
buildConfigFile.parentFile?.mkdirs()
buildConfigFile.writeText(
"""
|package $packageName
|
|object BuildConfig {
| const val VERSION = "$version"
|}
""".trimMargin()
)
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
dependsOn(generateBuildConfig)
}
//mavenPublishing {
// publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
//
// signAllPublications()
//
// coordinates(group.toString(), "library", version.toString())
//
// pom {
// name = "Lyng language"
// description = "Kotlin-bound scripting loanguage"
// inceptionYear = "2025"
//// url = "https://sergeych.net"
// licenses {
// license {
// name = "XXX"
// url = "YYY"
// distribution = "ZZZ"
// }
// }
// developers {
// developer {
// id = "XXX"
// name = "YYY"
// url = "ZZZ"
// }
// }
// scm {
// url = "XXX"
// connection = "YYY"
// developerConnection = "ZZZ"
// }
// }
//}

View File

@ -0,0 +1,8 @@
package net.sergeych.lyng
enum class AccessType(val isMutable: Boolean) {
Val(false), Var(true),
@Suppress("unused")
Initialization(false)
}

View File

@ -0,0 +1,17 @@
package net.sergeych.lyng
/**
* Special version of the [Context] used to `apply` new this object to
* _parent context property.
*
* @param _parent context to apply to
* @param args arguments for the new context
* @param appliedContext the new context to apply, it will have lower priority except for `this` which
* will be reset by appliedContext's `this`.
*/
class AppliedContext(_parent: Context, args: Arguments, val appliedContext: Context)
: Context(_parent, args, appliedContext.pos, appliedContext.thisObj) {
override fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else super.get(name) ?: appliedContext[name]
}

View File

@ -11,7 +11,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
val start = params.indexOfFirst { it.defaultValue != null }
if (start >= 0)
for (j in start + 1 until params.size)
if (params[j].defaultValue == null) throw ScriptError(
// last non-default could be lambda:
if (params[j].defaultValue == null && j != params.size - 1) throw ScriptError(
params[j].pos,
"required argument can't follow default one"
)
@ -22,22 +23,41 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
*/
suspend fun assignToContext(
context: Context,
fromArgs: Arguments = context.args,
defaultAccessType: Compiler.AccessType = Compiler.AccessType.Var
arguments: Arguments = context.args,
defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public
) {
fun assign(a: Item, value: Obj) {
context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value)
context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
a.visibility ?: defaultVisibility)
}
// will be used with last lambda arg fix
val callArgs: List<Obj>
val paramsSize: Int
if( arguments.tailBlockMode ) {
paramsSize = params.size - 1
assign(params.last(), arguments.list.last())
callArgs = arguments.list.dropLast(1)
} else {
paramsSize = params.size
callArgs = arguments.list
}
suspend fun processHead(index: Int): Int {
var i = index
while (i != params.size) {
while (i != paramsSize) {
val a = params[i]
if (a.isEllipsis) break
val value = when {
i < fromArgs.size -> fromArgs[i]
i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(context)
else -> context.raiseArgumentError("too few arguments for the call")
else -> {
println("callArgs: ${callArgs.joinToString()}")
println("tailBlockMode: ${arguments.tailBlockMode}")
context.raiseIllegalArgument("too few arguments for the call")
}
}
assign(a, value)
i++
@ -46,18 +66,18 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
}
suspend fun processTail(index: Int): Int {
var i = params.size - 1
var j = fromArgs.size - 1
var i = paramsSize - 1
var j = callArgs.size - 1
while (i > index) {
val a = params[i]
if (a.isEllipsis) break
val value = when {
j >= index -> {
fromArgs[j--]
callArgs[j--]
}
a.defaultValue != null -> a.defaultValue.execute(context)
else -> context.raiseArgumentError("too few arguments for the call")
else -> context.raiseIllegalArgument("too few arguments for the call")
}
assign(a, value)
i--
@ -68,17 +88,17 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
fun processEllipsis(index: Int, toFromIndex: Int) {
val a = params[index]
val l = if (index > toFromIndex) ObjList()
else ObjList(fromArgs.values.subList(index, toFromIndex + 1).toMutableList())
else ObjList(callArgs.subList(index, toFromIndex + 1).toMutableList())
assign(a, l)
}
val leftIndex = processHead(0)
if (leftIndex < params.size) {
if (leftIndex < paramsSize) {
val end = processTail(leftIndex)
processEllipsis(leftIndex, end)
} else {
if (leftIndex < fromArgs.size)
context.raiseArgumentError("too many arguments for the call")
if (leftIndex < callArgs.size)
context.raiseIllegalArgument("too many arguments for the call")
}
}
@ -90,7 +110,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
*/
data class Item(
val name: String,
val type: TypeDecl = TypeDecl.Obj,
val type: TypeDecl = TypeDecl.TypeAny,
val pos: Pos = Pos.builtIn,
val isEllipsis: Boolean = false,
/**
@ -98,7 +118,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
* So it is a [Statement] that must be executed on __caller context__.
*/
val defaultValue: Statement? = null,
val accessType: Compiler.AccessType? = null,
val visibility: Compiler.Visibility? = null,
val accessType: AccessType? = null,
val visibility: Visibility? = null,
)
}

View File

@ -0,0 +1,50 @@
package net.sergeych.lyng
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMode: Boolean): Arguments {
val list = mutableListOf<Obj>()
for (x in this) {
val value = x.value.execute(context)
if (x.isSplat) {
when {
value is ObjList -> {
for (subitem in value.list) list.add(subitem)
}
value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(context, "toList") as ObjList).list
i.forEach { list.add(it) }
}
else -> context.raiseClassCastError("expected list of objects for splat argument")
}
} else
list.add(value)
}
return Arguments(list,tailBlockMode)
}
data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : List<Obj> by list {
constructor(vararg values: Obj) : this(values.toList())
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
return list.first()
}
/**
* Convert to list of kotlin objects, see [Obj.toKotlin].
*/
suspend fun toKotlinList(context: Context): List<Any?> {
return list.map { it.toKotlin(context) }
}
companion object {
val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.toList())
}
}

View File

@ -11,18 +11,20 @@ class Compiler(
class Settings
fun compile(source: Source): Script {
return parseScript(source.startPos,
return parseScript(
source.startPos,
CompilerContext(parseLyng(source))
)
}
private fun parseScript(start: Pos, cc: CompilerContext): Script {
val statements = mutableListOf<Statement>()
// val returnScope = cc.startReturnScope()
while (parseStatement(cc, braceMeansLambda = true)?.also {
statements += it
} != null) {/**/
}
return Script(start, statements)
return Script(start, statements)//returnScope.needCatch)
}
private fun parseStatement(cc: CompilerContext, braceMeansLambda: Boolean = false): Statement? {
@ -57,7 +59,7 @@ class Compiler(
parseBlock(cc)
}
Token.Type.RBRACE -> {
Token.Type.RBRACE, Token.Type.RBRACKET -> {
cc.previous()
return null
}
@ -119,49 +121,91 @@ class Compiler(
operand = Accessor { op.getter(it).value.logicalNot(it).asReadonly }
}
Token.Type.DOT -> {
Token.Type.DOT, Token.Type.NULL_COALESCE -> {
val isOptional = t.type == Token.Type.NULL_COALESCE
operand?.let { left ->
// dotcall: calling method on the operand, if next is ID, "("
var isCall = false
val next = cc.next()
if (next.type == Token.Type.ID) {
cc.ifNextIs(Token.Type.LPAREN) {
// instance method call
val args = parseArgs(cc)
isCall = true
operand = Accessor { context ->
context.pos = next.pos
val v = left.getter(context).value
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
args.toArguments(context)
), isMutable = false
)
// could be () call or obj.method {} call
val nt = cc.current()
when (nt.type) {
Token.Type.LPAREN -> {
cc.next()
// instance method call
val args = parseArgs(cc).first
isCall = true
operand = Accessor { context ->
context.pos = next.pos
val v = left.getter(context).value
if (v == ObjNull && isOptional)
ObjNull.asReadonly
else
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
args.toArguments(context, false)
), isMutable = false
)
}
}
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
// isOptional = nt.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
// single lambda arg, like assertTrows { ... }
cc.next()
isCall = true
val lambda =
parseLambdaExpression(cc)
println(cc.current())
operand = Accessor { context ->
context.pos = next.pos
val v = left.getter(context).value
if (v == ObjNull && isOptional)
ObjNull.asReadonly
else
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
Arguments(listOf(lambda.getter(context).value), true)
), isMutable = false
)
}
}
else -> {}
}
}
if (!isCall) {
operand = Accessor({ context ->
left.getter(context).value.readField(context, next.value)
val x = left.getter(context).value
if (x == ObjNull && isOptional) ObjNull.asReadonly
else x.readField(context, next.value)
}) { cc, newValue ->
left.getter(cc).value.writeField(cc, next.value, newValue)
}
}
} ?: throw ScriptError(t.pos, "Expecting expression before dot")
}
?: throw ScriptError(t.pos, "Expecting expression before dot")
}
Token.Type.COLONCOLON -> {
operand = parseScopeOperator(operand, cc)
}
Token.Type.LPAREN -> {
Token.Type.LPAREN, Token.Type.NULL_COALESCE_INVOKE -> {
operand?.let { left ->
// this is function call from <left>
operand = parseFunctionCall(
cc,
left,
false,
t.type == Token.Type.NULL_COALESCE_INVOKE
)
} ?: run {
// Expression in parentheses
@ -174,15 +218,17 @@ class Compiler(
}
}
Token.Type.LBRACKET -> {
Token.Type.LBRACKET, Token.Type.NULL_COALESCE_INDEX -> {
operand?.let { left ->
// array access
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
val index = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting index expression")
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
operand = Accessor({ cxt ->
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
?: cxt.raiseError("index must be integer")
left.getter(cxt).value.getAt(cxt, i).asMutable
val i = index.execute(cxt)
val x = left.getter(cxt).value
if (x == ObjNull && isOptional) ObjNull.asReadonly
else x.getAt(cxt, i).asMutable
}) { cxt, newValue ->
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
?: cxt.raiseError("index must be integer")
@ -293,10 +339,10 @@ class Compiler(
}
Token.Type.DOTDOT, Token.Type.DOTDOTLT -> {
// closed-range operator
// range operator
val isEndInclusive = t.type == Token.Type.DOTDOT
val left = operand
val right = parseStatement(cc)
val right = parseExpression(cc)
operand = Accessor {
ObjRange(
left?.getter?.invoke(it)?.value ?: ObjNull,
@ -306,17 +352,27 @@ class Compiler(
}
}
Token.Type.LBRACE -> {
if (operand != null) {
throw ScriptError(t.pos, "syntax error: lambda expression not allowed here")
} else operand = parseLambdaExpression(cc)
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
operand = operand?.let { left ->
cc.previous()
parseFunctionCall(
cc,
left,
blockArgument = true,
t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
)
} ?: parseLambdaExpression(cc)
}
Token.Type.RBRACKET, Token.Type.RPAREN -> {
cc.previous()
return operand
}
else -> {
cc.previous()
operand?.let { return it }
operand = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression")
operand = parseAccessor(cc) ?: return null //throw ScriptError(t.pos, "Expecting expression")
}
}
}
@ -331,16 +387,17 @@ class Compiler(
val argsDeclaration = parseArgsDeclaration(cc)
if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW)
throw ScriptError(startPos, "lambda must have either valid arguments declaration with '->' or no arguments")
val pos = cc.currentPos()
val body = parseBlock(cc, skipLeadingBrace = true)
var closure: Context? = null
val callStatement = statement {
val context = closure!!.copy(pos, args)
// and the source closure of the lambda which might have other thisObj.
val context = AppliedContext(closure!!, args, this)
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = args.values
val l = args.list
val itValue: Obj = when (l.size) {
// no args: it == void
0 -> ObjVoid
@ -401,16 +458,6 @@ class Compiler(
}
}
enum class AccessType(val isMutable: Boolean) {
Val(false), Var(true),
@Suppress("unused")
Initialization(false)
}
enum class Visibility {
Public, Private, Protected, Internal
}
/**
* Parse argument declaration, used in lambda (and later in fn too)
* @return declaration or null if there is no valid list of arguments
@ -429,43 +476,14 @@ class Compiler(
}
Token.Type.NEWLINE -> {}
Token.Type.ID -> {
// visibility
val visibility = when (t.value) {
"private" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
Visibility.Private
}
val visibility = if (isClassDeclaration && t.value == "private") {
t = cc.next()
Visibility.Private
} else Visibility.Public
"protected" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
Visibility.Protected
}
"internal" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
Visibility.Internal
}
"public" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
Visibility.Public
}
else -> null
}
// val/var?
val access = when (t.value) {
"val" -> {
@ -542,14 +560,19 @@ class Compiler(
}
private fun parseTypeDeclaration(cc: CompilerContext): TypeDecl {
val result = TypeDecl.Obj
cc.ifNextIs(Token.Type.COLON) {
TODO("parse type declaration here")
}
return result
return if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
val tt = cc.requireToken(Token.Type.ID, "type name or type expression required")
val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
TypeDecl.Simple(tt.value, isNullable)
} else TypeDecl.TypeAny
}
private fun parseArgs(cc: CompilerContext): List<ParsedArgument> {
/**
* Parse arguments list during the call and detect last block argument
* _following the parenthesis_ call: `(1,2) { ... }`
*/
private fun parseArgs(cc: CompilerContext): Pair<List<ParsedArgument>, Boolean> {
val args = mutableListOf<ParsedArgument>()
do {
val t = cc.next()
@ -567,6 +590,8 @@ class Compiler(
cc.previous()
parseExpression(cc)?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
if (cc.current().type == Token.Type.COLON)
parseTypeDeclaration(cc)
// Here should be a valid termination:
}
}
@ -574,6 +599,7 @@ class Compiler(
// block after?
val pos = cc.savePos()
val end = cc.next()
var lastBlockArgument = false
if (end.type == Token.Type.LBRACE) {
// last argument - callable
val callableAccessor = parseLambdaExpression(cc)
@ -584,22 +610,40 @@ class Compiler(
},
end.pos
)
lastBlockArgument = true
} else
cc.restorePos(pos)
return args
return args to lastBlockArgument
}
private fun parseFunctionCall(cc: CompilerContext, left: Accessor): Accessor {
private fun parseFunctionCall(
cc: CompilerContext,
left: Accessor,
blockArgument: Boolean,
isOptional: Boolean
): Accessor {
// insofar, functions always return lvalue
val args = parseArgs(cc)
var detectedBlockArgument = blockArgument
val args = if (blockArgument) {
val blockArg = ParsedArgument(
parseExpression(cc)
?: throw ScriptError(cc.currentPos(), "lambda body expected"), cc.currentPos()
)
listOf(blockArg)
} else {
val r = parseArgs(cc)
detectedBlockArgument = r.second
r.first
}
return Accessor { context ->
val v = left.getter(context)
if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly
v.value.callOn(
context.copy(
context.pos,
args.toArguments(context)
args.toArguments(context, detectedBlockArgument)
// Arguments(
// args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) }
// ),
@ -639,7 +683,7 @@ class Compiler(
"void" -> Accessor { ObjVoid.asReadonly }
"null" -> Accessor { ObjNull.asReadonly }
"true" -> Accessor { ObjBool(true).asReadonly }
"false" -> Accessor { ObjBool(false).asReadonly }
"false" -> Accessor { ObjFalse.asReadonly }
else -> {
Accessor({
it.pos = t.pos
@ -682,28 +726,281 @@ class Compiler(
}
/**
* Parse keyword-starting statenment.
* Parse keyword-starting statement.
* @return parsed statement or null if, for example. [id] is not among keywords
*/
private fun parseKeywordStatement(id: Token, cc: CompilerContext): Statement? = when (id.value) {
"val" -> parseVarDeclaration(id.value, false, cc)
"var" -> parseVarDeclaration(id.value, true, cc)
"val" -> parseVarDeclaration(false, Visibility.Public, cc)
"var" -> parseVarDeclaration(true, Visibility.Public, cc)
"while" -> parseWhileStatement(cc)
"do" -> parseDoWhileStatement(cc)
"for" -> parseForStatement(cc)
"break" -> parseBreakStatement(id.pos, cc)
"continue" -> parseContinueStatement(id.pos, cc)
"fn", "fun" -> parseFunctionDeclaration(cc)
"if" -> parseIfStatement(cc)
"class" -> parseClassDeclaration(cc, false)
"struct" -> parseClassDeclaration(cc, true)
else -> null
"try" -> parseTryStatement(cc)
"throw" -> parseThrowStatement(cc)
"when" -> parseWhenStatement(cc)
else -> {
// triples
cc.previous()
val isExtern = cc.skipId("extern")
when {
cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(cc, Visibility.Private, isExtern)
cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(cc, Visibility.Private, isExtern)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(cc, isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(cc, isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fun") -> parseFunctionDeclaration(cc, isOpen = false, isExtern = isExtern)
cc.matchQualifiers("fn") -> parseFunctionDeclaration(cc, isOpen = false, isExtern = isExtern)
cc.matchQualifiers("val", "private") -> parseVarDeclaration(false, Visibility.Private, cc)
cc.matchQualifiers("var", "private") -> parseVarDeclaration(true, Visibility.Private, cc)
cc.matchQualifiers("val", "open") -> parseVarDeclaration(false, Visibility.Private, cc, true)
cc.matchQualifiers("var", "open") -> parseVarDeclaration(true, Visibility.Private, cc, true)
else -> {
cc.next()
null
}
}
}
}
data class WhenCase(val condition: Statement, val block: Statement)
private fun parseWhenStatement(cc: CompilerContext): Statement {
// has a value, when(value) ?
var t = cc.skipWsTokens()
return if (t.type == Token.Type.LPAREN) {
// when(value)
val value = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when(value) expected")
cc.skipTokenOfType(Token.Type.RPAREN)
t = cc.next()
if (t.type != Token.Type.LBRACE) throw ScriptError(t.pos, "when { ... } expected")
val cases = mutableListOf<WhenCase>()
var elseCase: Statement? = null
lateinit var whenValue: Obj
// there could be 0+ then clauses
// condition could be a value, in and is clauses:
// parse several conditions for one then clause
// loop cases
outer@ while (true) {
var skipParseBody = false
val currentCondition = mutableListOf<Statement>()
// loop conditions
while (true) {
t = cc.skipWsTokens()
when (t.type) {
Token.Type.IN,
Token.Type.NOTIN -> {
// we need a copy in the closure:
val isIn = t.type == Token.Type.IN
val container = parseExpression(cc) ?: throw ScriptError(cc.currentPos(), "type expected")
currentCondition += statement {
val r = container.execute(this).contains(this, whenValue)
ObjBool(if (isIn) r else !r)
}
}
Token.Type.IS, Token.Type.NOTIS -> {
// we need a copy in the closure:
val isIn = t.type == Token.Type.IS
val caseType = parseExpression(cc) ?: throw ScriptError(cc.currentPos(), "type expected")
currentCondition += statement {
val r = whenValue.isInstanceOf(caseType.execute(this))
ObjBool(if (isIn) r else !r)
}
}
Token.Type.COMMA ->
continue
Token.Type.ARROW ->
break
Token.Type.RBRACE ->
break@outer
else -> {
if (t.value == "else") {
cc.skipTokens(Token.Type.ARROW)
if (elseCase != null) throw ScriptError(
cc.currentPos(),
"when else block already defined"
)
elseCase =
parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when else block expected")
skipParseBody = true
} else {
cc.previous()
val x = parseExpression(cc)
?: throw ScriptError(cc.currentPos(), "when case condition expected")
currentCondition += statement {
ObjBool(x.execute(this).compareTo(this, whenValue) == 0)
}
}
}
}
}
// parsed conditions?
if (!skipParseBody) {
val block = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when case block expected")
for (c in currentCondition) cases += WhenCase(c, block)
}
}
statement {
var result: Obj = ObjVoid
// in / is and like uses whenValue from closure:
whenValue = value.execute(this)
var found = false
for (c in cases)
if (c.condition.execute(this).toBool()) {
result = c.block.execute(this)
found = true
break
}
if (!found && elseCase != null) result = elseCase.execute(this)
result
}
} else {
// when { cond -> ... }
TODO("when without object is not yet implemented")
}
}
private fun parseThrowStatement(cc: CompilerContext): Statement {
val throwStatement = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "throw object expected")
return statement {
var errorObject = throwStatement.execute(this)
if (errorObject is ObjString)
errorObject = ObjException(this, errorObject.value)
if (errorObject is ObjException)
raiseError(errorObject)
else raiseError("this is not an exception object: $errorObject")
}
}
private data class CatchBlockData(
val catchVar: Token,
val classNames: List<String>,
val block: Statement
)
private fun parseTryStatement(cc: CompilerContext): Statement {
val body = parseBlock(cc)
val catches = mutableListOf<CatchBlockData>()
cc.skipTokens(Token.Type.NEWLINE)
var t = cc.next()
while (t.value == "catch") {
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
t = cc.next()
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "expected catch variable")
val catchVar = t
val exClassNames = mutableListOf<String>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
// load list of exception classes
do {
t = cc.next()
if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "expected exception class name")
exClassNames += t.value
t = cc.next()
when (t.type) {
Token.Type.COMMA -> {
continue
}
Token.Type.RPAREN -> {
break
}
else -> throw ScriptError(t.pos, "syntax error: expected ',' or ')'")
}
} while (true)
} else {
// no type!
exClassNames += "Exception"
cc.skipTokenOfType(Token.Type.RPAREN)
}
val block = parseBlock(cc)
catches += CatchBlockData(catchVar, exClassNames, block)
cc.skipTokens(Token.Type.NEWLINE)
t = cc.next()
} else {
// no (e: Exception) block: should be shortest variant `catch { ... }`
cc.skipTokenOfType(Token.Type.LBRACE, "expected catch(...) or catch { ... } here")
catches += CatchBlockData(
Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"),
parseBlock(cc, true)
)
t = cc.next()
}
}
val finallyClause = if (t.value == "finally") {
parseBlock(cc)
} else {
cc.previous()
null
}
if (catches.isEmpty() && finallyClause == null)
throw ScriptError(cc.currentPos(), "try block must have either catch or finally clause or both")
return statement {
var result: Obj = ObjVoid
try {
// body is a parsed block, it already has separate context
result = body.execute(this)
} catch (e: Exception) {
// convert to appropriate exception
val objException = when (e) {
is ExecutionError -> e.errorObject
else -> ObjUnknownException(this, e.message ?: e.toString())
}
// let's see if we should catch it:
var isCaught = false
for (cdata in catches) {
var exceptionObject: ObjException? = null
for (exceptionClassName in cdata.classNames) {
val exObj = ObjException.getErrorClass(exceptionClassName)
?: raiseSymbolNotFound("error clas not exists: $exceptionClassName")
if (objException.isInstanceOf(exObj)) {
exceptionObject = objException
break
}
}
if (exceptionObject != null) {
val catchContext = this.copy(pos = cdata.catchVar.pos)
catchContext.addItem(cdata.catchVar.value, false, objException)
result = cdata.block.execute(catchContext)
isCaught = true
break
}
}
// rethrow if not caught this exception
if (!isCaught)
throw e
} finally {
// finally clause does not alter result!
finallyClause?.execute(this)
}
result
}
}
private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(cc)
parseArgsDeclaration(cc, isClassDeclaration = true)
else null
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
@ -743,41 +1040,12 @@ class Compiler(
// fields. Note that 'this' is already set by class
constructorArgsDeclaration?.assignToContext(this)
bodyInit?.execute(this)
// export public
for( (name,record) in objects ) {
println("-- $name $record")
when(record.visibility) {
Visibility.Public -> {
thisObj.publicFields += name
thisObj.protectedFields += name
}
Visibility.Protected ->
thisObj.protectedFields += name
else -> {
}
}
}
thisObj
}
// inheritance must alter this code:
val newClass = ObjClass(className).apply {
instanceConstructor = constructorCode
constructorArgsDeclaration?.let { cad ->
// we need accessors for all fields:
for (f in cad.params) {
createField(
f.name,
statement {
val context = (thisObj as ObjInstance).instanceContext
println("called on $thisObj")
context[f.name]?.value ?: raiseError("field is not initialized: ${f.name}")
},
true,
// (f.accessType ?: defaultAccess).isMutable,
)
}
}
}
return statement {
@ -856,7 +1124,7 @@ class Compiler(
var breakCaught = false
if (size > 0) {
var current = runCatching { sourceObj.getAt(forContext, 0) }
var current = runCatching { sourceObj.getAt(forContext, ObjInt(0)) }
.getOrElse {
throw ScriptError(
tOp.pos,
@ -883,7 +1151,7 @@ class Compiler(
}
} else result = body.execute(forContext)
if (++index >= size) break
current = sourceObj.getAt(forContext, index)
current = sourceObj.getAt(forContext, ObjInt(index.toLong()))
}
}
if (!breakCaught && elseStatement != null) {
@ -913,8 +1181,9 @@ class Compiler(
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
return lbe.result
}
return lbe.result
throw lbe
}
}
} else {
@ -941,8 +1210,9 @@ class Compiler(
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
return lbe.result
}
return lbe.result
throw lbe
}
else {
loopVar.value = iterObj.invokeInstanceMethod(forContext, "next")
@ -952,6 +1222,59 @@ class Compiler(
return elseStatement?.execute(forContext) ?: result
}
@Suppress("UNUSED_VARIABLE")
private fun parseDoWhileStatement(cc: CompilerContext): Statement {
val label = getLabel(cc)?.also { cc.labels += it }
val (breakFound, body) = cc.parseLoop {
parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "Bad while statement: expected statement")
}
label?.also { cc.labels -= it }
cc.skipTokens(Token.Type.NEWLINE)
val t = cc.next()
if (t.type != Token.Type.ID && t.value != "while")
cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here")
val conditionStart = ensureLparen(cc)
val condition =
parseExpression(cc) ?: throw ScriptError(conditionStart, "Bad while statement: expected expression")
ensureRparen(cc)
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
parseStatement(cc)
} else {
cc.previous()
null
}
return statement(body.pos) {
var wasBroken = false
var result: Obj = ObjVoid
lateinit var doContext: Context
do {
doContext = it.copy().apply { skipContextCreation = true }
try {
result = body.execute(doContext)
} catch (e: LoopBreakContinueException) {
if (e.label == label || e.label == null) {
if (e.doContinue) continue
else {
result = e.result
wasBroken = true
break
}
}
throw e
}
} while (condition.execute(doContext).toBool())
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
result
}
}
private fun parseWhileStatement(cc: CompilerContext): Statement {
val label = getLabel(cc)?.also { cc.labels += it }
val start = ensureLparen(cc)
@ -1101,26 +1424,36 @@ class Compiler(
}
}
private fun parseFunctionDeclaration(tokens: CompilerContext): Statement {
var t = tokens.next()
private fun parseFunctionDeclaration(
cc: CompilerContext,
visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isExtern: Boolean = false
): Statement {
var t = cc.next()
val start = t.pos
val name = if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "Expected identifier after 'fn'")
else t.value
t = tokens.next()
t = cc.next()
if (t.type != Token.Type.LPAREN)
throw ScriptError(t.pos, "Bad function definition: expected '(' after 'fn ${name}'")
val argsDeclaration = parseArgsDeclaration(tokens)
val argsDeclaration = parseArgsDeclaration(cc)
if (argsDeclaration == null || argsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
t.pos,
"Bad function definition: expected valid argument declaration or () after 'fn ${name}'"
)
if (cc.current().type == Token.Type.COLON) parseTypeDeclaration(cc)
// Here we should be at open body
val fnStatements = parseBlock(tokens)
val fnStatements = if (isExtern)
statement { raiseError("extern function not provided: $name") }
else
parseBlock(cc)
var closure: Context? = null
@ -1138,7 +1471,7 @@ class Compiler(
// we added fn in the context. now we must save closure
// for the function
closure = context
context.addItem(name, false, fnBody)
context.addItem(name, false, fnBody, visibility)
// as the function can be called from anywhere, we have
// saved the proper context in the closure
fnBody
@ -1155,7 +1488,7 @@ class Compiler(
val block = parseScript(startPos, cc)
return statement(startPos) {
// block run on inner context:
block.execute(it.copy(startPos))
block.execute(if (it.skipContextCreation) it else it.copy(startPos))
}.also {
val t1 = cc.next()
if (t1.type != Token.Type.RBRACE)
@ -1163,17 +1496,22 @@ class Compiler(
}
}
private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: CompilerContext): Statement {
private fun parseVarDeclaration(
isMutable: Boolean,
visibility: Visibility,
tokens: CompilerContext,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false
): Statement {
val nameToken = tokens.next()
val start = nameToken.pos
if (nameToken.type != Token.Type.ID)
throw ScriptError(nameToken.pos, "Expected identifier after '$kind'")
throw ScriptError(nameToken.pos, "Expected identifier here")
val name = nameToken.value
val eqToken = tokens.next()
var setNull = false
if (eqToken.type != Token.Type.ASSIGN) {
if (!mutable)
if (!isMutable)
throw ScriptError(start, "val must be initialized")
else {
tokens.previous()
@ -1181,7 +1519,7 @@ class Compiler(
}
}
val initialExpression = if (setNull) null else parseExpression(tokens)
val initialExpression = if (setNull) null else parseStatement(tokens, true)
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
return statement(nameToken.pos) { context ->
@ -1192,8 +1530,8 @@ class Compiler(
// create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, mutable, initValue)
ObjVoid
context.addItem(name, isMutable, initValue, visibility)
initValue
}
}
@ -1291,6 +1629,8 @@ class Compiler(
// bitwise or 2
// bitwise and 3
// equality/ne 4
Operator.simple(Token.Type.EQARROW, ++lastPrty) { c, a, b -> ObjMapEntry(a, b) },
//
Operator.simple(Token.Type.EQ, ++lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) == 0) },
Operator.simple(Token.Type.NEQ, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
Operator.simple(Token.Type.REF_EQ, lastPrty) { _, a, b -> ObjBool(a === b) },
@ -1305,6 +1645,9 @@ class Compiler(
Operator.simple(Token.Type.NOTIN, lastPrty) { c, a, b -> ObjBool(!b.contains(c, a)) },
Operator.simple(Token.Type.IS, lastPrty) { c, a, b -> ObjBool(a.isInstanceOf(b)) },
Operator.simple(Token.Type.NOTIS, lastPrty) { c, a, b -> ObjBool(!a.isInstanceOf(b)) },
Operator.simple(Token.Type.ELVIS, ++lastPrty) { c, a, b -> if (a == ObjNull) b else a },
// shuttle <=> 6
Operator.simple(Token.Type.SHUTTLE, ++lastPrty) { c, a, b ->
ObjInt(a.compareTo(c, b).toLong())
@ -1336,7 +1679,8 @@ class Compiler(
/**
* The keywords that stop processing of expression term
*/
val stopKeywords = setOf("break", "continue", "return", "if", "when", "do", "while", "for", "class", "struct")
val stopKeywords =
setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class")
}
}

View File

@ -9,7 +9,7 @@ internal class CompilerContext(val tokens: List<Token>) {
var loopLevel = 0
private set
inline fun <T> parseLoop(f: () -> T): Pair<Boolean,T> {
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
if (++loopLevel == 0) breakFound = false
val result = f()
return Pair(breakFound, result).also {
@ -21,7 +21,11 @@ internal class CompilerContext(val tokens: List<Token>) {
fun hasNext() = currentIndex < tokens.size
fun hasPrevious() = currentIndex > 0
fun next() = tokens.getOrElse(currentIndex) { throw IllegalStateException("No next token") }.also { currentIndex++ }
fun next() =
if( currentIndex < tokens.size ) tokens[currentIndex++]
else Token("", tokens.last().pos, Token.Type.EOF)
// throw IllegalStateException("No more tokens")
fun previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun savePos() = currentIndex
@ -47,9 +51,21 @@ internal class CompilerContext(val tokens: List<Token>) {
throw ScriptError(at, message)
}
fun currentPos() =
if (hasNext()) next().pos.also { previous() }
else previous().pos.also { next() }
fun currentPos(): Pos = tokens[currentIndex].pos
/**
* If the next token is identifier `name`, skip it and return `true`.
* else leave where it is and return `false`
*/
fun skipId(name: String): Boolean {
current().let { t ->
if( t.type == Token.Type.ID && t.value == name ) {
next()
return true
}
}
return false
}
/**
* Skips next token if its type is `tokenType`, returns `true` if so.
@ -98,4 +114,63 @@ internal class CompilerContext(val tokens: List<Token>) {
breakFound = true
}
/**
* Return value of the next token if it is an identifier, null otherwise.
* Does not change position.
*/
@Suppress("unused")
fun nextIdValue(): String? {
return if (hasNext()) {
val nt = tokens[currentIndex]
if (nt.type == Token.Type.ID)
nt.value
else null
} else null
}
@Suppress("unused")
fun current(): Token = tokens[currentIndex]
/**
* If the token at current position plus offset (could be negative) exists, returns it, otherwise returns null.
*/
@Suppress("unused")
fun atOffset(offset: Int): Token? =
if (currentIndex + offset in tokens.indices) tokens[currentIndex + offset] else null
fun matchQualifiers(keyword: String, vararg qualifiers: String): Boolean {
val pos = savePos()
var count = 0
while( count < qualifiers.size) {
val t = next()
when(t.type) {
Token.Type.ID -> {
if( t.value in qualifiers ) count++
else { restorePos(pos); return false }
}
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT, Token.Type.NEWLINE -> {}
else -> { restorePos(pos); return false }
}
}
val t = next()
if( t.type == Token.Type.ID && t.value == keyword ) {
return true
} else {
restorePos(pos)
return false
}
}
/**
* Skip newlines and comments. Returns (and reads) first non-whitespace token.
* Note that [Token.Type.EOF] is not considered a whitespace token.
*/
fun skipWsTokens(): Token {
while( current().type in wstokens ) next()
return next()
}
companion object {
val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT)
}
}

View File

@ -1,10 +1,11 @@
package net.sergeych.lyng
class Context(
open class Context(
val parent: Context?,
val args: Arguments = Arguments.EMPTY,
var pos: Pos = Pos.builtIn,
val thisObj: Obj = ObjVoid
var thisObj: Obj = ObjVoid,
var skipContextCreation: Boolean = false,
) {
constructor(
args: Arguments = Arguments.EMPTY,
@ -15,33 +16,38 @@ class Context(
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this))
fun raiseNPE(): Nothing = raiseError(ObjNullReferenceException(this))
@Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsError(this, message))
raiseError(ObjIndexOutOfBoundsException(this, message))
@Suppress("unused")
fun raiseArgumentError(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentError(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg))
fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
@Suppress("unused")
fun raiseSymbolNotFound(name: String): Nothing = raiseError(ObjSymbolNotDefinedError(this, "symbol is not defined: $name"))
fun raiseNoSuchElement(message: String = "No such element"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
@Suppress("unused")
fun raiseSymbolNotFound(name: String): Nothing =
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing {
throw ExecutionError(ObjError(this, message))
throw ExecutionError(ObjException(this, message))
}
fun raiseError(obj: ObjError): Nothing {
fun raiseError(obj: ObjException): Nothing {
throw ExecutionError(obj)
}
inline fun <reified T : Obj> requiredArg(index: Int): T {
if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}")
return (args.list[index].value as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index].value::class.simpleName}")
return (args.list[index] as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
}
inline fun <reified T : Obj> requireOnlyArg(): T {
@ -61,9 +67,12 @@ class Context(
internal val objects = mutableMapOf<String, ObjRecord>()
operator fun get(name: String): ObjRecord? =
objects[name]
?: parent?.get(name)
open operator fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else {
objects[name]
?: parent?.get(name)
}
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj)
@ -73,8 +82,13 @@ class Context(
fun copy() = Context(this, args, pos, thisObj)
fun addItem(name: String, isMutable: Boolean, value: Obj): ObjRecord {
return ObjRecord(value, isMutable).also { objects.put(name, it) }
fun addItem(
name: String,
isMutable: Boolean,
value: Obj,
visibility: Visibility = Visibility.Public
): ObjRecord {
return ObjRecord(value, isMutable, visibility).also { objects[name] = it }
}
fun getOrCreateNamespace(name: String): ObjClass {
@ -112,5 +126,4 @@ class Context(
fun containsLocal(name: String): Boolean = name in objects
}

View File

@ -4,7 +4,10 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts
/**
* Record to store object with access rules, e.g. [isMutable] and access level [visibility].
@ -12,7 +15,7 @@ import net.sergeych.synctools.ProtectedOp
data class ObjRecord(
var value: Obj,
val isMutable: Boolean,
val visibility: Compiler.Visibility = Compiler.Visibility.Public
val visibility: Visibility = Visibility.Public
)
/**
@ -39,6 +42,9 @@ data class Accessor(
}
open class Obj {
val isNull by lazy { this === ObjNull }
var isFrozen: Boolean = false
private val monitor = Mutex()
@ -58,12 +64,16 @@ open class Obj {
*/
open fun byValueCopy(): Obj = this
fun isInstanceOf(someClass: Obj) = someClass === objClass || objClass.allParentsSet.contains(someClass)
@Suppress("SuspiciousEqualsCombination")
fun isInstanceOf(someClass: Obj) = someClass === objClass ||
objClass.allParentsSet.contains(someClass) ||
someClass == rootObjectType
suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj =
invokeInstanceMethod(context, name, Arguments(args.map { Arguments.Info(it, context.pos) }))
invokeInstanceMethod(context, name, Arguments(args.toList()))
inline suspend fun <reified T : Obj> callMethod(
suspend inline fun <reified T : Obj> callMethod(
context: Context,
name: String,
args: Arguments = Arguments.EMPTY
@ -86,7 +96,7 @@ open class Obj {
}
open suspend fun contains(context: Context, other: Obj): Boolean {
context.raiseNotImplemented()
return invokeInstanceMethod(context, "contains", other).toBool()
}
open val asStr: ObjString by lazy {
@ -97,16 +107,7 @@ open class Obj {
* Class of the object: definition of member functions (top-level), etc.
* Note that using lazy allows to avoid endless recursion here
*/
open val objClass: ObjClass by lazy {
ObjClass("Obj").apply {
addFn("toString") {
thisObj.asStr
}
addFn("contains") {
ObjBool(thisObj.contains(this, args.firstAndOnly()))
}
}
}
open val objClass: ObjClass = rootObjectType
open suspend fun plus(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
@ -173,17 +174,23 @@ open class Obj {
context.raiseNotImplemented()
}
/**
* Convert Lyng object to its Kotlin counterpart
*/
open suspend fun toKotlin(context: Context): Any? {
return toString()
}
fun willMutate(context: Context) {
if (isFrozen) context.raiseError("attempt to mutate frozen object")
}
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
suspend open fun readField(context: Context, name: String): ObjRecord {
open suspend fun readField(context: Context, name: String): ObjRecord {
// could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
val value = obj.value
return when (value) {
return when (val value = obj.value) {
is Statement -> {
ObjRecord(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable)
}
@ -199,7 +206,7 @@ open class Obj {
if (field.isMutable) field.value = newValue else context.raiseError("can't assign to read-only field: $name")
}
open suspend fun getAt(context: Context, index: Int): Obj {
open suspend fun getAt(context: Context, index: Obj): Obj {
context.raiseNotImplemented("indexing")
}
@ -218,7 +225,7 @@ open class Obj {
callOn(
context.copy(
context.pos,
args = Arguments(args.map { Arguments.Info(it, context.pos) }),
args = Arguments(args.toList()),
newThisObj = thisObj
)
)
@ -241,7 +248,33 @@ open class Obj {
companion object {
inline fun <reified T> from(obj: T): Obj {
val rootObjectType = ObjClass("Obj").apply {
addFn("toString") {
thisObj.asStr
}
addFn("contains") {
ObjBool(thisObj.contains(this, args.firstAndOnly()))
}
// utilities
addFn("let") {
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
}
addFn("apply") {
val newContext = ( thisObj as? ObjInstance)?.instanceContext ?: this
args.firstAndOnly()
.callOn(newContext)
thisObj
}
addFn("also") {
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
thisObj
}
}
inline fun from(obj: Any?): Obj {
@Suppress("UNCHECKED_CAST")
return when (obj) {
is Obj -> obj
is Double -> ObjReal(obj)
@ -251,8 +284,15 @@ open class Obj {
is String -> ObjString(obj)
is CharSequence -> ObjString(obj.toString())
is Boolean -> ObjBool(obj)
is Set<*> -> ObjSet((obj as Set<Obj>).toMutableSet())
Unit -> ObjVoid
null -> ObjNull
is Iterator<*> -> ObjKotlinIterator(obj)
is Map.Entry<*, *> -> {
obj as MutableMap.MutableEntry<Obj, Obj>
ObjMapEntry(obj.key, obj.value)
}
else -> throw IllegalArgumentException("cannot convert to Obj: $obj")
}
}
@ -286,6 +326,32 @@ object ObjNull : Obj() {
override fun equals(other: Any?): Boolean {
return other is ObjNull || other == null
}
override suspend fun readField(context: Context, name: String): ObjRecord {
context.raiseNPE()
}
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj {
context.raiseNPE()
}
override suspend fun getAt(context: Context, index: Obj): Obj {
context.raiseNPE()
}
override suspend fun putAt(context: Context, index: Int, newValue: Obj) {
context.raiseNPE()
}
override suspend fun callOn(context: Context): Obj {
context.raiseNPE()
}
override fun toString(): String = "null"
override suspend fun toKotlin(context: Context): Any? {
return null
}
}
interface Numeric {
@ -325,20 +391,112 @@ data class ObjNamespace(val name: String) : Obj() {
}
}
open class ObjError(val context: Context, val message: String) : Obj() {
override val asStr: ObjString by lazy { ObjString("Error: $message") }
open class ObjException(exceptionClass: ExceptionClass, val context: Context, val message: String) : Obj() {
constructor(name: String, context: Context, message: String) : this(
getOrCreateExceptionClass(name),
context,
message
)
constructor(context: Context, message: String) : this(Root, context, message)
fun raise(): Nothing {
throw ExecutionError(this)
}
override val objClass: ObjClass = exceptionClass
override fun toString(): String {
return "ObjException:${objClass.className}:${context.pos}@${hashCode().encodeToHex()}"
}
companion object {
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun callOn(context: Context): Obj {
val message = context.args.getOrNull(0)?.toString() ?: name
return ObjException(this, context, message)
}
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
}
val Root = ExceptionClass("Throwable").apply {
addConst("message", statement {
(thisObj as ObjException).message.toObj()
})
}
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(context: Context) {
context.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullReferenceException",
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"IllegalArgumentException",
"NoSuchElementException",
"IllegalAssignmentException",
"SymbolNotDefinedException",
"IterationEndException",
"AccessException",
"UnknownException",
)) {
context.addConst(name, getOrCreateExceptionClass(name))
}
}
}
}
class ObjNullPointerError(context: Context) : ObjError(context, "object is null")
class ObjNullReferenceException(context: Context) : ObjException("NullReferenceException", context, "object is null")
class ObjAssertionError(context: Context, message: String) : ObjError(context, message)
class ObjClassCastError(context: Context, message: String) : ObjError(context, message)
class ObjIndexOutOfBoundsError(context: Context, message: String = "index out of bounds") : ObjError(context, message)
class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message)
class ObjIllegalAssignmentError(context: Context, message: String = "illegal assignment") : ObjError(context, message)
class ObjSymbolNotDefinedError(context: Context, message: String = "symbol is not defined") : ObjError(context, message)
class ObjIterationFinishedError(context: Context) : ObjError(context, "iteration finished")
class ObjAssertionFailedException(context: Context, message: String) :
ObjException("AssertionFailedException", context, message)
class ObjClassCastException(context: Context, message: String) : ObjException("ClassCastException", context, message)
class ObjIndexOutOfBoundsException(context: Context, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", context, message)
class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") :
ObjException("IllegalArgumentException", context, message)
@Suppress("unused")
class ObjNoSuchElementException(context: Context, message: String = "no such element") :
ObjException("IllegalArgumentException", context, message)
class ObjIllegalAssignmentException(context: Context, message: String = "illegal assignment") :
ObjException("NoSuchElementException", context, message)
class ObjSymbolNotDefinedException(context: Context, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", context, message)
class ObjIterationFinishedException(context: Context) :
ObjException("IterationEndException", context, "iteration finished")
class ObjAccessException(context: Context, message: String = "access not allowed error") :
ObjException("AccessException", context, message)
class ObjUnknownException(context: Context, message: String = "access not allowed error") :
ObjException("UnknownException", context, message)

View File

@ -0,0 +1,38 @@
package net.sergeych.lyng
val ObjArray by lazy {
/**
* Array abstract class is a [ObjCollection] with `getAt` method.
*/
ObjClass("Array", ObjCollection).apply {
// we can create iterators using size/getat:
addFn("iterator") {
ObjArrayIterator(thisObj).also { it.init(this) }
}
addFn("contains", isOpen = true) {
val obj = args.firstAndOnly()
for (i in 0..<thisObj.invokeInstanceMethod(this, "size").toInt()) {
if (thisObj.getAt(this, ObjInt(i.toLong())).compareTo(this, obj) == 0) return@addFn ObjTrue
}
ObjFalse
}
addFn("last") {
thisObj.invokeInstanceMethod(
this,
"getAt",
(thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj()
)
}
addFn("lastIndex") { (thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() }
addFn("indices") {
ObjRange(0.toObj(), thisObj.invokeInstanceMethod(this, "size"), false)
}
}
}

View File

@ -0,0 +1,32 @@
package net.sergeych.lyng
class ObjArrayIterator(val array: Obj) : Obj() {
override val objClass: ObjClass by lazy { type }
private var nextIndex = 0
private var lastIndex = 0
suspend fun init(context: Context) {
nextIndex = 0
lastIndex = array.invokeInstanceMethod(context, "size").toInt()
ObjVoid
}
companion object {
val type by lazy {
ObjClass("ArrayIterator", ObjIterator).apply {
addFn("next") {
val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) {
self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj())
} else raiseError(ObjIterationFinishedException(this))
}
addFn("hasNext") {
val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) ObjTrue else ObjFalse
}
}
}
}
}

View File

@ -18,6 +18,10 @@ data class ObjBool(val value: Boolean) : Obj() {
override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool())
override suspend fun toKotlin(context: Context): Any {
return value
}
companion object {
val type = ObjClass("Bool")
}

View File

@ -0,0 +1,73 @@
package net.sergeych.lyng
val ObjClassType by lazy { ObjClass("Class") }
open class ObjClass(
val className: String,
vararg val parents: ObjClass,
) : Obj() {
var instanceConstructor: Statement? = null
val allParentsSet: Set<ObjClass> =
parents.flatMap {
listOf(it) + it.allParentsSet
}.toMutableSet()
override val objClass: ObjClass by lazy { ObjClassType }
// members: fields most often
private val members = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className
override suspend fun compareTo(context: Context, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(context: Context): Obj {
val instance = ObjInstance(this)
instance.instanceContext = context.copy(newThisObj = instance,args = context.args)
if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceContext)
}
return instance
}
fun defaultInstance(): Obj = object : Obj() {
override val objClass: ObjClass = this@ObjClass
}
fun createField(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] }
if( existing?.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Context.() -> Obj) {
createField(name, statement { code() }, isOpen)
}
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
/**
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
*/
fun getInstanceMemberOrNull(name: String): ObjRecord? {
members[name]?.let { return it }
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } }
return rootObjectType.members[name]
}
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
}

View File

@ -0,0 +1,8 @@
package net.sergeych.lyng
/**
* Collection is an iterator with `size`]
*/
val ObjCollection = ObjClass("Collection", ObjIterable).apply {
}

View File

@ -0,0 +1,55 @@
package net.sergeych.lyng
class ObjInstance(override val objClass: ObjClass) : Obj() {
internal lateinit var instanceContext: Context
override suspend fun readField(context: Context, name: String): ObjRecord {
return instanceContext[name]?.let {
if (it.visibility.isPublic)
it
else
context.raiseError(ObjAccessException(context, "can't access non-public field $name"))
}
?: super.readField(context, name)
}
override suspend fun writeField(context: Context, name: String, newValue: Obj) {
instanceContext[name]?.let { f ->
if (!f.visibility.isPublic)
ObjIllegalAssignmentException(context, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentException(context, "can't reassign val $name").raise()
if (f.value.assign(context, newValue) == null)
f.value = newValue
} ?: super.writeField(context, name, newValue)
}
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj =
instanceContext[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(context, this, args)
else
context.raiseError(ObjAccessException(context, "can't invoke non-public method $name"))
}
?: super.invokeInstanceMethod(context, name, args)
private val publicFields: Map<String, ObjRecord>
get() = instanceContext.objects.filter { it.value.visibility.isPublic }
override fun toString(): String {
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
return "${objClass.className}($fields)"
}
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjInstance) return -1
if (other.objClass != objClass) return -1
for (f in publicFields) {
val a = f.value.value
val b = other.instanceContext[f.key]!!.value
val d = a.compareTo(context, b)
if (d != 0) return d
}
return 0
}
}

View File

@ -9,6 +9,10 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
override fun byValueCopy(): Obj = ObjInt(value)
override fun hashCode(): Int {
return value.hashCode()
}
override suspend fun getAndIncrement(context: Context): Obj {
return ObjInt(value).also { value++ }
}
@ -72,7 +76,22 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
} else null
}
override suspend fun toKotlin(context: Context): Any {
return value
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjInt
return value == other.value
}
companion object {
val Zero = ObjInt(0)
val One = ObjInt(1)
val type = ObjClass("Int")
}
}

View File

@ -0,0 +1,89 @@
package net.sergeych.lyng
/**
* Abstract class that must provide `iterator` method that returns [ObjIterator] instance.
*/
val ObjIterable by lazy {
ObjClass("Iterable").apply {
addFn("toList") {
val result = mutableListOf<Obj>()
val iterator = thisObj.invokeInstanceMethod(this, "iterator")
while (iterator.invokeInstanceMethod(this, "hasNext").toBool())
result += iterator.invokeInstanceMethod(this, "next")
ObjList(result)
}
// it is not effective, but it is open:
addFn("contains", isOpen = true) {
val obj = args.firstAndOnly()
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
if (obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0)
return@addFn ObjTrue
}
ObjFalse
}
addFn("indexOf", isOpen = true) {
val obj = args.firstAndOnly()
var index = 0
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
if (obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0)
return@addFn ObjInt(index.toLong())
index++
}
ObjInt(-1L)
}
addFn("toSet") {
val result = mutableSetOf<Obj>()
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
result += it.invokeInstanceMethod(this, "next")
}
ObjSet(result)
}
addFn("toMap") {
val result = mutableListOf<Obj>()
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
result += it.invokeInstanceMethod(this, "next")
}
ObjMap(ObjMap.listToMap(this, result))
}
addFn("forEach", isOpen = true) {
val it = thisObj.invokeInstanceMethod(this, "iterator")
val fn = requiredArg<Statement>(0)
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
val x = it.invokeInstanceMethod(this, "next")
fn.execute(this.copy(Arguments(listOf(x))))
}
ObjVoid
}
addFn("map", isOpen = true) {
val it = thisObj.invokeInstanceMethod(this, "iterator")
val fn = requiredArg<Statement>(0)
val result = mutableListOf<Obj>()
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
val x = it.invokeInstanceMethod(this, "next")
result += fn.execute(this.copy(Arguments(listOf(x))))
}
ObjList(result)
}
addFn("isEmpty") {
ObjBool(
thisObj.invokeInstanceMethod(this, "iterator")
.invokeInstanceMethod(this, "hasNext").toBool()
.not()
)
}
}
}

View File

@ -0,0 +1,3 @@
package net.sergeych.lyng
val ObjIterator by lazy { ObjClass("Iterator") }

View File

@ -0,0 +1,39 @@
@file:Suppress("unused")
package net.sergeych.lyng
/**
* Iterator wrapper to allow Kotlin collections to be returned from Lyng objects;
* each object is converted to a Lyng object.
*/
class ObjKotlinIterator(val iterator: Iterator<Any?>) : Obj() {
override val objClass = type
companion object {
val type = ObjClass("KotlinIterator", ObjIterator).apply {
addFn("next") { thisAs<ObjKotlinIterator>().iterator.next().toObj() }
addFn("hasNext") { thisAs<ObjKotlinIterator>().iterator.hasNext().toObj() }
}
}
}
/**
* Propagate kotlin iterator that already produces Lyng objects, no conversion
* is applied
*/
class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
override val objClass = type
companion object {
val type = ObjClass("KotlinIterator", ObjIterator).apply {
addFn("next") {
thisAs<ObjKotlinObjIterator>().iterator.next()
}
addFn("hasNext") { thisAs<ObjKotlinIterator>().iterator.hasNext().toObj() }
}
}
}

View File

@ -0,0 +1,230 @@
package net.sergeych.lyng
class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
init {
for (p in objClass.parents)
parentInstances.add(p.defaultInstance())
}
override fun toString(): String = "[${
list.joinToString(separator = ", ") { it.inspect() }
}]"
override suspend fun getAt(context: Context, index: Obj): Obj {
return when (index) {
is ObjInt -> {
list[index.toInt()]
}
is ObjRange -> {
when {
index.start is ObjInt && index.end is ObjInt -> {
if (index.isEndInclusive)
ObjList(list.subList(index.start.toInt(), index.end.toInt() + 1).toMutableList())
else
ObjList(list.subList(index.start.toInt(), index.end.toInt()).toMutableList())
}
index.isOpenStart && !index.isOpenEnd -> {
if (index.isEndInclusive)
ObjList(list.subList(0, index.end!!.toInt() + 1).toMutableList())
else
ObjList(list.subList(0, index.end!!.toInt()).toMutableList())
}
index.isOpenEnd && !index.isOpenStart -> {
ObjList(list.subList(index.start!!.toInt(), list.size).toMutableList())
}
index.isOpenStart && index.isOpenEnd -> {
ObjList(list.toMutableList())
}
else -> {
throw RuntimeException("Can't apply range for index: $index")
}
}
}
else -> context.raiseIllegalArgument("Illegal index object for a list: ${index.inspect()}")
}
}
override suspend fun putAt(context: Context, index: Int, newValue: Obj) {
val i = index
list[i] = newValue
}
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjList) return -2
val mySize = list.size
val otherSize = other.list.size
val commonSize = minOf(mySize, otherSize)
for (i in 0..<commonSize) {
if (list[i].compareTo(context, other.list[i]) != 0) {
return list[i].compareTo(context, other.list[i])
}
}
// equal so far, longer is greater:
return when {
mySize < otherSize -> -1
mySize > otherSize -> 1
else -> 0
}
}
override suspend fun plus(context: Context, other: Obj): Obj =
when {
other is ObjList ->
ObjList((list + other.list).toMutableList())
other.isInstanceOf(ObjIterable) -> {
val l = other.callMethod<ObjList>(context, "toList")
ObjList((list + l.list).toMutableList())
}
else ->
context.raiseError("'+': can't concatenate $this with $other")
}
override suspend fun plusAssign(context: Context, other: Obj): Obj {
// optimization
if (other is ObjList) {
list += other.list
return this
}
if (other.isInstanceOf(ObjIterable)) {
val otherList = other.invokeInstanceMethod(context, "toList") as ObjList
list += otherList.list
} else
list += other
return this
}
override suspend fun contains(context: Context, other: Obj): Boolean {
return list.contains(other)
}
override val objClass: ObjClass
get() = type
override suspend fun toKotlin(context: Context): Any {
return list.map { it.toKotlin(context) }
}
override fun hashCode(): Int {
// check?
return list.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjList
return list == other.list
}
companion object {
val type = ObjClass("List", ObjArray).apply {
createField("size",
statement {
(thisObj as ObjList).list.size.toObj()
}
)
addFn("getAt") {
requireExactCount(1)
thisAs<ObjList>().getAt(this, requiredArg<Obj>(0))
}
addFn("putAt") {
requireExactCount(2)
val newValue = args[1]
thisAs<ObjList>().putAt(this, requiredArg<ObjInt>(0).value.toInt(), newValue)
newValue
}
createField("add",
statement {
val l = thisAs<ObjList>().list
for (a in args) l.add(a)
ObjVoid
}
)
addFn("insertAt") {
if (args.size < 2) raiseError("addAt takes 2+ arguments")
val l = thisAs<ObjList>()
var index = requiredArg<ObjInt>(0).value.toInt()
for (i in 1..<args.size) l.list.add(index++, args[i])
ObjVoid
}
addFn("removeAt") {
val self = thisAs<ObjList>()
val start = requiredArg<ObjInt>(0).value.toInt()
if (args.size == 2) {
val end = requireOnlyArg<ObjInt>().value.toInt()
self.list.subList(start, end).clear()
} else
self.list.removeAt(start)
self
}
addFn("removeLast") {
val self = thisAs<ObjList>()
if (args.isNotEmpty()) {
val count = requireOnlyArg<ObjInt>().value.toInt()
val size = self.list.size
if (count >= size) self.list.clear()
else self.list.subList(size - count, size).clear()
} else self.list.removeLast()
self
}
addFn("removeRange") {
val self = thisAs<ObjList>()
val list = self.list
val range = requiredArg<Obj>(0)
if (range is ObjRange) {
val index = range
when {
index.start is ObjInt && index.end is ObjInt -> {
if (index.isEndInclusive)
list.subList(index.start.toInt(), index.end.toInt() + 1)
else
list.subList(index.start.toInt(), index.end.toInt())
}
index.isOpenStart && !index.isOpenEnd -> {
if (index.isEndInclusive)
list.subList(0, index.end!!.toInt() + 1)
else
list.subList(0, index.end!!.toInt())
}
index.isOpenEnd && !index.isOpenStart -> {
list.subList(index.start!!.toInt(), list.size)
}
index.isOpenStart && index.isOpenEnd -> {
list
}
else -> {
throw RuntimeException("Can't apply range for index: $index")
}
}.clear()
} else {
val start = range.toInt()
val end = requiredArg<ObjInt>(1).value.toInt() + 1
self.list.subList(start, end).clear()
}
self
}
}
}
}

View File

@ -0,0 +1,114 @@
package net.sergeych.lyng
class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjMapEntry) return -1
val c = key.compareTo(context, other.key)
if (c != 0) return c
return value.compareTo(context, other.value)
}
override suspend fun getAt(context: Context, index: Obj): Obj = when (index.toInt()) {
0 -> key
1 -> value
else -> context.raiseIndexOutOfBounds()
}
override fun toString(): String {
return "$key=>$value"
}
override val objClass = type
companion object {
val type = object : ObjClass("MapEntry", ObjArray) {
override suspend fun callOn(context: Context): Obj {
return ObjMapEntry(context.requiredArg<Obj>(0), context.requiredArg<Obj>(1))
}
}.apply {
addFn("key") { thisAs<ObjMapEntry>().key }
addFn("value") { thisAs<ObjMapEntry>().value }
addFn("size") { 2.toObj() }
}
}
}
class ObjMap(val map: MutableMap<Obj, Obj>) : Obj() {
override val objClass = type
override suspend fun getAt(context: Context, index: Obj): Obj =
map.getOrElse(index) { context.raiseNoSuchElement() }
override suspend fun contains(context: Context, other: Obj): Boolean {
return other in map
}
override suspend fun compareTo(context: Context, other: Obj): Int {
if( other is ObjMap && other.map == map) return 0
return -1
}
override fun toString(): String = map.toString()
companion object {
suspend fun listToMap(context: Context, list: List<Obj>): MutableMap<Obj, Obj> {
val map = mutableMapOf<Obj, Obj>()
if (list.isEmpty()) return map
val first = list.first()
if (first.isInstanceOf(ObjArray)) {
if (first.invokeInstanceMethod(context, "size").toInt() != 2)
context.raiseIllegalArgument(
"list to construct map entry should exactly be 2 element Array like [key,value], got $list"
)
} else context.raiseIllegalArgument("first element of map list be a Collection of 2 elements; got $first")
list.forEach {
map[it.getAt(context, ObjInt.Zero)] = it.getAt(context, ObjInt.One)
}
return map
}
val type = object : ObjClass("Map", ObjCollection) {
override suspend fun callOn(context: Context): Obj {
return ObjMap(listToMap(context, context.args.list))
}
}.apply {
addFn("getOrNull") {
val key = args.firstAndOnly(pos)
thisAs<ObjMap>().map.getOrElse(key) { ObjNull }
}
addFn("getOrPut") {
val key = requiredArg<Obj>(0)
thisAs<ObjMap>().map.getOrPut(key) {
val lambda = requiredArg<Statement>(1)
lambda.execute(this)
}
}
addFn("size") {
thisAs<ObjMap>().map.size.toObj()
}
addFn("remove") {
thisAs<ObjMap>().map.remove(requiredArg<Obj>(0))?.toObj() ?: ObjNull
}
addFn("clear") {
thisAs<ObjMap>().map.clear()
thisObj
}
addFn("keys") {
thisAs<ObjMap>().map.keys.toObj()
}
addFn("values") {
ObjList(thisAs<ObjMap>().map.values.toMutableList())
}
addFn("iterator") {
ObjKotlinIterator(thisAs<ObjMap>().map.entries.iterator())
}
}
}
}

View File

@ -2,6 +2,9 @@ package net.sergeych.lyng
class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() {
val isOpenStart by lazy { start == null || start.isNull }
val isOpenEnd by lazy { end == null || end.isNull }
override val objClass: ObjClass = type
override fun toString(): String {

View File

@ -33,7 +33,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x)
}
else {
context.raiseError(ObjIterationFinishedError(context))
context.raiseError(ObjIterationFinishedException(context))
}
companion object {

View File

@ -10,6 +10,8 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override val toObjInt: ObjInt by lazy { ObjInt(longValue) }
override val toObjReal: ObjReal by lazy { ObjReal(value) }
override val objClass: ObjClass = type
override fun byValueCopy(): Obj = ObjReal(value)
override suspend fun compareTo(context: Context, other: Obj): Int {
@ -19,7 +21,9 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override fun toString(): String = value.toString()
override val objClass: ObjClass = type
override fun hashCode(): Int {
return value.hashCode()
}
override suspend fun plus(context: Context, other: Obj): Obj =
ObjReal(this.value + other.toDouble())
@ -36,6 +40,22 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override suspend fun mod(context: Context, other: Obj): Obj =
ObjReal(this.value % other.toDouble())
/**
* Returns unboxed Double value
*/
override suspend fun toKotlin(context: Context): Any {
return value
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjReal
return value == other.value
}
companion object {
val type: ObjClass = ObjClass("Real").apply {
createField(

View File

@ -0,0 +1,99 @@
package net.sergeych.lyng
class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
override val objClass = type
override suspend fun contains(context: Context, other: Obj): Boolean {
return set.contains(other)
}
override suspend fun plus(context: Context, other: Obj): Obj {
return ObjSet(
if (other is ObjSet)
(set + other.set).toMutableSet()
else
(set + other).toMutableSet()
)
}
override suspend fun plusAssign(context: Context, other: Obj): Obj {
when (other) {
is ObjSet -> {
set += other.set
}
is ObjList -> {
set += other.list
}
else -> {
if (other.isInstanceOf(ObjIterable)) {
val i = other.invokeInstanceMethod(context, "iterable")
while (i.invokeInstanceMethod(context, "hasNext").toBool()) {
set += i.invokeInstanceMethod(context, "next")
}
}
set += other
}
}
return this
}
override suspend fun mul(context: Context, other: Obj): Obj {
return if (other is ObjSet) {
ObjSet(set.intersect(other.set).toMutableSet())
} else
context.raiseIllegalArgument("set operator * requires another set")
}
override suspend fun minus(context: Context, other: Obj): Obj {
if (other !is ObjSet)
context.raiseIllegalArgument("set operator - requires another set")
return ObjSet(set.minus(other.set).toMutableSet())
}
override fun toString(): String {
return "Set(${set.joinToString(", ")})"
}
override suspend fun compareTo(context: Context, other: Obj): Int {
return if (other !is ObjSet) -1
else {
if (set == other.set) 0
else -1
}
}
companion object {
val type = object : ObjClass("Set", ObjCollection) {
override suspend fun callOn(context: Context): Obj {
return ObjSet(context.args.list.toMutableSet())
}
}.apply {
addFn("size") {
thisAs<ObjSet>().set.size.toObj()
}
addFn("intersect") {
thisAs<ObjSet>().mul(this, args.firstAndOnly())
}
addFn("iterator") {
thisAs<ObjSet>().set.iterator().toObj()
}
addFn("union") {
thisAs<ObjSet>().plus(this, args.firstAndOnly())
}
addFn("subtract") {
thisAs<ObjSet>().minus(this, args.firstAndOnly())
}
addFn("remove") {
val set = thisAs<ObjSet>().set
val n = set.size
for( x in args.list ) set -= x
if( n == set.size ) ObjFalse else ObjTrue
}
}
}
}

View File

@ -0,0 +1,117 @@
package net.sergeych.lyng
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.sprintf.sprintf
@Serializable
@SerialName("string")
data class ObjString(val value: String) : Obj() {
// fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int {
// val i = if (index < 0) value.length + index else index
// if (allowsEndInclusive && i == value.length) return i
// if (i !in value.indices) context.raiseError("index $index out of bounds for length ${value.length} of \"$value\"")
// return i
// }
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjString) return -2
return this.value.compareTo(other.value)
}
override fun toString(): String = value
override val asStr: ObjString by lazy { this }
override fun inspect(): String {
return "\"$value\""
}
override val objClass: ObjClass
get() = type
override suspend fun plus(context: Context, other: Obj): Obj {
return ObjString(value + other.asStr.value)
}
override suspend fun getAt(context: Context, index: Obj): Obj {
if( index is ObjInt ) return ObjChar(value[index.toInt()])
if( index is ObjRange ) {
val start = if(index.start == null || index.start.isNull) 0 else index.start.toInt()
val end = if( index.end == null || index.end.isNull ) value.length else {
val e = index.end.toInt()
if( index.isEndInclusive) e + 1 else e
}
return ObjString(value.substring(start, end))
}
context.raiseIllegalArgument("String index must be Int or Range")
}
override fun hashCode(): Int {
return value.hashCode()
}
override suspend fun callOn(context: Context): Obj {
return ObjString(this.value.sprintf(*context.args.toKotlinList(context).toTypedArray()))
}
override suspend fun contains(context: Context, other: Obj): Boolean {
return if (other is ObjString)
value.contains(other.value)
else if (other is ObjChar)
value.contains(other.value)
else context.raiseIllegalArgument("String.contains can't take $other")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjString
return value == other.value
}
companion object {
val type = ObjClass("String").apply {
addFn("startsWith") {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
}
addFn("endsWith") {
ObjBool(thisAs<ObjString>().value.endsWith(requiredArg<ObjString>(0).value))
}
addConst("length",
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) }
)
addFn("takeLast") {
thisAs<ObjString>().value.takeLast(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("take") {
thisAs<ObjString>().value.take(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("drop") {
thisAs<ObjString>().value.drop(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("dropLast") {
thisAs<ObjString>().value.dropLast(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("lower") {
thisAs<ObjString>().value.lowercase().let(::ObjString)
}
addFn("upper") {
thisAs<ObjString>().value.uppercase().let(::ObjString)
}
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
}
}
}

View File

@ -45,12 +45,18 @@ private class Parser(fromPos: Pos) {
'=' -> {
if (pos.currentChar == '=') {
pos.advance()
if (currentChar == '=') {
pos.advance()
Token("===", from, Token.Type.REF_EQ)
} else
Token("==", from, Token.Type.EQ)
} else
when (currentChar) {
'=' -> {
pos.advance()
Token("===", from, Token.Type.REF_EQ)
}
else -> Token("==", from, Token.Type.EQ)
}
} else if( currentChar == '>' ) {
pos.advance()
Token("=>", from, Token.Type.EQARROW)
}
else
Token("=", from, Token.Type.ASSIGN)
}
@ -137,14 +143,20 @@ private class Parser(fromPos: Pos) {
if (currentChar == '.') {
pos.advance()
// .. already parsed:
if (currentChar == '.') {
pos.advance()
Token("...", from, Token.Type.ELLIPSIS)
} else if (currentChar == '<') {
pos.advance()
Token("..<", from, Token.Type.DOTDOTLT)
} else {
Token("..", from, Token.Type.DOTDOT)
when (currentChar) {
'.' -> {
pos.advance()
Token("...", from, Token.Type.ELLIPSIS)
}
'<' -> {
pos.advance()
Token("..<", from, Token.Type.DOTDOTLT)
}
else -> {
Token("..", from, Token.Type.DOTDOT)
}
}
} else
Token(".", from, Token.Type.DOT)
@ -153,11 +165,10 @@ private class Parser(fromPos: Pos) {
'<' -> {
if (currentChar == '=') {
pos.advance()
if( currentChar == '>' ) {
if (currentChar == '>') {
pos.advance()
Token("<=>", from, Token.Type.SHUTTLE)
}
else {
} else {
Token("<=", from, Token.Type.LTE)
}
} else
@ -236,6 +247,7 @@ private class Parser(fromPos: Pos) {
}
'"' -> loadStringToken()
in digitsSet -> {
pos.back()
decodeNumber(loadChars(digits), from)
@ -261,6 +273,21 @@ private class Parser(fromPos: Pos) {
Token(value.toString(), start, Token.Type.CHAR)
}
'?' -> {
when(currentChar.also { pos.advance() }) {
':' -> Token("??", from, Token.Type.ELVIS)
'?' -> Token("??", from, Token.Type.ELVIS)
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
'[' -> Token("?(", from, Token.Type.NULL_COALESCE_INDEX)
'(' -> Token("?(", from, Token.Type.NULL_COALESCE_INVOKE)
'{' -> Token("?{", from, Token.Type.NULL_COALESCE_BLOCKINVOKE)
else -> {
pos.back()
Token("?", from, Token.Type.QUESTION)
}
}
}
else -> {
// text infix operators:
// Labels processing is complicated!
@ -291,7 +318,19 @@ private class Parser(fromPos: Pos) {
private fun decodeNumber(p1: String, start: Pos): Token =
if (pos.end)
Token(p1, start, Token.Type.INT)
else if (currentChar == '.') {
else if (currentChar == 'e' || currentChar == 'E') {
pos.advance()
var negative = false
if (currentChar == '+')
pos.advance()
else if (currentChar == '-') {
negative = true
pos.advance()
}
var p3 = loadChars(digits)
if (negative) p3 = "-$p3"
Token("${p1}e$p3", start, Token.Type.REAL)
} else if (currentChar == '.') {
// could be decimal
pos.advance()
if (currentChar in digitsSet) {
@ -324,7 +363,7 @@ private class Parser(fromPos: Pos) {
// could be integer, also hex:
if (currentChar == 'x' && p1 == "0") {
pos.advance()
Token(loadChars({ it in hexDigits }), start, Token.Type.HEX).also {
Token(loadChars { it in hexDigits }, start, Token.Type.HEX).also {
if (currentChar.isLetter())
raise("invalid hex literal")
}
@ -337,10 +376,11 @@ private class Parser(fromPos: Pos) {
private val currentChar: Char get() = pos.currentChar
private fun loadStringToken(): Token {
var start = currentPos
val start = currentPos
if (currentChar == '"') pos.advance()
else start = start.back()
// if (currentChar == '"') pos.advance()
// else start = start.back()
// start = start.back()
val sb = StringBuilder()
while (currentChar != '"') {

View File

@ -5,6 +5,7 @@ data class Pos(val source: Source, val line: Int, val column: Int) {
return "${source.fileName}:${line+1}:${column}"
}
@Suppress("unused")
fun back(): Pos =
if( column > 0) Pos(source, line, column-1)
else if( line > 0) Pos(source, line-1, source.lines[line-1].length - 1)

View File

@ -6,6 +6,7 @@ import kotlin.math.*
class Script(
override val pos: Pos,
private val statements: List<Statement> = emptyList(),
// private val catchReturn: Boolean = false,
) : Statement() {
override suspend fun execute(context: Context): Obj {
@ -16,10 +17,11 @@ class Script(
return lastResult
}
suspend fun execute() = execute(defaultContext.copy(pos))
suspend fun execute() = execute(defaultContext.copy(pos = pos))
companion object {
val defaultContext: Context = Context().apply {
ObjException.addExceptionsToContext(this)
addFn("println") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value)
@ -116,14 +118,14 @@ class Script(
addVoidFn("assert") {
val cond = requiredArg<ObjBool>(0)
if( !cond.value == true )
raiseError(ObjAssertionError(this,"Assertion failed"))
raiseError(ObjAssertionFailedException(this,"Assertion failed"))
}
addVoidFn("assertEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionError(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
}
addFn("assertThrows") {
val code = requireOnlyArg<Statement>()
@ -137,33 +139,39 @@ class Script(
catch (e: ScriptError) {
ObjNull
}
result ?: raiseError(ObjAssertionError(this,"Expected exception but nothing was thrown"))
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
}
addVoidFn("delay") {
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
}
addConst("Object", rootObjectType)
addConst("Real", ObjReal.type)
addConst("String", ObjString.type)
addConst("Int", ObjInt.type)
addConst("Bool", ObjBool.type)
addConst("Char", ObjChar.type)
addConst("List", ObjList.type)
addConst("Set", ObjSet.type)
addConst("Range", ObjRange.type)
addConst("Map", ObjMap.type)
addConst("MapEntry", ObjMapEntry.type)
@Suppress("RemoveRedundantQualifierName")
addConst("Callable", Statement.type)
// interfaces
addConst("Iterable", ObjIterable)
addConst("Collection", ObjCollection)
addConst("Array", ObjArray)
addConst("Class", ObjClassType)
addConst("Object", Obj().objClass)
val pi = ObjReal(PI)
addConst("π", pi)
getOrCreateNamespace("Math").apply {
addConst("PI", pi)
}
}
}
}

View File

@ -12,4 +12,4 @@ open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=
cause
)
class ExecutionError(val errorObject: ObjError) : ScriptError(errorObject.context.pos, errorObject.message)
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.context.pos, errorObject.message)

View File

@ -14,12 +14,18 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
IN, NOTIN, IS, NOTIS,
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ,
SHUTTLE,
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
ELLIPSIS, DOTDOT, DOTDOTLT,
NEWLINE,
EOF,
NULL_COALESCE,
ELVIS,
NULL_COALESCE_INDEX,
NULL_COALESCE_INVOKE,
NULL_COALESCE_BLOCKINVOKE,
}
companion object {

View File

@ -0,0 +1,14 @@
@file:Suppress("unused")
package net.sergeych.lyng
// this is highly experimental and subject to complete redesign
// very soon
sealed class TypeDecl(val isNullable:Boolean = false) {
// ??
// data class Fn(val argTypes: List<ArgsDeclaration.Item>, val retType: TypeDecl) : TypeDecl()
object TypeAny : TypeDecl()
object TypeNullableAny : TypeDecl(true)
class Simple(val name: String,isNullable: Boolean) : TypeDecl(isNullable)
}

View File

@ -0,0 +1,8 @@
package net.sergeych.lyng
enum class Visibility {
Public, Private, Protected;//, Internal
val isPublic by lazy { this == Public }
@Suppress("unused")
val isProtected by lazy { this == Protected }
}

View File

@ -0,0 +1,4 @@
package net.sergeych.lyng
val LyngVersion = BuildKonfig.version

View File

@ -0,0 +1,53 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class TypesTest {
@Test
fun testTypeCollection1() = runTest {
eval("""
class Point(x: Real, y: Real)
assert(Point(1,2).x == 1)
assert(Point(1,2).y == 2)
assert(Point(1,2) is Point)
""".trimIndent())
}
@Test
fun testTypeCollection2() = runTest {
eval("""
fun fn1(x: Real, y: Real): Real { x + y }
""".trimIndent())
}
@Test
fun testTypeCollection3() = runTest {
eval("""
class Test(a: Int) {
fun fn1(x: Real, y: Real): Real { x + y }
}
""".trimIndent())
}
@Test
fun testExternDeclarations() = runTest {
eval("""
extern fun foo1(a: String): Void
assertThrows { foo1("1") }
class Test(a: Int) {
extern fun fn1(x: Real, y: Real): Real
// extern val b: Int
}
// println("1")
val t = Test(0)
// println(t.b)
// println("2")
assertThrows {
t.fn1(1,2)
}
// println("4")
""".trimIndent())
}
}

View File

@ -255,6 +255,16 @@ class BookTest {
runDocTests("../docs/Range.md")
}
@Test
fun testSet() = runTest {
runDocTests("../docs/Set.md")
}
@Test
fun testMap() = runTest {
runDocTests("../docs/Map.md")
}
@Test
fun testSampleBooks() = runTest {
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
@ -268,4 +278,9 @@ class BookTest {
fun testArgumentBooks() = runTest {
runDocTests("../docs/declaring_arguments.md")
}
@Test
fun testExceptionsBooks() = runTest {
runDocTests("../docs/exceptions_handling.md")
}
}

View File

@ -18,5 +18,5 @@ dependencyResolutionManagement {
}
rootProject.name = "lyng"
include(":library")
include(":lynglib")
include(":lyng")