diff --git a/docs/List.md b/docs/List.md new file mode 100644 index 0000000..8cf09af --- /dev/null +++ b/docs/List.md @@ -0,0 +1,24 @@ +# List built-in class + +Mutable list of any objects. + +It's class in Ling is `List`: + + [1,2,3]::class + >>> List + +you can use it's class to ensure type: + + []::class == List + >>> true + +## Members + +| name | meaning | type | +|---------|-------------------------------|------| +| `.size` | property returns current size | Int | +| | | | +| | | | +| | | | +| | | | +| | | | diff --git a/docs/Real.md b/docs/Real.md index 361f3c9..8010701 100644 --- a/docs/Real.md +++ b/docs/Real.md @@ -15,11 +15,11 @@ you can use it's class to ensure type: ## Member functions -| name | meaning | type | -|--------------|------------------------------------|------| -| `roundToInt` | round to nearest int like round(x) | Int | -| | | | -| | | | -| | | | -| | | | -| | | | +| name | meaning | type | +|-----------------|------------------------------------|------| +| `.roundToInt()` | round to nearest int like round(x) | Int | +| | | | +| | | | +| | | | +| | | | +| | | | diff --git a/docs/tutorial.md b/docs/tutorial.md index 85086d7..4a6e6fb 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -103,8 +103,7 @@ Notice the parentheses here: the assignment has low priority! These operators return rvalue, unmodifiable. -## Assignemnt return r-value! - +## Assignment return r-value! ## Math @@ -205,7 +204,7 @@ There are default parameters in Ling: } assert( "do: more" == check(10, "do: ") ) check(120) - >>> answer: enough + >>> "answer: enough" ## Closures @@ -245,6 +244,43 @@ to call it: If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?) +# Lists (arrays) + +Ling has built-in mutable array class `List` with simple literals: + + [1, "two", 3.33].size + >>> 3 + +Lists can contain any type of objects, lists too: + + val list = [1, [2, 3], 4] + assert(list.size == 3) + // second element is a list too: + assert(list[1].size == 2) + >>> void + +Notice usage of indexing. + +When you want to "flatten" it to single array, you can use splat syntax: + + [1, ...[2,3], 4] + >>> [1, 2, 3, 4] + +Of course, you can splat from anything that is List (or list-like, but it will be defined later): + + val a = ["one", "two"] + val b = [10.1, 20.2] + ["start", ...b, ...a, "end"] + >>> ["start", 10.1, 20.2, "one", "two", "end"] + +Of course, you can set any array element: + + val a = [1, 2, 3] + a[1] = 200 + a + >>> [1, 200, 3] + + # Flow control operators ## if-then-else @@ -299,7 +335,7 @@ exit value in the case: count = ++count * 10 "wrong "+count } - >>> too much + >>> "too much" ### Breaking nested loops @@ -319,7 +355,7 @@ If you have several loops and want to exit not the inner one, use labels: count = count + 1 count * 10 } - >>> 5/2 situation + >>> "5/2 situation" ### and continue @@ -333,7 +369,7 @@ We can skip the rest of the loop and restart it, as usual, with `continue` opera countEven = countEven + 1 } "found even numbers: " + countEven - >>> found even numbers: 5 + >>> "found even numbers: 5" `continue` can't "return" anything: it just restarts the loop. It can use labeled loops to restart outer ones: diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 430a881..ff89023 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -91,7 +91,7 @@ class Compiler( private fun parseExpressionLevel(tokens: CompilerContext, level: Int = 0): Accessor? { if (level == lastLevel) - return parseTerm3(tokens) + return parseTerm(tokens) var lvalue = parseExpressionLevel(tokens, level + 1) if (lvalue == null) return null @@ -113,80 +113,21 @@ class Compiler( } - /* - Term compiler - - Fn calls could be: - - 1) Fn(...) - 2) thisObj.method(...) - - 1 is a shortcut to this.Fn(...) - - In general, we can assume any Fn to be of the same king, with `this` that always exist, and set by invocation. - - In the case of (1), so called regular, or not bound function, it takes current this from the context. - In the case of (2), bound function, it creates sub-context binding thisObj to `this` in it. - - Suppose we do regular parsing. THen we get lparam = statement, and parse to the `(`. Now we have to - compile the invocation of lparam, which can be thisObj.method or just method(). Unfortunately we - already compiled it and can't easily restore its type, so we have to parse it different way. - - EBNF to parse term having lparam. - - boundcall = "." , identifier, "(" - - We then call instance method bound to `lparam`. - - call = "(', args, ") - - we treat current lparam as callable and invoke it on the current context with current value of 'this. - - Just traversing fields: - - traverse = ".", not (identifier , ".") - - Other cases to parse: - - index = lparam, "[" , ilist , "]" - - - */ - - /** - * Lower level of expr: - * - * assigning expressions: - * - * expr = expr: assignment - * ++expr, expr++, --expr, expr--, - * - * update-assigns: - * expr += expr, ... - * - * Dot!: expr , '.', ID - * Lambda: { } - * index: expr[ ilist ] - * call: ( ilist ) - * self updating: ++expr, expr++, --expr, expr--, expr+=, - * expr-=, expr*=, expr/= - * read expr: - */ - private fun parseTerm3(cc: CompilerContext): Accessor? { + private fun parseTerm(cc: CompilerContext): Accessor? { var operand: Accessor? = null while (true) { val t = cc.next() val startPos = t.pos when (t.type) { - Token.Type.NEWLINE, Token.Type.SEMICOLON, Token.Type.EOF -> { + Token.Type.NEWLINE, Token.Type.SEMICOLON, Token.Type.EOF, Token.Type.RBRACE, Token.Type.COMMA -> { cc.previous() return operand } Token.Type.NOT -> { if (operand != null) throw ScriptError(t.pos, "unexpected operator not '!'") - val op = parseTerm3(cc) ?: throw ScriptError(t.pos, "Expecting expression") + val op = parseTerm(cc) ?: throw ScriptError(t.pos, "Expecting expression") operand = Accessor { op.getter(it).value.logicalNot(it).asReadonly } } @@ -243,6 +184,45 @@ class Compiler( } } + Token.Type.LBRACKET -> { + operand?.let { left -> + // array access + val index = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting index expression") + cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal") + operand = Accessor({ cxt -> + val i = (index.execute(cxt) as? ObjInt)?.value?.toInt() + ?: cxt.raiseError("index must be integer") + left.getter(cxt).value.getAt(cxt, i).asMutable + }) { cxt, newValue -> + val i = (index.execute(cxt) as? ObjInt)?.value?.toInt() + ?: cxt.raiseError("index must be integer") + left.getter(cxt).value.putAt(cxt, i, newValue) + } + } ?: run { + // array literal + val entries = parseArrayLiteral(cc) + // if it didn't throw, ot parsed ot and consumed it all + operand = Accessor { cxt -> + val list = mutableListOf() + for (e in entries) { + when(e) { + is ListEntry.Element -> { + list += e.accessor.getter(cxt).value + } + is ListEntry.Spread -> { + val elements=e.accessor.getter(cxt).value + when { + elements is ObjList -> list.addAll(elements.list) + else -> cxt.raiseError("Spread element must be list") + } + } + } + } + ObjList(list).asReadonly + } + } + } + Token.Type.ID -> { // there could be terminal operators or keywords:// variable to read or like when (t.value) { @@ -276,9 +256,6 @@ class Compiler( operand = parseAccessor(cc) } } - // selector: , '.' , - // we replace operand with selector code, that - // is RW: } Token.Type.PLUS2 -> { @@ -335,6 +312,28 @@ class Compiler( } } + private fun parseArrayLiteral(cc: CompilerContext): List { + // it should be called after LBRACKET is consumed + val entries = mutableListOf() + while(true) { + val t = cc.next() + when(t.type) { + Token.Type.COMMA -> { + // todo: check commas sequences like [,] [,,] before, after or instead of expressions + } + Token.Type.RBRACKET -> return entries + Token.Type.ELLIPSIS -> { + parseExpressionLevel(cc)?.let { entries += ListEntry.Spread(it) } + } + else -> { + cc.previous() + parseExpressionLevel(cc)?.let { entries += ListEntry.Element(it) } + ?: throw ScriptError(t.pos, "invalid list literal: expecting expression") + } + } + } + } + private fun parseScopeOperator(operand: Accessor?, cc: CompilerContext): Accessor { // implement global scope maybe? if (operand == null) throw ScriptError(cc.next().pos, "Expecting expression before ::") @@ -389,7 +388,7 @@ class Compiler( Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> { cc.previous() val n = parseNumber(true, cc) - Accessor{ + Accessor { n.asReadonly } } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 93849d4..40dcdde 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -12,7 +12,7 @@ class Context( ) : this(Script.defaultContext, args, pos) - fun raiseNotImplemented(): Nothing = raiseError("operation not implemented") + fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented") @Suppress("unused") fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this)) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ListEntry.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ListEntry.kt new file mode 100644 index 0000000..82dbb58 --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ListEntry.kt @@ -0,0 +1,7 @@ +package net.sergeych.ling + +sealed class ListEntry { + data class Element(val accessor: Accessor) : ListEntry() + + data class Spread(val accessor: Accessor) : ListEntry() +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index a993e52..0eef63c 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -148,7 +148,19 @@ sealed class Obj { suspend fun sync(block: () -> T): T = monitor.withLock { block() } - fun readField(context: Context, name: String): WithAccess = getInstanceMember(context.pos, name) + suspend fun readField(context: Context, name: String): WithAccess { + // could be property or class field: + val obj = objClass.getInstanceMemberOrNull(name) + val value = obj?.value + return when(value) { + is Statement -> { + // readonly property, important: call it on this + value.execute(context.copy(context.pos, newThisObj = this)).asReadonly + } + // could be writable property naturally + else -> getInstanceMember(context.pos, name) + } + } fun writeField(context: Context, name: String, newValue: Obj) { willMutate(context) @@ -156,6 +168,14 @@ sealed class Obj { ?: context.raiseError("Can't reassign member: $name") } + open suspend fun getAt(context: Context, index: Int): Obj { + context.raiseNotImplemented("indexing") + } + + open suspend fun putAt(context: Context, index: Int, newValue: Obj) { + context.raiseNotImplemented("indexing") + } + fun createField(name: String, initialValue: Obj, isMutable: Boolean = false, pos: Pos = Pos.builtIn) { if (name in members || parentInstances.any { name in it.members }) throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes") @@ -235,7 +255,7 @@ data class ObjString(val value: String) : Obj() { return this.value.compareTo(other.value) } - override fun toString(): String = value + override fun toString(): String = "\"$value\"" override val objClass: ObjClass get() = type @@ -418,6 +438,40 @@ data class ObjBool(val value: Boolean) : Obj() { } } +//open class ObjProperty(var value: Obj =ObjVoid) { +// open suspend fun get(context: Context): Obj = value +// open suspend fun set(context: Context,newValue: Obj): Obj { +// return value.also { value = newValue } +// } +//} + +class ObjList(val list: MutableList) : Obj() { + + override fun toString(): String = "[${list.joinToString(separator = ", ")}]" + + override suspend fun getAt(context: Context, index: Int): Obj { + return list[index] + } + + override suspend fun putAt(context: Context, index: Int, newValue: Obj) { + list[index] = newValue + } + + override val objClass: ObjClass + get() = type + + companion object { + val type = ObjClass("List").apply { + createField("size", + statement(Pos.builtIn) { + (it.thisObj as ObjList).list.size.toObj() + }, + false + ) + } + } +} + data class ObjNamespace(val name: String) : Obj() { override fun toString(): String { return "namespace ${name}" diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index 204eede..ef5cb16 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -33,7 +33,7 @@ private class Parser(fromPos: Pos) { skipws() if (pos.end) return Token("", currentPos, Token.Type.EOF) val from = currentPos - return when (val ch = pos.currentChar.also { advance() }) { + return when (val ch = pos.currentChar.also { pos.advance() }) { '(' -> Token("(", from, Token.Type.LPAREN) ')' -> Token(")", from, Token.Type.RPAREN) '{' -> Token("{", from, Token.Type.LBRACE) @@ -44,7 +44,7 @@ private class Parser(fromPos: Pos) { ';' -> Token(";", from, Token.Type.SEMICOLON) '=' -> { if (pos.currentChar == '=') { - advance() + pos.advance() Token("==", from, Token.Type.EQ) } else Token("=", from, Token.Type.ASSIGN) @@ -53,12 +53,12 @@ private class Parser(fromPos: Pos) { '+' -> { when (currentChar) { '+' -> { - advance() + pos.advance() Token("+", from, Token.Type.PLUS2) } '=' -> { - advance() + pos.advance() Token("+", from, Token.Type.PLUSASSIGN) } @@ -70,12 +70,12 @@ private class Parser(fromPos: Pos) { '-' -> { when (currentChar) { '-' -> { - advance() + pos.advance() Token("--", from, Token.Type.MINUS2) } '=' -> { - advance() + pos.advance() Token("-", from, Token.Type.MINUSASSIGN) } @@ -85,7 +85,7 @@ private class Parser(fromPos: Pos) { '*' -> { if (currentChar == '=') { - advance() + pos.advance() Token("*=", from, Token.Type.STARASSIGN) } else Token("*", from, Token.Type.STAR) @@ -93,24 +93,47 @@ private class Parser(fromPos: Pos) { '/' -> when (currentChar) { '/' -> { - advance() + pos.advance() Token(loadToEnd().trim(), from, Token.Type.SINLGE_LINE_COMMENT) } + '=' -> { - advance() + pos.advance() Token("/=", from, Token.Type.SLASHASSIGN) } + else -> Token("/", from, Token.Type.SLASH) } - '%' -> when(currentChar) { - '=' -> { advance(); Token("%=", from, Token.Type.PERCENTASSIGN) } - else -> Token("%", from, Token.Type.PERCENT) } + '%' -> when (currentChar) { + '=' -> { + pos.advance(); Token("%=", from, Token.Type.PERCENTASSIGN) + } + + else -> Token("%", from, Token.Type.PERCENT) + } + + '.' -> { + // could be: dot, range .. or ..<, or ellipsis ...: + if (currentChar == '.') { + pos.advance() + // .. already parsed: + if (currentChar == '.') { + pos.advance() + Token("...", from, Token.Type.ELLIPSIS) + } else if (currentChar == '<') { + Token("..<", from, Token.Type.DOTDOTLT) + } else { + pos.back() + Token("..", from, Token.Type.DOTDOT) + } + } else + Token(".", from, Token.Type.DOT) + } - '.' -> Token(".", from, Token.Type.DOT) '<' -> { if (currentChar == '=') { - advance() + pos.advance() Token("<=", from, Token.Type.LTE) } else Token("<", from, Token.Type.LT) @@ -118,7 +141,7 @@ private class Parser(fromPos: Pos) { '>' -> { if (currentChar == '=') { - advance() + pos.advance() Token(">=", from, Token.Type.GTE) } else Token(">", from, Token.Type.GT) @@ -126,7 +149,7 @@ private class Parser(fromPos: Pos) { '!' -> { if (currentChar == '=') { - advance() + pos.advance() Token("!=", from, Token.Type.NEQ) } else Token("!", from, Token.Type.NOT) @@ -134,7 +157,7 @@ private class Parser(fromPos: Pos) { '|' -> { if (currentChar == '|') { - advance() + pos.advance() Token("||", from, Token.Type.OR) } else Token("|", from, Token.Type.BITOR) @@ -142,7 +165,7 @@ private class Parser(fromPos: Pos) { '&' -> { if (currentChar == '&') { - advance() + pos.advance() Token("&&", from, Token.Type.AND) } else Token("&", from, Token.Type.BITAND) @@ -158,7 +181,7 @@ private class Parser(fromPos: Pos) { ':' -> { if (currentChar == ':') { - advance() + pos.advance() Token("::", from, Token.Type.COLONCOLON) } else Token(":", from, Token.Type.COLON) @@ -177,7 +200,7 @@ private class Parser(fromPos: Pos) { if (ch.isLetter() || ch == '_') { val text = ch + loadChars(idNextChars) if (currentChar == '@') { - advance() + pos.advance() if (currentChar.isLetter()) { // break@label or like pos.back() @@ -197,19 +220,19 @@ private class Parser(fromPos: Pos) { Token(p1, start, Token.Type.INT) else if (currentChar == '.') { // could be decimal - advance() + pos.advance() if (currentChar in digitsSet) { // decimal part val p2 = loadChars(digits) // with exponent? if (currentChar == 'e' || currentChar == 'E') { - advance() + pos.advance() var negative = false if (currentChar == '+') - advance() + pos.advance() else if (currentChar == '-') { negative = true - advance() + pos.advance() } var p3 = loadChars(digits) if (negative) p3 = "-$p3" @@ -227,7 +250,7 @@ private class Parser(fromPos: Pos) { } else { // could be integer, also hex: if (currentChar == 'x' && p1 == "0") { - advance() + pos.advance() Token(loadChars({ it in hexDigits }), start, Token.Type.HEX).also { if (currentChar.isLetter()) raise("invalid hex literal") @@ -243,15 +266,15 @@ private class Parser(fromPos: Pos) { private fun loadStringToken(): Token { var start = currentPos - if (currentChar == '"') advance() + if (currentChar == '"') pos.advance() else start = start.back() val sb = StringBuilder() while (currentChar != '"') { - if (pos.end) raise("unterminated string") + if (pos.end) throw ScriptError(start, "unterminated string started there") when (currentChar) { '\\' -> { - advance() ?: raise("unterminated string") + pos.advance() ?: raise("unterminated string") when (currentChar) { 'n' -> sb.append('\n') 'r' -> sb.append('\r') @@ -263,11 +286,11 @@ private class Parser(fromPos: Pos) { else -> { sb.append(currentChar) - advance() + pos.advance() } } } - advance() + pos.advance() return Token(sb.toString(), start, Token.Type.STRING) } @@ -287,7 +310,7 @@ private class Parser(fromPos: Pos) { val ch = pos.currentChar if (isValidChar(ch)) { result.append(ch) - advance() + pos.advance() } else break } @@ -314,7 +337,7 @@ private class Parser(fromPos: Pos) { val l = pos.line do { result.append(pos.currentChar) - advance() + pos.advance() } while (pos.line == l) return result.toString() } @@ -327,12 +350,11 @@ private class Parser(fromPos: Pos) { val ch = pos.currentChar if (ch == '\n') break if (ch.isWhitespace()) - advance() + pos.advance() else return ch } return null } - private fun advance() = pos.advance() } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 7e5020e..1706064 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -57,9 +57,8 @@ class Script( addConst("String", ObjString.type) addConst("Int", ObjInt.type) addConst("Bool", ObjBool.type) + addConst("List", ObjList.type) val pi = ObjReal(PI) - val z = pi.objClass - println("PI class $z") addConst("π", pi) getOrCreateNamespace("Math").apply { addConst("PI", pi) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index d687aca..27a8063 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -13,7 +13,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) { EQ, NEQ, LT, LTE, GT, GTE, AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON, SINLGE_LINE_COMMENT, MULTILINE_COMMENT, - LABEL,ATLABEL, // label@ at@label + LABEL, ATLABEL, // label@ at@label + ELLIPSIS, DOTDOT, DOTDOTLT, NEWLINE, EOF, } diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 7a1327c..244e575 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -485,7 +485,8 @@ class ScriptTest { @Test fun testWhileBlockIsolation3() = runTest { - eval(""" + eval( + """ var outer = 7 var sum = 0 var cnt1 = 0 @@ -504,7 +505,7 @@ class ScriptTest { } println("sum "+sum) """.trimIndent() - ) + ) } @Test @@ -713,20 +714,96 @@ class ScriptTest { assertEquals(11, cxt.eval("x").toInt()) } + @Test + fun testValVarConverting() = runTest { + eval( + """ + val x = 5 + var y = x + y = 1 + assert(x == 5) + """.trimIndent() + ) + assertFails { + eval( + """ + val x = 5 + fun fna(t) { + t = 11 + } + fna(1) + """.trimIndent() + ) + } + eval( + """ + var x = 5 + val y = x + x = 10 + assert(y == 5) + assert(x == 10) + """.trimIndent() + ) + } + + @Test + fun testListLiteral() = runTest { + eval(""" + val list = [1,22,3] + assert(list[0] == 1) + assert(list[1] == 22) + assert(list[2] == 3) + """.trimIndent()) + + eval(""" + val x0 = 100 + val list = [x0 + 1, x0 * 10, 3] + assert(list[0] == 101) + assert(list[1] == 1000) + assert(list[2] == 3) + """.trimIndent()) + + eval(""" + val x0 = 100 + val list = [x0 + 1, x0 * 10, if(x0 < 100) "low" else "high", 5] + assert(list[0] == 101) + assert(list[1] == 1000) + assert(list[2] == "high") + assert(list[3] == 5) + """.trimIndent()) + + } + + @Test + fun testListLiteralSpread() = runTest { + eval(""" + val list1 = [1,22,3] + val list = ["start", ...list1, "end"] + assert(list[0] == "start") + assert(list[1] == 1) + assert(list[2] == 22) + assert(list[3] == 3) + assert(list[4] == "end") + """.trimIndent()) + } + + @Test + fun testListSize() = runTest { + eval(""" + val a = [4,3] + assert(a.size == 2) + """.trimIndent()) + } + // @Test -// fun testMultiAssign() = runTest { -// assertEquals( -// 7, -// eval(""" -// var x = 10 -// var y = 2 -// (x = 1) = 5 -// println(x) -// println(y) -// x + y -// """.trimIndent()).toInt() -// ) +// fun testLambda1() = runTest { +// val l = eval(""" +// x = { +// 122 +// } +// x +// """.trimIndent()) +// println(l) // } // - } \ No newline at end of file diff --git a/library/src/jvmTest/kotlin/BookTest.kt b/library/src/jvmTest/kotlin/BookTest.kt index 21b6024..5a99d43 100644 --- a/library/src/jvmTest/kotlin/BookTest.kt +++ b/library/src/jvmTest/kotlin/BookTest.kt @@ -139,7 +139,7 @@ suspend fun DocTest.test() { val context = Context().apply { addFn("println") { for ((i, a) in args.withIndex()) { - if (i > 0) collectedOutput.append(' '); collectedOutput.append(a) + if (i > 0) collectedOutput.append(' '); collectedOutput.append(a.asStr.value) collectedOutput.append('\n') } ObjVoid @@ -198,4 +198,9 @@ class BookTest { runDocTests("../docs/Real.md") } + @Test + fun testFromList() = runTest { + runDocTests("../docs/List.md") + } + } \ No newline at end of file