Compare commits

..
22 Commits
Author SHA1 Message Date
sergeych 0981d8370e v0.5.1-SNAPSHOT: Exceptions Handling! 2025-06-13 01:36:21 +04:00
sergeych c4122e8243 refs #22 more docs 2025-06-13 01:32:40 +04:00
sergeych c3bf536bab refs #22 more docs, fixed EH bug
+shortcut throw "message"
2025-06-13 01:30:26 +04:00
sergeych 5ed8b2f123 refs #22 @35m 2 shorter forms for catch block, fixed catch class instance type checks 2025-06-13 00:12:21 +04:00
sergeych 6c71f0a2e6 fix #18 basic exceptions handling 2025-06-12 19:17:35 +04:00
sergeych 95aae0b231 fix #21 fixed loop break with label behavior 2025-06-12 12:34:03 +04:00
sergeych b3f08b4cac fix #1 for-continue test 2025-06-12 12:23:50 +04:00
sergeych badeea9b28 0.4.0-SNAPSHOT: do-while, val/var returns initial value and accept any statement as initialization, etc. 2025-06-12 11:24:35 +04:00
sergeych a1267c4395 fix #17 do-while loops
+mode string functions
+val/var cold be initialized with statement and return assigned values
2025-06-12 11:22:27 +04:00
sergeych 55fd3ea716 more docs 2025-06-11 14:06:19 +04:00
sergeych 697bafdcee fix for gitea KaTex parser bug 2025-06-11 09:57:35 +03:00
sergeych 28b83f9892 ref #16 better processing of the last-block argument
+sample with national-characters vars
2025-06-11 10:54:06 +04:00
sergeych 194fc8aca6 fix #16 last-block argument now can be combined with defaults and ellipsis 2025-06-11 09:43:42 +04:00
sergeych 382532e0e1 further Arguments optimizations 2025-06-11 09:12:07 +04:00
sergeych c0eba1ecf0 Arguments.Info optimized out 2025-06-11 09:06:01 +04:00
sergeych b253eed032 fixed 1e-6 type Real literal 2025-06-11 08:59:09 +04:00
sergeych d482401b15 ObjInstance / ObjClass optimization no more list of accessible fields! 2025-06-10 23:51:01 +04:00
sergeych 88b355c40d ObjInstanceClass optimization 2025-06-10 23:14:44 +04:00
sergeych 652e1d3af4 fix #15, default toString for a class
+default comparison for a class
!give up struct in favor of class (simpler)
2025-06-10 18:51:04 +04:00
sergeych 2a93e6f7da fix #9 block argument {} call without () 2025-06-10 18:32:38 +04:00
sergeych 20c81dbf2e fix #11 and private visibility for constructor params, fields and methods. 2025-06-10 12:07:07 +04:00
sergeych fffa3d31bb - rewritten/fixed buildconfig
- less noise
- cosmetics in CLI tool
2025-06-10 02:06:03 +04:00
31 changed files with 1602 additions and 349 deletions

No files matched your search

