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 | `+` `-` | | 2 | `+` `-` |
| 3 | bit shifts (NI) | | 3 | bit shifts (NI) |
| 4 | `<=>` (NI) | | 4 | `<=>` (NI) |
| 5 | `<=` `>=` `<` `>` (NI) | | 5 | `<=` `>=` `<` `>` |
| 6 | `==` `!=` (NI) | | 6 | `==` `!=` |
| 7 | `&` (NI) | | 7 | bitwise and `&` (NI) |
| 9 | `\|` (NI) | | 9 | bitwise or `\|` (NI) |
| 10 | `&&` | | 10 | `&&` |
| 11<br/>lowest | `\|\|` | | 11<br/>lowest | `\|\|` |
@ -22,7 +22,16 @@ Same as in C++.
## Operators ## 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 ## Round and range
@ -46,6 +55,11 @@ or transformed `Real` otherwise.
| | | | | |
| | | | | |
For example:
sin(π/2)
>>> 1.0
## Scientific constant ## Scientific constant
| name | meaning | | name | meaning |

View File

@ -154,13 +154,19 @@ Each __block has an isolated context that can be accessed from closures__. For e
} }
>>> void >>> 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: to call it:
val taskAlias = fun someTask() {
println("Hello")
}
// call the callable stored in the var // call the callable stored in the var
taskAlias() taskAlias()
// or directly: // or directly:
someTask() someTask()
>>> Hello
>>> Hello
>>> void
If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?) If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?)
@ -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. 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. 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 # Comments
// single line comment // 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 | | Null | missing value, singleton | null |
| Fn | callable type | | | Fn | callable type | |
See also [math operations](math.md)
## String details ## String details
### String operations ### String operations

View File

@ -498,11 +498,16 @@ class Compiler {
// Here we should be at open body // Here we should be at open body
val fnStatements = parseBlock(tokens) val fnStatements = parseBlock(tokens)
val fnBody = statement(t.pos) { context -> var closure: Context? = null
// load params
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()) { for ((i, d) in params.withIndex()) {
if (i < context.args.size) if (i < callerContext.args.size)
context.addItem(d.name, false, context.args.list[i].value) context.addItem(d.name, false, callerContext.args.list[i].value)
else else
context.addItem( context.addItem(
d.name, d.name,
@ -514,10 +519,12 @@ class Compiler {
) )
) )
} }
// save closure
fnStatements.execute(context) fnStatements.execute(context)
} }
return statement(start) { context -> return statement(start) { context ->
println("adding function $name to context $context")
closure = context
context.addItem(name, false, fnBody) context.addItem(name, false, fnBody)
fnBody fnBody
} }

View File

