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 1581 additions and 328 deletions

No files matched your search

+1
View File
@@ -13,3 +13,4 @@ xcuserdata
*.gpg *.gpg
.gigaide .gigaide
/kotlin-js-store/yarn.lock /kotlin-js-store/yarn.lock
/test.lyng
+163 -36
View File
@@ -1,6 +1,168 @@
# OO implementation in Lyng # 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 - Everything is an instance of some class
- Every class except Obj has at least one parent - 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; 2. parents method: no guarantee but we enumerate parents in order of appearance;
3. possible extension methods (scoped) 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 TBD
[argument list](declaring_arguments.md) [argument list](declaring_arguments.md)
+1 -1
View File
@@ -1,7 +1,7 @@
# Declaring arguments in Lyng # Declaring arguments in Lyng
It is a common thing that occurs in many places in Lyng, function declarations, It is a common thing that occurs in many places in Lyng, function declarations,
lambdas, struct and class declarations. lambdas and class declarations.
## Regular ## Regular
+2 -19
View File
@@ -39,22 +39,5 @@ We can just put the code into the module code:
## class initialization ## class initialization
class foo { already done using `ObjInstance` class and instance-bound context with local
context stored in ObjInstance and class constructor statement in ObjClass.
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
}
}
+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: __Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md) - [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) - [math in Lyng](math.md)
- Some class references: [List], [Real], [Range], [Iterable], [Iterator] - Some class references: [List], [Real], [Range], [Iterable], [Iterator]
- Some samples: [combinatorics](samples/combinatorics.lyng.md) - Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
See [samples folder](samples)
# Expressions # Expressions
@@ -555,16 +554,16 @@ Or, more neat:
## while ## 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 var count = 0
while( count < 5 ) { val result = while( count < 5 ) count++
count++ result
count * 10 >>> 4
}
>>> 50
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 var count = 0
while( count < 5 ) { 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 `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. - 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 ## For loops
@@ -724,6 +753,62 @@ We can use labels too:
assert( search(["hello", "world"], 'z') == null) assert( search(["hello", "world"], 'z') == null)
>>> void >>> 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 # Self-assignments in expression
There are auto-increments and auto-decrements: There are auto-increments and auto-decrements:
@@ -792,7 +877,7 @@ See [Ranges](Range.md) for detailed documentation on it.
// single line comment // single line comment
var result = null // here we will store the result var result = null // here we will store the result
>>> void >>> null
# Integral data types # Integral data types
@@ -838,10 +923,36 @@ Are the same as in string literals with little difference:
## String details ## 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 ### String operations
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. 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 ### Literals
String literal could be multiline: 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 com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget 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 { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary) alias(libs.plugins.androidLibrary)
alias(libs.plugins.vanniktech.mavenPublish) alias(libs.plugins.vanniktech.mavenPublish)
kotlin("plugin.serialization") version "2.1.20" kotlin("plugin.serialization") version "2.1.20"
id("com.codingfeline.buildkonfig") version "0.17.1"
} }
group = "net.sergeych" buildkonfig {
version = "0.2.0-SNAPSHOT" packageName = "net.sergeych.lyng"
// objectName = "YourAwesomeConfig"
// exposeObjectWithName = "YourAwesomePublicConfig"
defaultConfigs {
buildConfigField(STRING, "bcprovider", "codingfeline")
buildConfigField(STRING, "version", version.toString())
}
}
kotlin { kotlin {
jvm() jvm()
@@ -39,6 +62,7 @@ kotlin {
sourceSets { sourceSets {
all { all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts::class")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes") languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi") languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
} }
@@ -110,40 +134,40 @@ mavenPublishing {
} }
} }
} }
//
val projectVersion by project.extra(provider { //val projectVersion by project.extra(provider {
// Compute value lazily // // Compute value lazily
(version as String) // (version as String)
}) //})
//
val generateBuildConfig by tasks.registering { //val generateBuildConfig by tasks.registering {
// Declare outputs safely // // Declare outputs safely
val outputDir = layout.buildDirectory.dir("generated/buildConfig/commonMain/kotlin") // val outputDir = layout.buildDirectory.dir("generated/buildConfig/commonMain/kotlin")
outputs.dir(outputDir) // outputs.dir(outputDir)
//
val version = projectVersion.get() // val version = projectVersion.get()
//
// Inputs: Version is tracked as an input // // Inputs: Version is tracked as an input
inputs.property("version", version) // inputs.property("version", version)
//
doLast { // doLast {
val packageName = "net.sergeych.lyng.buildconfig" // val packageName = "net.sergeych.lyng.buildconfig"
val packagePath = packageName.replace('.', '/') // val packagePath = packageName.replace('.', '/')
val buildConfigFile = outputDir.get().file("$packagePath/BuildConfig.kt").asFile // val buildConfigFile = outputDir.get().file("$packagePath/BuildConfig.kt").asFile
//
buildConfigFile.parentFile?.mkdirs() // buildConfigFile.parentFile?.mkdirs()
buildConfigFile.writeText( // buildConfigFile.writeText(
""" // """
|package $packageName // |package $packageName
| // |
|object BuildConfig { // |object BuildConfig {
| const val VERSION = "$version" // | const val VERSION = "$version"
|} // |}
""".trimMargin() // """.trimMargin()
) // )
} // }
} //}
//
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { //tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
dependsOn(generateBuildConfig) // 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 } val start = params.indexOfFirst { it.defaultValue != null }
if (start >= 0) if (start >= 0)
for (j in start + 1 until params.size) 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, params[j].pos,
"required argument can't follow default one" "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( suspend fun assignToContext(
context: Context, context: Context,
fromArgs: Arguments = context.args, arguments: Arguments = context.args,
defaultAccessType: Compiler.AccessType = Compiler.AccessType.Var defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public
) { ) {
fun assign(a: Item, value: Obj) { 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 { suspend fun processHead(index: Int): Int {
var i = index var i = index
while (i != params.size) { while (i != paramsSize) {
val a = params[i] val a = params[i]
if (a.isEllipsis) break if (a.isEllipsis) break
val value = when { val value = when {
i < fromArgs.size -> fromArgs[i] i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(context) 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) assign(a, value)
i++ i++
@@ -46,14 +66,14 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
} }
suspend fun processTail(index: Int): Int { suspend fun processTail(index: Int): Int {
var i = params.size - 1 var i = paramsSize - 1
var j = fromArgs.size - 1 var j = callArgs.size - 1
while (i > index) { while (i > index) {
val a = params[i] val a = params[i]
if (a.isEllipsis) break if (a.isEllipsis) break
val value = when { val value = when {
j >= index -> { j >= index -> {
fromArgs[j--] callArgs[j--]
} }
a.defaultValue != null -> a.defaultValue.execute(context) 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) { fun processEllipsis(index: Int, toFromIndex: Int) {
val a = params[index] val a = params[index]
val l = if (index > toFromIndex) ObjList() 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) assign(a, l)
} }
val leftIndex = processHead(0) val leftIndex = processHead(0)
if (leftIndex < params.size) { if (leftIndex < paramsSize) {
val end = processTail(leftIndex) val end = processTail(leftIndex)
processEllipsis(leftIndex, end) processEllipsis(leftIndex, end)
} else { } else {
if (leftIndex < fromArgs.size) if (leftIndex < callArgs.size)
context.raiseArgumentError("too many arguments for the call") 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__. * So it is a [Statement] that must be executed on __caller context__.
*/ */
val defaultValue: Statement? = null, val defaultValue: Statement? = null,
val accessType: Compiler.AccessType? = null, val accessType: AccessType? = null,
val visibility: Compiler.Visibility? = 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) data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
suspend fun Collection<ParsedArgument>.toArguments(context: Context): Arguments { suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMode: Boolean): Arguments {
val list = mutableListOf<Arguments.Info>() val list = mutableListOf<Obj>()
for (x in this) { for (x in this) {
val value = x.value.execute(context) val value = x.value.execute(context)
if (x.isSplat) { if (x.isSplat) {
when { when {
value is ObjList -> { 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) -> { value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(context, "toList") as ObjList).list 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 -> context.raiseClassCastError("expected list of objects for splat argument")
} }
} else } 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) fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
val size by list::size return list.first()
operator fun get(index: Int): Obj = list[index].value
val values: List<Obj> by lazy { list.map { it.value } }
fun firstAndOnly(): Obj {
if (list.size != 1) throw IllegalArgumentException("Expected one argument, got ${list.size}")
return list.first().value
} }
companion object { companion object {
val EMPTY = Arguments(emptyList()) 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 class Settings
fun compile(source: Source): Script { fun compile(source: Source): Script {
return parseScript(source.startPos, return parseScript(
source.startPos,
CompilerContext(parseLyng(source)) CompilerContext(parseLyng(source))
) )
} }
private fun parseScript(start: Pos, cc: CompilerContext): Script { private fun parseScript(start: Pos, cc: CompilerContext): Script {
val statements = mutableListOf<Statement>() val statements = mutableListOf<Statement>()
// val returnScope = cc.startReturnScope()
while (parseStatement(cc, braceMeansLambda = true)?.also { while (parseStatement(cc, braceMeansLambda = true)?.also {
statements += it statements += it
} != null) {/**/ } != null) {/**/
} }
return Script(start, statements) return Script(start, statements)//returnScope.needCatch)
} }
private fun parseStatement(cc: CompilerContext, braceMeansLambda: Boolean = false): Statement? { 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 -> { Token.Type.PLUS2, Token.Type.MINUS2 -> {
cc.previous() cc.previous()
parseExpression(cc) parseExpression(cc)
@@ -125,9 +134,13 @@ class Compiler(
var isCall = false var isCall = false
val next = cc.next() val next = cc.next()
if (next.type == Token.Type.ID) { 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 // instance method call
val args = parseArgs(cc) val args = parseArgs(cc).first
isCall = true isCall = true
operand = Accessor { context -> operand = Accessor { context ->
context.pos = next.pos context.pos = next.pos
@@ -136,11 +149,35 @@ class Compiler(
v.invokeInstanceMethod( v.invokeInstanceMethod(
context, context,
next.value, next.value,
args.toArguments(context) args.toArguments(context,false)
), isMutable = 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) { if (!isCall) {
operand = Accessor({ context -> operand = Accessor({ context ->
@@ -162,6 +199,7 @@ class Compiler(
operand = parseFunctionCall( operand = parseFunctionCall(
cc, cc,
left, left,
false,
) )
} ?: run { } ?: run {
// Expression in parentheses // Expression in parentheses
@@ -307,9 +345,10 @@ class Compiler(
} }
Token.Type.LBRACE -> { Token.Type.LBRACE -> {
if (operand != null) { operand = operand?.let { left ->
throw ScriptError(t.pos, "syntax error: lambda expression not allowed here") cc.previous()
} else operand = parseLambdaExpression(cc) parseFunctionCall(cc, left, blockArgument = true)
} ?: parseLambdaExpression(cc)
} }
@@ -340,7 +379,7 @@ class Compiler(
val context = closure!!.copy(pos, args) val context = closure!!.copy(pos, args)
if (argsDeclaration == null) { if (argsDeclaration == null) {
// no args: automatic var 'it' // no args: automatic var 'it'
val l = args.values val l = args.list
val itValue: Obj = when (l.size) { val itValue: Obj = when (l.size) {
// no args: it == void // no args: it == void
0 -> ObjVoid 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) * Parse argument declaration, used in lambda (and later in fn too)
* @return declaration or null if there is no valid list of arguments * @return declaration or null if there is no valid list of arguments
@@ -429,43 +458,17 @@ class Compiler(
} }
Token.Type.NEWLINE -> {} Token.Type.NEWLINE -> {}
Token.Type.PROTECTED, Token.Type.PRIVATE -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
}
Token.Type.ID -> { Token.Type.ID -> {
// visibility // visibility
val visibility = when (t.value) { val visibility = if (isClassDeclaration)
"private" -> { cc.getVisibility(Visibility.Public)
if (!isClassDeclaration) { else Visibility.Public
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/var? // val/var?
val access = when (t.value) { val access = when (t.value) {
"val" -> { "val" -> {
@@ -549,7 +552,12 @@ class Compiler(
return result 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>() val args = mutableListOf<ParsedArgument>()
do { do {
val t = cc.next() val t = cc.next()
@@ -574,6 +582,7 @@ class Compiler(
// block after? // block after?
val pos = cc.savePos() val pos = cc.savePos()
val end = cc.next() val end = cc.next()
var lastBlockArgument = false
if (end.type == Token.Type.LBRACE) { if (end.type == Token.Type.LBRACE) {
// last argument - callable // last argument - callable
val callableAccessor = parseLambdaExpression(cc) val callableAccessor = parseLambdaExpression(cc)
@@ -584,22 +593,34 @@ class Compiler(
}, },
end.pos end.pos
) )
lastBlockArgument = true
} else } else
cc.restorePos(pos) 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 // 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 -> return Accessor { context ->
val v = left.getter(context) val v = left.getter(context)
v.value.callOn( v.value.callOn(
context.copy( context.copy(
context.pos, context.pos,
args.toArguments(context) args.toArguments(context, detectedBlockArgument)
// Arguments( // Arguments(
// args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) } // args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) }
// ), // ),
@@ -689,21 +710,144 @@ class Compiler(
"val" -> parseVarDeclaration(id.value, false, cc) "val" -> parseVarDeclaration(id.value, false, cc)
"var" -> parseVarDeclaration(id.value, true, cc) "var" -> parseVarDeclaration(id.value, true, cc)
"while" -> parseWhileStatement(cc) "while" -> parseWhileStatement(cc)
"do" -> parseDoWhileStatement(cc)
"for" -> parseForStatement(cc) "for" -> parseForStatement(cc)
"break" -> parseBreakStatement(id.pos, cc) "break" -> parseBreakStatement(id.pos, cc)
"continue" -> parseContinueStatement(id.pos, cc) "continue" -> parseContinueStatement(id.pos, cc)
"fn", "fun" -> parseFunctionDeclaration(cc) "fn", "fun" -> parseFunctionDeclaration(cc)
"if" -> parseIfStatement(cc) "if" -> parseIfStatement(cc)
"class" -> parseClassDeclaration(cc, false) "class" -> parseClassDeclaration(cc, false)
"struct" -> parseClassDeclaration(cc, true) "try" -> parseTryStatement(cc)
"throw" -> parseThrowStatement(cc)
else -> null 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 { private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement {
val nameToken = cc.requireToken(Token.Type.ID) val nameToken = cc.requireToken(Token.Type.ID)
val constructorArgsDeclaration = val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(cc) parseArgsDeclaration(cc, isClassDeclaration = true)
else null else null
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN) if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
@@ -743,41 +887,12 @@ class Compiler(
// fields. Note that 'this' is already set by class // fields. Note that 'this' is already set by class
constructorArgsDeclaration?.assignToContext(this) constructorArgsDeclaration?.assignToContext(this)
bodyInit?.execute(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 thisObj
} }
// inheritance must alter this code: // inheritance must alter this code:
val newClass = ObjClass(className).apply { val newClass = ObjClass(className).apply {
instanceConstructor = constructorCode 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 { return statement {
@@ -913,9 +1028,10 @@ class Compiler(
} catch (lbe: LoopBreakContinueException) { } catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) { if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue if (lbe.doContinue) continue
}
return lbe.result return lbe.result
} }
throw lbe
}
} }
} else { } else {
for (i in start.toLong()..<end.toLong()) { for (i in start.toLong()..<end.toLong()) {
@@ -941,9 +1057,10 @@ class Compiler(
} catch (lbe: LoopBreakContinueException) { } catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) { if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue if (lbe.doContinue) continue
}
return lbe.result return lbe.result
} }
throw lbe
}
else { else {
loopVar.value = iterObj.invokeInstanceMethod(forContext, "next") loopVar.value = iterObj.invokeInstanceMethod(forContext, "next")
result = body.execute(forContext) result = body.execute(forContext)
@@ -952,6 +1069,60 @@ class Compiler(
return elseStatement?.execute(forContext) ?: result 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 { private fun parseWhileStatement(cc: CompilerContext): Statement {
val label = getLabel(cc)?.also { cc.labels += it } val label = getLabel(cc)?.also { cc.labels += it }
val start = ensureLparen(cc) val start = ensureLparen(cc)
@@ -1102,6 +1273,8 @@ class Compiler(
} }
private fun parseFunctionDeclaration(tokens: CompilerContext): Statement { private fun parseFunctionDeclaration(tokens: CompilerContext): Statement {
val visibility = tokens.getVisibility()
var t = tokens.next() var t = tokens.next()
val start = t.pos val start = t.pos
val name = if (t.type != Token.Type.ID) 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 // we added fn in the context. now we must save closure
// for the function // for the function
closure = context closure = context
context.addItem(name, false, fnBody) context.addItem(name, false, fnBody, visibility)
// as the function can be called from anywhere, we have // as the function can be called from anywhere, we have
// saved the proper context in the closure // saved the proper context in the closure
fnBody fnBody
@@ -1155,7 +1328,7 @@ class Compiler(
val block = parseScript(startPos, cc) val block = parseScript(startPos, cc)
return statement(startPos) { return statement(startPos) {
// block run on inner context: // block run on inner context:
block.execute(it.copy(startPos)) block.execute(if( it.skipContextCreation ) it else it.copy(startPos))
}.also { }.also {
val t1 = cc.next() val t1 = cc.next()
if (t1.type != Token.Type.RBRACE) if (t1.type != Token.Type.RBRACE)
@@ -1164,6 +1337,15 @@ class Compiler(
} }
private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: CompilerContext): Statement { 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 nameToken = tokens.next()
val start = nameToken.pos val start = nameToken.pos
if (nameToken.type != Token.Type.ID) 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") ?: throw ScriptError(eqToken.pos, "Expected initializer expression")
return statement(nameToken.pos) { context -> return statement(nameToken.pos) { context ->
@@ -1192,8 +1374,8 @@ class Compiler(
// create a separate copy: // create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, mutable, initValue) context.addItem(name, mutable, initValue, visibility)
ObjVoid initValue
} }
} }
@@ -1336,7 +1518,7 @@ class Compiler(
/** /**
* The keywords that stop processing of expression term * 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")
} }
} }
@@ -98,4 +98,55 @@ internal class CompilerContext(val tokens: List<Token>) {
breakFound = true 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 parent: Context?,
val args: Arguments = Arguments.EMPTY, val args: Arguments = Arguments.EMPTY,
var pos: Pos = Pos.builtIn, var pos: Pos = Pos.builtIn,
val thisObj: Obj = ObjVoid val thisObj: Obj = ObjVoid,
var skipContextCreation: Boolean = false,
) { ) {
constructor( constructor(
args: Arguments = Arguments.EMPTY, args: Arguments = Arguments.EMPTY,
@@ -15,33 +16,34 @@ class Context(
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented") fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused") @Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this)) fun raiseNPE(): Nothing = raiseError(ObjNullPointerException(this))
@Suppress("unused") @Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing = fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsError(this, message)) raiseError(ObjIndexOutOfBoundsException(this, message))
@Suppress("unused") @Suppress("unused")
fun raiseArgumentError(message: String = "Illegal argument error"): Nothing = 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") @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 { 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) throw ExecutionError(obj)
} }
inline fun <reified T : Obj> requiredArg(index: Int): T { 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}") if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}")
return (args.list[index].value as? T) return (args.list[index] as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index].value::class.simpleName}") ?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
} }
inline fun <reified T : Obj> requireOnlyArg(): T { inline fun <reified T : Obj> requireOnlyArg(): T {
@@ -73,8 +75,13 @@ class Context(
fun copy() = Context(this, args, pos, thisObj) fun copy() = Context(this, args, pos, thisObj)
fun addItem(name: String, isMutable: Boolean, value: Obj): ObjRecord { fun addItem(
return ObjRecord(value, isMutable).also { objects.put(name, it) } 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 { fun getOrCreateNamespace(name: String): ObjClass {
@@ -4,7 +4,10 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex
import net.sergeych.synctools.ProtectedOp 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]. * 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( data class ObjRecord(
var value: Obj, var value: Obj,
val isMutable: Boolean, 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) fun isInstanceOf(someClass: Obj) = someClass === objClass || objClass.allParentsSet.contains(someClass)
suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj = 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, context: Context,
name: String, name: String,
args: Arguments = Arguments.EMPTY args: Arguments = Arguments.EMPTY
@@ -179,11 +182,10 @@ open class Obj {
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() } 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: // could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name") val obj = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
val value = obj.value return when (val value = obj.value) {
return when (value) {
is Statement -> { is Statement -> {
ObjRecord(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable) ObjRecord(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable)
} }
@@ -218,7 +220,7 @@ open class Obj {
callOn( callOn(
context.copy( context.copy(
context.pos, context.pos,
args = Arguments(args.map { Arguments.Info(it, context.pos) }), args = Arguments(args.toList()),
newThisObj = thisObj newThisObj = thisObj
) )
) )
@@ -286,6 +288,8 @@ object ObjNull : Obj() {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return other is ObjNull || other == null return other is ObjNull || other == null
} }
override fun toString(): String = "null"
} }
interface Numeric { interface Numeric {
@@ -325,20 +329,100 @@ data class ObjNamespace(val name: String) : Obj() {
} }
} }
open class ObjError(val context: Context, val message: String) : Obj() { open class ObjException(exceptionClass: ExceptionClass, val context: Context, val message: String) : Obj() {
override val asStr: ObjString by lazy { ObjString("Error: $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 { fun raise(): Nothing {
throw ExecutionError(this) throw ExecutionError(this)
} }
override val objClass: ObjClass = exceptionClass
override fun toString(): String {
return "ObjException:${objClass.className}:${context.pos}@${hashCode().encodeToHex()}"
} }
class ObjNullPointerError(context: Context) : ObjError(context, "object is null") companion object {
class ObjAssertionError(context: Context, message: String) : ObjError(context, message) class ExceptionClass(val name: String,vararg parents: ObjClass) : ObjClass(name, *parents) {
class ObjClassCastError(context: Context, message: String) : ObjError(context, message) override suspend fun callOn(context: Context): Obj {
class ObjIndexOutOfBoundsError(context: Context, message: String = "index out of bounds") : ObjError(context, message) val message = context.args.getOrNull(0)?.toString() ?: name
class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message) return ObjException(this, 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) override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
class ObjIterationFinishedError(context: Context) : ObjError(context, "iteration finished") }
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 ObjNullPointerException(context: Context) : ObjException("NullPointerException", context, "object is null")
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") } val ObjClassType by lazy { ObjClass("Class") }
class ObjClass( open class ObjClass(
val className: String, val className: String,
vararg val parents: ObjClass, vararg val parents: ObjClass,
) : Obj() { ) : 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 compareTo(context: Context, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(context: Context): Obj { override suspend fun callOn(context: Context): Obj {
println("callOn $this constructing....")
println("on context: $context")
val instance = ObjInstance(this) val instance = ObjInstance(this)
instance.instanceContext = context.copy(newThisObj = instance,args = context.args) instance.instanceContext = context.copy(newThisObj = instance,args = context.args)
if (instanceConstructor != null) { if (instanceConstructor != null) {
@@ -41,10 +39,10 @@ class ObjClass(
name: String, name: String,
initialValue: Obj, initialValue: Obj,
isMutable: Boolean = false, isMutable: Boolean = false,
visibility: Compiler.Visibility = Compiler.Visibility.Public, visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn 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") throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility) members[name] = ObjRecord(initialValue, isMutable, visibility)
} }
@@ -124,7 +122,7 @@ class ObjArrayIterator(val array: Obj) : Obj() {
val self = thisAs<ObjArrayIterator>() val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) { if (self.nextIndex < self.lastIndex) {
self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj()) self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj())
} else raiseError(ObjIterationFinishedError(this)) } else raiseError(ObjIterationFinishedException(this))
} }
addFn("hasNext") { addFn("hasNext") {
val self = thisAs<ObjArrayIterator>() val self = thisAs<ObjArrayIterator>()
@@ -2,28 +2,54 @@ package net.sergeych.lyng
class ObjInstance(override val objClass: ObjClass) : Obj() { class ObjInstance(override val objClass: ObjClass) : Obj() {
internal val publicFields = mutableSetOf<String>()
internal val protectedFields = mutableSetOf<String>()
internal lateinit var instanceContext: Context internal lateinit var instanceContext: Context
override suspend fun readField(context: Context, name: String): ObjRecord { override suspend fun readField(context: Context, name: String): ObjRecord {
return if( name in publicFields ) instanceContext[name]!! return instanceContext[name]?.let {
else super.readField(context, name) 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) { override suspend fun writeField(context: Context, name: String, newValue: Obj) {
if( name in publicFields ) { instanceContext[name]?.let { f ->
val f = instanceContext[name]!! if (!f.visibility.isPublic)
if( !f.isMutable ) ObjIllegalAssignmentError(context, "can't reassign val $name").raise() 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) if (f.value.assign(context, newValue) == null)
f.value = newValue f.value = newValue
} } ?: super.writeField(context, name, newValue)
else super.writeField(context, name, newValue)
} }
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj { override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj =
if( name in publicFields ) return instanceContext[name]!!.value.invoke(context, this, args) instanceContext[name]?.let {
return super.invokeInstanceMethod(context, name, args) 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) if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x)
} }
else { else {
context.raiseError(ObjIterationFinishedError(context)) context.raiseError(ObjIterationFinishedException(context))
} }
companion object { companion object {
@@ -40,6 +40,32 @@ data class ObjString(val value: String) : Obj() {
addConst("length", addConst("length",
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) } 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()) } addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
} }
} }
@@ -280,6 +280,8 @@ private class Parser(fromPos: Pos) {
when (text) { when (text) {
"in" -> Token("in", from, Token.Type.IN) "in" -> Token("in", from, Token.Type.IN)
"is" -> Token("is", from, Token.Type.IS) "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 -> Token(text, from, Token.Type.ID)
} }
} else } else
@@ -291,7 +293,19 @@ private class Parser(fromPos: Pos) {
private fun decodeNumber(p1: String, start: Pos): Token = private fun decodeNumber(p1: String, start: Pos): Token =
if (pos.end) if (pos.end)
Token(p1, start, Token.Type.INT) 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 // could be decimal
pos.advance() pos.advance()
if (currentChar in digitsSet) { if (currentChar in digitsSet) {
@@ -6,6 +6,7 @@ import kotlin.math.*
class Script( class Script(
override val pos: Pos, override val pos: Pos,
private val statements: List<Statement> = emptyList(), private val statements: List<Statement> = emptyList(),
// private val catchReturn: Boolean = false,
) : Statement() { ) : Statement() {
override suspend fun execute(context: Context): Obj { override suspend fun execute(context: Context): Obj {
@@ -16,10 +17,11 @@ class Script(
return lastResult return lastResult
} }
suspend fun execute() = execute(defaultContext.copy(pos)) suspend fun execute() = execute(defaultContext.copy(pos = pos))
companion object { companion object {
val defaultContext: Context = Context().apply { val defaultContext: Context = Context().apply {
ObjException.addExceptionsToContext(this)
addFn("println") { addFn("println") {
for ((i, a) in args.withIndex()) { for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value) if (i > 0) print(' ' + a.asStr.value)
@@ -116,14 +118,14 @@ class Script(
addVoidFn("assert") { addVoidFn("assert") {
val cond = requiredArg<ObjBool>(0) val cond = requiredArg<ObjBool>(0)
if( !cond.value == true ) if( !cond.value == true )
raiseError(ObjAssertionError(this,"Assertion failed")) raiseError(ObjAssertionFailedException(this,"Assertion failed"))
} }
addVoidFn("assertEquals") { addVoidFn("assertEquals") {
val a = requiredArg<Obj>(0) val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1) val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) != 0 ) 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") { addFn("assertThrows") {
val code = requireOnlyArg<Statement>() val code = requireOnlyArg<Statement>()
@@ -137,7 +139,7 @@ class Script(
catch (e: ScriptError) { catch (e: ScriptError) {
ObjNull ObjNull
} }
result ?: raiseError(ObjAssertionError(this,"Expected exception but nothing was thrown")) result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
} }
addVoidFn("delay") { addVoidFn("delay") {
@@ -12,4 +12,4 @@ open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=
cause 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, AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT, SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label 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, ELLIPSIS, DOTDOT, DOTDOTLT,
NEWLINE, NEWLINE,
EOF, 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 package net.sergeych.lyng
import net.sergeych.lyng.buildconfig.BuildConfig
val LyngVersion = BuildConfig.VERSION val LyngVersion = BuildKonfig.version
+402 -22
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.2E+22")
check("17.2e22", Token.Type.REAL, 0, 0, "17.2E22") 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("17.2e-22", Token.Type.REAL, 0, 0, "17.2E-22")
check("1e-22", Token.Type.REAL, 0, 0, "1E-22")
// hex // hex
check("1", Token.Type.HEX, 0, 0, "0x1") check("1", Token.Type.HEX, 0, 0, "0x1")
@@ -172,7 +174,7 @@ class ScriptTest {
fun varsAndConstsTest() = runTest { fun varsAndConstsTest() = runTest {
val context = Context(pos = Pos.builtIn) val context = Context(pos = Pos.builtIn)
assertEquals( assertEquals(
ObjVoid, context.eval( ObjInt(3L), context.eval(
""" """
val a = 17 val a = 17
var b = 3 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 @Test
fun whileTest() = runTest { fun whileTest() = runTest {
assertEquals( assertEquals(
@@ -490,11 +504,13 @@ class ScriptTest {
fun testAssignArgumentsNoEllipsis() = runTest { fun testAssignArgumentsNoEllipsis() = runTest {
// equal args, no ellipsis, no defaults, ok // equal args, no ellipsis, no defaults, ok
val ttEnd = Token.Type.RBRACE val ttEnd = Token.Type.RBRACE
var pa = ArgsDeclaration(listOf( var pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("a"), ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b"), ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"), ArgsDeclaration.Item("c"),
), ttEnd) ), ttEnd
)
var c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2, 3).map { it.toObj() })) var c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
assertEquals(ObjInt(1), c["a"]?.value) assertEquals(ObjInt(1), c["a"]?.value)
@@ -506,11 +522,13 @@ class ScriptTest {
pa.assignToContext(c) pa.assignToContext(c)
} }
// less args, no ellipsis, defaults, ok // less args, no ellipsis, defaults, ok
pa = ArgsDeclaration(listOf( pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("a"), ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b"), ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c", defaultValue = statement { ObjInt(100) }), ArgsDeclaration.Item("c", defaultValue = statement { ObjInt(100) }),
), ttEnd) ), ttEnd
)
pa.assignToContext(c) pa.assignToContext(c)
assertEquals(ObjInt(1), c["a"]?.value) assertEquals(ObjInt(1), c["a"]?.value)
assertEquals(ObjInt(2), c["b"]?.value) assertEquals(ObjInt(2), c["b"]?.value)
@@ -528,10 +546,12 @@ class ScriptTest {
// equal args, // equal args,
// less args, no ellipsis, defaults, ok // less args, no ellipsis, defaults, ok
val ttEnd = Token.Type.RBRACE val ttEnd = Token.Type.RBRACE
val pa = ArgsDeclaration(listOf( val pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("a"), ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b", isEllipsis = true), ArgsDeclaration.Item("b", isEllipsis = true),
), ttEnd) ), ttEnd
)
var c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() })) var c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assert( a == 1 ); println(b)") c.eval("assert( a == 1 ); println(b)")
@@ -551,11 +571,13 @@ class ScriptTest {
@Test @Test
fun testAssignArgumentsStartEllipsis() = runTest { fun testAssignArgumentsStartEllipsis() = runTest {
val ttEnd = Token.Type.RBRACE val ttEnd = Token.Type.RBRACE
val pa = ArgsDeclaration(listOf( val pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("a", isEllipsis = true), ArgsDeclaration.Item("a", isEllipsis = true),
ArgsDeclaration.Item("b"), ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"), ArgsDeclaration.Item("c"),
), ttEnd) ), ttEnd
)
var c = Context(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() })) var c = Context(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( a,[0,1] )") c.eval("assertEquals( a,[0,1] )")
@@ -583,12 +605,14 @@ class ScriptTest {
@Test @Test
fun testAssignArgumentsmiddleEllipsis() = runTest { fun testAssignArgumentsmiddleEllipsis() = runTest {
val ttEnd = Token.Type.RBRACE val ttEnd = Token.Type.RBRACE
val pa = ArgsDeclaration(listOf( val pa = ArgsDeclaration(
listOf(
ArgsDeclaration.Item("i"), ArgsDeclaration.Item("i"),
ArgsDeclaration.Item("a", isEllipsis = true), ArgsDeclaration.Item("a", isEllipsis = true),
ArgsDeclaration.Item("b"), ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"), ArgsDeclaration.Item("c"),
), ttEnd) ), ttEnd
)
var c = Context(args = Arguments.from(listOf(-1, 0, 1, 2, 3).map { it.toObj() })) var c = Context(args = Arguments.from(listOf(-1, 0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( i, -1 )") c.eval("assertEquals( i, -1 )")
@@ -1385,8 +1409,9 @@ class ScriptTest {
@Test @Test
fun testSimpleStruct() = runTest { fun testSimpleStruct() = runTest {
val c = Context() val c = Context()
c.eval(""" c.eval(
struct Point(x,y) """
class Point(x,y)
assert( Point::class is Class ) assert( Point::class is Class )
val p = Point(2,3) val p = Point(2,3)
assert(p is Point) assert(p is Point)
@@ -1398,14 +1423,16 @@ class ScriptTest {
val p2 = Point(p.x+1,p.y+1) val p2 = Point(p.x+1,p.y+1)
p.x = 0 p.x = 0
assertEquals( 0, p.x ) assertEquals( 0, p.x )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testNonAssignalbeFieldInStruct() = runTest { fun testNonAssignalbeFieldInStruct() = runTest {
val c = Context() val c = Context()
c.eval(""" c.eval(
struct Point(x,y) """
class Point(x,y)
val p = Point("2",3) val p = Point("2",3)
assert(p is Point) assert(p is Point)
assert( p.x == "2" ) assert( p.x == "2" )
@@ -1413,14 +1440,16 @@ class ScriptTest {
p.x = 0 p.x = 0
assertEquals( 0, p.x ) assertEquals( 0, p.x )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testStructBodyVal() = runTest { fun testStructBodyVal() = runTest {
val c = Context() val c = Context()
c.eval(""" c.eval(
struct Point(x,y) { """
class Point(x,y) {
val length = sqrt(x*x+y*y) val length = sqrt(x*x+y*y)
var foo = "zero" var foo = "zero"
} }
@@ -1433,14 +1462,16 @@ class ScriptTest {
assert( p.foo == "bar") assert( p.foo == "bar")
// length is a val, is shoud not change // length is a val, is shoud not change
assert( p.length == 5 ) assert( p.length == 5 )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testStructBodyFun() = runTest { fun testStructBodyFun() = runTest {
val c = Context() val c = Context()
c.eval(""" c.eval(
struct Point(x,y) { """
class Point(x,y) {
fun length() { fun length() {
sqrt(x*x+y*y) sqrt(x*x+y*y)
} }
@@ -1451,6 +1482,355 @@ class ScriptTest {
p.y = 10 p.y = 10
println(p.length()) println(p.length())
assertEquals(sqrt(109), 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()) """.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 { fun testArgumentBooks() = runTest {
runDocTests("../docs/declaring_arguments.md") 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 override val printHelpOnEmptyArgs = true
@@ -2,8 +2,8 @@ package net.sergeych.lyng_cli
import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.core.main
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.sergeych.LyngCLI import net.sergeych.Lyng
fun main(args: Array<String>) { 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 com.github.ajalt.clikt.core.main
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.sergeych.LyngCLI import net.sergeych.Lyng
fun main(args: Array<String>) { fun main(args: Array<String>) {
LyngCLI( { runBlocking { it() } }).main(args) Lyng( { runBlocking { it() } }).main(args)
} }