Compare commits

...

22 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
65 changed files with 2171 additions and 649 deletions

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)

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

@ -131,7 +131,7 @@ _this functionality is not yet released_
| class | notes |
|----------------------------|-------------------------------------------------------|
| Exception | root of al throwable objects |
| NullPointerException | |
| NullReferenceException | |
| AssertionFailedException | |
| ClassCastException | |
| IndexOutOfBoundsException | |

View File

@ -14,7 +14,7 @@ __Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md)
- Some class references: [List], [Real], [Range], [Iterable], [Iterator]
- 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
@ -118,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:
@ -469,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
@ -1006,31 +1142,79 @@ Are the same as in string literals with little difference:
## String details
Strings are much like Kotlin ones:
Strings are arrays of Unicode characters. It can be indexed, and indexing will
return a valid Unicode character at position. No utf hassle:
"Hello".length
"Парашют"[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 |
| fun/prop | description / notes |
|--------------------|------------------------------------------------------------|
| lower() | change case to unicode upper |
| upper() | change case to unicode lower |
| startsWith(prefix) | true if starts 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] |
| 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 |
@ -1066,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,173 +0,0 @@
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
}.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 {
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 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).apply {
// 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
}
}
}
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(ObjIterationFinishedException(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("contains", isOpen = true) {
val obj = args.firstAndOnly()
for( i in 0..< thisObj.invokeInstanceMethod(this, "size").toInt()) {
if( thisObj.getAt(this, i).compareTo(this, obj) == 0 ) return@addFn ObjTrue
}
ObjFalse
}
addFn("isample") { "ok".toObj() }
}
}

View File

@ -1,141 +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 suspend fun contains(context: Context, other: Obj): Boolean {
return list.contains(other)
}
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,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

