Compare commits

...
10 Commits
39 changed files with 1155 additions and 643 deletions

No files matched your search

+7 -5
View File
@@ -75,14 +75,16 @@ runBlocking {
### Exchanging information
Script is executed over some `Context`. Create instance of the context,
add your specific vars and functions to it, an call over it:
Script is executed over some `Scope`. Create instance,
add your specific vars and functions to it, and call:
```kotlin
import com.sun.source.tree.Scope
import new.sergeych.lyng.*
// simple function
val context = Context().apply {
val scope = Scope().apply {
addFn("addArgs") {
var sum = 0.0
for (a in args) sum += a.toDouble()
@@ -99,9 +101,9 @@ val context = Context().apply {
}
}
// adding constant:
context.eval("addArgs(1,2,3)") // <- 6
scope.eval("addArgs(1,2,3)") // <- 6
```
Note that the context stores all changes in it so you can make calls on a single context to preserve state between calls.
Note that the scope stores all changes in it so you can make calls on a single scope to preserve state between calls.
## Why?
+15
View File
@@ -21,6 +21,20 @@ Having `Iterable` in base classes allows to use it in for loop. Also, each `Iter
## Instance methods
| fun/method | description |
|-----------------|---------------------------------------------------------------------------------|
| toList() | create a list from iterable |
| toSet() | create a set from iterable |
| contains(i) | check that iterable contains `i` |
| `i in iterator` | same as `contains(i)` |
| isEmpty() | check iterable is empty |
| forEach(f) | call f for each element |
| toMap() | create a map from list of key-value pairs (arrays of 2 items or like) |
| map(f) | create a list of values returned by `f` called for each element of the iterable |
| indexOf(i) | return index if the first encounter of i or a negative value if not found |
| associateBy(kf) | create a map where keys are returned by kf that will be called for each element |
fun Iterable.toList(): List
fun Iterable.toSet(): Set
fun Iterable.indexOf(element): Int
@@ -28,6 +42,7 @@ Having `Iterable` in base classes allows to use it in for loop. Also, each `Iter
fun Iterable.isEmpty(element): Bool
fun Iterable.forEach(block: (Any?)->Void ): Void
fun Iterable.map(block: (Any?)->Void ): List
fun Iterable.associateBy( keyMaker: (Any?)->Any): Map
## Abstract methods
+34
View File
@@ -160,6 +160,40 @@ For example, for our class Point:
Point(1,1+1)
>>> Point(x=1,y=2)
# Extending classes
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method
that would test if some object of unknown type contains something that can be interpreted
as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type:
fun Object.isInteger() {
when(this) {
// already Int?
is Int -> true
// real, but with no declimal part?
is Real -> toInt() == this
// string with int or real reuusig code above
is String -> toReal().isInteger()
// otherwise, no:
else -> false
}
}
// Let's test:
assert( 12.isInteger() == true )
assert( 12.1.isInteger() == false )
assert( "5".isInteger() )
assert( ! "5.2".isInteger() )
>>> void
__Important note__ as for version 0.6.9, extensions are in __global scope__. It means, that once applied to a global type (Int in our sample), they will be available for _all_ contexts, even new created,
as they are modifying the type, not the context.
Beware of it. We might need to reconsider it later.
# Theory
## Basic principles:
+2 -2
View File
@@ -16,9 +16,9 @@ you can use it's class to ensure type:
## Member functions
| name | meaning | type |
|-----------------|------------------------------------|------|
|-----------------|-------------------------------------------------------------|------|
| `.roundToInt()` | round to nearest int like round(x) | Int |
| | | |
| `.toInt()` | convert integer part of real to `Int` dropping decimal part | Int |
| | | |
| | | |
| | | |
+3
View File
@@ -1214,6 +1214,9 @@ Typical set of String functions includes:
| [Range] | substring at range |
| s1 + s2 | concatenation |
| s1 += s2 | self-modifying concatenation |
| toReal() | attempts to parse string as a Real value |
| toInt() | parse string to Int value |
| | |
+5 -5
View File
@@ -31,7 +31,7 @@ data class CommandResult(
val error: String
)
val baseContext = Context().apply {
val baseScope = Scope().apply {
addFn("exit") {
exit(requireOnlyArg<ObjInt>().toInt())
ObjVoid
@@ -74,7 +74,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
val objargs = mutableListOf<String>()
script?.let { objargs += it }
objargs += args
baseContext.addConst(
baseScope.addConst(
"ARGV", ObjList(
objargs.map { ObjString(it) }.toMutableList()
)
@@ -82,7 +82,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
launcher {
// there is no script name, it is a first argument instead:
processErrors {
baseContext.eval(execute!!)
baseScope.eval(execute!!)
}
}
}
@@ -98,7 +98,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
)
echoFormattedHelp()
} else {
baseContext.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
launcher { executeFile(script!!) }
}
}
@@ -118,7 +118,7 @@ suspend fun executeFile(fileName: String) {
text = text.substring(pos + 1)
}
processErrors {
Compiler().compile(Source(fileName, text)).execute(baseContext)
Compiler.compile(Source(fileName, text)).execute(baseScope)
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "0.6.7-SNAPSHOT"
version = "0.7.0-SNAPSHOT"
buildscript {
repositories {
@@ -0,0 +1,17 @@
package net.sergeych.lyng
/**
* Special version of the [Scope] used to `apply` new this object to
* _parent context property.
*
* @param _parent context to apply to
* @param args arguments for the new context
* @param appliedScope the new context to apply, it will have lower priority except for `this` which
* will be reset by appliedContext's `this`.
*/
class AppliedScope(_parent: Scope, args: Arguments, val appliedScope: Scope)
: Scope(_parent, args, appliedScope.pos, appliedScope.thisObj) {
override fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else super.get(name) ?: appliedScope[name]
}
@@ -22,13 +22,13 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
* parse args and create local vars in a given context
*/
suspend fun assignToContext(
context: Context,
arguments: Arguments = context.args,
scope: Scope,
arguments: Arguments = scope.args,
defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public
) {
fun assign(a: Item, value: Obj) {
context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
a.visibility ?: defaultVisibility)
}
@@ -52,11 +52,11 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
if (a.isEllipsis) break
val value = when {
i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(context)
a.defaultValue != null -> a.defaultValue.execute(scope)
else -> {
println("callArgs: ${callArgs.joinToString()}")
println("tailBlockMode: ${arguments.tailBlockMode}")
context.raiseIllegalArgument("too few arguments for the call")
scope.raiseIllegalArgument("too few arguments for the call")
}
}
assign(a, value)
@@ -76,8 +76,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
callArgs[j--]
}
a.defaultValue != null -> a.defaultValue.execute(context)
else -> context.raiseIllegalArgument("too few arguments for the call")
a.defaultValue != null -> a.defaultValue.execute(scope)
else -> scope.raiseIllegalArgument("too few arguments for the call")
}
assign(a, value)
i--
@@ -98,7 +98,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
processEllipsis(leftIndex, end)
} else {
if (leftIndex < callArgs.size)
context.raiseIllegalArgument("too many arguments for the call")
scope.raiseIllegalArgument("too many arguments for the call")
}
}
@@ -110,7 +110,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
*/
data class Item(
val name: String,
val type: TypeDecl = TypeDecl.Obj,
val type: TypeDecl = TypeDecl.TypeAny,
val pos: Pos = Pos.builtIn,
val isEllipsis: Boolean = false,
/**
@@ -2,11 +2,11 @@ package net.sergeych.lyng
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMode: Boolean): Arguments {
suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
val list = mutableListOf<Obj>()
for (x in this) {
val value = x.value.execute(context)
val value = x.value.execute(scope)
if (x.isSplat) {
when {
value is ObjList -> {
@@ -14,11 +14,11 @@ suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMod
}
value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(context, "toList") as ObjList).list
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
i.forEach { list.add(it) }
}
else -> context.raiseClassCastError("expected list of objects for splat argument")
else -> scope.raiseClassCastError("expected list of objects for splat argument")
}
} else
list.add(value)
@@ -38,8 +38,8 @@ data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : L
/**
* Convert to list of kotlin objects, see [Obj.toKotlin].
*/
suspend fun toKotlinList(context: Context): List<Any?> {
return list.map { it.toKotlin(context) }
suspend fun toKotlinList(scope: Scope): List<Any?> {
return list.map { it.toKotlin(scope) }
}
companion object {
File diff suppressed because it is too large. Load diff
@@ -1,13 +1,11 @@
package net.sergeych.lyng
internal class CompilerContext(val tokens: List<Token>) {
class CompilerContext(val tokens: List<Token>) {
val labels = mutableSetOf<String>()
var breakFound = false
private set
var loopLevel = 0
private set
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
if (++loopLevel == 0) breakFound = false
@@ -53,6 +51,20 @@ internal class CompilerContext(val tokens: List<Token>) {
fun currentPos(): Pos = tokens[currentIndex].pos
/**
* If the next token is identifier `name`, skip it and return `true`.
* else leave where it is and return `false`
*/
fun skipId(name: String): Boolean {
current().let { t ->
if( t.type == Token.Type.ID && t.value == name ) {
next()
return true
}
}
return false
}
/**
* Skips next token if its type is `tokenType`, returns `true` if so.
* @param errorMessage message to throw if next token is not `tokenType`
@@ -85,7 +97,7 @@ internal class CompilerContext(val tokens: List<Token>) {
}
fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
val t = next()
return if (t.type == typeId) {
f(t)
@@ -96,7 +108,6 @@ internal class CompilerContext(val tokens: List<Token>) {
}
}
@Suppress("NOTHING_TO_INLINE")
inline fun addBreak() {
breakFound = true
}
@@ -0,0 +1,130 @@
package net.sergeych.lyng
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.mp_tools.globalDefer
/**
* Package manager. Chained manager, too simple. Override [createModuleScope] to return either
* valid [ModuleScope] or call [parent] - or return null.
*/
abstract class Pacman(
val parent: Pacman? = null,
val rootScope: Scope = parent!!.rootScope,
val securityManager: SecurityManager = parent!!.securityManager
) {
private val opScopes = Mutex()
private val cachedScopes = mutableMapOf<String, Scope>()
/**
* Create a new module scope if this pacman can import the module, return null otherwise so
* the manager can decide what to do
*/
abstract suspend fun createModuleScope(name: String): ModuleScope?
suspend fun prepareImport(pos: Pos, name: String, symbols: Map<String, String>?) {
if (!securityManager.canImportModule(name))
throw ImportException(pos, "Module $name is not allowed")
symbols?.keys?.forEach {
if (!securityManager.canImportSymbol(name, it)) throw ImportException(
pos,
"Symbol $name.$it is not allowed"
)
}
// if we can import the module, cache it, or go further
opScopes.withLock {
cachedScopes[name] ?: createModuleScope(name)?.let { cachedScopes[name] = it }
} ?: parent?.prepareImport(pos, name, symbols) ?: throw ImportException(pos, "Module $name is not found")
}
suspend fun performImport(scope: Scope, name: String, symbols: Map<String, String>?) {
val module = opScopes.withLock { cachedScopes[name] }
?: scope.raiseSymbolNotFound("module $name not found")
val symbolsToImport = symbols?.keys?.toMutableSet()
for ((symbol, record) in module.objects) {
if (record.visibility.isPublic) {
println("import $name: $symbol: $record")
val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol]
?.also { symbolsToImport!!.remove(it) }
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached")
} ?: symbol
println("import $name.$symbol as $newName")
if (newName in scope.objects)
scope.raiseError("symbol $newName already exists, redefinition on import is not allowed")
scope.objects[newName] = record
}
}
if (!symbolsToImport.isNullOrEmpty())
scope.raiseSymbolNotFound("symbols $name.{$symbolsToImport} are.were not found")
}
companion object {
val emptyAllowAll = object : Pacman(rootScope = Script.defaultScope, securityManager = SecurityManager.allowAll) {
override suspend fun createModuleScope(name: String): ModuleScope? {
return null
}
}
}
}
/**
* Module scope supports importing and contains the [pacman]; it should be the same
* used in [Compiler];
*/
class ModuleScope(
val pacman: Pacman,
pos: Pos = Pos.builtIn,
val packageName: String
) : Scope(pacman.rootScope, Arguments.EMPTY, pos) {
constructor(pacman: Pacman,source: Source) : this(pacman, source.startPos, source.fileName)
override suspend fun checkImport(pos: Pos, name: String, symbols: Map<String, String>?) {
pacman.prepareImport(pos, name, symbols)
}
/**
* Import symbols into the scope. It _is called_ after the module is imported by [checkImport].
* If [checkImport] was not called, the symbols will not be imported with exception as module is not found.
*/
override suspend fun importInto(scope: Scope, name: String, symbols: Map<String, String>?) {
pacman.performImport(scope, name, symbols)
}
val packageNameObj by lazy { ObjString(packageName).asReadonly}
override fun get(name: String): ObjRecord? {
return if( name == "__PACKAGE__")
packageNameObj
else
super.get(name)
}
}
class InlineSourcesPacman(pacman: Pacman,val sources: List<Source>) : Pacman(pacman) {
val modules: Deferred<Map<String,Deferred<ModuleScope>>> = globalDefer {
val result = mutableMapOf<String, Deferred<ModuleScope>>()
for (source in sources) {
// retrieve the module name and script for deferred execution:
val (name, script) = Compiler.compilePackage(source)
// scope is created used pacman's root scope:
val scope = ModuleScope(this@InlineSourcesPacman, source.startPos, name)
// we execute scripts in parallel which allow cross-imports to some extent:
result[name] = globalDefer { script.execute(scope); scope }
}
result
}
override suspend fun createModuleScope(name: String): ModuleScope? =
modules.await()[name]?.await()
}
@@ -27,13 +27,13 @@ data class ObjRecord(
* operator is implemented in [Compiler.allOps].
*/
data class Accessor(
val getter: suspend (Context) -> ObjRecord,
val setterOrNull: (suspend (Context, Obj) -> Unit)?
val getter: suspend (Scope) -> ObjRecord,
val setterOrNull: (suspend (Scope, Obj) -> Unit)?
) {
/**
* Simplified constructor for immutable stores.
*/
constructor(getter: suspend (Context) -> ObjRecord) : this(getter, null)
constructor(getter: suspend (Scope) -> ObjRecord) : this(getter, null)
/**
* Get the setter or throw.
@@ -70,33 +70,41 @@ open class Obj {
someClass == rootObjectType
suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj =
invokeInstanceMethod(context, name, Arguments(args.toList()))
suspend fun invokeInstanceMethod(scope: Scope, name: String, vararg args: Obj): Obj =
invokeInstanceMethod(scope, name, Arguments(args.toList()))
suspend inline fun <reified T : Obj> callMethod(
context: Context,
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY
): T = invokeInstanceMethod(context, name, args) as T
): T = invokeInstanceMethod(scope, name, args) as T
open suspend fun invokeInstanceMethod(
context: Context,
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY
): Obj =
// note that getInstanceMember traverses the hierarchy
objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args)
objClass.getInstanceMember(scope.pos, name).value.invoke(scope, this, args)
open suspend fun getInstanceMethod(
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY
): Obj =
// note that getInstanceMember traverses the hierarchy
objClass.getInstanceMember(scope.pos, name).value
fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value
// methods that to override
open suspend fun compareTo(context: Context, other: Obj): Int {
context.raiseNotImplemented()
open suspend fun compareTo(scope: Scope, other: Obj): Int {
scope.raiseNotImplemented()
}
open suspend fun contains(context: Context, other: Obj): Boolean {
return invokeInstanceMethod(context, "contains", other).toBool()
open suspend fun contains(scope: Scope, other: Obj): Boolean {
return invokeInstanceMethod(scope, "contains", other).toBool()
}
open val asStr: ObjString by lazy {
@@ -109,90 +117,90 @@ open class Obj {
*/
open val objClass: ObjClass = rootObjectType
open suspend fun plus(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
open suspend fun plus(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun minus(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
open suspend fun minus(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun mul(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
open suspend fun mul(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun div(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
open suspend fun div(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun mod(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
open suspend fun mod(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun logicalNot(context: Context): Obj {
context.raiseNotImplemented()
open suspend fun logicalNot(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun logicalAnd(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
open suspend fun logicalAnd(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun logicalOr(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
open suspend fun logicalOr(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun assign(context: Context, other: Obj): Obj? = null
open suspend fun assign(scope: Scope, other: Obj): Obj? = null
/**
* a += b
* if( the operation is not defined, it returns null and the compiler would try
* to generate it as 'this = this + other', reassigning its variable
*/
open suspend fun plusAssign(context: Context, other: Obj): Obj? = null
open suspend fun plusAssign(scope: Scope, other: Obj): Obj? = null
/**
* `-=` operations, see [plusAssign]
*/
open suspend fun minusAssign(context: Context, other: Obj): Obj? = null
open suspend fun mulAssign(context: Context, other: Obj): Obj? = null
open suspend fun divAssign(context: Context, other: Obj): Obj? = null
open suspend fun modAssign(context: Context, other: Obj): Obj? = null
open suspend fun minusAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun mulAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun divAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun modAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun getAndIncrement(context: Context): Obj {
context.raiseNotImplemented()
open suspend fun getAndIncrement(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun incrementAndGet(context: Context): Obj {
context.raiseNotImplemented()
open suspend fun incrementAndGet(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun decrementAndGet(context: Context): Obj {
context.raiseNotImplemented()
open suspend fun decrementAndGet(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun getAndDecrement(context: Context): Obj {
context.raiseNotImplemented()
open suspend fun getAndDecrement(scope: Scope): Obj {
scope.raiseNotImplemented()
}
/**
* Convert Lyng object to its Kotlin counterpart
*/
open suspend fun toKotlin(context: Context): Any? {
open suspend fun toKotlin(scope: Scope): Any? {
return toString()
}
fun willMutate(context: Context) {
if (isFrozen) context.raiseError("attempt to mutate frozen object")
fun willMutate(scope: Scope) {
if (isFrozen) scope.raiseError("attempt to mutate frozen object")
}
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
open suspend fun readField(context: Context, name: String): ObjRecord {
open suspend fun readField(scope: Scope, name: String): ObjRecord {
// could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
return when (val value = obj.value) {
is Statement -> {
ObjRecord(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable)
ObjRecord(value.execute(scope.copy(scope.pos, newThisObj = this)), obj.isMutable)
}
// could be writable property naturally
// null -> ObjNull.asReadonly
@@ -200,47 +208,49 @@ open class Obj {
}
}
open suspend fun writeField(context: Context, name: String, newValue: Obj) {
willMutate(context)
val field = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
if (field.isMutable) field.value = newValue else context.raiseError("can't assign to read-only field: $name")
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
willMutate(scope)
val field = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
}
open suspend fun getAt(context: Context, index: Obj): Obj {
context.raiseNotImplemented("indexing")
open suspend fun getAt(scope: Scope, index: Obj): Obj {
scope.raiseNotImplemented("indexing")
}
open suspend fun putAt(context: Context, index: Int, newValue: Obj) {
context.raiseNotImplemented("indexing")
suspend fun getAt(scope: Scope, index: Int): Obj = getAt(scope, ObjInt(index.toLong()))
open suspend fun putAt(scope: Scope, index: Int, newValue: Obj) {
scope.raiseNotImplemented("indexing")
}
open suspend fun callOn(context: Context): Obj {
context.raiseNotImplemented()
open suspend fun callOn(scope: Scope): Obj {
scope.raiseNotImplemented()
}
suspend fun invoke(context: Context, thisObj: Obj, args: Arguments): Obj =
callOn(context.copy(context.pos, args = args, newThisObj = thisObj))
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj =
callOn(scope.copy(scope.pos, args = args, newThisObj = thisObj))
suspend fun invoke(context: Context, thisObj: Obj, vararg args: Obj): Obj =
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
callOn(
context.copy(
context.pos,
scope.copy(
scope.pos,
args = Arguments(args.toList()),
newThisObj = thisObj
)
)
suspend fun invoke(context: Context, thisObj: Obj): Obj =
suspend fun invoke(scope: Scope, thisObj: Obj): Obj =
callOn(
context.copy(
context.pos,
scope.copy(
scope.pos,
args = Arguments.EMPTY,
newThisObj = thisObj
)
)
suspend fun invoke(context: Context, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
callOn(context.copy(atPos, args = args, newThisObj = thisObj))
suspend fun invoke(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
callOn(scope.copy(atPos, args = args, newThisObj = thisObj))
val asReadonly: ObjRecord by lazy { ObjRecord(this, false) }
@@ -261,7 +271,7 @@ open class Obj {
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
}
addFn("apply") {
val newContext = ( thisObj as? ObjInstance)?.instanceContext ?: this
val newContext = ( thisObj as? ObjInstance)?.instanceScope ?: this
args.firstAndOnly()
.callOn(newContext)
thisObj
@@ -309,7 +319,7 @@ object ObjVoid : Obj() {
return other is ObjVoid || other is Unit
}
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if (other === this) 0 else -1
}
@@ -319,7 +329,7 @@ object ObjVoid : Obj() {
@Serializable
@SerialName("null")
object ObjNull : Obj() {
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if (other === this) 0 else -1
}
@@ -327,29 +337,29 @@ object ObjNull : Obj() {
return other is ObjNull || other == null
}
override suspend fun readField(context: Context, name: String): ObjRecord {
context.raiseNPE()
override suspend fun readField(scope: Scope, name: String): ObjRecord {
scope.raiseNPE()
}
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj {
context.raiseNPE()
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments): Obj {
scope.raiseNPE()
}
override suspend fun getAt(context: Context, index: Obj): Obj {
context.raiseNPE()
override suspend fun getAt(scope: Scope, index: Obj): Obj {
scope.raiseNPE()
}
override suspend fun putAt(context: Context, index: Int, newValue: Obj) {
context.raiseNPE()
override suspend fun putAt(scope: Scope, index: Int, newValue: Obj) {
scope.raiseNPE()
}
override suspend fun callOn(context: Context): Obj {
context.raiseNPE()
override suspend fun callOn(scope: Scope): Obj {
scope.raiseNPE()
}
override fun toString(): String = "null"
override suspend fun toKotlin(context: Context): Any? {
override suspend fun toKotlin(scope: Scope): Any? {
return null
}
}
@@ -391,14 +401,14 @@ data class ObjNamespace(val name: String) : Obj() {
}
}
open class ObjException(exceptionClass: ExceptionClass, val context: Context, val message: String) : Obj() {
constructor(name: String, context: Context, message: String) : this(
open class ObjException(exceptionClass: ExceptionClass, val scope: Scope, val message: String) : Obj() {
constructor(name: String, scope: Scope, message: String) : this(
getOrCreateExceptionClass(name),
context,
scope,
message
)
constructor(context: Context, message: String) : this(Root, context, message)
constructor(scope: Scope, message: String) : this(Root, scope, message)
fun raise(): Nothing {
throw ExecutionError(this)
@@ -407,15 +417,15 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
override val objClass: ObjClass = exceptionClass
override fun toString(): String {
return "ObjException:${objClass.className}:${context.pos}@${hashCode().encodeToHex()}"
return "ObjException:${objClass.className}:${scope.pos}@${hashCode().encodeToHex()}"
}
companion object {
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun callOn(context: Context): Obj {
val message = context.args.getOrNull(0)?.toString() ?: name
return ObjException(this, context, message)
override suspend fun callOn(scope: Scope): Obj {
val message = scope.args.getOrNull(0)?.toString() ?: name
return ObjException(this, scope, message)
}
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
@@ -448,8 +458,8 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
existingErrorClasses[name]
}
fun addExceptionsToContext(context: Context) {
context.addConst("Exception", Root)
fun addExceptionsToContext(scope: Scope) {
scope.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullReferenceException",
@@ -464,39 +474,42 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
"AccessException",
"UnknownException",
)) {
context.addConst(name, getOrCreateExceptionClass(name))
scope.addConst(name, getOrCreateExceptionClass(name))
}
}
}
}
class ObjNullReferenceException(context: Context) : ObjException("NullReferenceException", context, "object is null")
class ObjNullReferenceException(scope: Scope) : ObjException("NullReferenceException", scope, "object is null")
class ObjAssertionFailedException(context: Context, message: String) :
ObjException("AssertionFailedException", context, message)
class ObjAssertionFailedException(scope: Scope, message: String) :
ObjException("AssertionFailedException", scope, 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 ObjClassCastException(scope: Scope, message: String) : ObjException("ClassCastException", scope, message)
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", scope, message)
class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") :
ObjException("IllegalArgumentException", context, message)
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
ObjException("IllegalArgumentException", scope, message)
@Suppress("unused")
class ObjNoSuchElementException(context: Context, message: String = "no such element") :
ObjException("IllegalArgumentException", context, message)
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
ObjException("IllegalArgumentException", scope, message)
class ObjIllegalAssignmentException(context: Context, message: String = "illegal assignment") :
ObjException("NoSuchElementException", context, message)
class ObjIllegalAssignmentException(scope: Scope, message: String = "illegal assignment") :
ObjException("NoSuchElementException", scope, message)
class ObjSymbolNotDefinedException(context: Context, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", context, message)
class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", scope, message)
class ObjIterationFinishedException(context: Context) :
ObjException("IterationEndException", context, "iteration finished")
class ObjIterationFinishedException(scope: Scope) :
ObjException("IterationEndException", scope, "iteration finished")
class ObjAccessException(context: Context, message: String = "access not allowed error") :
ObjException("AccessException", context, message)
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
ObjException("AccessException", scope, message)
class ObjUnknownException(context: Context, message: String = "access not allowed error") :
ObjException("UnknownException", context, message)
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
ObjException("UnknownException", scope, message)
class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") :
ObjException("IllegalOperationException", scope, message)
@@ -7,9 +7,9 @@ class ObjArrayIterator(val array: Obj) : Obj() {
private var nextIndex = 0
private var lastIndex = 0
suspend fun init(context: Context) {
suspend fun init(scope: Scope) {
nextIndex = 0
lastIndex = array.invokeInstanceMethod(context, "size").toInt()
lastIndex = array.invokeInstanceMethod(scope, "size").toInt()
ObjVoid
}
@@ -3,7 +3,7 @@ package net.sergeych.lyng
data class ObjBool(val value: Boolean) : Obj() {
override val asStr by lazy { ObjString(value.toString()) }
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjBool) return -2
return value.compareTo(other.value)
}
@@ -12,13 +12,13 @@ data class ObjBool(val value: Boolean) : Obj() {
override val objClass: ObjClass = type
override suspend fun logicalNot(context: Context): Obj = ObjBool(!value)
override suspend fun logicalNot(scope: Scope): Obj = ObjBool(!value)
override suspend fun logicalAnd(context: Context, other: Obj): Obj = ObjBool(value && other.toBool())
override suspend fun logicalAnd(scope: Scope, other: Obj): Obj = ObjBool(value && other.toBool())
override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool())
override suspend fun logicalOr(scope: Scope, other: Obj): Obj = ObjBool(value || other.toBool())
override suspend fun toKotlin(context: Context): Any {
override suspend fun toKotlin(scope: Scope): Any {
return value
}
@@ -4,7 +4,7 @@ class ObjChar(val value: Char): Obj() {
override val objClass: ObjClass = type
override suspend fun compareTo(context: Context, other: Obj): Int =
override suspend fun compareTo(scope: Scope, other: Obj): Int =
(other as? ObjChar)?.let { value.compareTo(it.value) } ?: -1
override fun toString(): String = value.toString()
@@ -21,13 +21,13 @@ open class ObjClass(
override fun toString(): String = className
override suspend fun compareTo(context: Context, other: Obj): Int = if (other === this) 0 else -1
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(context: Context): Obj {
override suspend fun callOn(scope: Scope): Obj {
val instance = ObjInstance(this)
instance.instanceContext = context.copy(newThisObj = instance,args = context.args)
instance.instanceScope = scope.copy(newThisObj = instance,args = scope.args)
if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceContext)
instanceConstructor!!.execute(instance.instanceScope)
}
return instance
}
@@ -49,7 +49,7 @@ open class ObjClass(
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Context.() -> Obj) {
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createField(name, statement { code() }, isOpen)
}
@@ -2,52 +2,52 @@ package net.sergeych.lyng
class ObjInstance(override val objClass: ObjClass) : Obj() {
internal lateinit var instanceContext: Context
internal lateinit var instanceScope: Scope
override suspend fun readField(context: Context, name: String): ObjRecord {
return instanceContext[name]?.let {
override suspend fun readField(scope: Scope, name: String): ObjRecord {
return instanceScope[name]?.let {
if (it.visibility.isPublic)
it
else
context.raiseError(ObjAccessException(context, "can't access non-public field $name"))
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name"))
}
?: super.readField(context, name)
?: super.readField(scope, name)
}
override suspend fun writeField(context: Context, name: String, newValue: Obj) {
instanceContext[name]?.let { f ->
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
instanceScope[name]?.let { f ->
if (!f.visibility.isPublic)
ObjIllegalAssignmentException(context, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentException(context, "can't reassign val $name").raise()
if (f.value.assign(context, newValue) == null)
ObjIllegalAssignmentException(scope, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null)
f.value = newValue
} ?: super.writeField(context, name, newValue)
} ?: super.writeField(scope, name, newValue)
}
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj =
instanceContext[name]?.let {
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments): Obj =
instanceScope[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(context, this, args)
it.value.invoke(scope, this, args)
else
context.raiseError(ObjAccessException(context, "can't invoke non-public method $name"))
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
}
?: super.invokeInstanceMethod(context, name, args)
?: super.invokeInstanceMethod(scope, name, args)
private val publicFields: Map<String, ObjRecord>
get() = instanceContext.objects.filter { it.value.visibility.isPublic }
get() = instanceScope.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 {
override suspend fun compareTo(scope: Scope, 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)
val b = other.instanceScope[f.key]!!.value
val d = a.compareTo(scope, b)
if (d != 0) return d
}
return 0
@@ -13,23 +13,23 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
return value.hashCode()
}
override suspend fun getAndIncrement(context: Context): Obj {
override suspend fun getAndIncrement(scope: Scope): Obj {
return ObjInt(value).also { value++ }
}
override suspend fun getAndDecrement(context: Context): Obj {
override suspend fun getAndDecrement(scope: Scope): Obj {
return ObjInt(value).also { value-- }
}
override suspend fun incrementAndGet(context: Context): Obj {
override suspend fun incrementAndGet(scope: Scope): Obj {
return ObjInt(++value)
}
override suspend fun decrementAndGet(context: Context): Obj {
override suspend fun decrementAndGet(scope: Scope): Obj {
return ObjInt(--value)
}
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is Numeric) return -2
return value.compareTo(other.doubleValue)
}
@@ -38,29 +38,29 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
override val objClass: ObjClass = type
override suspend fun plus(context: Context, other: Obj): Obj =
override suspend fun plus(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value + other.value)
else
ObjReal(this.doubleValue + other.toDouble())
override suspend fun minus(context: Context, other: Obj): Obj =
override suspend fun minus(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value - other.value)
else
ObjReal(this.doubleValue - other.toDouble())
override suspend fun mul(context: Context, other: Obj): Obj =
override suspend fun mul(scope: Scope, other: Obj): Obj =
if (other is ObjInt) {
ObjInt(this.value * other.value)
} else ObjReal(this.value * other.toDouble())
override suspend fun div(context: Context, other: Obj): Obj =
override suspend fun div(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value / other.value)
else ObjReal(this.value / other.toDouble())
override suspend fun mod(context: Context, other: Obj): Obj =
override suspend fun mod(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value % other.value)
else ObjReal(this.value.toDouble() % other.toDouble())
@@ -69,14 +69,14 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
* We are by-value type ([byValueCopy] is implemented) so we can do in-place
* assignment
*/
override suspend fun assign(context: Context, other: Obj): Obj? {
override suspend fun assign(scope: Scope, other: Obj): Obj? {
return if (other is ObjInt) {
value = other.value
this
} else null
}
override suspend fun toKotlin(context: Context): Any {
override suspend fun toKotlin(scope: Scope): Any {
return value
}
@@ -48,12 +48,20 @@ val ObjIterable by lazy {
}
addFn("toMap") {
val result = mutableListOf<Obj>()
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
result += it.invokeInstanceMethod(this, "next")
val result = ObjMap()
thisObj.toFlow(this).collect { pair ->
result.map[pair.getAt(this,0)] = pair.getAt(this, 1)
}
ObjMap(ObjMap.listToMap(this, result))
result
}
addFn("associateBy") {
val association = requireOnlyArg<Statement>()
val result = ObjMap()
thisObj.toFlow(this).collect {
result.map[association.call(this, it)] = it
}
result
}
addFn("forEach", isOpen = true) {
@@ -67,12 +75,10 @@ val ObjIterable by lazy {
}
addFn("map", isOpen = true) {
val it = thisObj.invokeInstanceMethod(this, "iterator")
val fn = requiredArg<Statement>(0)
val result = mutableListOf<Obj>()
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
val x = it.invokeInstanceMethod(this, "next")
result += fn.execute(this.copy(Arguments(listOf(x))))
thisObj.toFlow(this).collect {
result += fn.call(this, it)
}
ObjList(result)
}
@@ -2,6 +2,9 @@
package net.sergeych.lyng
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/**
* Iterator wrapper to allow Kotlin collections to be returned from Lyng objects;
* each object is converted to a Lyng object.
@@ -37,3 +40,18 @@ class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
}
}
/**
* Convert Lyng's Iterable to Kotlin's Flow.
*
* As Lyng is totally asynchronous, its iterator can't be trivially converted to Kotlin's synchronous iterator.
* It is, though, trivially convertible to Kotlin's Flow.
*/
fun Obj.toFlow(scope: Scope): Flow<Obj> = flow {
val iterator = invokeInstanceMethod(scope, "iterator")
val hasNext = iterator.getInstanceMethod(scope, "hasNext")
val next = iterator.getInstanceMethod(scope, "next")
while (hasNext.invoke(scope, iterator).toBool()) {
emit(next.invoke(scope, iterator))
}
}
@@ -11,7 +11,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
list.joinToString(separator = ", ") { it.inspect() }
}]"
override suspend fun getAt(context: Context, index: Obj): Obj {
override suspend fun getAt(scope: Scope, index: Obj): Obj {
return when (index) {
is ObjInt -> {
list[index.toInt()]
@@ -47,23 +47,23 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
}
}
else -> context.raiseIllegalArgument("Illegal index object for a list: ${index.inspect()}")
else -> scope.raiseIllegalArgument("Illegal index object for a list: ${index.inspect()}")
}
}
override suspend fun putAt(context: Context, index: Int, newValue: Obj) {
override suspend fun putAt(scope: Scope, index: Int, newValue: Obj) {
val i = index
list[i] = newValue
}
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjList) return -2
val mySize = list.size
val otherSize = other.list.size
val commonSize = minOf(mySize, otherSize)
for (i in 0..<commonSize) {
if (list[i].compareTo(context, other.list[i]) != 0) {
return list[i].compareTo(context, other.list[i])
if (list[i].compareTo(scope, other.list[i]) != 0) {
return list[i].compareTo(scope, other.list[i])
}
}
// equal so far, longer is greater:
@@ -74,44 +74,44 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
}
}
override suspend fun plus(context: Context, other: Obj): Obj =
override suspend fun plus(scope: Scope, other: Obj): Obj =
when {
other is ObjList ->
ObjList((list + other.list).toMutableList())
other.isInstanceOf(ObjIterable) -> {
val l = other.callMethod<ObjList>(context, "toList")
val l = other.callMethod<ObjList>(scope, "toList")
ObjList((list + l.list).toMutableList())
}
else ->
context.raiseError("'+': can't concatenate $this with $other")
scope.raiseError("'+': can't concatenate $this with $other")
}
override suspend fun plusAssign(context: Context, other: Obj): Obj {
override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
// optimization
if (other is ObjList) {
list += other.list
return this
}
if (other.isInstanceOf(ObjIterable)) {
val otherList = other.invokeInstanceMethod(context, "toList") as ObjList
val otherList = other.invokeInstanceMethod(scope, "toList") as ObjList
list += otherList.list
} else
list += other
return this
}
override suspend fun contains(context: Context, other: Obj): Boolean {
override suspend fun contains(scope: Scope, other: Obj): Boolean {
return list.contains(other)
}
override val objClass: ObjClass
get() = type
override suspend fun toKotlin(context: Context): Any {
return list.map { it.toKotlin(context) }
override suspend fun toKotlin(scope: Scope): Any {
return list.map { it.toKotlin(scope) }
}
override fun hashCode(): Int {
@@ -2,17 +2,17 @@ package net.sergeych.lyng
class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjMapEntry) return -1
val c = key.compareTo(context, other.key)
val c = key.compareTo(scope, other.key)
if (c != 0) return c
return value.compareTo(context, other.value)
return value.compareTo(scope, other.value)
}
override suspend fun getAt(context: Context, index: Obj): Obj = when (index.toInt()) {
override suspend fun getAt(scope: Scope, index: Obj): Obj = when (index.toInt()) {
0 -> key
1 -> value
else -> context.raiseIndexOutOfBounds()
else -> scope.raiseIndexOutOfBounds()
}
override fun toString(): String {
@@ -23,8 +23,8 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
companion object {
val type = object : ObjClass("MapEntry", ObjArray) {
override suspend fun callOn(context: Context): Obj {
return ObjMapEntry(context.requiredArg<Obj>(0), context.requiredArg<Obj>(1))
override suspend fun callOn(scope: Scope): Obj {
return ObjMapEntry(scope.requiredArg<Obj>(0), scope.requiredArg<Obj>(1))
}
}.apply {
addFn("key") { thisAs<ObjMapEntry>().key }
@@ -34,18 +34,18 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
}
}
class ObjMap(val map: MutableMap<Obj, Obj>) : Obj() {
class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
override val objClass = type
override suspend fun getAt(context: Context, index: Obj): Obj =
map.getOrElse(index) { context.raiseNoSuchElement() }
override suspend fun getAt(scope: Scope, index: Obj): Obj =
map.getOrElse(index) { scope.raiseNoSuchElement() }
override suspend fun contains(context: Context, other: Obj): Boolean {
override suspend fun contains(scope: Scope, other: Obj): Boolean {
return other in map
}
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other is ObjMap && other.map == map) return 0
return -1
}
@@ -53,30 +53,30 @@ class ObjMap(val map: MutableMap<Obj, Obj>) : Obj() {
companion object {
suspend fun listToMap(context: Context, list: List<Obj>): MutableMap<Obj, Obj> {
suspend fun listToMap(scope: Scope, list: List<Obj>): MutableMap<Obj, Obj> {
val map = mutableMapOf<Obj, Obj>()
if (list.isEmpty()) return map
val first = list.first()
if (first.isInstanceOf(ObjArray)) {
if (first.invokeInstanceMethod(context, "size").toInt() != 2)
context.raiseIllegalArgument(
if (first.invokeInstanceMethod(scope, "size").toInt() != 2)
scope.raiseIllegalArgument(
"list to construct map entry should exactly be 2 element Array like [key,value], got $list"
)
} else context.raiseIllegalArgument("first element of map list be a Collection of 2 elements; got $first")
} else scope.raiseIllegalArgument("first element of map list be a Collection of 2 elements; got $first")
list.forEach {
map[it.getAt(context, ObjInt.Zero)] = it.getAt(context, ObjInt.One)
map[it.getAt(scope, ObjInt.Zero)] = it.getAt(scope, ObjInt.One)
}
return map
}
val type = object : ObjClass("Map", ObjCollection) {
override suspend fun callOn(context: Context): Obj {
return ObjMap(listToMap(context, context.args.list))
override suspend fun callOn(scope: Scope): Obj {
return ObjMap(listToMap(scope, scope.args.list))
}
}.apply {
addFn("getOrNull") {
@@ -15,10 +15,10 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
return result.toString()
}
suspend fun containsRange(context: Context, other: ObjRange): Boolean {
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {
if (start != null) {
// our start is not -∞ so other start should be GTE or is not contained:
if (other.start != null && start.compareTo(context, other.start) > 0) return false
if (other.start != null && start.compareTo(scope, other.start) > 0) return false
}
if (end != null) {
// same with the end: if it is open, it can't be contained in ours:
@@ -26,16 +26,16 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
// both exists, now there could be 4 cases:
return when {
other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0
end.compareTo(scope, other.end) >= 0
!other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) >= 0
end.compareTo(scope, other.end) >= 0
other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) > 0
end.compareTo(scope, other.end) > 0
!other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0
end.compareTo(scope, other.end) >= 0
else -> throw IllegalStateException("unknown comparison")
}
@@ -43,17 +43,17 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
return true
}
override suspend fun contains(context: Context, other: Obj): Boolean {
override suspend fun contains(scope: Scope, other: Obj): Boolean {
if (other is ObjRange)
return containsRange(context, other)
return containsRange(scope, other)
if (start == null && end == null) return true
if (start != null) {
if (start.compareTo(context, other) > 0) return false
if (start.compareTo(scope, other) > 0) return false
}
if (end != null) {
val cmp = end.compareTo(context, other)
val cmp = end.compareTo(scope, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
}
return true
@@ -67,7 +67,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
start is ObjChar && end is ObjChar
}
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return (other as? ObjRange)?.let {
if( start == other.start && end == other.end ) 0 else -1
}
@@ -8,7 +8,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
override val objClass: ObjClass = type
fun Context.init() {
fun Scope.init() {
if (self.start == null || self.end == null)
raiseError("next is only available for finite ranges")
isCharRange = self.isCharRange
@@ -24,7 +24,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
fun hasNext(): Boolean = nextIndex < lastIndex
fun next(context: Context): Obj =
fun next(scope: Scope): Obj =
if (nextIndex < lastIndex) {
val x = if (self.isEndInclusive)
self.start!!.toLong() + nextIndex++
@@ -33,7 +33,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x)
}
else {
context.raiseError(ObjIterationFinishedException(context))
scope.raiseError(ObjIterationFinishedException(scope))
}
companion object {
@@ -14,7 +14,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override fun byValueCopy(): Obj = ObjReal(value)
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is Numeric) return -2
return value.compareTo(other.doubleValue)
}
@@ -25,25 +25,25 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
return value.hashCode()
}
override suspend fun plus(context: Context, other: Obj): Obj =
override suspend fun plus(scope: Scope, other: Obj): Obj =
ObjReal(this.value + other.toDouble())
override suspend fun minus(context: Context, other: Obj): Obj =
override suspend fun minus(scope: Scope, other: Obj): Obj =
ObjReal(this.value - other.toDouble())
override suspend fun mul(context: Context, other: Obj): Obj =
override suspend fun mul(scope: Scope, other: Obj): Obj =
ObjReal(this.value * other.toDouble())
override suspend fun div(context: Context, other: Obj): Obj =
override suspend fun div(scope: Scope, other: Obj): Obj =
ObjReal(this.value / other.toDouble())
override suspend fun mod(context: Context, other: Obj): Obj =
override suspend fun mod(scope: Scope, other: Obj): Obj =
ObjReal(this.value % other.toDouble())
/**
* Returns unboxed Double value
*/
override suspend fun toKotlin(context: Context): Any {
override suspend fun toKotlin(scope: Scope): Any {
return value
}
@@ -64,6 +64,9 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
(it.thisObj as ObjReal).value.roundToLong().toObj()
},
)
addFn("toInt") {
ObjInt(thisAs<ObjReal>().value.toLong())
}
}
}
}
@@ -4,11 +4,11 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
override val objClass = type
override suspend fun contains(context: Context, other: Obj): Boolean {
override suspend fun contains(scope: Scope, other: Obj): Boolean {
return set.contains(other)
}
override suspend fun plus(context: Context, other: Obj): Obj {
override suspend fun plus(scope: Scope, other: Obj): Obj {
return ObjSet(
if (other is ObjSet)
(set + other.set).toMutableSet()
@@ -17,7 +17,7 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
)
}
override suspend fun plusAssign(context: Context, other: Obj): Obj {
override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
when (other) {
is ObjSet -> {
set += other.set
@@ -29,9 +29,9 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
else -> {
if (other.isInstanceOf(ObjIterable)) {
val i = other.invokeInstanceMethod(context, "iterable")
while (i.invokeInstanceMethod(context, "hasNext").toBool()) {
set += i.invokeInstanceMethod(context, "next")
val i = other.invokeInstanceMethod(scope, "iterable")
while (i.invokeInstanceMethod(scope, "hasNext").toBool()) {
set += i.invokeInstanceMethod(scope, "next")
}
}
set += other
@@ -40,16 +40,16 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
return this
}
override suspend fun mul(context: Context, other: Obj): Obj {
override suspend fun mul(scope: Scope, other: Obj): Obj {
return if (other is ObjSet) {
ObjSet(set.intersect(other.set).toMutableSet())
} else
context.raiseIllegalArgument("set operator * requires another set")
scope.raiseIllegalArgument("set operator * requires another set")
}
override suspend fun minus(context: Context, other: Obj): Obj {
override suspend fun minus(scope: Scope, other: Obj): Obj {
if (other !is ObjSet)
context.raiseIllegalArgument("set operator - requires another set")
scope.raiseIllegalArgument("set operator - requires another set")
return ObjSet(set.minus(other.set).toMutableSet())
}
@@ -57,7 +57,7 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
return "Set(${set.joinToString(", ")})"
}
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if (other !is ObjSet) -1
else {
if (set == other.set) 0
@@ -69,8 +69,8 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
val type = object : ObjClass("Set", ObjCollection) {
override suspend fun callOn(context: Context): Obj {
return ObjSet(context.args.list.toMutableSet())
override suspend fun callOn(scope: Scope): Obj {
return ObjSet(scope.args.list.toMutableSet())
}
}.apply {
addFn("size") {
@@ -16,7 +16,7 @@ data class ObjString(val value: String) : Obj() {
// }
override suspend fun compareTo(context: Context, other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjString) return -2
return this.value.compareTo(other.value)
}
@@ -32,11 +32,11 @@ data class ObjString(val value: String) : Obj() {
override val objClass: ObjClass
get() = type
override suspend fun plus(context: Context, other: Obj): Obj {
override suspend fun plus(scope: Scope, other: Obj): Obj {
return ObjString(value + other.asStr.value)
}
override suspend fun getAt(context: Context, index: Obj): Obj {
override suspend fun getAt(scope: Scope, index: Obj): Obj {
if( index is ObjInt ) return ObjChar(value[index.toInt()])
if( index is ObjRange ) {
val start = if(index.start == null || index.start.isNull) 0 else index.start.toInt()
@@ -46,23 +46,23 @@ data class ObjString(val value: String) : Obj() {
}
return ObjString(value.substring(start, end))
}
context.raiseIllegalArgument("String index must be Int or Range")
scope.raiseIllegalArgument("String index must be Int or Range")
}
override fun hashCode(): Int {
return value.hashCode()
}
override suspend fun callOn(context: Context): Obj {
return ObjString(this.value.sprintf(*context.args.toKotlinList(context).toTypedArray()))
override suspend fun callOn(scope: Scope): Obj {
return ObjString(this.value.sprintf(*scope.args.toKotlinList(scope).toTypedArray()))
}
override suspend fun contains(context: Context, other: Obj): Boolean {
override suspend fun contains(scope: Scope, other: Obj): Boolean {
return if (other is ObjString)
value.contains(other.value)
else if (other is ObjChar)
value.contains(other.value)
else context.raiseIllegalArgument("String.contains can't take $other")
else scope.raiseIllegalArgument("String.contains can't take $other")
}
override fun equals(other: Any?): Boolean {
@@ -76,6 +76,9 @@ data class ObjString(val value: String) : Obj() {
companion object {
val type = ObjClass("String").apply {
addFn("toInt") {
ObjInt(thisAs<ObjString>().value.toLong())
}
addFn("startsWith") {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
}
@@ -112,6 +115,7 @@ data class ObjString(val value: String) : Obj() {
thisAs<ObjString>().value.uppercase().let(::ObjString)
}
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble())}
}
}
}
@@ -1,26 +1,26 @@
package net.sergeych.lyng
class Context(
val parent: Context?,
/**
* Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes.
* Each block usually creates a scope. Accessing Lyng closures usually is done via a scope.
*
* There are special types of scopes:
*
* - [Script.defaultScope] - root scope for a script, safe one
* - [AppliedScope] - scope used to apply a closure to some thisObj scope
*/
open class Scope(
val parent: Scope?,
val args: Arguments = Arguments.EMPTY,
var pos: Pos = Pos.builtIn,
var thisObj: Obj = ObjVoid,
var skipContextCreation: Boolean = false,
var skipScopeCreation: Boolean = false,
) {
constructor(
args: Arguments = Arguments.EMPTY,
pos: Pos = Pos.builtIn,
)
: this(Script.defaultContext, args, pos)
/**
* Making this context priority one
*/
fun applyContext(other: Context): Context {
if (other.thisObj != ObjVoid) thisObj = other.thisObj
appliedContext = other
return this
}
: this(Script.defaultScope, args, pos)
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@@ -74,24 +74,23 @@ class Context(
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T)
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
internal var appliedContext: Context? = null
internal val objects = mutableMapOf<String, ObjRecord>()
operator fun get(name: String): ObjRecord? =
open operator fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else {
objects[name]
?: parent?.get(name)
?: appliedContext?.get(name)
?: thisObj.objClass.getInstanceMemberOrNull(name)
}
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj)
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
Scope(this, args, pos, newThisObj ?: thisObj)
fun copy(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj)
fun copy(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
Scope(this, args, pos, newThisObj ?: thisObj)
fun copy() = Context(this, args, pos, thisObj)
fun copy() = Scope(this, args, pos, thisObj)
fun addItem(
name: String,
@@ -107,18 +106,18 @@ class Context(
return ns.objClass
}
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Context.() -> Unit) {
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Scope.() -> Unit) {
addFn<ObjVoid>(*names) {
fn(this)
ObjVoid
}
}
inline fun <reified T : Obj> addFn(vararg names: String, crossinline fn: suspend Context.() -> T) {
inline fun <reified T : Obj> addFn(vararg names: String, crossinline fn: suspend Scope.() -> T) {
val newFn = object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(context: Context): Obj = context.fn()
override suspend fun execute(scope: Scope): Obj = scope.fn()
}
for (name in names) {
@@ -133,9 +132,21 @@ class Context(
fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj =
Compiler().compile(code.toSource()).execute(this)
Compiler.compile(code.toSource()).execute(this)
suspend fun eval(source: Source): Obj =
Compiler.compile(
source,
(this as? ModuleScope)?.pacman?.also { println("pacman found: $pacman")} ?: Pacman.emptyAllowAll
).execute(this)
fun containsLocal(name: String): Boolean = name in objects
open suspend fun checkImport(pos: Pos, name: String, symbols: Map<String, String>? = null) {
throw ImportException(pos, "Import is not allowed here: $name")
}
open suspend fun importInto(scope: Scope, name: String, symbols: Map<String, String>? = null) {
scope.raiseError(ObjIllegalOperationException(scope,"Import is not allowed here: import $name"))
}
}
@@ -9,18 +9,18 @@ class Script(
// private val catchReturn: Boolean = false,
) : Statement() {
override suspend fun execute(context: Context): Obj {
override suspend fun execute(scope: Scope): Obj {
var lastResult: Obj = ObjVoid
for (s in statements) {
lastResult = s.execute(context)
lastResult = s.execute(scope)
}
return lastResult
}
suspend fun execute() = execute(defaultContext.copy(pos = pos))
suspend fun execute() = execute(defaultScope.copy(pos = pos))
companion object {
val defaultContext: Context = Context().apply {
val defaultScope: Scope = Scope().apply {
ObjException.addExceptionsToContext(this)
addFn("println") {
for ((i, a) in args.withIndex()) {
@@ -12,4 +12,6 @@ open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=
cause
)
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.context.pos, errorObject.message)
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.scope.pos, errorObject.message)
class ImportException(pos: Pos, message: String) : ScriptError(pos, message)
@@ -0,0 +1,22 @@
package net.sergeych.lyng
interface SecurityManager {
/**
* Check that any symbol from the corresponding module can be imported. If it returns false,
* the module will not be imported and no further processing will be done
*/
fun canImportModule(name: String): Boolean
/**\
* if [canImportModule] this method allows fine-grained control over symbols.
*/
fun canImportSymbol(moduleName: String, symbolName: String): Boolean = true
companion object {
val allowAll: SecurityManager = object : SecurityManager {
override fun canImportModule(name: String): Boolean = true
override fun canImportSymbol(moduleName: String, symbolName: String): Boolean = true
}
}
}
@@ -1,9 +1,14 @@
@file:Suppress("unused")
package net.sergeych.lyng
// this is highly experimental and subject to complete redesign
// very soon
sealed class TypeDecl {
sealed class TypeDecl(val isNullable:Boolean = false) {
// ??
// data class Fn(val argTypes: List<ArgsDeclaration.Item>, val retType: TypeDecl) : TypeDecl()
object Obj : TypeDecl()
object TypeAny : TypeDecl()
object TypeNullableAny : TypeDecl(true)
class Simple(val name: String,isNullable: Boolean) : TypeDecl(isNullable)
}
@@ -18,14 +18,14 @@ abstract class Statement(
override val objClass: ObjClass = type
abstract val pos: Pos
abstract suspend fun execute(context: Context): Obj
abstract suspend fun execute(scope: Scope): Obj
override suspend fun compareTo(context: Context,other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
throw UnsupportedOperationException("not comparable")
}
override suspend fun callOn(context: Context): Obj {
return execute(context)
override suspend fun callOn(scope: Scope): Obj {
return execute(scope)
}
override fun toString(): String = "Callable@${this.hashCode()}"
@@ -34,6 +34,8 @@ abstract class Statement(
val type = ObjClass("Callable")
}
suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.copy(args = Arguments(*args)))
}
fun Statement.raise(text: String): Nothing {
@@ -45,16 +47,16 @@ fun Statement.require(cond: Boolean, message: () -> String) {
if (!cond) raise(message())
}
fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend (Context) -> Obj): Statement =
fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend (Scope) -> Obj): Statement =
object : Statement(isStaticConst, isConst) {
override val pos: Pos = pos
override suspend fun execute(context: Context): Obj = f(context)
override suspend fun execute(scope: Scope): Obj = f(scope)
}
fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend Context.() -> Obj): Statement =
fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend Scope.() -> Obj): Statement =
object : Statement(isStaticConst, isConst) {
override val pos: Pos = Pos.builtIn
override suspend fun execute(context: Context): Obj = f(context)
override suspend fun execute(scope: Scope): Obj = f(scope)
}
+151 -71
View File
@@ -1,5 +1,6 @@
package io.github.kotlin.fibonacci
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.*
import kotlin.test.*
@@ -172,55 +173,55 @@ class ScriptTest {
@Test
fun varsAndConstsTest() = runTest {
val context = Context(pos = Pos.builtIn)
val scope = Scope(pos = Pos.builtIn)
assertEquals(
ObjInt(3L), context.eval(
ObjInt(3L), scope.eval(
"""
val a = 17
var b = 3
""".trimIndent()
)
)
assertEquals(17, context.eval("a").toInt())
assertEquals(20, context.eval("b + a").toInt())
assertEquals(17, scope.eval("a").toInt())
assertEquals(20, scope.eval("b + a").toInt())
assertFailsWith<ScriptError> {
context.eval("a = 10")
scope.eval("a = 10")
}
assertEquals(17, context.eval("a").toInt())
assertEquals(5, context.eval("b = a - 7 - 5").toInt())
assertEquals(5, context.eval("b").toInt())
assertEquals(17, scope.eval("a").toInt())
assertEquals(5, scope.eval("b = a - 7 - 5").toInt())
assertEquals(5, scope.eval("b").toInt())
}
@Test
fun functionTest() = runTest {
val context = Context(pos = Pos.builtIn)
context.eval(
val scope = Scope(pos = Pos.builtIn)
scope.eval(
"""
fun foo(a, b) {
a + b
}
""".trimIndent()
)
assertEquals(17, context.eval("foo(3,14)").toInt())
assertEquals(17, scope.eval("foo(3,14)").toInt())
assertFailsWith<ScriptError> {
assertEquals(17, context.eval("foo(3)").toInt())
assertEquals(17, scope.eval("foo(3)").toInt())
}
context.eval(
scope.eval(
"""
fn bar(a, b=10) {
a + b + 1
}
""".trimIndent()
)
assertEquals(10, context.eval("bar(3, 6)").toInt())
assertEquals(14, context.eval("bar(3)").toInt())
assertEquals(10, scope.eval("bar(3, 6)").toInt())
assertEquals(14, scope.eval("bar(3)").toInt())
}
@Test
fun simpleClosureTest() = runTest {
val context = Context(pos = Pos.builtIn)
context.eval(
val scope = Scope(pos = Pos.builtIn)
scope.eval(
"""
var global = 10
@@ -229,16 +230,16 @@ class ScriptTest {
}
""".trimIndent()
)
assertEquals(27, context.eval("foo(3,14)").toInt())
context.eval("global = 20")
assertEquals(37, context.eval("foo(3,14)").toInt())
assertEquals(27, scope.eval("foo(3,14)").toInt())
scope.eval("global = 20")
assertEquals(37, scope.eval("foo(3,14)").toInt())
}
@Test
fun nullAndVoidTest() = runTest {
val context = Context(pos = Pos.builtIn)
assertEquals(ObjVoid, context.eval("void"))
assertEquals(ObjNull, context.eval("null"))
val scope = Scope(pos = Pos.builtIn)
assertEquals(ObjVoid, scope.eval("void"))
assertEquals(ObjNull, scope.eval("null"))
}
@Test
@@ -345,8 +346,8 @@ class ScriptTest {
@Test
fun ifTest() = runTest {
// if - single line
var context = Context(pos = Pos.builtIn)
context.eval(
var scope = Scope(pos = Pos.builtIn)
scope.eval(
"""
fn test1(n) {
var result = "more"
@@ -356,12 +357,12 @@ class ScriptTest {
}
""".trimIndent()
)
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
assertEquals("enough", scope.eval("test1(11)").toString())
assertEquals("more", scope.eval("test1(1)").toString())
// if - multiline (block)
context = Context(pos = Pos.builtIn)
context.eval(
scope = Scope(pos = Pos.builtIn)
scope.eval(
"""
fn test1(n) {
var prefix = "answer: "
@@ -375,12 +376,12 @@ class ScriptTest {
}
""".trimIndent()
)
assertEquals("answer: enough", context.eval("test1(11)").toString())
assertEquals("answer: more", context.eval("test1(1)").toString())
assertEquals("answer: enough", scope.eval("test1(11)").toString())
assertEquals("answer: more", scope.eval("test1(1)").toString())
// else single line1
context = Context(pos = Pos.builtIn)
context.eval(
scope = Scope(pos = Pos.builtIn)
scope.eval(
"""
fn test1(n) {
if( n >= 10 )
@@ -390,12 +391,12 @@ class ScriptTest {
}
""".trimIndent()
)
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
assertEquals("enough", scope.eval("test1(11)").toString())
assertEquals("more", scope.eval("test1(1)").toString())
// if/else with blocks
context = Context(pos = Pos.builtIn)
context.eval(
scope = Scope(pos = Pos.builtIn)
scope.eval(
"""
fn test1(n) {
if( n > 20 ) {
@@ -409,9 +410,9 @@ class ScriptTest {
}
""".trimIndent()
)
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
assertEquals("too much", context.eval("test1(100)").toString())
assertEquals("enough", scope.eval("test1(11)").toString())
assertEquals("more", scope.eval("test1(1)").toString())
assertEquals("too much", scope.eval("test1(100)").toString())
}
@Test
@@ -511,13 +512,13 @@ class ScriptTest {
ArgsDeclaration.Item("c"),
), ttEnd
)
var c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
var c = Scope(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
assertEquals(ObjInt(1), c["a"]?.value)
assertEquals(ObjInt(2), c["b"]?.value)
assertEquals(ObjInt(3), c["c"]?.value)
// less args: error
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2).map { it.toObj() }))
c = Scope(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2).map { it.toObj() }))
assertFailsWith<ScriptError> {
pa.assignToContext(c)
}
@@ -534,7 +535,7 @@ class ScriptTest {
assertEquals(ObjInt(2), c["b"]?.value)
assertEquals(ObjInt(100), c["c"]?.value)
// enough args. default value is ignored:
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(10, 2, 5).map { it.toObj() }))
c = Scope(pos = Pos.builtIn, args = Arguments.from(listOf(10, 2, 5).map { it.toObj() }))
pa.assignToContext(c)
assertEquals(ObjInt(10), c["a"]?.value)
assertEquals(ObjInt(2), c["b"]?.value)
@@ -552,17 +553,17 @@ class ScriptTest {
ArgsDeclaration.Item("b", isEllipsis = true),
), ttEnd
)
var c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
var c = Scope(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assert( a == 1 ); println(b)")
c.eval("assert( b == [2,3] )")
c = Context(args = Arguments.from(listOf(1, 2).map { it.toObj() }))
c = Scope(args = Arguments.from(listOf(1, 2).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a, 1 ); println(b)")
c.eval("assertEquals( b, [2] )")
c = Context(args = Arguments.from(listOf(1).map { it.toObj() }))
c = Scope(args = Arguments.from(listOf(1).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assert( a == 1 ); println(b)")
c.eval("assert( b == [] )")
@@ -578,25 +579,25 @@ class ScriptTest {
ArgsDeclaration.Item("c"),
), ttEnd
)
var c = Context(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
var c = Scope(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[0,1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
c = Scope(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(2, 3).map { it.toObj() }))
c = Scope(args = Arguments.from(listOf(2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(3).map { it.toObj() }))
c = Scope(args = Arguments.from(listOf(3).map { it.toObj() }))
assertFailsWith<ExecutionError> {
pa.assignToContext(c)
}
@@ -613,28 +614,28 @@ class ScriptTest {
ArgsDeclaration.Item("c"),
), ttEnd
)
var c = Context(args = Arguments.from(listOf(-1, 0, 1, 2, 3).map { it.toObj() }))
var c = Scope(args = Arguments.from(listOf(-1, 0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, -1 )")
c.eval("assertEquals( a,[0,1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
c = Scope(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, 0 )")
c.eval("assertEquals( a,[1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
c = Scope(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, 1)")
c.eval("assertEquals( a,[] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(2, 3).map { it.toObj() }))
c = Scope(args = Arguments.from(listOf(2, 3).map { it.toObj() }))
assertFailsWith<ExecutionError> {
pa.assignToContext(c)
}
@@ -755,7 +756,7 @@ class ScriptTest {
@Test
fun testIncr() = runTest {
val c = Context()
val c = Scope()
c.eval("var x = 10")
assertEquals(10, c.eval("x++").toInt())
assertEquals(11, c.eval("x++").toInt())
@@ -767,7 +768,7 @@ class ScriptTest {
@Test
fun testDecr() = runTest {
val c = Context()
val c = Scope()
c.eval("var x = 9")
assertEquals(9, c.eval("x--").toInt())
assertEquals(8, c.eval("x--").toInt())
@@ -778,7 +779,7 @@ class ScriptTest {
@Test
fun testDecrIncr() = runTest {
val c = Context()
val c = Scope()
c.eval("var x = 9")
assertEquals(9, c.eval("x++").toInt())
assertEquals(10, c.eval("x++").toInt())
@@ -792,7 +793,7 @@ class ScriptTest {
@Test
fun testDecrIncr2() = runTest {
val c = Context()
val c = Scope()
c.eval("var x = 9")
assertEquals(9, c.eval("x--").toInt())
assertEquals(8, c.eval("x--").toInt())
@@ -809,7 +810,7 @@ class ScriptTest {
@Test
fun testDecrIncr3() = runTest {
val c = Context()
val c = Scope()
c.eval("var x = 9")
assertEquals(9, c.eval("x++").toInt())
assertEquals(10, c.eval("x++").toInt())
@@ -821,7 +822,7 @@ class ScriptTest {
@Test
fun testIncrAndDecr() = runTest {
val c = Context()
val c = Scope()
assertEquals(
"8", c.eval(
"""
@@ -856,7 +857,7 @@ class ScriptTest {
@Test
fun testAssign1() = runTest {
assertEquals(10, eval("var x = 5; x=10; x").toInt())
val ctx = Context()
val ctx = Scope()
ctx.eval(
"""
var a = 1
@@ -870,7 +871,7 @@ class ScriptTest {
@Test
fun testAssign2() = runTest {
val ctx = Context()
val ctx = Scope()
ctx.eval("var x = 10")
assertEquals(14, ctx.eval("x += 4").toInt())
assertEquals(14, ctx.eval("x").toInt())
@@ -888,7 +889,7 @@ class ScriptTest {
@Test
fun testVals() = runTest {
val cxt = Context()
val cxt = Scope()
cxt.eval("val x = 11")
assertEquals(11, cxt.eval("x").toInt())
assertFails { cxt.eval("x = 12") }
@@ -1435,7 +1436,7 @@ class ScriptTest {
@Test
fun testSimpleStruct() = runTest {
val c = Context()
val c = Scope()
c.eval(
"""
class Point(x,y)
@@ -1456,7 +1457,7 @@ class ScriptTest {
@Test
fun testNonAssignalbeFieldInStruct() = runTest {
val c = Context()
val c = Scope()
c.eval(
"""
class Point(x,y)
@@ -1473,7 +1474,7 @@ class ScriptTest {
@Test
fun testStructBodyVal() = runTest {
val c = Context()
val c = Scope()
c.eval(
"""
class Point(x,y) {
@@ -1495,7 +1496,7 @@ class ScriptTest {
@Test
fun testStructBodyFun() = runTest {
val c = Context()
val c = Scope()
c.eval(
"""
class Point(x,y) {
@@ -1515,7 +1516,7 @@ class ScriptTest {
@Test
fun testPrivateConstructorParams() = runTest {
val c = Context()
val c = Scope()
c.eval(
"""
class Point(private var x,y)
@@ -1870,7 +1871,7 @@ class ScriptTest {
@Test
fun testTryFinally() = runTest {
val c = Context()
val c = Scope()
assertFails {
c.eval(
"""
@@ -1893,7 +1894,7 @@ class ScriptTest {
@Test
fun testThrowFromKotlin() = runTest {
val c = Context()
val c = Scope()
c.addFn("callThrow") {
raiseIllegalArgument("fromKotlin")
}
@@ -2285,4 +2286,83 @@ class ScriptTest {
""".trimIndent())
}
@Test
fun testExtend() = runTest() {
eval("""
fun Int.isEven() {
this % 2 == 0
}
fun Object.isInteger() {
when(this) {
is Int -> true
is Real -> toInt() == this
is String -> toReal().isInteger()
else -> false
}
}
assert( 4.isEven() )
assert( !5.isEven() )
assert( 12.isInteger() == true )
assert( 12.1.isInteger() == false )
assert( "5".isInteger() )
assert( ! "5.2".isInteger() )
""".trimIndent())
}
@Test
fun testToFlow() = runTest() {
val c = Scope()
val arr = c.eval("[1,2,3]")
// array is iterable so we can:
assertEquals(listOf(1,2,3), arr.toFlow(c).map { it.toInt() }.toList())
}
@Test
fun testAssociateBy() = runTest() {
eval("""
val m = [123, 456].associateBy { "k:%s"(it) }
println(m)
assertEquals(123, m["k:123"])
assertEquals(456, m["k:456"])
""")
listOf(1,2,3).associateBy { it * 10 }
}
@Test
fun testImports1() = runTest() {
val foosrc = """
package lyng.foo
fun foo() { "foo1" }
""".trimIndent()
val pm = InlineSourcesPacman(Pacman.emptyAllowAll, listOf(Source("foosrc", foosrc)))
assertNotNull(pm.modules.await()["lyng.foo"])
assertIs<ModuleScope>(pm.modules.await()["lyng.foo"]!!.await())
assertEquals("foo1", pm.modules.await()["lyng.foo"]!!.await().eval("foo()").toString())
}
@Test
fun testImports2() = runTest() {
val foosrc = """
package lyng.foo
fun foo() { "foo1" }
""".trimIndent()
val pm = InlineSourcesPacman(Pacman.emptyAllowAll, listOf(Source("foosrc", foosrc)))
val src = """
import lyng.foo
foo()
""".trimIndent().toSource("test")
val scope = ModuleScope(pm, src)
assertEquals("foo1", scope.eval(src).toString())
}
}
@@ -0,0 +1,53 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class TypesTest {
@Test
fun testTypeCollection1() = runTest {
eval("""
class Point(x: Real, y: Real)
assert(Point(1,2).x == 1)
assert(Point(1,2).y == 2)
assert(Point(1,2) is Point)
""".trimIndent())
}
@Test
fun testTypeCollection2() = runTest {
eval("""
fun fn1(x: Real, y: Real): Real { x + y }
""".trimIndent())
}
@Test
fun testTypeCollection3() = runTest {
eval("""
class Test(a: Int) {
fun fn1(x: Real, y: Real): Real { x + y }
}
""".trimIndent())
}
@Test
fun testExternDeclarations() = runTest {
eval("""
extern fun foo1(a: String): Void
assertThrows { foo1("1") }
class Test(a: Int) {
extern fun fn1(x: Real, y: Real): Real
// extern val b: Int
}
// println("1")
val t = Test(0)
// println(t.b)
// println("2")
assertThrows {
t.fn1(1,2)
}
// println("4")
""".trimIndent())
}
}
+6 -6
View File
@@ -3,8 +3,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Context
import net.sergeych.lyng.ObjVoid
import net.sergeych.lyng.Scope
import java.nio.file.Files
import java.nio.file.Files.readAllLines
import java.nio.file.Paths
@@ -158,10 +158,10 @@ fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> =
}
.flowOn(Dispatchers.IO)
suspend fun DocTest.test(context: Context = Context()) {
suspend fun DocTest.test(scope: Scope = Scope()) {
val collectedOutput = StringBuilder()
val currentTest = this
context.apply {
scope.apply {
addFn("println") {
if( bookMode ) {
println("${currentTest.fileNamePart}:${currentTest.line}> ${args.joinToString(" "){it.asStr.value}}")
@@ -177,7 +177,7 @@ suspend fun DocTest.test(context: Context = Context()) {
}
var error: Throwable? = null
val result = try {
context.eval(code)
scope.eval(code)
} catch (e: Throwable) {
error = e
null
@@ -204,10 +204,10 @@ suspend fun DocTest.test(context: Context = Context()) {
}
suspend fun runDocTests(fileName: String, bookMode: Boolean = false) {
val bookContext = Context()
val bookScope = Scope()
var count = 0
parseDocTests(fileName, bookMode).collect { dt ->
if (bookMode) dt.test(bookContext)
if (bookMode) dt.test(bookScope)
else dt.test()
count++
}
+2 -2
View File
@@ -2,7 +2,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import net.sergeych.lyng.Context
import net.sergeych.lyng.Scope
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.io.path.extension
@@ -13,7 +13,7 @@ suspend fun executeSampleTests(fileName: String) {
Files.readString(Paths.get(fileName))
}
runBlocking {
val c = Context()
val c = Scope()
val start = Clock.System.now()
c.eval(sample)
val time = Clock.System.now() - start