Compare commits

...
15 Commits
Author SHA1 Message Date
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
27 changed files with 1014 additions and 296 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
}
}
+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читается от предыдущего члена, в нашем случае это можно было бы записать через итератор, что мы вскоре добавим.
(продолжение следует)
+65 -10
View File
@@ -15,8 +15,7 @@ __Other documents to read__ maybe after this one:
- [OOP notes](OOP.md) - [OOP notes](OOP.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
@@ -792,7 +821,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 +867,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:
+62 -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.4.0-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()
@@ -110,40 +133,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,6 +710,7 @@ 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)
@@ -703,7 +725,7 @@ class Compiler(
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 +765,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 {
@@ -952,6 +945,59 @@ 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
}
}
}
} 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 +1148,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 +1186,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 +1203,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 +1212,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 +1238,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 +1249,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 +1393,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,
@@ -28,7 +29,8 @@ class Context(
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg)) fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(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(ObjSymbolNotDefinedError(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing { fun raiseError(message: String): Nothing {
throw ExecutionError(ObjError(this, message)) throw ExecutionError(ObjError(this, message))
@@ -40,8 +42,8 @@ class Context(
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 {
@@ -12,7 +12,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,7 +61,7 @@ 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( inline suspend fun <reified T : Obj> callMethod(
context: Context, context: Context,
@@ -218,7 +218,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 +286,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 {
@@ -342,3 +344,4 @@ class ObjIllegalArgumentError(context: Context, message: String = "illegal argum
class ObjIllegalAssignmentError(context: Context, message: String = "illegal assignment") : 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 ObjSymbolNotDefinedError(context: Context, message: String = "symbol is not defined") : ObjError(context, message)
class ObjIterationFinishedError(context: Context) : ObjError(context, "iteration finished") class ObjIterationFinishedError(context: Context) : ObjError(context, "iteration finished")
class ObjAccessError(context: Context, message: String = "access not allowed error") : ObjError(context, message)
@@ -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)
} }
@@ -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(ObjAccessError(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)
ObjIllegalAssignmentError(context, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentError(context, "can't reassign val $name").raise() if (!f.isMutable) ObjIllegalAssignmentError(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(ObjAccessError(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
} }
} }
@@ -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,7 +17,7 @@ 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 {
@@ -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
+265 -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,16 @@ 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 +502,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 +520,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 +544,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 +569,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 +603,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 +1407,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 +1421,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 +1438,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 +1460,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 +1480,220 @@ 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()) """.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())
}
} }
+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)
} }