@ -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,11 +17,28 @@ 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 Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
@ -90,11 +107,16 @@ class Lyng(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

@ -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,11 +1,10 @@
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import com.vanniktech.maven.publish.SonatypeHost
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.0-SNAPSHOT"
version = "0.6.8-SNAPSHOT"
buildscript {
repositories {
@ -20,9 +19,10 @@ buildscript {
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`
}
buildkonfig {
@ -62,7 +62,7 @@ kotlin {
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts::class")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
}
@ -101,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"
}
}
}
//mavenPublishing {
// publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
//
//val projectVersion by project.extra(provider {
// // Compute value lazily
// (version as String)
//})
// signAllPublications()
//
//val generateBuildConfig by tasks.registering {
// // Declare outputs safely
// val outputDir = layout.buildDirectory.dir("generated/buildConfig/commonMain/kotlin")
// outputs.dir(outputDir)
// coordinates(group.toString(), "library", version.toString())
//
// 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()
// )
// 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"
// }
// }
//}
//
//tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
// dependsOn(generateBuildConfig)
//}

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

@ -56,7 +56,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
else -> {
println("callArgs: ${callArgs.joinToString()}")
println("tailBlockMode: ${arguments.tailBlockMode}")
context.raiseArgumentError("too few arguments for the call")
context.raiseIllegalArgument("too few arguments for the call")
}
}
assign(a, value)
@ -77,7 +77,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
}
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--
@ -98,7 +98,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
processEllipsis(leftIndex, end)
} else {
if (leftIndex < callArgs.size)
context.raiseArgumentError("too many arguments for the call")
context.raiseIllegalArgument("too many arguments for the call")
}
}
@ -110,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,
/**

View File

@ -28,11 +28,20 @@ suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMod
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

@ -59,7 +59,7 @@ class Compiler(
parseBlock(cc)
}
Token.Type.RBRACE -> {
Token.Type.RBRACE, Token.Type.RBRACKET -> {
cc.previous()
return null
}
@ -121,7 +121,8 @@ 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
@ -138,34 +139,41 @@ class Compiler(
operand = Accessor { context ->
context.pos = next.pos
val v = left.getter(context).value
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
args.toArguments(context, false)
), isMutable = false
)
if (v == ObjNull && isOptional)
ObjNull.asReadonly
else
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
args.toArguments(context, false)
), isMutable = false
)
}
}
Token.Type.LBRACE -> {
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 =
parseExpression(cc) ?: throw ScriptError(t.pos, "expected valid lambda here")
parseLambdaExpression(cc)
println(cc.current())
cc.skipTokenOfType(Token.Type.RBRACE)
operand = Accessor { context ->
context.pos = next.pos
val v = left.getter(context).value
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
Arguments(listOf(lambda), true)
), isMutable = false
)
if (v == ObjNull && isOptional)
ObjNull.asReadonly
else
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
Arguments(listOf(lambda.getter(context).value), true)
), isMutable = false
)
}
}
@ -174,25 +182,30 @@ class Compiler(
}
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
@ -205,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")
@ -324,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,
@ -337,18 +352,27 @@ class Compiler(
}
}
Token.Type.LBRACE -> {
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
operand = operand?.let { left ->
cc.previous()
parseFunctionCall(cc, left, blockArgument = true)
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")
}
}
}
@ -363,13 +387,14 @@ 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.list
@ -535,11 +560,11 @@ 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
}
/**
@ -565,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:
}
}
@ -590,7 +617,12 @@ class Compiler(
}
private fun parseFunctionCall(cc: CompilerContext, left: Accessor, blockArgument: Boolean): Accessor {
private fun parseFunctionCall(
cc: CompilerContext,
left: Accessor,
blockArgument: Boolean,
isOptional: Boolean
): Accessor {
// insofar, functions always return lvalue
var detectedBlockArgument = blockArgument
val args = if (blockArgument) {
@ -607,6 +639,7 @@ class Compiler(
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,
@ -693,7 +726,7 @@ 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) {
@ -704,7 +737,6 @@ class Compiler(
"for" -> parseForStatement(cc)
"break" -> parseBreakStatement(id.pos, cc)
"continue" -> parseContinueStatement(id.pos, cc)
"fn", "fun" -> parseFunctionDeclaration(cc)
"if" -> parseIfStatement(cc)
"class" -> parseClassDeclaration(cc, false)
"try" -> parseTryStatement(cc)
@ -713,11 +745,16 @@ class Compiler(
else -> {
// triples
cc.previous()
val isExtern = cc.skipId("extern")
when {
cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(cc, Visibility.Private)
cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(cc, Visibility.Private)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(cc, isOpen = true)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(cc, isOpen = true)
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)
@ -1087,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,
@ -1114,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) {
@ -1388,29 +1425,35 @@ class Compiler(
}
private fun parseFunctionDeclaration(
tokens: CompilerContext,
cc: CompilerContext,
visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isExtern: Boolean = false
): Statement {
var t = tokens.next()
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
@ -1586,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) },
@ -1600,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())
@ -1632,7 +1680,7 @@ class Compiler(
* The keywords that stop processing of expression term
*/
val stopKeywords =
setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class", "struct")
setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class")
}
}

View File

@ -53,6 +53,20 @@ internal class CompilerContext(val tokens: List<Token>) {
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.
* @param errorMessage message to throw if next token is not `tokenType`

View File

@ -1,10 +1,10 @@
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(
@ -16,14 +16,18 @@ class Context(
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullPointerException(this))
fun raiseNPE(): Nothing = raiseError(ObjNullReferenceException(this))
@Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsException(this, message))
@Suppress("unused")
fun raiseArgumentError(message: String = "Illegal argument error"): Nothing =
fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
@Suppress("unused")
fun raiseNoSuchElement(message: String = "No such element"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
@ -63,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)
@ -119,5 +126,4 @@ class Context(
fun containsLocal(name: String): Boolean = name in objects
}

View File

@ -42,6 +42,9 @@ data class Accessor(
}
open class Obj {
val isNull by lazy { this === ObjNull }
var isFrozen: Boolean = false
private val monitor = Mutex()
@ -61,7 +64,11 @@ 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.toList()))
@ -100,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()
@ -176,6 +174,13 @@ 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")
}
@ -201,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")
}
@ -243,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)
@ -253,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")
}
}
@ -289,7 +327,31 @@ object ObjNull : Obj() {
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 {
@ -330,7 +392,12 @@ data class ObjNamespace(val name: String) : Obj() {
}
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(name: String, context: Context, message: String) : this(
getOrCreateExceptionClass(name),
context,
message
)
constructor(context: Context, message: String) : this(Root, context, message)
fun raise(): Nothing {
@ -345,13 +412,15 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
companion object {
class ExceptionClass(val name: String,vararg parents: ObjClass) : ObjClass(name, *parents) {
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()
@ -383,11 +452,12 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
context.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullPointerException",
"NullReferenceException",
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"IllegalArgumentException",
"NoSuchElementException",
"IllegalAssignmentException",
"SymbolNotDefinedException",
"IterationEndException",
@ -400,7 +470,7 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
}
}
class ObjNullPointerException(context: Context) : ObjException("NullPointerException", context, "object is null")
class ObjNullReferenceException(context: Context) : ObjException("NullReferenceException", context, "object is null")
class ObjAssertionFailedException(context: Context, message: String) :
ObjException("AssertionFailedException", context, message)
@ -412,8 +482,12 @@ class ObjIndexOutOfBoundsException(context: Context, message: String = "index ou
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("IllegalAssignmentException", context, message)
ObjException("NoSuchElementException", context, message)
class ObjSymbolNotDefinedException(context: Context, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", 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

@ -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

@ -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

@ -2,11 +2,20 @@ 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)
@ -27,8 +36,25 @@ data class ObjString(val value: String) : Obj() {
return ObjString(value + other.asStr.value)
}
override suspend fun getAt(context: Context, index: Int): Obj {
return ObjChar(value[index])
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 {
@ -36,15 +62,26 @@ data class ObjString(val value: String) : Obj() {
value.contains(other.value)
else if (other is ObjChar)
value.contains(other.value)
else context.raiseArgumentError("String.contains can't take $other")
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 {
addConst("startsWith",
statement {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
})
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()) }
)

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)
}
@ -142,10 +148,12 @@ private class Parser(fromPos: Pos) {
pos.advance()
Token("...", from, Token.Type.ELLIPSIS)
}
'<' -> {
pos.advance()
Token("..<", from, Token.Type.DOTDOTLT)
}
else -> {
Token("..", from, Token.Type.DOTDOT)
}
@ -157,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
@ -240,6 +247,7 @@ private class Parser(fromPos: Pos) {
}
'"' -> loadStringToken()
in digitsSet -> {
pos.back()
decodeNumber(loadChars(digits), from)
@ -265,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!
@ -295,7 +318,7 @@ 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 == 'e' || currentChar == 'E' ) {
else if (currentChar == 'e' || currentChar == 'E') {
pos.advance()
var negative = false
if (currentChar == '+')
@ -353,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

@ -146,26 +146,32 @@ class Script(
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

@ -14,13 +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

@ -1,4 +1,3 @@
package io.github.kotlin.fibonacci
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.*
@ -111,25 +110,25 @@ class ScriptTest {
assertEquals(Token("label", src.posAt(0, 12), Token.Type.ATLABEL), tt[2])
}
@Test
fun parse0Test() {
val src = """
println("Hello")
println( "world" )
""".trimIndent().toSource()
val p = parseLyng(src).listIterator()
assertEquals(Token("println", src.posAt(0, 0), Token.Type.ID), p.next())
assertEquals(Token("(", src.posAt(0, 7), Token.Type.LPAREN), p.next())
assertEquals(Token("Hello", src.posAt(0, 8), Token.Type.STRING), p.next())
assertEquals(Token(")", src.posAt(0, 15), Token.Type.RPAREN), p.next())
assertEquals(Token("\n", src.posAt(0, 16), Token.Type.NEWLINE), p.next())
assertEquals(Token("println", src.posAt(1, 0), Token.Type.ID), p.next())
assertEquals(Token("(", src.posAt(1, 7), Token.Type.LPAREN), p.next())
assertEquals(Token("world", src.posAt(1, 9), Token.Type.STRING), p.next())
assertEquals(Token(")", src.posAt(1, 17), Token.Type.RPAREN), p.next())
}
// @Test
// fun parse0Test() {
// val src = """
// println("Hello")
// println( "world" )
// """.trimIndent().toSource()
//
// val p = parseLyng(src).listIterator()
//
// assertEquals(Token("println", src.posAt(0, 0), Token.Type.ID), p.next())
// assertEquals(Token("(", src.posAt(0, 7), Token.Type.LPAREN), p.next())
// assertEquals(Token("Hello", src.posAt(0, 9), Token.Type.STRING), p.next())
// assertEquals(Token(")", src.posAt(0, 15), Token.Type.RPAREN), p.next())
// assertEquals(Token("\n", src.posAt(0, 16), Token.Type.NEWLINE), p.next())
// assertEquals(Token("println", src.posAt(1, 0), Token.Type.ID), p.next())
// assertEquals(Token("(", src.posAt(1, 7), Token.Type.LPAREN), p.next())
// assertEquals(Token("world", src.posAt(1, 9), Token.Type.STRING), p.next())
// assertEquals(Token(")", src.posAt(1, 17), Token.Type.RPAREN), p.next())
// }
@Test
fun parse1Test() {
@ -1056,7 +1055,7 @@ class ScriptTest {
}
@Test
fun testIntOpenRangeInclusive() = runTest {
fun testIntClosedRangeInclusive() = runTest {
eval(
"""
val r = 10 .. 20
@ -1090,7 +1089,7 @@ class ScriptTest {
}
@Test
fun testIntOpenRangeExclusive() = runTest {
fun testIntClosedRangeExclusive() = runTest {
eval(
"""
val r = 10 ..< 20
@ -1126,7 +1125,7 @@ class ScriptTest {
}
@Test
fun testIntOpenRangeInExclusive() = runTest {
fun testIntClosedRangeInExclusive() = runTest {
eval(
"""
assert( (1..3) !in (1..<3) )
@ -1135,6 +1134,43 @@ class ScriptTest {
)
}
@Test
fun testOpenStartRanges() = runTest {
eval(
"""
var r = ..5
assert( r::class == Range)
assert( r.start == null)
assert( r.end == 5)
assert( r.isEndInclusive)
r = ..< 5
assert( r::class == Range)
assert( r.start == null)
assert( r.end == 5)
assert( !r.isEndInclusive)
assert( r.start == null)
assert( (-2..3) in r)
assert( (-2..12) !in r)
""".trimIndent()
)
}
@Test
fun testOpenEndRanges() = runTest {
eval(
"""
var r = 5..
assert( r::class == Range)
assert( r.end == null)
assert( r.start == 5)
""".trimIndent()
)
}
@Test
fun testCharacterRange() = runTest {
eval(
@ -1184,21 +1220,6 @@ class ScriptTest {
println(a)
}
@Test
fun iterableList() = runTest {
// 473
eval(
"""
for( i in 0..<1024 ) {
val list = (1..1024).toList()
assert(list.size == 1024)
assert(list[0] == 1)
assert(list[-1] == 1024)
}
""".trimIndent()
)
}
@Test
fun testLambdaWithIt1() = runTest {
eval(
@ -1245,7 +1266,12 @@ class ScriptTest {
eval(
"""
val x = { x, y, z ->
println("-- x=",x)
println("-- y=",y)
println("-- z=",z)
println([x,y,z])
assert( [x, y, z] == [1,2,"end"])
println("----:")
}
assert( x(1, 2, "end") == void)
""".trimIndent()
@ -1868,7 +1894,7 @@ class ScriptTest {
fun testThrowFromKotlin() = runTest {
val c = Context()
c.addFn("callThrow") {
raiseArgumentError("fromKotlin")
raiseIllegalArgument("fromKotlin")
}
c.eval(
"""
@ -2113,4 +2139,149 @@ class ScriptTest {
""".trimIndent()
)
}
@Test
fun testNull1() = runTest {
eval(
"""
var s = null
assertThrows { s.length }
assertThrows { s.size() }
assertEquals( null, s?.size() )
assertEquals( null, s?.length )
assertEquals( null, s?.length ?{ "test" } )
assertEquals( null, s?[1] )
assertEquals( null, s ?{ "test" } )
s = "xx"
assert(s.lower().size == 2)
assert(s.length == 2)
""".trimIndent()
)
}
@Test
fun testSprintf() = runTest {
eval(
"""
assertEquals( "123.45", "%3.2f"(123.451678) )
assertEquals( "123.45: hello", "%3.2f: %s"(123.451678, "hello") )
assertEquals( "123.45: true", "%3.2f: %s"(123.451678, true) )
""".trimIndent()
)
}
@Test
fun testSubstringRangeFailure() = runTest {
eval(
"""
assertEquals("pult", "catapult"[4..])
assertEquals("cat", "catapult"[..2])
""".trimIndent()
)
}
@Test
fun passingOpenEndedRangeAsParam() = runTest {
eval(
"""
fun test(r) {
assert( r is Range )
}
test( 1.. )
""".trimIndent()
)
}
@Test
fun testCollectionStructure() = runTest {
eval(
"""
val list = [1,2,3]
assert( 1 in list )
assert( list.indexOf(3) == 2 )
assert( list.indexOf(5) == -1 )
assert( list is List )
assert( list is Array )
assert( list is Iterable )
assert( list is Collection )
val other = []
list.forEach { other += it }
assertEquals( list, other )
assert( list.isEmpty() == false )
assertEquals( [10, 20, 30], list.map { it * 10 } )
assertEquals( [10, 20, 30], (1..3).map { it * 10 } )
""".trimIndent()
)
}
@Test
fun testSet() = runTest {
eval(
"""
val set = Set(1,2,3)
assert( set.contains(1) )
assert( 1 in set )
assert(set is Set)
assert(set is Iterable)
assert(set is Collection)
println(set)
for( x in set ) println(x)
assert([1,2,3] == set.toList())
set += 4
assert(set.toList() == [1,2,3,4])
assert(set == Set(1,2,3,4))
val s1 = [1, 2].toSet()
assertEquals( Set(1,2), s1 * set)
""".trimIndent()
)
}
@Test
fun testLet() = runTest {
eval("""
class Point(x=0,y=0)
assert( Point() is Object)
Point().let { println(it.x, it.y) }
val x = null
x?.let { println(it.x, it.y) }
""".trimIndent())
}
@Test
fun testApply() = runTest {
eval("""
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
""".trimIndent())
}
@Test
fun testApplyThis() = runTest {
eval("""
class Point(x,y)
// see the difference: apply changes this to newly created Point:
val p = Point(1,2).apply {
this.x++; this.y++
}
assertEquals(p, Point(2,3))
>>> void
""".trimIndent())
}
}

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()) {

View File

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