diff --git a/docs/Iterable.md b/docs/Iterable.md index 6c2ba71..aa8c4d0 100644 --- a/docs/Iterable.md +++ b/docs/Iterable.md @@ -26,36 +26,58 @@ Just remember at this stage typed declarations are not yet supported. Having `Iterable` in base classes allows to use it in for loop. Also, each `Iterable` has some utility functions available, for example val r = 1..10 // Range is Iterable! - assertEquals( [9,10] r.takeLast(2) ) - assertEquals( [1,2,3] r.take(3) ) - assertEquals( [9,10] r.drop(8) ) - assertEquals( [1,2] r.dropLast(8) ) + assertEquals( [9,10], r.takeLast(2).toList() ) + assertEquals( [1,2,3], r.take(3).toList() ) + assertEquals( [9,10], r.drop(8).toList() ) + assertEquals( [1,2], r.dropLast(8).toList() ) + >>> void + +## joinToString + +This methods convert any iterable to a string joining string representation of each element, optionally transforming it and joining using specified suffix. + + Iterable.joinToString(suffux=' ', transform=null) + +- if `Iterable` `isEmpty`, the empty string `""` is returned. +- `suffix` is inserted between items when there are more than one. +- `transform` of specified is applied to each element, otherwise its `toString()` method is used. + +Here is the sample: + + assertEquals( (1..3).joinToString(), "1 2 3") + assertEquals( (1..3).joinToString(":"), "1:2:3") + assertEquals( (1..3).joinToString { it * 10 }, "10 20 30") >>> void ## Instance methods: -| fun/method | description | -|-----------------|---------------------------------------------------------------------------------| -| toList() | create a list from iterable | -| toSet() | create a set from iterable | -| contains(i) | check that iterable contains `i` | -| `i in iterator` | same as `contains(i)` | -| isEmpty() | check iterable is empty | -| forEach(f) | call f for each element | -| toMap() | create a map from list of key-value pairs (arrays of 2 items or like) | -| map(f) | create a list of values returned by `f` called for each element of the iterable | -| indexOf(i) | return index if the first encounter of i or a negative value if not found | -| associateBy(kf) | create a map where keys are returned by kf that will be called for each element | -| first | first element (1) | -| last | last element (1) | -| take(n) | return [Iterable] of up to n first elements | -| taleLast(n) | return [Iterable] of up to n last elements | -| drop(n) | return new [Iterable] without first n elements | -| dropLast(n) | return new [Iterable] without last n elements | + +| fun/method | description | +|-------------------|---------------------------------------------------------------------------| +| toList() | create a list from iterable | +| toSet() | create a set from iterable | +| contains(i) | check that iterable contains `i` | +| `i in iterator` | same as `contains(i)` | +| isEmpty() | check iterable is empty | +| forEach(f) | call f for each element | +| toMap() | create a map from list of key-value pairs (arrays of 2 items or like) | +| map(f) | create a list of values returned by `f` called for each element of the iterable | +| indexOf(i) | return index if the first encounter of i or a negative value if not found | +| associateBy(kf) | create a map where keys are returned by kf that will be called for each element | +| first | first element (1) | +| last | last element (1) | +| take(n) | return [Iterable] of up to n first elements | +| taleLast(n) | return [Iterable] of up to n last elements | +| drop(n) | return new [Iterable] without first n elements | +| dropLast(n) | return new [Iterable] without last n elements | +| joinToString(s,t) | convert iterable to string, see (2) | (1) : throws `NoSuchElementException` if there is no such element +(2) +: `joinToString(suffix=" ",transform=null)`: suffix is inserted between items if there are more than one, trasnfom is optional function applied to each item that must return result string for an item, otherwise `item.toString()` is used. + fun Iterable.toList(): List fun Iterable.toSet(): Set fun Iterable.indexOf(element): Int diff --git a/docs/tutorial.md b/docs/tutorial.md index 03c61f8..1696f9d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -747,7 +747,7 @@ You can thest that _when expression_ is _contained_, or not contained, in some o `!in container`. The container is any object that provides `contains` method, otherwise the runtime exception will be thrown. -Typical builtin types that are containers (e.g. support `conain`): +Typical builtin types that are containers (e.g. support `contains`): | class | notes | |------------|------------------------------------------------| @@ -756,6 +756,8 @@ Typical builtin types that are containers (e.g. support `conain`): | List | faster than Array's | | String | character in string or substring in string (3) | | Range | object is included in the range (2) | +| Buffer | byte is in buffer | +| RingBuffer | object is in buffer | (1) : Iterable is not the container as it can be infinite @@ -1296,18 +1298,20 @@ if blank, will be removed too, for example: See [math functions](math.md). Other general purpose functions are: -| name | description | -|--------------------------------------------|-----------------------------------------------------------| -| assert(condition,message="assertion failed") | runtime code check. There will be an option to skip them | -| assertEquals(a,b) | | -| assertNotEquals(a,b) | | -| assertTrows { /* block */ } | | -| check(condition, message=) | throws IllegalStateException" of condition isn't met | -| require(condition, message=) | throws IllegalArgumentException" of condition isn't met | -| println(args...) | Open for overriding, it prints to stdout with newline. | -| print(args...) | Open for overriding, it prints to stdout without newline. | -| flow {} | create flow sequence, see [parallelism] | -| delay, launch, yield | see [parallelism] | +| name | description | +|----------------------------------------------|------------------------------------------------------------| +| assert(condition,message="assertion failed") | runtime code check. There will be an option to skip them | +| assertEquals(a,b) | | +| assertNotEquals(a,b) | | +| assertTrows { /* block */ } | | +| check(condition, message=) | throws IllegalStateException" of condition isn't met | +| require(condition, message=) | throws IllegalArgumentException" of condition isn't met | +| println(args...) | Open for overriding, it prints to stdout with newline. | +| print(args...) | Open for overriding, it prints to stdout without newline. | +| flow {} | create flow sequence, see [parallelism] | +| delay, launch, yield | see [parallelism] | +| cached(builder) | remembers builder() on first invocation and return it then | + # Built-in constants diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index c2e64dd..f29003d 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "0.8.9-SNAPSHOT" +version = "0.8.10-SNAPSHOT" buildscript { repositories { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 67d7b2a..0d39aca 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -87,7 +87,7 @@ class Compiler( statements += it } if (s == null) { - when( t.type ) { + when (t.type) { Token.Type.RBRACE, Token.Type.EOF, Token.Type.SEMICOLON -> {} else -> throw ScriptError(t.pos, "unexpeced `${t.value}` here") @@ -114,7 +114,7 @@ class Compiler( return result.toString() } - private var lastAnnotation: (suspend (Scope, ObjString,Statement) -> Statement)? = null + private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? { lastAnnotation = null @@ -138,6 +138,7 @@ class Compiler( lastAnnotation = parseAnnotation(t) continue } + Token.Type.LABEL -> continue Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue @@ -843,7 +844,7 @@ class Compiler( return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number") } - suspend fun parseAnnotation(t: Token): (suspend (Scope, ObjString,Statement)->Statement) { + suspend fun parseAnnotation(t: Token): (suspend (Scope, ObjString, Statement) -> Statement) { val extraArgs = parseArgsOrNull() println("annotation ${t.value}: args: $extraArgs") return { scope, name, body -> @@ -851,14 +852,14 @@ class Compiler( val required = listOf(name, body) val args = extras?.let { required + it } ?: required val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}") - if( fn !is Statement ) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}") + if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}") (fn.execute(scope.copy(Arguments(args))) as? Statement) ?: scope.raiseClassCastError("function annotation must return callable") } } suspend fun parseArgsOrNull(): Pair, Boolean>? = - if( cc.skipNextIf(Token.Type.LPAREN)) + if (cc.skipNextIf(Token.Type.LPAREN)) parseArgs() else null @@ -1173,22 +1174,24 @@ class Compiler( do { val t = cc.skipWsTokens() - when(t.type) { + when (t.type) { Token.Type.ID -> { names += t.value val t1 = cc.skipWsTokens() - when(t1.type) { + when (t1.type) { Token.Type.COMMA -> continue + Token.Type.RBRACE -> break else -> { t1.raiseSyntax("unexpected token") } } } + else -> t.raiseSyntax("expected enum entry name") } - } while(true) + } while (true) return statement { ObjEnumClass.createSimpleEnum(nameToken.value, names).also { @@ -1263,7 +1266,6 @@ class Compiler( newClass.classScope = classScope for (s in initScope) s.execute(classScope) - .also { println("executed, ${classScope.objects}") } } newClass } @@ -1766,24 +1768,31 @@ class Compiler( val eqToken = cc.next() var setNull = false - if (eqToken.type != Token.Type.ASSIGN) { - if (!isMutable) - throw ScriptError(start, "val must be initialized") - else { - cc.previous() - setNull = true + + val isDelegate = if (eqToken.isId("by")) { + true + } else { + if (eqToken.type != Token.Type.ASSIGN) { + if (!isMutable) + throw ScriptError(start, "val must be initialized") + else { + cc.previous() + setNull = true + } } + false } - val initialExpression = if (setNull) null else parseStatement(true) + val initialExpression = if (setNull) null + else parseStatement(true) ?: throw ScriptError(eqToken.pos, "Expected initializer expression") if (isStatic) { // find objclass instance: this is tricky: this code executes in object initializer, // when creating instance, but we need to execute it in the class initializer which // is missing as for now. Add it to the compiler context? - // add there - // return + + if (isDelegate) throw ScriptError(start, "static delegates are not yet implemented") currentInitScope += statement { val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull (thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, pos) @@ -1797,12 +1806,31 @@ class Compiler( if (context.containsLocal(name)) throw ScriptError(nameToken.pos, "Variable $name is already defined") - // init value could be a val; when we initialize by-value type var with it, we need to - // create a separate copy: - val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull - - context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field) - initValue + if (isDelegate) { + println("initial expr = $initialExpression") + val initValue = + (initialExpression?.execute(context.copy(Arguments(ObjString(name)))) as? Statement) + ?.execute(context.copy(Arguments(ObjString(name)))) + ?: context.raiseError("delegate initialization required") + println("delegate init: $initValue") + if (!initValue.isInstanceOf(ObjArray)) + context.raiseIllegalArgument("delegate initialized must be an array") + val s = initValue.getAt(context, 1) + val setter = if (s == ObjNull) statement { raiseNotImplemented("setter is not provided") } + else (s as? Statement) ?: context.raiseClassCastError("setter must be a callable") + ObjDelegate( + (initValue.getAt(context, 0) as? Statement) + ?: context.raiseClassCastError("getter must be a callable"), setter + ).also { + context.addItem(name, isMutable, it, visibility, recordType = ObjRecord.Type.Field) + } + } else { + // init value could be a val; when we initialize by-value type var with it, we need to + // create a separate copy: + val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull + context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field) + initValue + } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt index bece6bc..6d5bc97 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt @@ -24,6 +24,10 @@ data class Token(val value: String, val pos: Pos, val type: Type) { val isComment: Boolean by lazy { type == Type.SINLGE_LINE_COMMENT || type == Type.MULTILINE_COMMENT } + fun isId(text: String) = + type == Type.ID && value == text + + @Suppress("unused") enum class Type { ID, INT, REAL, HEX, STRING, CHAR, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index aa338a7..e409cd9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -161,6 +161,8 @@ open class Obj { open suspend fun assign(scope: Scope, other: Obj): Obj? = null + open fun getValue(scope: Scope) = this + /** * a += b * if( the operation is not defined, it returns null and the compiler would try diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDelegate.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDelegate.kt new file mode 100644 index 0000000..572dd38 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDelegate.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng.obj + +import net.sergeych.lyng.Arguments +import net.sergeych.lyng.Scope +import net.sergeych.lyng.Statement +import net.sergeych.lyng.statement + +class ObjDelegateContext() + +class ObjDelegate( + val getter: Statement, + val setter: Statement = statement { raiseNotImplemented("setter is not implemented") } +): Obj() { + + override suspend fun assign(scope: Scope, other: Obj): Obj? { + setter.execute(scope.copy(Arguments(other))) + return other + } + + companion object { + val type = object: ObjClass("Delegate") { + override suspend fun callOn(scope: Scope): Obj { + scope.raiseError("Delegate should not be constructed directly") + } + }.apply { + + } + } + +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt index 6a9f1d3..8c41d89 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt @@ -56,7 +56,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() { } companion object { - val type = ObjClass("RangeIterator", ObjIterable).apply { + val type = ObjClass("RangeIterator", ObjIterator).apply { addFn("hasNext") { thisAs().hasNext().toObj() } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/root_lyng.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/root_lyng.kt index 5a1115c..6639357 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/root_lyng.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/root_lyng.kt @@ -19,6 +19,18 @@ package net.sergeych.lyng.stdlib_included internal val rootLyng = """ package lyng.stdlib +fun cached(builder) { + var calculated = false + var value = null + { + if( !calculated ) { + value = builder() + calculated = true + } + value + } +} + fun Iterable.filter(predicate) { val list = this flow { @@ -38,7 +50,7 @@ fun Iterable.drop(n) { fun Iterable.first() { val i = iterator() if( !i.hasNext() ) throw NoSuchElementException() - i.next() + i.next().also { i.cancelIteration() } } fun Iterable.last() { @@ -70,6 +82,16 @@ fun Iterable.takeLast(n) { for( item in list ) buffer += item buffer } + +fun Iterable.joinToString(prefix=" ", transformer=null) { + var result = null + for( part in this ) { + val transformed = transformer?(part)?.toString() ?: part.toString() + if( result == null ) result = transformed + else result += prefix + transformed + } + result ?: "" +} """.trimIndent() diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 9583316..35b33df 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -2913,5 +2913,33 @@ class ScriptTest { """.trimIndent()) } + @Test + fun cachedTest() = runTest { + eval( """ + + var counter = 0 + var value = cached { + counter++ + "ok" + } + + assertEquals(0, counter) + assertEquals("ok", value()) + assertEquals(1, counter) + assertEquals("ok", value()) + assertEquals(1, counter) + """.trimIndent()) + } + + @Test + fun testJoinToString() = runTest { + eval(""" + assertEquals( (1..3).joinToString(), "1 2 3") + assertEquals( (1..3).joinToString(":"), "1:2:3") + assertEquals( (1..3).joinToString { it * 10 }, "10 20 30") + """.trimIndent()) + } + + } \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/StdlibTest.kt b/lynglib/src/commonTest/kotlin/StdlibTest.kt index 5eb9531..d170e2a 100644 --- a/lynglib/src/commonTest/kotlin/StdlibTest.kt +++ b/lynglib/src/commonTest/kotlin/StdlibTest.kt @@ -40,8 +40,9 @@ class StdlibTest { @Test fun testTake() = runTest { eval(""" - assertEquals([1,2,3], (1..8).take(3).toList() ) - assertEquals([7,8], (1..8).takeLast(2).toList() ) + val r = 1..8 + assertEquals([1,2,3], r.take(3).toList() ) + assertEquals([7,8], r.takeLast(2).toList() ) """.trimIndent()) } diff --git a/lynglib/src/jvmTest/kotlin/BookTest.kt b/lynglib/src/jvmTest/kotlin/BookTest.kt index eb4c63a..333c1bf 100644 --- a/lynglib/src/jvmTest/kotlin/BookTest.kt +++ b/lynglib/src/jvmTest/kotlin/BookTest.kt @@ -317,4 +317,9 @@ class BookTest { runDocTests("../docs/RingBuffer.md") } + @Test + fun testIterable() = runBlocking { + runDocTests("../docs/Iterable.md") + } + } \ No newline at end of file