closure tests and fix
This commit is contained in:
parent
ebeed385e9
commit
758f49603f
50
docs/advanced_topics.md
Normal file
50
docs/advanced_topics.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Advanced topics
|
||||
|
||||
## Closures/scopes isolation
|
||||
|
||||
Each block has own scope, in which it can safely uses closures and override
|
||||
outer vars:
|
||||
|
||||
var param = "global"
|
||||
val prefix = "param in "
|
||||
val scope1 = {
|
||||
var param = prefix + "scope1"
|
||||
param
|
||||
}
|
||||
val scope2 = {
|
||||
var param = prefix + "scope2"
|
||||
param
|
||||
}
|
||||
// note that block returns its last value
|
||||
println(scope1)
|
||||
println(scope2)
|
||||
println(param)
|
||||
>>> param in scope1
|
||||
>>> param in scope2
|
||||
>>> global
|
||||
>>> void
|
||||
|
||||
One interesting way of using closure isolation is to keep state of the functions:
|
||||
|
||||
val getAndIncrement = {
|
||||
// will be updated by doIt()
|
||||
var counter = 0
|
||||
|
||||
// we return callable fn from the block:
|
||||
fun doit() {
|
||||
val was = counter
|
||||
counter = counter + 1
|
||||
was
|
||||
}
|
||||
}
|
||||
println(getAndIncrement())
|
||||
println(getAndIncrement())
|
||||
println(getAndIncrement())
|
||||
>>> 0
|
||||
>>> 1
|
||||
>>> 2
|
||||
>>> void
|
||||
|
||||
Inner `counter` is not accessible from outside, no way; still it is kept
|
||||
between calls in the closure, as inner function `doit`, returned from the
|
||||
block, keeps reference to it and keeps it alive.
|
24
docs/math.md
24
docs/math.md
@ -11,10 +11,10 @@ Same as in C++.
|
||||
| 2 | `+` `-` |
|
||||
| 3 | bit shifts (NI) |
|
||||
| 4 | `<=>` (NI) |
|
||||
| 5 | `<=` `>=` `<` `>` (NI) |
|
||||
| 6 | `==` `!=` (NI) |
|
||||
| 7 | `&` (NI) |
|
||||
| 9 | `\|` (NI) |
|
||||
| 5 | `<=` `>=` `<` `>` |
|
||||
| 6 | `==` `!=` |
|
||||
| 7 | bitwise and `&` (NI) |
|
||||
| 9 | bitwise or `\|` (NI) |
|
||||
| 10 | `&&` |
|
||||
| 11<br/>lowest | `\|\|` |
|
||||
|
||||
@ -22,7 +22,16 @@ Same as in C++.
|
||||
|
||||
## Operators
|
||||
|
||||
`+ - * / % `: if both operand is `Int`, calculates as int. Otherwise, as real.
|
||||
`+ - * / % `: if both operand is `Int`, calculates as int. Otherwise, as real:
|
||||
|
||||
// integer division:
|
||||
3 / 2
|
||||
>>> 1
|
||||
|
||||
but:
|
||||
|
||||
3 / 2.0
|
||||
>>> 1.5
|
||||
|
||||
## Round and range
|
||||
|
||||
@ -46,6 +55,11 @@ or transformed `Real` otherwise.
|
||||
| | |
|
||||
| | |
|
||||
|
||||
For example:
|
||||
|
||||
sin(π/2)
|
||||
>>> 1.0
|
||||
|
||||
## Scientific constant
|
||||
|
||||
| name | meaning |
|
||||
|
@ -154,14 +154,20 @@ Each __block has an isolated context that can be accessed from closures__. For e
|
||||
}
|
||||
>>> void
|
||||
|
||||
As was told, `def` statement return callable for the function, it could be used as a parameter, or elsewhere
|
||||
As was told, `fun` statement return callable for the function, it could be used as a parameter, or elsewhere
|
||||
to call it:
|
||||
|
||||
val taskAlias = fun someTask() {
|
||||
println("Hello")
|
||||
}
|
||||
// call the callable stored in the var
|
||||
taskAlias()
|
||||
// or directly:
|
||||
someTask()
|
||||
|
||||
>>> Hello
|
||||
>>> Hello
|
||||
>>> void
|
||||
|
||||
If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?)
|
||||
|
||||
# Flow control operators
|
||||
@ -276,8 +282,12 @@ We can skip the rest of the loop and restart it, as usual, with `continue` opera
|
||||
|
||||
Notice that `total` remains 0 as the end of the outerLoop@ is not reachable: `continue` is always called and always make Ling to skip it.
|
||||
|
||||
## Labels@
|
||||
|
||||
The label can be any valid identifier, even a keyword, labels exist in their own, isolated world, so no risk of occasional clash. Labels are also scoped to their context and do not exist outside it.
|
||||
|
||||
Right now labels are implemented only for the while loop. It is intended to be implemented for all loops and returns.
|
||||
|
||||
# Comments
|
||||
|
||||
// single line comment
|
||||
@ -296,6 +306,8 @@ The label can be any valid identifier, even a keyword, labels exist in their own
|
||||
| Null | missing value, singleton | null |
|
||||
| Fn | callable type | |
|
||||
|
||||
See also [math operations](math.md)
|
||||
|
||||
## String details
|
||||
|
||||
### String operations
|
||||
|
@ -498,11 +498,16 @@ class Compiler {
|
||||
// Here we should be at open body
|
||||
val fnStatements = parseBlock(tokens)
|
||||
|
||||
val fnBody = statement(t.pos) { context ->
|
||||
// load params
|
||||
var closure: Context? = null
|
||||
|
||||
val fnBody = statement(t.pos) { callerContext ->
|
||||
// remember closure where the function was defined:
|
||||
val context = closure ?: Context()
|
||||
// load params from caller context
|
||||
println("calling function $name in context $context <- ${context.parent}")
|
||||
for ((i, d) in params.withIndex()) {
|
||||
if (i < context.args.size)
|
||||
context.addItem(d.name, false, context.args.list[i].value)
|
||||
if (i < callerContext.args.size)
|
||||
context.addItem(d.name, false, callerContext.args.list[i].value)
|
||||
else
|
||||
context.addItem(
|
||||
d.name,
|
||||
@ -514,10 +519,12 @@ class Compiler {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// save closure
|
||||
fnStatements.execute(context)
|
||||
}
|
||||
return statement(start) { context ->
|
||||
println("adding function $name to context $context")
|
||||
closure = context
|
||||
context.addItem(name, false, fnBody)
|
||||
fnBody
|
||||
}
|
||||
|
@ -5,24 +5,21 @@ class Context(
|
||||
val args: Arguments = Arguments.EMPTY
|
||||
) {
|
||||
|
||||
data class Item(
|
||||
val name: String,
|
||||
var value: Obj?,
|
||||
val isMutable: Boolean = false
|
||||
)
|
||||
private val objects = mutableMapOf<String, StoredObj>()
|
||||
|
||||
private val objects = mutableMapOf<String, Item>()
|
||||
|
||||
operator fun get(name: String): Item? = objects[name] ?: parent?.get(name)
|
||||
operator fun get(name: String): StoredObj? =
|
||||
objects[name]
|
||||
?: parent?.get(name)
|
||||
|
||||
fun copy(args: Arguments = Arguments.EMPTY): Context = Context(this, args)
|
||||
|
||||
fun addItem(name: String, isMutable: Boolean, value: Obj?) {
|
||||
objects.put(name, Item(name, value, isMutable))
|
||||
println("ading item $name=$value in $this <- ${this.parent}")
|
||||
objects.put(name, StoredObj(name, value, isMutable))
|
||||
}
|
||||
|
||||
fun getOrCreateNamespace(name: String) =
|
||||
(objects.getOrPut(name) { Item(name, ObjNamespace(name,copy()), isMutable = false) }.value as ObjNamespace)
|
||||
(objects.getOrPut(name) { StoredObj(name, ObjNamespace(name,copy()), isMutable = false) }.value as ObjNamespace)
|
||||
.context
|
||||
|
||||
inline fun <reified T> addFn(vararg names: String, crossinline fn: suspend Context.() -> T) {
|
||||
|
@ -1,33 +1,77 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.math.floor
|
||||
|
||||
typealias InstanceMethod = (Context, Obj) -> Obj
|
||||
|
||||
data class Item<T>(var value: T, val isMutable: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
sealed class ClassDef(
|
||||
val className: String
|
||||
) {
|
||||
val baseClasses: List<ClassDef> get() = emptyList()
|
||||
private val instanceMethods: MutableMap<String, Item<InstanceMethod>> get() = mutableMapOf()
|
||||
|
||||
private val instanceLock = Mutex()
|
||||
|
||||
suspend fun addInstanceMethod(
|
||||
name: String,
|
||||
freeze: Boolean = false,
|
||||
pos: Pos = Pos.builtIn,
|
||||
body: InstanceMethod
|
||||
) {
|
||||
instanceLock.withLock {
|
||||
instanceMethods[name]?.let {
|
||||
if( !it.isMutable )
|
||||
throw ScriptError(pos, "existing method $name is frozen and can't be updated")
|
||||
it.value = body
|
||||
} ?: instanceMethods.put(name, Item(body, freeze))
|
||||
}
|
||||
}
|
||||
|
||||
//suspend fun callInstanceMethod(context: Context, self: Obj,args: Arguments): Obj {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
object ObjClassDef : ClassDef("Obj")
|
||||
|
||||
@Serializable
|
||||
sealed class Obj : Comparable<Obj> {
|
||||
open val asStr: ObjString by lazy {
|
||||
if (this is ObjString) this else ObjString(this.toString())
|
||||
}
|
||||
|
||||
open val type: Type = Type.Any
|
||||
open val definition: ClassDef = ObjClassDef
|
||||
|
||||
@Suppress("unused")
|
||||
enum class Type {
|
||||
@SerialName("Void")
|
||||
Void,
|
||||
|
||||
@SerialName("Null")
|
||||
Null,
|
||||
|
||||
@SerialName("String")
|
||||
String,
|
||||
|
||||
@SerialName("Int")
|
||||
Int,
|
||||
|
||||
@SerialName("Real")
|
||||
Real,
|
||||
|
||||
@SerialName("Bool")
|
||||
Bool,
|
||||
|
||||
@SerialName("Fn")
|
||||
Fn,
|
||||
|
||||
@SerialName("Any")
|
||||
Any,
|
||||
}
|
||||
@ -112,7 +156,8 @@ fun Obj.toLong(): Long =
|
||||
|
||||
fun Obj.toInt(): Int = toLong().toInt()
|
||||
|
||||
fun Obj.toBool(): Boolean = (this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean ${this.type}:$this")
|
||||
fun Obj.toBool(): Boolean =
|
||||
(this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean $this")
|
||||
|
||||
|
||||
@Serializable
|
||||
@ -125,7 +170,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
override val toObjReal: ObjReal by lazy { ObjReal(value) }
|
||||
|
||||
override fun compareTo(other: Obj): Int {
|
||||
if( other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other")
|
||||
if (other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other")
|
||||
return value.compareTo(other.doubleValue)
|
||||
}
|
||||
|
||||
@ -142,7 +187,7 @@ data class ObjInt(val value: Long) : Obj(), Numeric {
|
||||
override val toObjReal: ObjReal by lazy { ObjReal(doubleValue) }
|
||||
|
||||
override fun compareTo(other: Obj): Int {
|
||||
if( other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other")
|
||||
if (other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other")
|
||||
return value.compareTo(other.doubleValue)
|
||||
}
|
||||
|
||||
@ -155,9 +200,10 @@ data class ObjBool(val value: Boolean) : Obj() {
|
||||
override val asStr by lazy { ObjString(value.toString()) }
|
||||
|
||||
override fun compareTo(other: Obj): Int {
|
||||
if( other !is ObjBool) throw IllegalArgumentException("cannot compare $this with $other")
|
||||
if (other !is ObjBool) throw IllegalArgumentException("cannot compare $this with $other")
|
||||
return value.compareTo(other.value)
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ class Script(
|
||||
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
// todo: run script
|
||||
println("exec script in $context <- ${context.parent}")
|
||||
var lastResult: Obj = ObjVoid
|
||||
for (s in statements) {
|
||||
lastResult = s.execute(context)
|
||||
|
10
library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt
Normal file
10
library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
/**
|
||||
* Whatever [Obj] stored somewhere
|
||||
*/
|
||||
data class StoredObj(
|
||||
val name: String,
|
||||
var value: Obj?,
|
||||
val isMutable: Boolean = false
|
||||
)
|
@ -34,22 +34,21 @@ data class DocTest(
|
||||
val sourceLines by lazy { code.lines() }
|
||||
|
||||
override fun toString(): String {
|
||||
return "DocTest:$fileName:${line+1}..${line + sourceLines.size}"
|
||||
return "DocTest:$fileName:${line + 1}..${line + sourceLines.size}"
|
||||
}
|
||||
|
||||
val detailedString by lazy {
|
||||
val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line}: $s" }.joinToString("\n")
|
||||
"$this\n" +
|
||||
codeWithLines + "\n" +
|
||||
"--------expected output--------\n" +
|
||||
expectedOutput +
|
||||
"-----expected return value-----\n" +
|
||||
expectedResult
|
||||
var result = "$this\n$codeWithLines\n"
|
||||
if (expectedOutput.isNotBlank())
|
||||
result += "--------expected output--------\n$expectedOutput\n"
|
||||
|
||||
"$result-----expected return value-----\n$expectedResult"
|
||||
}
|
||||
}
|
||||
|
||||
fun parseDocTests(name: String): Flow<DocTest> = flow {
|
||||
val book = readAllLines(Paths.get("../docs/tutorial.md"))
|
||||
fun parseDocTests(fileName: String): Flow<DocTest> = flow {
|
||||
val book = readAllLines(Paths.get(fileName))
|
||||
var startOffset = 0
|
||||
val block = mutableListOf<String>()
|
||||
var startIndex = 0
|
||||
@ -94,10 +93,10 @@ fun parseDocTests(name: String): Flow<DocTest> = flow {
|
||||
if (isValid) {
|
||||
emit(
|
||||
DocTest(
|
||||
name, startIndex,
|
||||
fileName, startIndex,
|
||||
block.joinToString("\n"),
|
||||
if (result.size > 1)
|
||||
result.dropLast(1).joinToString { it + "\n" }
|
||||
result.dropLast(1).joinToString("") { it + "\n" }
|
||||
else "",
|
||||
result.last()
|
||||
)
|
||||
@ -149,8 +148,7 @@ suspend fun DocTest.test() {
|
||||
var error: Throwable? = null
|
||||
val result = try {
|
||||
context.eval(code)
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
} catch (e: Throwable) {
|
||||
error = e
|
||||
null
|
||||
}?.toString()?.replace(Regex("@\\d+"), "@...")
|
||||
@ -166,12 +164,27 @@ suspend fun DocTest.test() {
|
||||
// println("OK: $this")
|
||||
}
|
||||
|
||||
suspend fun runDocTests(fileName: String) {
|
||||
parseDocTests(fileName).collect { dt ->
|
||||
dt.test()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class BookTest {
|
||||
|
||||
@Test
|
||||
fun testsFromTutorial() = runTest {
|
||||
parseDocTests("../docs/tutorial.md").collect { dt ->
|
||||
dt.test()
|
||||
}
|
||||
runDocTests("../docs/tutorial.md")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testsFromMath() = runTest {
|
||||
runDocTests("../docs/math.md")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testsFromAdvanced() = runTest {
|
||||
runDocTests("../docs/advanced_topics.md")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user