diff --git a/docs/String.md b/docs/String.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/tutorial.md b/docs/tutorial.md index 221e978..dd76c1d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1051,31 +1051,79 @@ Are the same as in string literals with little difference: ## String details -Strings are much like Kotlin ones: +Strings are arrays of Unicode characters. It can be indexed, and indexing will +return a valid Unicode character at position. No utf hassle: - "Hello".length + "Парашют"[5] + >>> 'ю' + +Its length is, of course, in characters: + + "разум".length >>> 5 + And supports growing set of kotlin-borrowed operations, see below, for example: assertEquals("Hell", "Hello".dropLast(1)) >>> void +To format a string use sprintf-style modifiers like: + + val a = "hello" + val b = 11 + assertEquals( "hello:11", "%s:%d"(a, b) ) + assertEquals( " hello: 11", "%6s:%6d"(a, b) ) + assertEquals( "hello :11 ", "%-6s:%-6d"(a, b) ) + >>> void + +List of format specifiers closely resembles C sprintf() one. See [format specifiers](https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary), this is doe using [mp_stools kotlin multiplatform library](https://github.com/sergeych/mp_stools). Currently supported Lyng types are `String`, `Int`, `Real`, `Bool`, the rest are displayed using their `toString()` representation. + +This list will be extended. + +To get the substring use: + + assertEquals("pult", "catapult".takeLast(4)) + assertEquals("cat", "catapult".take(3)) + assertEquals("cat", "catapult".dropLast(5)) + assertEquals("pult", "catapult".drop(4)) + >>> void + +And to get a portion you can slice it with range as the index: + + assertEquals( "tap", "catapult"[ 2 .. 4 ]) + assertEquals( "tap", "catapult"[ 2 ..< 5 ]) + >>> void + +Open-ended ranges could be used to get start and end too: + + assertEquals( "cat", "catapult"[ ..< 3 ]) + assertEquals( "cat", "catapult"[ .. 2 ]) + assertEquals( "pult", "catapult"[ 4.. ]) + >>> void + ### String operations Concatenation is a `+`: `"hello " + name` works as expected. No confusion. Typical set of String functions includes: -| fun/prop | description / notes | -|------------------|------------------------------------------------------------| -| lower() | change case to unicode upper | -| upper() | change case to unicode lower | +| fun/prop | description / notes | +|--------------------|------------------------------------------------------------| +| lower() | change case to unicode upper | +| upper() | change case to unicode lower | | startsWith(prefix) | true if starts with a prefix | -| take(n) | get a new string from up to n first characters | -| takeLast(n) | get a new string from up to n last characters | -| drop(n) | get a new string dropping n first chars, or empty string | -| dropLast(n) | get a new string dropping n last chars, or empty string | -| size | size in characters like `length` because String is [Array] | +| endsWith(prefix) | true if ends with a prefix | +| take(n) | get a new string from up to n first characters | +| takeLast(n) | get a new string from up to n last characters | +| drop(n) | get a new string dropping n first chars, or empty string | +| dropLast(n) | get a new string dropping n last chars, or empty string | +| size | size in characters like `length` because String is [Array] | +| (args...) | sprintf-like formatting, see [string formatting] | +| [index] | character at index | +| [Range] | substring at range | +| s1 + s2 | concatenation | +| s1 += s2 | self-modifying concatenation | + @@ -1111,5 +1159,5 @@ See [math functions](math.md). Other general purpose functions are: [Iterator]: Iterator.md [Real]: Real.md [Range]: Range.md - - +[String]: String.md +[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 02add43..9cc72d4 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "0.6.-SNAPSHOT" +version = "0.6.4-SNAPSHOT" buildscript { repositories { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 414a5f4..18045c7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -33,6 +33,13 @@ data class Arguments(val list: List,val tailBlockMode: Boolean = false) : L return list.first() } + /** + * Convert to list of kotlin objects, see [Obj.toKotlin]. + */ + suspend fun toKotlinList(context: Context): List { + return list.map { it.toKotlin(context) } + } + companion object { val EMPTY = Arguments(emptyList()) fun from(values: Collection) = Arguments(values.toList()) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 95b7f85..535bc0c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -59,7 +59,7 @@ class Compiler( parseBlock(cc) } - Token.Type.RBRACE -> { + Token.Type.RBRACE, Token.Type.RBRACKET -> { cc.previous() return null } @@ -226,8 +226,7 @@ class Compiler( 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") + val i = index.execute(cxt) val x = left.getter(cxt).value if( x == ObjNull && isOptional) ObjNull.asReadonly else x.getAt(cxt, i).asMutable @@ -341,10 +340,10 @@ class Compiler( } Token.Type.DOTDOT, Token.Type.DOTDOTLT -> { - // closed-range operator + // range operator val isEndInclusive = t.type == Token.Type.DOTDOT val left = operand - val right = parseStatement(cc) + val right = parseExpression(cc) operand = Accessor { ObjRange( left?.getter?.invoke(it)?.value ?: ObjNull, @@ -366,11 +365,15 @@ class Compiler( } ?: parseLambdaExpression(cc) } + Token.Type.RBRACKET -> { + cc.previous() + return operand + } else -> { cc.previous() operand?.let { return it } - operand = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression") + operand = parseAccessor(cc) ?: return null //throw ScriptError(t.pos, "Expecting expression") } } } @@ -1115,7 +1118,7 @@ class Compiler( var breakCaught = false if (size > 0) { - var current = runCatching { sourceObj.getAt(forContext, 0) } + var current = runCatching { sourceObj.getAt(forContext, ObjInt(0)) } .getOrElse { throw ScriptError( tOp.pos, @@ -1142,7 +1145,7 @@ class Compiler( } } else result = body.execute(forContext) if (++index >= size) break - current = sourceObj.getAt(forContext, index) + current = sourceObj.getAt(forContext, ObjInt(index.toLong())) } } if (!breakCaught && elseStatement != null) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index 16e06f1..8659df8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -42,6 +42,9 @@ data class Accessor( } open class Obj { + + val isNull by lazy { this === ObjNull } + var isFrozen: Boolean = false private val monitor = Mutex() @@ -176,6 +179,13 @@ open class Obj { context.raiseNotImplemented() } + /** + * Convert Lyng object to its Kotlin counterpart + */ + open suspend fun toKotlin(context: Context): Any? { + return toString() + } + fun willMutate(context: Context) { if (isFrozen) context.raiseError("attempt to mutate frozen object") } @@ -201,7 +211,7 @@ open class Obj { if (field.isMutable) field.value = newValue else context.raiseError("can't assign to read-only field: $name") } - open suspend fun getAt(context: Context, index: Int): Obj { + open suspend fun getAt(context: Context, index: Obj): Obj { context.raiseNotImplemented("indexing") } @@ -297,7 +307,7 @@ object ObjNull : Obj() { context.raiseNPE() } - override suspend fun getAt(context: Context, index: Int): Obj { + override suspend fun getAt(context: Context, index: Obj): Obj { context.raiseNPE() } @@ -310,6 +320,10 @@ object ObjNull : Obj() { } override fun toString(): String = "null" + + override suspend fun toKotlin(context: Context): Any? { + return null + } } interface Numeric { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjBool.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjBool.kt index 772afea..a81cfa2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjBool.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjBool.kt @@ -18,6 +18,10 @@ data class ObjBool(val value: Boolean) : Obj() { override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool()) + override suspend fun toKotlin(context: Context): Any { + return value + } + companion object { val type = ObjClass("Bool") } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt index 68cf650..aad0024 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt @@ -162,7 +162,7 @@ val ObjArray by lazy { addFn("contains", isOpen = true) { val obj = args.firstAndOnly() for( i in 0..< thisObj.invokeInstanceMethod(this, "size").toInt()) { - if( thisObj.getAt(this, i).compareTo(this, obj) == 0 ) return@addFn ObjTrue + if( thisObj.getAt(this, ObjInt(i.toLong())).compareTo(this, obj) == 0 ) return@addFn ObjTrue } ObjFalse } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInt.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInt.kt index 12a0deb..455c032 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInt.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInt.kt @@ -72,6 +72,10 @@ data class ObjInt(var value: Long) : Obj(), Numeric { } else null } + override suspend fun toKotlin(context: Context): Any { + return value + } + companion object { val type = ObjClass("Int") } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt index 00f2b49..87a50b3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt @@ -11,15 +11,15 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { list.joinToString(separator = ", ") { it.inspect() } }]" - fun normalize(context: Context, index: Int, allowisEndInclusive: Boolean = false): Int { + fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int { val i = if (index < 0) list.size + index else index - if (allowisEndInclusive && i == list.size) return i + if (allowsEndInclusive && i == list.size) return i if (i !in list.indices) context.raiseError("index $index out of bounds for size ${list.size}") return i } - override suspend fun getAt(context: Context, index: Int): Obj { - val i = normalize(context, index) + override suspend fun getAt(context: Context, index: Obj): Obj { + val i = normalize(context, index.toInt()) return list[i] } @@ -82,6 +82,10 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { override val objClass: ObjClass get() = type + override suspend fun toKotlin(context: Context): Any { + return list.map { it.toKotlin(context) } + } + companion object { val type = ObjClass("List", ObjArray).apply { @@ -92,7 +96,7 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { ) addFn("getAt") { requireExactCount(1) - thisAs().getAt(this, requiredArg(0).value.toInt()) + thisAs().getAt(this, requiredArg(0)) } addFn("putAt") { requireExactCount(2) @@ -113,7 +117,7 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { val l = thisAs() var index = l.normalize( this, requiredArg(0).value.toInt(), - allowisEndInclusive = true + allowsEndInclusive = true ) for (i in 1..().value.startsWith(requiredArg(0).value)) - }) + addFn("startsWith") { + ObjBool(thisAs().value.startsWith(requiredArg(0).value)) + } + addFn("endsWith") { + ObjBool(thisAs().value.endsWith(requiredArg(0).value)) + } addConst("length", statement { ObjInt(thisAs().value.length.toLong()) } ) diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index e44f62a..ca60393 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -1056,7 +1056,7 @@ class ScriptTest { } @Test - fun testIntOpenRangeInclusive() = runTest { + fun testIntClosedRangeInclusive() = runTest { eval( """ val r = 10 .. 20 @@ -1090,7 +1090,7 @@ class ScriptTest { } @Test - fun testIntOpenRangeExclusive() = runTest { + fun testIntClosedRangeExclusive() = runTest { eval( """ val r = 10 ..< 20 @@ -1126,7 +1126,7 @@ class ScriptTest { } @Test - fun testIntOpenRangeInExclusive() = runTest { + fun testIntClosedRangeInExclusive() = runTest { eval( """ assert( (1..3) !in (1..<3) ) @@ -1135,6 +1135,39 @@ class ScriptTest { ) } + @Test + fun testOpenStartRanges() = runTest { + eval(""" + var r = ..5 + assert( r::class == Range) + assert( r.start == null) + assert( r.end == 5) + assert( r.isEndInclusive) + + r = ..< 5 + assert( r::class == Range) + assert( r.start == null) + assert( r.end == 5) + assert( !r.isEndInclusive) + + assert( r.start == null) + + assert( (-2..3) in r) + assert( (-2..12) !in r) + + """.trimIndent()) + } + + @Test + fun testOpenEndRanges() = runTest { + eval(""" + var r = 5.. + assert( r::class == Range) + assert( r.end == null) + assert( r.start == 5) + """.trimIndent()) + } + @Test fun testCharacterRange() = runTest { eval( @@ -2133,4 +2166,23 @@ class ScriptTest { assert(s.length == 2) """.trimIndent()) } + + @Test + fun testSprintf() = runTest { + eval(""" + assertEquals( "123.45", "%3.2f"(123.451678) ) + assertEquals( "123.45: hello", "%3.2f: %s"(123.451678, "hello") ) + assertEquals( "123.45: true", "%3.2f: %s"(123.451678, true) ) + """.trimIndent()) + } + + @Test + fun testSubstringRangeFailure() = runTest { + eval(""" + assertEquals("pult", "catapult"[4..]) + assertEquals("cat", "catapult"[..2]) + """.trimIndent() + ) + } + } \ No newline at end of file