@ -5,24 +5,21 @@ class Context(
val args: Arguments = Arguments.EMPTY val args: Arguments = Arguments.EMPTY
) { ) {
data class Item( private val objects = mutableMapOf<String, StoredObj>()
val name: String,
var value: Obj?,
val isMutable: Boolean = false
)
private val objects = mutableMapOf<String, Item>() operator fun get(name: String): StoredObj? =
objects[name]
operator fun get(name: String): Item? = objects[name] ?: parent?.get(name) ?: parent?.get(name)
fun copy(args: Arguments = Arguments.EMPTY): Context = Context(this, args) fun copy(args: Arguments = Arguments.EMPTY): Context = Context(this, args)
fun addItem(name: String, isMutable: Boolean, value: Obj?) { 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) = 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 .context
inline fun <reified T> addFn(vararg names: String, crossinline fn: suspend Context.() -> T) { inline fun <reified T> addFn(vararg names: String, crossinline fn: suspend Context.() -> T) {

View File

@ -1,33 +1,77 @@
package net.sergeych.ling package net.sergeych.ling
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.math.floor 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 @Serializable
sealed class Obj : Comparable<Obj> { sealed class Obj : Comparable<Obj> {
open val asStr: ObjString by lazy { open val asStr: ObjString by lazy {
if (this is ObjString) this else ObjString(this.toString()) if (this is ObjString) this else ObjString(this.toString())
} }
open val type: Type = Type.Any open val definition: ClassDef = ObjClassDef
@Suppress("unused") @Suppress("unused")
enum class Type { enum class Type {
@SerialName("Void") @SerialName("Void")
Void, Void,
@SerialName("Null") @SerialName("Null")
Null, Null,
@SerialName("String") @SerialName("String")
String, String,
@SerialName("Int") @SerialName("Int")
Int, Int,
@SerialName("Real") @SerialName("Real")
Real, Real,
@SerialName("Bool") @SerialName("Bool")
Bool, Bool,
@SerialName("Fn") @SerialName("Fn")
Fn, Fn,
@SerialName("Any") @SerialName("Any")
Any, Any,
} }
@ -112,7 +156,8 @@ fun Obj.toLong(): Long =
fun Obj.toInt(): Int = toLong().toInt() 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 @Serializable
@ -125,7 +170,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override val toObjReal: ObjReal by lazy { ObjReal(value) } override val toObjReal: ObjReal by lazy { ObjReal(value) }
override fun compareTo(other: Obj): Int { 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) 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 val toObjReal: ObjReal by lazy { ObjReal(doubleValue) }
override fun compareTo(other: Obj): Int { 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) 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 val asStr by lazy { ObjString(value.toString()) }
override fun compareTo(other: Obj): Int { 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) return value.compareTo(other.value)
} }
override fun toString(): String = value.toString() override fun toString(): String = value.toString()
} }

View File

@ -9,6 +9,7 @@ class Script(
override suspend fun execute(context: Context): Obj { override suspend fun execute(context: Context): Obj {
// todo: run script // todo: run script
println("exec script in $context <- ${context.parent}")
var lastResult: Obj = ObjVoid var lastResult: Obj = ObjVoid
for (s in statements) { for (s in statements) {
lastResult = s.execute(context) 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() } val sourceLines by lazy { code.lines() }
override fun toString(): String { 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 detailedString by lazy {
val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line}: $s" }.joinToString("\n") val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line}: $s" }.joinToString("\n")
"$this\n" + var result = "$this\n$codeWithLines\n"
codeWithLines + "\n" + if (expectedOutput.isNotBlank())
"--------expected output--------\n" + result += "--------expected output--------\n$expectedOutput\n"
expectedOutput +
"-----expected return value-----\n" + "$result-----expected return value-----\n$expectedResult"
expectedResult
} }
} }
fun parseDocTests(name: String): Flow<DocTest> = flow { fun parseDocTests(fileName: String): Flow<DocTest> = flow {
val book = readAllLines(Paths.get("../docs/tutorial.md")) val book = readAllLines(Paths.get(fileName))
var startOffset = 0 var startOffset = 0
val block = mutableListOf<String>() val block = mutableListOf<String>()
var startIndex = 0 var startIndex = 0
@ -94,10 +93,10 @@ fun parseDocTests(name: String): Flow<DocTest> = flow {
if (isValid) { if (isValid) {
emit( emit(
DocTest( DocTest(
name, startIndex, fileName, startIndex,
block.joinToString("\n"), block.joinToString("\n"),
if (result.size > 1) if (result.size > 1)
result.dropLast(1).joinToString { it + "\n" } result.dropLast(1).joinToString("") { it + "\n" }
else "", else "",
result.last() result.last()
) )
@ -149,8 +148,7 @@ suspend fun DocTest.test() {
var error: Throwable? = null var error: Throwable? = null
val result = try { val result = try {
context.eval(code) context.eval(code)
} } catch (e: Throwable) {
catch (e: Throwable) {
error = e error = e
null null
}?.toString()?.replace(Regex("@\\d+"), "@...") }?.toString()?.replace(Regex("@\\d+"), "@...")
@ -166,12 +164,27 @@ suspend fun DocTest.test() {
// println("OK: $this") // println("OK: $this")
} }
suspend fun runDocTests(fileName: String) {
parseDocTests(fileName).collect { dt ->
dt.test()
}
}
class BookTest { class BookTest {
@Test @Test
fun testsFromTutorial() = runTest { fun testsFromTutorial() = runTest {
parseDocTests("../docs/tutorial.md").collect { dt -> runDocTests("../docs/tutorial.md")
dt.test() }
}
@Test
fun testsFromMath() = runTest {
runDocTests("../docs/math.md")
}
@Test
fun testsFromAdvanced() = runTest {
runDocTests("../docs/advanced_topics.md")
} }
} }