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