diff --git a/docs/advanced_topics.md b/docs/advanced_topics.md new file mode 100644 index 0000000..725af9b --- /dev/null +++ b/docs/advanced_topics.md @@ -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. \ No newline at end of file diff --git a/docs/math.md b/docs/math.md index f0228d1..acb3f01 100644 --- a/docs/math.md +++ b/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
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 | diff --git a/docs/tutorial.md b/docs/tutorial.md index 6561a47..f17d05e 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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 diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 52fa331..498b228 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -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 } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 0291ec5..f0b51da 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -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() - private val objects = mutableMapOf() - - 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 addFn(vararg names: String, crossinline fn: suspend Context.() -> T) { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index 20c859e..b01f90e 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -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(var value: T, val isMutable: Boolean = false) + +@Serializable +sealed class ClassDef( + val className: String +) { + val baseClasses: List get() = emptyList() + private val instanceMethods: MutableMap> 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 { 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() } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index b5458ff..28d5bb8 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -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) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt new file mode 100644 index 0000000..e298430 --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/StoredObj.kt @@ -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 +) \ No newline at end of file diff --git a/library/src/jvmTest/kotlin/BookTest.kt b/library/src/jvmTest/kotlin/BookTest.kt index 2d7f382..91be19f 100644 --- a/library/src/jvmTest/kotlin/BookTest.kt +++ b/library/src/jvmTest/kotlin/BookTest.kt @@ -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 = flow { - val book = readAllLines(Paths.get("../docs/tutorial.md")) +fun parseDocTests(fileName: String): Flow = flow { + val book = readAllLines(Paths.get(fileName)) var startOffset = 0 val block = mutableListOf() var startIndex = 0 @@ -94,10 +93,10 @@ fun parseDocTests(name: String): Flow = 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") } } \ No newline at end of file