closure tests and fix

This commit is contained in:
Sergey Chernov 2025-05-20 22:59:15 +04:00
parent ebeed385e9
commit 758f49603f
9 changed files with 193 additions and 43 deletions

50
docs/advanced_topics.md Normal file
View 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.

View File

@ -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 |

View File

@ -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

View File

@ -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
}

View File

@ -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) {

View File

@ -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()
}

View File

@ -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)

View 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
)

View File

@ -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")
}
}