+1
View File
@@ -13,3 +13,4 @@ xcuserdata
*.gpg
.gigaide
/kotlin-js-store/yarn.lock
/test.lyng
+163 -36
View File
@@ -1,6 +1,168 @@
# OO implementation in Lyng
Basic principles:
## Declaration
The class clause looks like
class Point(x,y)
assert( Point is Class )
>>> void
It creates new `Class` with two fields. Here is the more practical sample:
class Point(x,y) {
fun length() { sqrt(x*x + y*y) }
}
val p = Point(3,4)
assert(p is Point)
assertEquals(5, p.length())
// we can access the fields:
assert( p.x == 3 )
assert( p.y == 4 )
// we can assign new values to fields:
p.x = 1
p.y = 1
assertEquals(sqrt(2), p.length())
>>> void
Let's see in details. The statement `class Point(x,y)` creates a class,
with two field, which are mutable and publicly visible.`(x,y)` here
is the [argument list], same as when defining a function. All together creates a class with
a _constructor_ that requires two parameters for fields. So when creating it with
`Point(10, 20)` we say _calling Point constructor_ with these parameters.
Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the
example above.
Class point has a _method_, or a _member function_ `length()` that uses its _fields_ `x` and `y` to
calculate the magnitude. Length is called
### default values in constructor
Constructor arguments are the same as function arguments except visibility
statements discussed later, there could be default values, ellipsis, etc.
class Point(x=0,y=0)
val p = Point()
assert( p.x == 0 && p.y == 0 )
>>> void
## Methods
Functions defined inside a class body are methods, and unless declared
`private` are available to be called from outside the class:
class Point(x,y) {
// public method declaration:
fun length() { sqrt(d2()) }
// private method:
private fun d2() {x*x + y*y}
}
val p = Point(3,4)
// private called from inside public: OK
assertEquals( 5, p.length() )
// but us not available directly
assertThrows { p.d2() }
void
>>> void
## fields and visibility
It is possible to add non-constructor fields:
class Point(x,y) {
fun length() { sqrt(x*x + y*y) }
// set at construction time:
val initialLength = length()
}
val p = Point(3,4)
p.x = 3
p.y = 0
assertEquals( 3, p.length() )
// but initial length could not be changed after as declard val:
assert( p.initialLength == 5 )
>>> void
### Mutable fields
Are declared with var
class Point(x,y) {
var isSpecial = false
}
val p = Point(0,0)
assert( p.isSpecial == false )
p.isSpecial = true
assert( p.isSpecial == true )
>>> void
### Private fields
Private fields are visible only _inside the class instance_:
class SecretCounter {
private var count = 0
fun increment() {
count++
void // hide counter
}
fun isEnough() {
count > 10
}
}
val c = SecretCounter()
assert( c.isEnough() == false )
assert( c.increment() == void )
for( i in 0..10 ) c.increment()
assert( c.isEnough() )
// but the count is not available outside:
assertThrows { c.count }
void
>>> void
It is possible to provide private constructor parameters so they can be
set at construction but not available outside the class:
class SecretCounter(private var count = 0) {
// ...
}
val c = SecretCounter(10)
assertThrows { c.count }
void
>>> void
## Default class methods
In many cases it is necessary to implement custom comparison and `toString`, still
each class is provided with default implementations:
- default toString outputs class name and its _public_ fields.
- default comparison compares compares all public fields in order of appearance.
For example, for our class Point:
class Point(x,y)
assert( Point(1,2) == Point(1,2) )
assert( Point(1,2) !== Point(1,2) )
assert( Point(1,2) != Point(1,3) )
assert( Point(1,2) < Point(2,2) )
assert( Point(1,2) < Point(1,3) )
Point(1,1+1)
>>> Point(x=1,y=2)
# Theory
## Basic principles:
- Everything is an instance of some class
- Every class except Obj has at least one parent
@@ -71,41 +233,6 @@ Regular methods are called on instances as usual `instance.method()`. The method
2. parents method: no guarantee but we enumerate parents in order of appearance;
3. possible extension methods (scoped)
# Defining a new class
The class is a some data record with named fields and fixed order, in fact. To define a class,
just Provide a name and a record like this:
// creating new class with main constructor
// with all fields public and mutable:
struct Point(x,y)
assert( Point is Class )
// now we can create instance
val p1 = Point(3,4)
// is is of the newly created type:
assert( p1 is Point )
// we can read and write its fields:
assert( p1.x == 3 )
assert( p1.y == 4 )
p1.y++
assert( p1.y == 5 )
>>> void
Let's see in details. The statement `struct Point(x,y)` creates a struct, or public class,
with two field, which are mutable and publicly visible, because it is _struct_. `(x,y)` here
is the [argument list], same as when defining a function. All together creates a class with
a _constructor_ that requires two parameters for fields. So when creating it with
`Point(10, 20)` we say _calling Point constructor_ with these parameters.
Such declaration is identical to `class Point(var x,var y)` which does exactly the same.
TBD
[argument list](declaring_arguments.md)
+1 -1
View File
@@ -1,7 +1,7 @@
# Declaring arguments in Lyng
It is a common thing that occurs in many places in Lyng, function declarations,
lambdas, struct and class declarations.
lambdas and class declarations.
## Regular
+2 -19
View File
@@ -39,22 +39,5 @@ We can just put the code into the module code:
## class initialization
class foo {
private static var instanceCounter = 0
val instanceId = instanceCounter
fun close() {
instanceCounter--
}
// instance initializatino could be as this:
if( instanceId > 100 )
throw Exception("Too many instances")
static {
// static, one-per-class initializer could be posted here
instanceCounter = 1
}
}
already done using `ObjInstance` class and instance-bound context with local
context stored in ObjInstance and class constructor statement in ObjClass.
+129
View File
@@ -0,0 +1,129 @@
# Exceptions handling
Exceptions are widely used in modern programming languages, so
they are implemented also in Lyng and in the most complete way.
# Exception classes
Exceptions are throwing instances of some class that inherits `Exception`
across the code. Below is the list of built-in exceptions. Note that
only objects that inherit `Exception` can be thrown. For example:
assert( IllegalArgumentException() is Exception)
>>> void
# Try statement: catching exceptions
There general pattern is:
```
try_statement = try_clause, [catch_clause, ...], [finally_clause]
try_clause = "try", "{", statements, "}"
catch_clause = "catch", [(full_catch | shorter_catch)], "{", statements "}"
full_catch = "(", catch_var, ":", exception_class [, excetpion_class...], ")
shorter_catch = "(", catch_var, ")"
finally_clause = "{", statements, "}"
```
Let's in details.
## Full catch block:
val result = try {
throw IllegalArgumentException("the test")
}
catch( x: IndexOutOfBoundsException, IllegalArgumentException) {
x.message
}
catch(x: Exception) {
"bad"
}
assertEquals(result, "the test")
>>> void
Because our exception is listed in a first catch block, it is processed there.
The full form allow a single catch block to process exceptions with specified classes and bind actual caught object to
the given variable. This is most common and well known form, implemented like this or similar in many other languages,
like Kotlin, Java or C++.
## Shorter form
When you want to catch _all_ the exceptions, you should write `catch(e: Exception)`,
but it is somewhat redundant, so there is simpler variant:
val sample2 = try {
throw IllegalArgumentException("sample 2")
}
catch(x) {
x.message
}
assertEquals( sample2, "sample 2" )
>>> void
But well most likely you will find default variable `it`, like in Kotlin, more than enough
to catch all exceptions to, then you can write it even shorter:
val sample2 = try {
throw IllegalArgumentException("sample 3")
}
catch {
it.message
}
assertEquals( sample2, "sample 3" )
>>> void
You can even check the type of the `it` and create more convenient and sophisticated processing logic. Such approach is
used, for example, in Scala.
# Conveying data with exceptions
The simplest way is to provide exception string and `Exception` class:
try {
throw Exception("this is my exception")
}
catch {
it.message
}
>>> "this is my exception"
This way, in turn, can also be shortened, as it is overly popular:
try {
throw "this is my exception"
}
catch {
it.message
}
>>> "this is my exception"
The trick, though, works with strings only, and always provide `Exception` instances, which is good for debugging but
most often not enough.
# Custom error classes
_this functionality is not yet released_
# Standard exception classes
| class | notes |
|----------------------------|-------------------------------------------------------|
| Exception | root of al throwable objects |
| NullPointerException | |
| AssertionFailedException | |
| ClassCastException | |
| IndexOutOfBoundsException | |
| IllegalArgumentException | |
| IllegalAssignmentException | assigning to val, etc. |
| SymbolNotDefinedException | |
| IterationEndException | attempt to read iterator past end, `hasNext == false` |
| AccessException | attempt to access private members or like |
| UnknownException | unexpected kotlin exception caught |
| | |
+26
View File
@@ -0,0 +1,26 @@
/*
Calculate the limit of Sum( f(n) )
until it reaches asymptotic limit 0.00001% change
return null or found limit
*/
fun findSumLimit(f) {
var sum = 0.0
for( n in 1..1000000 ) {
val s0 = sum
sum += f(n)
if( abs(sum - s0) / abs(sum) < 1.0e-6 ) {
println("limit reached after "+n+" rounds")
break sum
}
n++
}
else {
println("limit not reached")
null
}
}
val limit = findSumLimit { n -> 1.0/n/n }
println("Result: "+limit)
+53
View File
@@ -0,0 +1,53 @@
# Пример расчета суммы ряда
Рассмотрим как можно посчитать предел суммы ряда на lyng. Для наивной реализации
представим что у нас есть функция рассчитывающая n-й член ряда. Тогда мы можем
считать сумму до тех пор, пока отклонение при расчете следующего члена не станет
меньше чем заданная погрешность:
fun сумма_ряда(x, погрешность=0.0001, f) {
var сумма = 0
for( n in 1..100000) {
val следующая_сумма = сумма + f(x, n)
if( n > 1 && abs(следующая_сумма - сумма) < погрешность )
break следующая_сумма
сумма = следующая_сумма
}
else null
}
Для проверки можно посчитать на хорошо известном ряду Меркатора
$$ \ln(1+x)=x-{\dfrac {x^{2}}{2}}+{\dfrac {x^{3}}{3}}-\cdots =\sum \limits _{n=0}^{\infty }{\dfrac {(-1)^{n}x^{n+1}}{(n+1)}}=\sum \limits _{n=1}^{\infty }{\dfrac {(-1)^{n-1}x^{n}}{n}} $$
Который в нашем случае для точки $ x = 1 $ можно записать так:
val x = сумма_ряда(1) { x, n ->
val sign = if( n % 2 == 1 ) 1 else -1
sign * pow(x, n) / n
}
Проверим:
assert( x - ln(2) < 0.001 )
В нашем примере есть изъян - погрешность вычисляется примитивно: `abs(следующая_сумма - сумма) < погрешность`, что совершенно неверно, если значения малы. Значительно более корректно вычислять погрешность, нормированную на диапазон сравниваемых величин:
fun погрешность(x0, x1) {
abs( x1 - x0 ) / (abs( x1 + x0 ) / 2.0)
}
// относительная погрешность одинакова и в разных диапазонах
assertEquals( погрешность(5,6), погрешность(0.005,0.006))
Теперь мы могли бы написать более корректное сравнение для вещественных
// расхождение не больше 1% или сколько укажете:
fun почти_равны(a,b,epsilon=0.01) {
погрешность(a,b) <= epsilon
}
assert( почти_равны( 0.0005, 0.000501 ) )
Во многих случаях вычисление $n+1$ члена значительно проще cчитается от предыдущего члена, в нашем случае это можно было бы записать через итератор, что мы вскоре добавим.
(продолжение следует)
+122 -11
View File
@@ -12,11 +12,10 @@ In other word, the code usually works as expected when you see it. So, nothing u
__Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md)
- Some class references: [List], [Real], [Range], [Iterable], [Iterator]
- Some samples: [combinatorics](samples/combinatorics.lyng.md)
See [samples folder](samples)
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
# Expressions
@@ -555,16 +554,16 @@ Or, more neat:
## while
Regular pre-condition while loop, as expression, loop returns it's last line result:
Regular pre-condition while loop, as expression, loop returns the last expression as everything else:
var count = 0
while( count < 5 ) {
count++
count * 10
}
>>> 50
val result = while( count < 5 ) count++
result
>>> 4
We can break as usual:
Notice it _is 4 because when count became 5, the loop body was not executed!_.
We can break while as usual:
var count = 0
while( count < 5 ) {
@@ -683,6 +682,36 @@ So the returned value, as seen from diagram could be one of:
- value returned from the `else` clause, of the loop was not broken
- value returned from the last execution of loop body, if there was no `break` and no `else` clause.
## do-while loops
There works exactly as while loops but the body is executed prior to checking the while condition:
var i = 0
do { i++ } while( i < 1 )
i
>>> 1
The important feature of the do-while loop is that the condition expression is
evaluated on the body scope, e.g., variables, intruduced in the loop body are
available in the condition:
do {
var continueLoop = false
"OK"
} while( continueLoop )
>>> "OK"
This is sometimes convenient when condition is complex and has to be calculated inside the loop body. Notice the value returning by the loop:
fun readLine() { "done: result" }
val result = do {
val line = readLine()
} while( !line.startsWith("done:") )
result.drop(6)
>>> "result"
Suppose readLine() here reads some stream of lines.
## For loops
@@ -724,6 +753,62 @@ We can use labels too:
assert( search(["hello", "world"], 'z') == null)
>>> void
# Exception handling
Very much like in Kotlin. Try block returns its body block result, if no exception was cauht, or the result from the catch block that caught the exception:
var error = "not caught"
var finallyCaught = false
val result = try {
throw IllegalArgumentException()
"invalid"
}
catch(nd: SymbolNotDefinedException) {
error = "bad catch"
}
catch(x: IllegalArgumentException) {
error = "no error"
"OK"
}
finally {
// finally does not affect returned value
"too bad"
}
assertEquals( "no error", error)
assertEquals( "OK", result)
>>> void
There is shorter form of catch block when you want to catch any exception:
var caught = null
try {
throw IllegalArgumentException()
}
catch(t) { // same as catch(t: Exception), but simpler
caught = t
}
assert( caught is IllegalArgumentException )
>>> void
And even shortest, for the Lying lang tradition, missing var is `it`:
var caught = null
try {
throw IllegalArgumentException()
}
catch { // same as catch(it: Exception), but simpler
caught = it
}
assert( caught is IllegalArgumentException )
>>> void
It is possible to catch several exceptions in the same block too, use
`catch( varName: ExceptionClass1, ExceptionClass2)`, etc, use short form of throw and
many more.
- see [exception handling](exceptions_handling.md) for detailed exceptions tutorial and reference.
# Self-assignments in expression
There are auto-increments and auto-decrements:
@@ -792,7 +877,7 @@ See [Ranges](Range.md) for detailed documentation on it.
// single line comment
var result = null // here we will store the result
>>> void
>>> null
# Integral data types
@@ -838,10 +923,36 @@ Are the same as in string literals with little difference:
## String details
Strings are much like Kotlin ones:
"Hello".length
>>> 5
And supports growing set of kotlin-borrowed operations, see below, for example:
assertEquals("Hell", "Hello".dropLast(1))
>>> void
### String operations
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
Typical set of String functions includes:
| fun/prop | description / notes |
|------------------|------------------------------------------------------------|
| lower() | change case to unicode upper |
| upper() | change case to unicode lower |
| startsWith(prefix) | true if starts with a prefix |
| 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] |
### Literals
String literal could be multiline:
+63 -39
View File
@@ -1,17 +1,40 @@
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.5.1-SNAPSHOT"
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0")
classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:latest_version")
}
}
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.vanniktech.mavenPublish)
kotlin("plugin.serialization") version "2.1.20"
id("com.codingfeline.buildkonfig") version "0.17.1"
}
group = "net.sergeych"
version = "0.2.0-SNAPSHOT"
buildkonfig {
packageName = "net.sergeych.lyng"
// objectName = "YourAwesomeConfig"
// exposeObjectWithName = "YourAwesomePublicConfig"
defaultConfigs {
buildConfigField(STRING, "bcprovider", "codingfeline")
buildConfigField(STRING, "version", version.toString())
}
}
kotlin {
jvm()
@@ -39,6 +62,7 @@ kotlin {
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts::class")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
}
@@ -110,40 +134,40 @@ mavenPublishing {
}
}
}
val projectVersion by project.extra(provider {
// Compute value lazily
(version as String)
})
val generateBuildConfig by tasks.registering {
// Declare outputs safely
val outputDir = layout.buildDirectory.dir("generated/buildConfig/commonMain/kotlin")
outputs.dir(outputDir)
val version = projectVersion.get()
// Inputs: Version is tracked as an input
inputs.property("version", version)
doLast {
val packageName = "net.sergeych.lyng.buildconfig"
val packagePath = packageName.replace('.', '/')
val buildConfigFile = outputDir.get().file("$packagePath/BuildConfig.kt").asFile
buildConfigFile.parentFile?.mkdirs()
buildConfigFile.writeText(
"""
|package $packageName
|
|object BuildConfig {
| const val VERSION = "$version"
|}
""".trimMargin()
)
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
dependsOn(generateBuildConfig)
}
//
//val projectVersion by project.extra(provider {
// // Compute value lazily
// (version as String)
//})
//
//val generateBuildConfig by tasks.registering {
// // Declare outputs safely
// val outputDir = layout.buildDirectory.dir("generated/buildConfig/commonMain/kotlin")
// outputs.dir(outputDir)
//
// val version = projectVersion.get()
//
// // Inputs: Version is tracked as an input
// inputs.property("version", version)
//
// doLast {
// val packageName = "net.sergeych.lyng.buildconfig"
// val packagePath = packageName.replace('.', '/')
// val buildConfigFile = outputDir.get().file("$packagePath/BuildConfig.kt").asFile
//
// buildConfigFile.parentFile?.mkdirs()
// buildConfigFile.writeText(
// """
// |package $packageName
// |
// |object BuildConfig {
// | const val VERSION = "$version"
// |}
// """.trimMargin()
// )
// }
//}
//
//tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
// dependsOn(generateBuildConfig)
//}
@@ -0,0 +1,8 @@
package net.sergeych.lyng
enum class AccessType(val isMutable: Boolean) {
Val(false), Var(true),
@Suppress("unused")
Initialization(false)
}
@@ -11,7 +11,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
val start = params.indexOfFirst { it.defaultValue != null }
if (start >= 0)
for (j in start + 1 until params.size)
if (params[j].defaultValue == null) throw ScriptError(
// last non-default could be lambda:
if (params[j].defaultValue == null && j != params.size - 1) throw ScriptError(
params[j].pos,
"required argument can't follow default one"
)
@@ -22,22 +23,41 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
*/
suspend fun assignToContext(
context: Context,
fromArgs: Arguments = context.args,
defaultAccessType: Compiler.AccessType = Compiler.AccessType.Var
arguments: Arguments = context.args,
defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public
) {
fun assign(a: Item, value: Obj) {
context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value)
context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
a.visibility ?: defaultVisibility)
}
// will be used with last lambda arg fix
val callArgs: List<Obj>
val paramsSize: Int
if( arguments.tailBlockMode ) {
paramsSize = params.size - 1
assign(params.last(), arguments.list.last())
callArgs = arguments.list.dropLast(1)
} else {
paramsSize = params.size
callArgs = arguments.list
}
suspend fun processHead(index: Int): Int {
var i = index
while (i != params.size) {
while (i != paramsSize) {
val a = params[i]
if (a.isEllipsis) break
val value = when {
i < fromArgs.size -> fromArgs[i]
i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(context)
else -> context.raiseArgumentError("too few arguments for the call")
else -> {
println("callArgs: ${callArgs.joinToString()}")
println("tailBlockMode: ${arguments.tailBlockMode}")
context.raiseArgumentError("too few arguments for the call")
}
}
assign(a, value)
i++
@@ -46,14 +66,14 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
}
suspend fun processTail(index: Int): Int {
var i = params.size - 1
var j = fromArgs.size - 1
var i = paramsSize - 1
var j = callArgs.size - 1
while (i > index) {
val a = params[i]
if (a.isEllipsis) break
val value = when {
j >= index -> {
fromArgs[j--]
callArgs[j--]
}
a.defaultValue != null -> a.defaultValue.execute(context)
@@ -68,16 +88,16 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
fun processEllipsis(index: Int, toFromIndex: Int) {
val a = params[index]
val l = if (index > toFromIndex) ObjList()
else ObjList(fromArgs.values.subList(index, toFromIndex + 1).toMutableList())
else ObjList(callArgs.subList(index, toFromIndex + 1).toMutableList())
assign(a, l)
}
val leftIndex = processHead(0)
if (leftIndex < params.size) {
if (leftIndex < paramsSize) {
val end = processTail(leftIndex)
processEllipsis(leftIndex, end)
} else {
if (leftIndex < fromArgs.size)
if (leftIndex < callArgs.size)
context.raiseArgumentError("too many arguments for the call")
}
}
@@ -98,7 +118,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
* So it is a [Statement] that must be executed on __caller context__.
*/
val defaultValue: Statement? = null,
val accessType: Compiler.AccessType? = null,
val visibility: Compiler.Visibility? = null,
val accessType: AccessType? = null,
val visibility: Visibility? = null,
)
}
@@ -2,53 +2,40 @@ package net.sergeych.lyng
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
suspend fun Collection<ParsedArgument>.toArguments(context: Context): Arguments {
val list = mutableListOf<Arguments.Info>()
suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMode: Boolean): Arguments {
val list = mutableListOf<Obj>()
for (x in this) {
val value = x.value.execute(context)
if (x.isSplat) {
when {
value is ObjList -> {
for (subitem in value.list) list.add(Arguments.Info(subitem, x.pos))
for (subitem in value.list) list.add(subitem)
}
value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(context, "toList") as ObjList).list
i.forEach { list.add(Arguments.Info(it, x.pos)) }
i.forEach { list.add(it) }
}
else -> context.raiseClassCastError("expected list of objects for splat argument")
}
} else
list.add(Arguments.Info(value, x.pos))
list.add(value)
}
return Arguments(list)
return Arguments(list,tailBlockMode)
}
data class Arguments(val list: List<Info>) : Iterable<Obj> {
data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : List<Obj> by list {
data class Info(val value: Obj, val pos: Pos)
val size by list::size
operator fun get(index: Int): Obj = list[index].value
val values: List<Obj> by lazy { list.map { it.value } }
fun firstAndOnly(): Obj {
if (list.size != 1) throw IllegalArgumentException("Expected one argument, got ${list.size}")
return list.first().value
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
return list.first()
}
companion object {
val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.map { Info(it, Pos.UNKNOWN) })
fun from(values: Collection<Obj>) = Arguments(values.toList())
}
override fun iterator(): Iterator<Obj> {
return list.map { it.value }.iterator()
}
}
@@ -11,18 +11,20 @@ class Compiler(
class Settings
fun compile(source: Source): Script {
return parseScript(source.startPos,
return parseScript(
source.startPos,
CompilerContext(parseLyng(source))
)
}
private fun parseScript(start: Pos, cc: CompilerContext): Script {
val statements = mutableListOf<Statement>()
// val returnScope = cc.startReturnScope()
while (parseStatement(cc, braceMeansLambda = true)?.also {
statements += it
} != null) {/**/
}
return Script(start, statements)
return Script(start, statements)//returnScope.needCatch)
}
private fun parseStatement(cc: CompilerContext, braceMeansLambda: Boolean = false): Statement? {
@@ -37,6 +39,13 @@ class Compiler(
}
}
Token.Type.PRIVATE, Token.Type.PROTECTED -> {
if (cc.nextIdValue() in setOf("var", "val", "class", "fun", "fn")) {
continue
} else
throw ScriptError(t.pos, "unexpected keyword ${t.value}")
}
Token.Type.PLUS2, Token.Type.MINUS2 -> {
cc.previous()
parseExpression(cc)
@@ -125,9 +134,13 @@ class Compiler(
var isCall = false
val next = cc.next()
if (next.type == Token.Type.ID) {
cc.ifNextIs(Token.Type.LPAREN) {
// could be () call or obj.method {} call
val nt = cc.current()
when (nt.type) {
Token.Type.LPAREN -> {
cc.next()
// instance method call
val args = parseArgs(cc)
val args = parseArgs(cc).first
isCall = true
operand = Accessor { context ->
context.pos = next.pos
@@ -136,11 +149,35 @@ class Compiler(
v.invokeInstanceMethod(
context,
next.value,
args.toArguments(context)
args.toArguments(context,false)
), isMutable = false
)
}
}
Token.Type.LBRACE -> {
// single lambda arg, like assertTrows { ... }
cc.next()
isCall = true
val lambda =
parseExpression(cc) ?: throw ScriptError(t.pos, "expected valid lambda here")
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
)
}
}
else -> {}
}
}
if (!isCall) {
operand = Accessor({ context ->
@@ -162,6 +199,7 @@ class Compiler(
operand = parseFunctionCall(
cc,
left,
false,
)
} ?: run {
// Expression in parentheses
@@ -307,9 +345,10 @@ class Compiler(
}
Token.Type.LBRACE -> {
if (operand != null) {
throw ScriptError(t.pos, "syntax error: lambda expression not allowed here")
} else operand = parseLambdaExpression(cc)
operand = operand?.let { left ->
cc.previous()
parseFunctionCall(cc, left, blockArgument = true)
} ?: parseLambdaExpression(cc)
}
@@ -340,7 +379,7 @@ class Compiler(
val context = closure!!.copy(pos, args)
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = args.values
val l = args.list
val itValue: Obj = when (l.size) {
// no args: it == void
0 -> ObjVoid
@@ -401,16 +440,6 @@ class Compiler(
}
}
enum class AccessType(val isMutable: Boolean) {
Val(false), Var(true),
@Suppress("unused")
Initialization(false)
}
enum class Visibility {
Public, Private, Protected, Internal
}
/**
* Parse argument declaration, used in lambda (and later in fn too)
* @return declaration or null if there is no valid list of arguments
@@ -429,43 +458,17 @@ class Compiler(
}
Token.Type.NEWLINE -> {}
Token.Type.PROTECTED, Token.Type.PRIVATE -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
}
Token.Type.ID -> {
// visibility
val visibility = when (t.value) {
"private" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
Visibility.Private
}
"protected" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
Visibility.Protected
}
"internal" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
Visibility.Internal
}
"public" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
Visibility.Public
}
else -> null
}
val visibility = if (isClassDeclaration)
cc.getVisibility(Visibility.Public)
else Visibility.Public
// val/var?
val access = when (t.value) {
"val" -> {
@@ -549,7 +552,12 @@ class Compiler(
return result
}
private fun parseArgs(cc: CompilerContext): List<ParsedArgument> {
/**
* Parse arguments list during the call and detect last block argument
* _following the parenthesis_ call: `(1,2) { ... }`
*/
private fun parseArgs(cc: CompilerContext): Pair<List<ParsedArgument>, Boolean> {
val args = mutableListOf<ParsedArgument>()
do {
val t = cc.next()
@@ -574,6 +582,7 @@ class Compiler(
// block after?
val pos = cc.savePos()
val end = cc.next()
var lastBlockArgument = false
if (end.type == Token.Type.LBRACE) {
// last argument - callable
val callableAccessor = parseLambdaExpression(cc)
@@ -584,22 +593,34 @@ class Compiler(
},
end.pos
)
lastBlockArgument = true
} else
cc.restorePos(pos)
return args
return args to lastBlockArgument
}
private fun parseFunctionCall(cc: CompilerContext, left: Accessor): Accessor {
private fun parseFunctionCall(cc: CompilerContext, left: Accessor, blockArgument: Boolean): Accessor {
// insofar, functions always return lvalue
val args = parseArgs(cc)
var detectedBlockArgument = blockArgument
val args = if (blockArgument) {
val blockArg = ParsedArgument(
parseExpression(cc)
?: throw ScriptError(cc.currentPos(), "lambda body expected"), cc.currentPos()
)
listOf(blockArg)
} else {
val r = parseArgs(cc)
detectedBlockArgument = r.second
r.first
}
return Accessor { context ->
val v = left.getter(context)
v.value.callOn(
context.copy(
context.pos,
args.toArguments(context)
args.toArguments(context, detectedBlockArgument)
// Arguments(
// args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) }
// ),
@@ -689,21 +710,144 @@ class Compiler(
"val" -> parseVarDeclaration(id.value, false, cc)
"var" -> parseVarDeclaration(id.value, true, cc)
"while" -> parseWhileStatement(cc)
"do" -> parseDoWhileStatement(cc)
"for" -> parseForStatement(cc)
"break" -> parseBreakStatement(id.pos, cc)
"continue" -> parseContinueStatement(id.pos, cc)
"fn", "fun" -> parseFunctionDeclaration(cc)
"if" -> parseIfStatement(cc)
"class" -> parseClassDeclaration(cc, false)
"struct" -> parseClassDeclaration(cc, true)
"try" -> parseTryStatement(cc)
"throw" -> parseThrowStatement(cc)
else -> null
}
private fun parseThrowStatement(cc: CompilerContext): Statement {
val throwStatement = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "throw object expected")
return statement {
var errorObject = throwStatement.execute(this)
if( errorObject is ObjString )
errorObject = ObjException(this, errorObject.value)
if( errorObject is ObjException )
raiseError(errorObject)
else raiseError("this is not an exception object: $errorObject")
}
}
private data class CatchBlockData(
val catchVar: Token,
val classNames: List<String>,
val block: Statement
)
private fun parseTryStatement(cc: CompilerContext): Statement {
val body = parseBlock(cc)
val catches = mutableListOf<CatchBlockData>()
cc.skipTokens(Token.Type.NEWLINE)
var t = cc.next()
while( t.value == "catch" ) {
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
t = cc.next()
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "expected catch variable")
val catchVar = t
val exClassNames = mutableListOf<String>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
// load list of exception classes
do {
t = cc.next()
if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "expected exception class name")
exClassNames += t.value
t = cc.next()
when (t.type) {
Token.Type.COMMA -> {
continue
}
Token.Type.RPAREN -> {
break
}
else -> throw ScriptError(t.pos, "syntax error: expected ',' or ')'")
}
} while (true)
} else {
// no type!
exClassNames += "Exception"
cc.skipTokenOfType(Token.Type.RPAREN)
}
val block = parseBlock(cc)
catches += CatchBlockData(catchVar, exClassNames, block)
cc.skipTokens(Token.Type.NEWLINE)
t = cc.next()
} else {
// no (e: Exception) block: should be shortest variant `catch { ... }`
cc.skipTokenOfType(Token.Type.LBRACE, "expected catch(...) or catch { ... } here")
catches += CatchBlockData(Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"),
parseBlock(cc,true))
t = cc.next()
}
}
if( catches.isEmpty() )
throw ScriptError(cc.currentPos(), "try block must have at least one catch clause")
val finallyClause = if( t.value == "finally" ) {
parseBlock(cc)
} else {
cc.previous()
null
}
return statement {
var result: Obj = ObjVoid
try {
// body is a parsed block, it already has separate context
result = body.execute(this)
}
catch (e: Exception) {
// convert to appropriate exception
val objException = when(e) {
is ExecutionError -> e.errorObject
else -> ObjUnknownException(this, e.message ?: e.toString())
}
// let's see if we should catch it:
var isCaught = false
for( cdata in catches ) {
var exceptionObject: ObjException? = null
for (exceptionClassName in cdata.classNames) {
val exObj = ObjException.getErrorClass(exceptionClassName)
?: raiseSymbolNotFound("error clas not exists: $exceptionClassName")
if( objException.isInstanceOf(exObj) ) {
exceptionObject = objException
break
}
}
if( exceptionObject != null ) {
val catchContext = this.copy(pos = cdata.catchVar.pos)
catchContext.addItem(cdata.catchVar.value, false, objException)
result = cdata.block.execute(catchContext)
isCaught = true
break
}
}
// rethrow if not caught this exception
if( !isCaught )
throw e
}
finally {
// finally clause does not alter result!
finallyClause?.execute(this)
}
result
}
}
private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(cc)
parseArgsDeclaration(cc, isClassDeclaration = true)
else null
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
@@ -743,41 +887,12 @@ class Compiler(
// fields. Note that 'this' is already set by class
constructorArgsDeclaration?.assignToContext(this)
bodyInit?.execute(this)
// export public
for( (name,record) in objects ) {
println("-- $name $record")
when(record.visibility) {
Visibility.Public -> {
thisObj.publicFields += name
thisObj.protectedFields += name
}
Visibility.Protected ->
thisObj.protectedFields += name
else -> {
}
}
}
thisObj
}
// inheritance must alter this code:
val newClass = ObjClass(className).apply {
instanceConstructor = constructorCode
constructorArgsDeclaration?.let { cad ->
// we need accessors for all fields:
for (f in cad.params) {
createField(
f.name,
statement {
val context = (thisObj as ObjInstance).instanceContext
println("called on $thisObj")
context[f.name]?.value ?: raiseError("field is not initialized: ${f.name}")
},
true,
// (f.accessType ?: defaultAccess).isMutable,
)
}
}
}
return statement {
@@ -913,9 +1028,10 @@ class Compiler(
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
}
return lbe.result
}
throw lbe
}
}
} else {
for (i in start.toLong()..<end.toLong()) {
@@ -941,9 +1057,10 @@ class Compiler(
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
}
return lbe.result
}
throw lbe
}
else {
loopVar.value = iterObj.invokeInstanceMethod(forContext, "next")
result = body.execute(forContext)
@@ -952,6 +1069,60 @@ class Compiler(
return elseStatement?.execute(forContext) ?: result
}
@Suppress("UNUSED_VARIABLE")
private fun parseDoWhileStatement(cc: CompilerContext): Statement {
val label = getLabel(cc)?.also { cc.labels += it }
val (breakFound, body) = cc.parseLoop {
parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "Bad while statement: expected statement")
}
label?.also { cc.labels -= it }
cc.skipTokens(Token.Type.NEWLINE)
val t = cc.next()
if( t.type != Token.Type.ID && t.value != "while" )
cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here")
val conditionStart = ensureLparen(cc)
val condition =
parseExpression(cc) ?: throw ScriptError(conditionStart, "Bad while statement: expected expression")
ensureRparen(cc)
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
parseStatement(cc)
} else {
cc.previous()
null
}
return statement(body.pos) {
var wasBroken = false
var result: Obj = ObjVoid
lateinit var doContext: Context
do {
doContext = it.copy().apply { skipContextCreation = true }
try {
result = body.execute(doContext)
}
catch( e: LoopBreakContinueException) {
if( e.label == label || e.label == null ) {
if( e.doContinue ) continue
else {
result = e.result
wasBroken = true
break
}
}
throw e
}
} while( condition.execute(doContext).toBool() )
if( !wasBroken ) elseStatement?.let { s -> result = s.execute(it) }
result
}
}
private fun parseWhileStatement(cc: CompilerContext): Statement {
val label = getLabel(cc)?.also { cc.labels += it }
val start = ensureLparen(cc)
@@ -1102,6 +1273,8 @@ class Compiler(
}
private fun parseFunctionDeclaration(tokens: CompilerContext): Statement {
val visibility = tokens.getVisibility()
var t = tokens.next()
val start = t.pos
val name = if (t.type != Token.Type.ID)
@@ -1138,7 +1311,7 @@ class Compiler(
// we added fn in the context. now we must save closure
// for the function
closure = context
context.addItem(name, false, fnBody)
context.addItem(name, false, fnBody, visibility)
// as the function can be called from anywhere, we have
// saved the proper context in the closure
fnBody
@@ -1155,7 +1328,7 @@ class Compiler(
val block = parseScript(startPos, cc)
return statement(startPos) {
// block run on inner context:
block.execute(it.copy(startPos))
block.execute(if( it.skipContextCreation ) it else it.copy(startPos))
}.also {
val t1 = cc.next()
if (t1.type != Token.Type.RBRACE)
@@ -1164,6 +1337,15 @@ class Compiler(
}
private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: CompilerContext): Statement {
// we are just after var/val, visibility if exists is 2 steps behind
val visibility = when (tokens.atOffset(-2)?.type) {
Token.Type.PRIVATE ->
Visibility.Private
Token.Type.PROTECTED -> Visibility.Protected
else -> Visibility.Public
}
val nameToken = tokens.next()
val start = nameToken.pos
if (nameToken.type != Token.Type.ID)
@@ -1181,7 +1363,7 @@ class Compiler(
}
}
val initialExpression = if (setNull) null else parseExpression(tokens)
val initialExpression = if (setNull) null else parseStatement(tokens, true)
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
return statement(nameToken.pos) { context ->
@@ -1192,8 +1374,8 @@ class Compiler(
// create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, mutable, initValue)
ObjVoid
context.addItem(name, mutable, initValue, visibility)
initValue
}
}
@@ -1336,7 +1518,7 @@ class Compiler(
/**
* The keywords that stop processing of expression term
*/
val stopKeywords = setOf("break", "continue", "return", "if", "when", "do", "while", "for", "class", "struct")
val stopKeywords = setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class", "struct")
}
}
@@ -9,7 +9,7 @@ internal class CompilerContext(val tokens: List<Token>) {
var loopLevel = 0
private set
inline fun <T> parseLoop(f: () -> T): Pair<Boolean,T> {
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
if (++loopLevel == 0) breakFound = false
val result = f()
return Pair(breakFound, result).also {
@@ -98,4 +98,55 @@ internal class CompilerContext(val tokens: List<Token>) {
breakFound = true
}
/**
* Return value of the next token if it is an identifier, null otherwise.
* Does not change position.
*/
fun nextIdValue(): String? {
return if (hasNext()) {
val nt = tokens[currentIndex]
if (nt.type == Token.Type.ID)
nt.value
else null
} else null
}
@Suppress("unused")
fun current(): Token = tokens[currentIndex]
/**
* If the token at current position plus offset (could be negative) exists, returns it, otherwise returns null.
*/
fun atOffset(offset: Int): Token? =
if (currentIndex + offset in tokens.indices) tokens[currentIndex + offset] else null
/**
* Scan backwards as deep as specified looking for visibility token. Does not change position.
*/
fun getVisibility(default: Visibility = Visibility.Public, depths: Int = 2): Visibility {
for( i in -depths .. -1) {
when( atOffset(i)?.type) {
Token.Type.PROTECTED -> return Visibility.Protected
Token.Type.PRIVATE -> return Visibility.Private
else -> {}
}
}
return default
}
// fun expectKeyword(vararg keyword: String): String {
// val t = next()
// if (t.type != Token.Type.ID && t.value !in keyword) {
// throw ScriptError(t.pos, "expected one of ${keyword.joinToString()}")
//
// }
// data class ReturnScope(val needCatch: Boolean = false)
// private val
// fun startReturnScope(): ReturnScope {
// return ReturnScope()
// }
}
@@ -4,7 +4,8 @@ class Context(
val parent: Context?,
val args: Arguments = Arguments.EMPTY,
var pos: Pos = Pos.builtIn,
val thisObj: Obj = ObjVoid
val thisObj: Obj = ObjVoid,
var skipContextCreation: Boolean = false,
) {
constructor(
args: Arguments = Arguments.EMPTY,
@@ -15,33 +16,34 @@ class Context(
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this))
fun raiseNPE(): Nothing = raiseError(ObjNullPointerException(this))
@Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsError(this, message))
raiseError(ObjIndexOutOfBoundsException(this, message))
@Suppress("unused")
fun raiseArgumentError(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentError(this, message))
raiseError(ObjIllegalArgumentException(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
@Suppress("unused")
fun raiseSymbolNotFound(name: String): Nothing = raiseError(ObjSymbolNotDefinedError(this, "symbol is not defined: $name"))
fun raiseSymbolNotFound(name: String): Nothing =
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing {
throw ExecutionError(ObjError(this, message))
throw ExecutionError(ObjException(this, message))
}
fun raiseError(obj: ObjError): Nothing {
fun raiseError(obj: ObjException): Nothing {
throw ExecutionError(obj)
}
inline fun <reified T : Obj> requiredArg(index: Int): T {
if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}")
return (args.list[index].value as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index].value::class.simpleName}")
return (args.list[index] as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
}
inline fun <reified T : Obj> requireOnlyArg(): T {
@@ -73,8 +75,13 @@ class Context(
fun copy() = Context(this, args, pos, thisObj)
fun addItem(name: String, isMutable: Boolean, value: Obj): ObjRecord {
return ObjRecord(value, isMutable).also { objects.put(name, it) }
fun addItem(
name: String,
isMutable: Boolean,
value: Obj,
visibility: Visibility = Visibility.Public
): ObjRecord {
return ObjRecord(value, isMutable, visibility).also { objects[name] = it }
}
fun getOrCreateNamespace(name: String): ObjClass {
@@ -4,7 +4,10 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts
/**
* Record to store object with access rules, e.g. [isMutable] and access level [visibility].
@@ -12,7 +15,7 @@ import net.sergeych.synctools.ProtectedOp
data class ObjRecord(
var value: Obj,
val isMutable: Boolean,
val visibility: Compiler.Visibility = Compiler.Visibility.Public
val visibility: Visibility = Visibility.Public
)
/**
@@ -61,9 +64,9 @@ open class Obj {
fun isInstanceOf(someClass: Obj) = someClass === objClass || objClass.allParentsSet.contains(someClass)
suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj =
invokeInstanceMethod(context, name, Arguments(args.map { Arguments.Info(it, context.pos) }))
invokeInstanceMethod(context, name, Arguments(args.toList()))
inline suspend fun <reified T : Obj> callMethod(
suspend inline fun <reified T : Obj> callMethod(
context: Context,
name: String,
args: Arguments = Arguments.EMPTY
@@ -179,11 +182,10 @@ open class Obj {
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
suspend open fun readField(context: Context, name: String): ObjRecord {
open suspend fun readField(context: Context, name: String): ObjRecord {
// could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
val value = obj.value
return when (value) {
return when (val value = obj.value) {
is Statement -> {
ObjRecord(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable)
}
@@ -218,7 +220,7 @@ open class Obj {
callOn(
context.copy(
context.pos,
args = Arguments(args.map { Arguments.Info(it, context.pos) }),
args = Arguments(args.toList()),
newThisObj = thisObj
)
)
@@ -286,6 +288,8 @@ object ObjNull : Obj() {
override fun equals(other: Any?): Boolean {
return other is ObjNull || other == null
}
override fun toString(): String = "null"
}
interface Numeric {
@@ -325,20 +329,100 @@ data class ObjNamespace(val name: String) : Obj() {
}
}
open class ObjError(val context: Context, val message: String) : Obj() {
override val asStr: ObjString by lazy { ObjString("Error: $message") }
open class ObjException(exceptionClass: ExceptionClass, val context: Context, val message: String) : Obj() {
constructor(name: String,context: Context, message: String) : this(getOrCreateExceptionClass(name), context, message)
constructor(context: Context, message: String) : this(Root, context, message)
fun raise(): Nothing {
throw ExecutionError(this)
}
override val objClass: ObjClass = exceptionClass
override fun toString(): String {
return "ObjException:${objClass.className}:${context.pos}@${hashCode().encodeToHex()}"
}
companion object {
class ExceptionClass(val name: String,vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun callOn(context: Context): Obj {
val message = context.args.getOrNull(0)?.toString() ?: name
return ObjException(this, context, message)
}
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
}
val Root = ExceptionClass("Throwable").apply {
addConst("message", statement {
(thisObj as ObjException).message.toObj()
})
}
private val op = ProtectedOp()
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
@OptIn(ExperimentalContracts::class)
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
return op.withLock {
existingErrorClasses.getOrPut(name) {
ExceptionClass(name, Root)
}
}
}
/**
* Get [ObjClass] for error class by name if exists.
*/
@OptIn(ExperimentalContracts::class)
fun getErrorClass(name: String): ObjClass? = op.withLock {
existingErrorClasses[name]
}
fun addExceptionsToContext(context: Context) {
context.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullPointerException",
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"IllegalArgumentException",
"IllegalAssignmentException",
"SymbolNotDefinedException",
"IterationEndException",
"AccessException",
"UnknownException",
)) {
context.addConst(name, getOrCreateExceptionClass(name))
}
}
}
}
class ObjNullPointerError(context: Context) : ObjError(context, "object is null")
class ObjNullPointerException(context: Context) : ObjException("NullPointerException", context, "object is null")
class ObjAssertionError(context: Context, message: String) : ObjError(context, message)
class ObjClassCastError(context: Context, message: String) : ObjError(context, message)
class ObjIndexOutOfBoundsError(context: Context, message: String = "index out of bounds") : ObjError(context, message)
class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message)
class ObjIllegalAssignmentError(context: Context, message: String = "illegal assignment") : ObjError(context, message)
class ObjSymbolNotDefinedError(context: Context, message: String = "symbol is not defined") : ObjError(context, message)
class ObjIterationFinishedError(context: Context) : ObjError(context, "iteration finished")
class ObjAssertionFailedException(context: Context, message: String) :
ObjException("AssertionFailedException", context, message)
class ObjClassCastException(context: Context, message: String) : ObjException("ClassCastException", context, message)
class ObjIndexOutOfBoundsException(context: Context, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", context, message)
class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") :
ObjException("IllegalArgumentException", context, message)
class ObjIllegalAssignmentException(context: Context, message: String = "illegal assignment") :
ObjException("IllegalAssignmentException", context, message)
class ObjSymbolNotDefinedException(context: Context, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", context, message)
class ObjIterationFinishedException(context: Context) :
ObjException("IterationEndException", context, "iteration finished")
class ObjAccessException(context: Context, message: String = "access not allowed error") :
ObjException("AccessException", context, message)
class ObjUnknownException(context: Context, message: String = "access not allowed error") :
ObjException("UnknownException", context, message)
@@ -2,7 +2,7 @@ package net.sergeych.lyng
val ObjClassType by lazy { ObjClass("Class") }
class ObjClass(
open class ObjClass(
val className: String,
vararg val parents: ObjClass,
) : Obj() {
@@ -23,8 +23,6 @@ class ObjClass(
override suspend fun compareTo(context: Context, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(context: Context): Obj {
println("callOn $this constructing....")
println("on context: $context")
val instance = ObjInstance(this)
instance.instanceContext = context.copy(newThisObj = instance,args = context.args)
if (instanceConstructor != null) {
@@ -41,10 +39,10 @@ class ObjClass(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Compiler.Visibility = Compiler.Visibility.Public,
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
if (name in members || allParentsSet.any { name in it.members } == true)
if (name in members || allParentsSet.any { name in it.members })
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
@@ -124,7 +122,7 @@ class ObjArrayIterator(val array: Obj) : Obj() {
val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) {
self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj())
} else raiseError(ObjIterationFinishedError(this))
} else raiseError(ObjIterationFinishedException(this))
}
addFn("hasNext") {
val self = thisAs<ObjArrayIterator>()
@@ -2,28 +2,54 @@ package net.sergeych.lyng
class ObjInstance(override val objClass: ObjClass) : Obj() {
internal val publicFields = mutableSetOf<String>()
internal val protectedFields = mutableSetOf<String>()
internal lateinit var instanceContext: Context
override suspend fun readField(context: Context, name: String): ObjRecord {
return if( name in publicFields ) instanceContext[name]!!
else super.readField(context, name)
return instanceContext[name]?.let {
if (it.visibility.isPublic)
it
else
context.raiseError(ObjAccessException(context, "can't access non-public field $name"))
}
?: super.readField(context, name)
}
override suspend fun writeField(context: Context, name: String, newValue: Obj) {
if( name in publicFields ) {
val f = instanceContext[name]!!
if( !f.isMutable ) ObjIllegalAssignmentError(context, "can't reassign val $name").raise()
if( f.value.assign(context, newValue) == null)
instanceContext[name]?.let { f ->
if (!f.visibility.isPublic)
ObjIllegalAssignmentException(context, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentException(context, "can't reassign val $name").raise()
if (f.value.assign(context, newValue) == null)
f.value = newValue
}
else super.writeField(context, name, newValue)
} ?: super.writeField(context, name, newValue)
}
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj {
if( name in publicFields ) return instanceContext[name]!!.value.invoke(context, this, args)
return super.invokeInstanceMethod(context, name, args)
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj =
instanceContext[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(context, this, args)
else
context.raiseError(ObjAccessException(context, "can't invoke non-public method $name"))
}
?: super.invokeInstanceMethod(context, name, args)
private val publicFields: Map<String, ObjRecord>
get() = instanceContext.objects.filter { it.value.visibility.isPublic }
override fun toString(): String {
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
return "${objClass.className}($fields)"
}
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjInstance) return -1
if (other.objClass != objClass) return -1
for (f in publicFields) {
val a = f.value.value
val b = other.instanceContext[f.key]!!.value
val d = a.compareTo(context, b)
if (d != 0) return d
}
return 0
}
}
@@ -33,7 +33,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x)
}
else {
context.raiseError(ObjIterationFinishedError(context))
context.raiseError(ObjIterationFinishedException(context))
}
companion object {
@@ -40,6 +40,32 @@ data class ObjString(val value: String) : Obj() {
addConst("length",
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) }
)
addFn("takeLast") {
thisAs<ObjString>().value.takeLast(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("take") {
thisAs<ObjString>().value.take(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("drop") {
thisAs<ObjString>().value.drop(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("dropLast") {
thisAs<ObjString>().value.dropLast(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
addFn("lower") {
thisAs<ObjString>().value.lowercase().let(::ObjString)
}
addFn("upper") {
thisAs<ObjString>().value.uppercase().let(::ObjString)
}
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
}
}
@@ -280,6 +280,8 @@ private class Parser(fromPos: Pos) {
when (text) {
"in" -> Token("in", from, Token.Type.IN)
"is" -> Token("is", from, Token.Type.IS)
"protected" -> Token("protected", from, Token.Type.PROTECTED)
"private" -> Token("private", from, Token.Type.PRIVATE)
else -> Token(text, from, Token.Type.ID)
}
} else
@@ -291,7 +293,19 @@ private class Parser(fromPos: Pos) {
private fun decodeNumber(p1: String, start: Pos): Token =
if (pos.end)
Token(p1, start, Token.Type.INT)
else if (currentChar == '.') {
else if( currentChar == 'e' || currentChar == 'E' ) {
pos.advance()
var negative = false
if (currentChar == '+')
pos.advance()
else if (currentChar == '-') {
negative = true
pos.advance()
}
var p3 = loadChars(digits)
if (negative) p3 = "-$p3"
Token("${p1}e$p3", start, Token.Type.REAL)
} else if (currentChar == '.') {
// could be decimal
pos.advance()
if (currentChar in digitsSet) {
@@ -6,6 +6,7 @@ import kotlin.math.*
class Script(
override val pos: Pos,
private val statements: List<Statement> = emptyList(),
// private val catchReturn: Boolean = false,
) : Statement() {
override suspend fun execute(context: Context): Obj {
@@ -16,10 +17,11 @@ class Script(
return lastResult
}
suspend fun execute() = execute(defaultContext.copy(pos))
suspend fun execute() = execute(defaultContext.copy(pos = pos))
companion object {
val defaultContext: Context = Context().apply {
ObjException.addExceptionsToContext(this)
addFn("println") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value)
@@ -116,14 +118,14 @@ class Script(
addVoidFn("assert") {
val cond = requiredArg<ObjBool>(0)
if( !cond.value == true )
raiseError(ObjAssertionError(this,"Assertion failed"))
raiseError(ObjAssertionFailedException(this,"Assertion failed"))
}
addVoidFn("assertEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionError(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
}
addFn("assertThrows") {
val code = requireOnlyArg<Statement>()
@@ -137,7 +139,7 @@ class Script(
catch (e: ScriptError) {
ObjNull
}
result ?: raiseError(ObjAssertionError(this,"Expected exception but nothing was thrown"))
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
}
addVoidFn("delay") {
@@ -12,4 +12,4 @@ open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=
cause
)
class ExecutionError(val errorObject: ObjError) : ScriptError(errorObject.context.pos, errorObject.message)
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.context.pos, errorObject.message)
@@ -17,6 +17,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label
PRIVATE, PROTECTED,
//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,
@@ -0,0 +1,8 @@
package net.sergeych.lyng
enum class Visibility {
Public, Private, Protected;//, Internal
val isPublic by lazy { this == Public }
@Suppress("unused")
val isProtected by lazy { this == Protected }
}
@@ -1,5 +1,4 @@
package net.sergeych.lyng
import net.sergeych.lyng.buildconfig.BuildConfig
val LyngVersion = BuildConfig.VERSION
val LyngVersion = BuildKonfig.version
+422 -42
View File
@@ -57,6 +57,8 @@ class ScriptTest {
check("17.2e22", Token.Type.REAL, 0, 0, "17.2E+22")
check("17.2e22", Token.Type.REAL, 0, 0, "17.2E22")
check("17.2e-22", Token.Type.REAL, 0, 0, "17.2E-22")
check("17.2e-22", Token.Type.REAL, 0, 0, "17.2E-22")
check("1e-22", Token.Type.REAL, 0, 0, "1E-22")
// hex
check("1", Token.Type.HEX, 0, 0, "0x1")
@@ -172,7 +174,7 @@ class ScriptTest {
fun varsAndConstsTest() = runTest {
val context = Context(pos = Pos.builtIn)
assertEquals(
ObjVoid, context.eval(
ObjInt(3L), context.eval(
"""
val a = 17
var b = 3
@@ -432,6 +434,18 @@ class ScriptTest {
}
@Test
fun whileAssignTest() = runTest {
eval(
"""
var t = 0
val x = while( t < 5 ) { t++ }
// last returned value is 4 - when t was 5 body was not executed
assertEquals( 4, x )
""".trimIndent()
)
}
@Test
fun whileTest() = runTest {
assertEquals(
@@ -490,37 +504,41 @@ class ScriptTest {
fun testAssignArgumentsNoEllipsis() = runTest {
// equal args, no ellipsis, no defaults, ok
val ttEnd = Token.Type.RBRACE
var pa = ArgsDeclaration(listOf(
var pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"),
), ttEnd)
var c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1,2,3).map { it.toObj() }))
), ttEnd
)
var c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
assertEquals( ObjInt(1), c["a"]?.value)
assertEquals( ObjInt(2), c["b"]?.value)
assertEquals( ObjInt(3), c["c"]?.value)
assertEquals(ObjInt(1), c["a"]?.value)
assertEquals(ObjInt(2), c["b"]?.value)
assertEquals(ObjInt(3), c["c"]?.value)
// less args: error
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1,2).map { it.toObj() }))
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2).map { it.toObj() }))
assertFailsWith<ScriptError> {
pa.assignToContext(c)
}
// less args, no ellipsis, defaults, ok
pa = ArgsDeclaration(listOf(
pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c", defaultValue = statement { ObjInt(100) }),
), ttEnd)
), ttEnd
)
pa.assignToContext(c)
assertEquals( ObjInt(1), c["a"]?.value)
assertEquals( ObjInt(2), c["b"]?.value)
assertEquals( ObjInt(100), c["c"]?.value)
assertEquals(ObjInt(1), c["a"]?.value)
assertEquals(ObjInt(2), c["b"]?.value)
assertEquals(ObjInt(100), c["c"]?.value)
// enough args. default value is ignored:
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(10, 2, 5).map { it.toObj() }))
pa.assignToContext(c)
assertEquals( ObjInt(10), c["a"]?.value)
assertEquals( ObjInt(2), c["b"]?.value)
assertEquals( ObjInt(5), c["c"]?.value)
assertEquals(ObjInt(10), c["a"]?.value)
assertEquals(ObjInt(2), c["b"]?.value)
assertEquals(ObjInt(5), c["c"]?.value)
}
@Test
@@ -528,16 +546,18 @@ class ScriptTest {
// equal args,
// less args, no ellipsis, defaults, ok
val ttEnd = Token.Type.RBRACE
val pa = ArgsDeclaration(listOf(
val pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b", isEllipsis = true),
), ttEnd)
var c = Context(args = Arguments.from(listOf(1,2,3).map { it.toObj() }))
), ttEnd
)
var c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assert( a == 1 ); println(b)")
c.eval("assert( b == [2,3] )")
c = Context(args = Arguments.from(listOf(1,2).map { it.toObj() }))
c = Context(args = Arguments.from(listOf(1, 2).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a, 1 ); println(b)")
c.eval("assertEquals( b, [2] )")
@@ -551,24 +571,26 @@ class ScriptTest {
@Test
fun testAssignArgumentsStartEllipsis() = runTest {
val ttEnd = Token.Type.RBRACE
val pa = ArgsDeclaration(listOf(
val pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("a", isEllipsis = true),
ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"),
), ttEnd)
var c = Context(args = Arguments.from(listOf(0,1,2,3).map { it.toObj() }))
), ttEnd
)
var c = Context(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[0,1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(1,2,3).map { it.toObj() }))
c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(2,3).map { it.toObj() }))
c = Context(args = Arguments.from(listOf(2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[] )")
c.eval("assertEquals( b, 2 )")
@@ -583,34 +605,36 @@ class ScriptTest {
@Test
fun testAssignArgumentsmiddleEllipsis() = runTest {
val ttEnd = Token.Type.RBRACE
val pa = ArgsDeclaration(listOf(
val pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("i"),
ArgsDeclaration.Item("a", isEllipsis = true),
ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"),
), ttEnd)
var c = Context(args = Arguments.from(listOf(-1,0,1,2,3).map { it.toObj() }))
), ttEnd
)
var c = Context(args = Arguments.from(listOf(-1, 0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, -1 )")
c.eval("assertEquals( a,[0,1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(0, 1,2,3).map { it.toObj() }))
c = Context(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, 0 )")
c.eval("assertEquals( a,[1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(1,2,3).map { it.toObj() }))
c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, 1)")
c.eval("assertEquals( a,[] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(2,3).map { it.toObj() }))
c = Context(args = Arguments.from(listOf(2, 3).map { it.toObj() }))
assertFailsWith<ExecutionError> {
pa.assignToContext(c)
}
@@ -1385,8 +1409,9 @@ class ScriptTest {
@Test
fun testSimpleStruct() = runTest {
val c = Context()
c.eval("""
struct Point(x,y)
c.eval(
"""
class Point(x,y)
assert( Point::class is Class )
val p = Point(2,3)
assert(p is Point)
@@ -1398,14 +1423,16 @@ class ScriptTest {
val p2 = Point(p.x+1,p.y+1)
p.x = 0
assertEquals( 0, p.x )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testNonAssignalbeFieldInStruct() = runTest {
val c = Context()
c.eval("""
struct Point(x,y)
c.eval(
"""
class Point(x,y)
val p = Point("2",3)
assert(p is Point)
assert( p.x == "2" )
@@ -1413,14 +1440,16 @@ class ScriptTest {
p.x = 0
assertEquals( 0, p.x )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testStructBodyVal() = runTest {
val c = Context()
c.eval("""
struct Point(x,y) {
c.eval(
"""
class Point(x,y) {
val length = sqrt(x*x+y*y)
var foo = "zero"
}
@@ -1433,14 +1462,16 @@ class ScriptTest {
assert( p.foo == "bar")
// length is a val, is shoud not change
assert( p.length == 5 )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testStructBodyFun() = runTest {
val c = Context()
c.eval("""
struct Point(x,y) {
c.eval(
"""
class Point(x,y) {
fun length() {
sqrt(x*x+y*y)
}
@@ -1451,6 +1482,355 @@ class ScriptTest {
p.y = 10
println(p.length())
assertEquals(sqrt(109), p.length())
""".trimIndent()
)
}
@Test
fun testPrivateConstructorParams() = runTest {
val c = Context()
c.eval(
"""
class Point(private var x,y)
val p = Point(1,2)
p.y = 101
assertThrows { p.x = 10 }
"""
)
}
@Test
fun testLBraceMethodCall() = runTest {
eval(
"""
class Foo() {
fun cond(block) {
block()
}
}
val f = Foo()
assertEquals( 1, f.cond { 1 } )
""".trimIndent()
)
}
@Test
fun testLBraceFnCall() = runTest {
eval(
"""
fun cond(block) {
block()
}
assertEquals( 1, cond { 1 } )
""".trimIndent()
)
}
@Test
fun testClasstoString() = runTest {
eval(
"""
class Point {
var x
var y
}
val p = Point()
p.x = 1
p.y = 2
println(p)
""".trimIndent()
)
}
@Test
fun testClassDefaultCompare() = runTest {
eval(
"""
class Point(x,y)
assert( Point(1,2) == Point(1,2) )
assert( Point(1,2) !== Point(1,2) )
assert( Point(1,2) != Point(1,3) )
assert( Point(1,2) < Point(2,2) )
assert( Point(1,2) < Point(1,3) )
""".trimIndent()
)
}
@Test
fun testAccessShortcuts() {
assertTrue(Visibility.Public.isPublic)
assertFalse(Visibility.Private.isPublic)
assertFalse(Visibility.Protected.isPublic)
}
@Test
fun segfault1Test() = runTest {
eval(
"""
fun findSumLimit(f) {
var sum = 0.0
for( n in 1..1000000 ) {
val s0 = sum
sum += f(n)
if( abs(sum - s0) < 0.00001 ) {
println("limit reached after "+n+" rounds")
break sum
}
n++
}
else {
println("limit not reached")
null
}
}
val limit = findSumLimit { n -> 1.0/n/n }
println("Result: "+limit)
"""
)
}
@Test
fun testIntExponentRealForm() = runTest {
assertEquals("1.0E-6", eval("1e-6").toString())
}
@Test
fun testCallLastBlockAfterDetault() = runTest {
eval(
"""
// this means last is lambda:
fun f(e=1, f) {
"e="+e+"f="+f()
}
assertEquals("e=1f=xx", f { "xx" })
""".trimIndent()
)
}
@Test
fun testCallLastBlockWithEllipsis() = runTest {
eval(
"""
// this means last is lambda:
fun f(e..., f) {
"e="+e+"f="+f()
}
assertEquals("e=[]f=xx", f { "xx" })
assertEquals("e=[1, 2]f=xx", f(1,2) { "xx" })
""".trimIndent()
)
}
@Test
fun testMethodCallLastBlockAfterDefault() = runTest {
eval(
"""
class Foo {
// this means last is lambda:
fun f(e=1, f) {
"e="+e+"f="+f()
}
}
val f = Foo()
assertEquals("e=1f=xx", f.f { "xx" })
""".trimIndent()
)
}
@Test
fun testMethodCallLastBlockWithEllipsis() = runTest {
eval(
"""
class Foo {
// this means last is lambda:
fun f(e..., f) {
"e="+e+"f="+f()
}
}
val f = Foo()
assertEquals("e=[]f=xx", f.f { "xx" })
assertEquals("e=[1, 2]f=xx", f.f(1,2) { "xx" })
""".trimIndent()
)
}
@Test
fun nationalCharsTest() = runTest {
eval(
"""
fun сумма_ряда(x, погрешность=0.0001, f) {
var сумма = 0
for( n in 1..100000) {
val следующая_сумма = сумма + f(x, n)
if( n > 1 && abs(следующая_сумма - сумма) < погрешность )
break следующая_сумма
сумма = следующая_сумма
}
else null
}
val x = сумма_ряда(1) { x, n ->
val sign = if( n % 2 == 1 ) 1 else -1
sign * pow(x, n) / n
}
assert( x - ln(2) < 0.001 )
""".trimIndent()
)
}
@Test
fun doWhileSimpleTest() = runTest {
eval(
"""
var sum = 0
var x = do {
val s = sum
sum += 1
} while( s < 10 )
assertEquals(11, x)
""".trimIndent()
)
}
@Test
fun testFailDoWhileSample1() = runTest {
eval(
"""
fun readLine() { "done: result" }
val result = do {
val line = readLine()
} while( !line.startsWith("done:") )
assertEquals("result", result.drop(6))
result
""".trimIndent()
)
}
@Test
fun testForContinue() = runTest {
eval(
"""
var x = 0
for( i in 1..10 ) {
if( i % 2 == 0 ) continue
println(i)
x++
}
assertEquals(5, x)
""".trimIndent()
)
}
@Test
fun testForLabelNreakTest() = runTest {
eval(
"""
var x = 0
var y = 0
FOR0@ for( i in 1..10 ) {
x = i
for( j in 1..20 ) {
y = j
if( i == 3 && j == 5 ) {
println("bb")
break@FOR0
}
}
}
assertEquals( 5, y )
assertEquals( 3, x )
"""
)
}
@Test
fun testThrowExisting()= runTest {
eval("""
val x = IllegalArgumentException("test")
assert( x is Exception )
var t = 0
var finallyCaught = false
try {
t = 1
throw x
t = 2
}
catch( e: SymbolNotDefinedException ) {
t = 101
}
catch( e: IllegalArgumentException ) {
t = 3
}
finally {
finallyCaught = true
}
assertEquals(3, t)
assert(finallyCaught)
""".trimIndent())
}
@Test
fun testCatchShort1()= runTest {
eval("""
val x = IllegalArgumentException("test")
var t = 0
var finallyCaught = false
try {
t = 1
throw x
t = 2
}
catch(e) {
t = 31
}
finally {
finallyCaught = true
}
assertEquals(31, t)
assert(finallyCaught)
""".trimIndent())
}
@Test
fun testCatchShort2()= runTest {
eval("""
val x = IllegalArgumentException("test")
var caught = null
try {
throw x
}
catch {
caught = it
}
assert( caught is IllegalArgumentException )
""".trimIndent())
}
@Test
fun testAccessEHData() = runTest {
eval("""
val x = IllegalArgumentException("test")
val m = try {
throw x
null
}
catch(e) {
println(e)
println(e::class)
println(e.message)
println("--------------")
e.message
}
println(m)
assert( m == "test" )
""".trimIndent())
}
}
+5
View File
@@ -268,4 +268,9 @@ class BookTest {
fun testArgumentBooks() = runTest {
runDocTests("../docs/declaring_arguments.md")
}
@Test
fun testExceptionsBooks() = runTest {
runDocTests("../docs/exceptions_handling.md")
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ val baseContext = Context().apply {
}
}
class LyngCLI(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
override val printHelpOnEmptyArgs = true
@@ -2,8 +2,8 @@ package net.sergeych.lyng_cli
import com.github.ajalt.clikt.core.main
import kotlinx.coroutines.runBlocking
import net.sergeych.LyngCLI
import net.sergeych.Lyng
fun main(args: Array<String>) {
LyngCLI({ runBlocking { it() } }).main(args)
Lyng({ runBlocking { it() } }).main(args)
}
+2 -2
View File
@@ -1,7 +1,7 @@
import com.github.ajalt.clikt.core.main
import kotlinx.coroutines.runBlocking
import net.sergeych.LyngCLI
import net.sergeych.Lyng
fun main(args: Array<String>) {
LyngCLI( { runBlocking { it() } }).main(args)
Lyng( { runBlocking { it() } }).main(args)
}