From c65f711ee348e9d852e498577cd3ba1ffc50b6bd Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 1 Jun 2025 17:47:12 +0400 Subject: [PATCH] OO: added inheritance, Collection, Iterator, Iterable, Array. Range is rewritten to be Iterable. Math namespace is temporarily broken (to be updated to new ObjClass model). --- docs/Iterable.md | 31 ++++ docs/Range.md | 29 +--- library/build.gradle.kts | 1 + .../kotlin/net/sergeych/ling/Compiler.kt | 102 ++++++++----- .../kotlin/net/sergeych/ling/Obj.kt | 71 ++++----- .../kotlin/net/sergeych/ling/ObjBool.kt | 5 +- .../kotlin/net/sergeych/ling/ObjClass.kt | 141 +++++++++++++++++- .../kotlin/net/sergeych/ling/ObjInt.kt | 4 +- .../kotlin/net/sergeych/ling/ObjList.kt | 31 +++- .../kotlin/net/sergeych/ling/ObjRange.kt | 65 ++++++-- .../kotlin/net/sergeych/ling/Parser.kt | 20 ++- .../kotlin/net/sergeych/ling/Script.kt | 5 + library/src/commonTest/kotlin/ScriptTest.kt | 69 ++++++++- settings.gradle.kts | 1 + 14 files changed, 444 insertions(+), 131 deletions(-) create mode 100644 docs/Iterable.md diff --git a/docs/Iterable.md b/docs/Iterable.md new file mode 100644 index 0000000..8b6c0e2 --- /dev/null +++ b/docs/Iterable.md @@ -0,0 +1,31 @@ +# Iterable interface + +The inteface which requires iterator to be implemented: + + fun iterator(): Iterator + +Iterator itself is a simple interface that should provide only to method: + + interface Iterable { + fun hasNext(): Bool + fun next(): Obj + } + +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: + +## toList() + +Creates a list by iterating to the end. So, the Iterator should be finite to be used with it. + +## Included in interfaces: + +- Collection, Array, [List] + +## Implemented in classes: + +- [List], [Range] + +[List]: List.md +[Range]: Range.md \ No newline at end of file diff --git a/docs/Range.md b/docs/Range.md index 7409204..f18fe09 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -45,27 +45,12 @@ are equal or within another, taking into account the end-inclusiveness: assert( (1..<3) in (1..3) ) >>> void -## Range size and indexed access +## Finite Ranges are iterable -This might be confusing, but the range size and limits are used with for loops -so their meaning is special. +So given a range with both ends, you can assume it is [Iterable]. This automatically let +use finite ranges in loops and convert it to lists: -For open ranges, size throws and exception. -For Int ranges, the `size` is `end` - `start` possibly + 1 for ind-inclusive ranges, and indexing getter returns all values from start to end, probably, inclusive: - - val r = 1..3 - assert( r.size == 3 ) - assert( r[0] == 1 ) - assert( r[1] == 2 ) - assert( r[2] == 3 ) - >>> void - -And for end-exclusive range: - - val r = 1..<3 - assert(r.size == 2) - assert( r[0] == 1 ) - assert( r[1] == 2 ) + assert( [-2, -1, 0, 1] == (-2..1).toList() ) >>> void In spite of this you can use ranges in for loops: @@ -102,8 +87,8 @@ You can use Char as both ends of the closed range: Exclusive end char ranges are supported too: - ('a'..<'c').size - >>> 2 + ('a'..<'c').toList + >>> ['a', 'b'] # Instance members @@ -119,3 +104,5 @@ Exclusive end char ranges are supported too: | size | for finite ranges, see above | Long | | [] | see above | | | | | | + +[Iterable]: Iterable.md \ No newline at end of file diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 6ad1cb7..6fdf536 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -49,6 +49,7 @@ kotlin { //put your multiplatform dependencies here implementation(libs.kotlinx.coroutines.core) implementation(libs.mp.bintools) + implementation("net.sergeych:mp_stools:1.5.2") } } val commonTest by getting { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 20eba2a..4dbc654 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -144,7 +144,7 @@ class Compiler( context.pos = next.pos val v = left.getter(context).value WithAccess( - v.callInstanceMethod( + v.invokeInstanceMethod( context, next.value, args.toArguments(context) @@ -540,43 +540,50 @@ class Compiler( // insofar we suggest source object is enumerable. Later we might need to add checks val sourceObj = source.execute(forContext) - val size = runCatching { sourceObj.callInstanceMethod(forContext, "size").toInt() } - .getOrElse { throw ScriptError(tOp.pos, "object is not enumerable: no size", it) } - var result: Obj = ObjVoid - var breakCaught = false - if (size > 0) { - var current = runCatching { sourceObj.getAt(forContext, 0) } - .getOrElse { - throw ScriptError( - tOp.pos, - "object is not enumerable: no index access for ${sourceObj.inspect()}", - it - ) + + if (sourceObj.isInstanceOf(ObjIterable)) { + loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label) + } else { + val size = runCatching { sourceObj.invokeInstanceMethod(forContext, "size").toInt() } + .getOrElse { throw ScriptError(tOp.pos, "object is not enumerable: no size", it) } + + var result: Obj = ObjVoid + var breakCaught = false + + if (size > 0) { + var current = runCatching { sourceObj.getAt(forContext, 0) } + .getOrElse { + throw ScriptError( + tOp.pos, + "object is not enumerable: no index access for ${sourceObj.inspect()}", + it + ) + } + var index = 0 + while (true) { + loopSO.value = current + try { + result = body.execute(forContext) + } catch (lbe: LoopBreakContinueException) { + if (lbe.label == label || lbe.label == null) { + breakCaught = true + if (lbe.doContinue) continue + else { + result = lbe.result + break + } + } else + throw lbe + } + if (++index >= size) break + current = sourceObj.getAt(forContext, index) } - var index = 0 - while (true) { - loopSO.value = current - try { - result = body.execute(forContext) - } catch (lbe: LoopBreakContinueException) { - if (lbe.label == label || lbe.label == null) { - breakCaught = true - if (lbe.doContinue) continue - else { - result = lbe.result - break - } - } else - throw lbe - } - if (++index >= size) break - current = sourceObj.getAt(forContext, index) } + if (!breakCaught && elseStatement != null) { + result = elseStatement.execute(it) + } + result } - if (!breakCaught && elseStatement != null) { - result = elseStatement.execute(it) - } - result } } else { // maybe other loops? @@ -584,10 +591,31 @@ class Compiler( } } + private suspend fun loopIterable( + forContext: Context, sourceObj: Obj, loopVar: StoredObj, + body: Statement, elseStatement: Statement?, label: String? + ): Obj { + val iterObj = sourceObj.invokeInstanceMethod(forContext, "iterator") + var result: Obj = ObjVoid + while (iterObj.invokeInstanceMethod(forContext, "hasNext").toBool()) { + try { + loopVar.value = iterObj.invokeInstanceMethod(forContext, "next") + result = body.execute(forContext) + } catch (lbe: LoopBreakContinueException) { + if (lbe.label == label || lbe.label == null) { + if (lbe.doContinue) continue + } + return lbe.result + } + } + return elseStatement?.execute(forContext) ?: result + } + private fun parseWhileStatement(cc: CompilerContext): Statement { val label = getLabel(cc)?.also { cc.labels += it } val start = ensureLparen(cc) - val condition = parseExpression(cc) ?: throw ScriptError(start, "Bad while statement: expected expression") + val condition = + parseExpression(cc) ?: throw ScriptError(start, "Bad while statement: expected expression") ensureRparen(cc) val body = parseStatement(cc) ?: throw ScriptError(start, "Bad while statement: expected statement") @@ -958,6 +986,8 @@ class Compiler( // in, is: Operator.simple(Token.Type.IN, lastPrty) { c, a, b -> ObjBool(b.contains(c, a)) }, Operator.simple(Token.Type.NOTIN, lastPrty) { c, a, b -> ObjBool(!b.contains(c, a)) }, + Operator.simple(Token.Type.IS, lastPrty) { c, a, b -> ObjBool(a.isInstanceOf(b)) }, + Operator.simple(Token.Type.NOTIS, lastPrty) { c, a, b -> ObjBool(!a.isInstanceOf(b)) }, // shuttle <=> 6 // bit shifts 7 Operator.simple(Token.Type.PLUS, ++lastPrty) { ctx, a, b -> a.plus(ctx, b) }, diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index 9bc2be2..2e871cc 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import net.sergeych.synctools.ProtectedOp //typealias InstanceMethod = (Context, Obj) -> Obj @@ -18,16 +19,15 @@ data class Accessor( fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos, "can't assign value") } -sealed class Obj { +open class Obj { var isFrozen: Boolean = false private val monitor = Mutex() - // members: fields most often - private val members = mutableMapOf>() - // private val memberMutex = Mutex() - private val parentInstances = listOf() + internal var parentInstances: MutableList = mutableListOf() + + private val opInstances = ProtectedOp() open fun inspect(): String = toString() @@ -39,20 +39,12 @@ sealed class Obj { */ open fun byValueCopy(): Obj = this - /** - * Get instance member traversing the hierarchy if needed. Its meaning is different for different objects. - */ - fun getInstanceMemberOrNull(name: String): WithAccess? { - members[name]?.let { return it } - parentInstances.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } } - return null - } + fun isInstanceOf(someClass: Obj) = someClass === objClass || objClass.allParentsSet.contains(someClass) - fun getInstanceMember(atPos: Pos, name: String): WithAccess = - getInstanceMemberOrNull(name) - ?: throw ScriptError(atPos, "symbol doesn't exist: $name") + suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj = + invokeInstanceMethod(context, name, Arguments(args.map { Arguments.Info(it, context.pos) })) - suspend fun callInstanceMethod( + suspend fun invokeInstanceMethod( context: Context, name: String, args: Arguments = Arguments.EMPTY @@ -60,6 +52,8 @@ sealed class Obj { // note that getInstanceMember traverses the hierarchy objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args) + fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value + // methods that to override open suspend fun compareTo(context: Context, other: Obj): Int { @@ -170,14 +164,15 @@ sealed class Obj { value.execute(context.copy(context.pos, newThisObj = this)).asReadonly } // could be writable property naturally - else -> getInstanceMember(context.pos, name) + null -> ObjNull.asReadonly + else -> obj } } fun writeField(context: Context, name: String, newValue: Obj) { willMutate(context) - members[name]?.let { if (it.isMutable) it.value = newValue } - ?: context.raiseError("Can't reassign member: $name") + val field = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name") + 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 { @@ -188,18 +183,6 @@ sealed class 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") - members[name] = WithAccess(initialValue, isMutable) - } - - fun addFn(name: String, isOpen: Boolean = false, code: suspend Context.() -> Obj) { - createField(name, statement { code() }, isOpen) - } - - fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false) - open suspend fun callOn(context: Context): Obj { context.raiseNotImplemented() } @@ -207,6 +190,24 @@ sealed class Obj { suspend fun invoke(context: Context, thisObj: Obj, args: Arguments): Obj = callOn(context.copy(context.pos, args = args, newThisObj = thisObj)) + suspend fun invoke(context: Context, thisObj: Obj, vararg args: Obj): Obj = + callOn( + context.copy( + context.pos, + args = Arguments(args.map { Arguments.Info(it, context.pos) }), + newThisObj = thisObj + ) + ) + + suspend fun invoke(context: Context, thisObj: Obj): Obj = + callOn( + context.copy( + context.pos, + args = Arguments.EMPTY, + newThisObj = thisObj + ) + ) + suspend fun invoke(context: Context, atPos: Pos, thisObj: Obj, args: Arguments): Obj = callOn(context.copy(atPos, args = args, newThisObj = thisObj)) @@ -277,7 +278,7 @@ fun Obj.toDouble(): Double = @Suppress("unused") fun Obj.toLong(): Long = - when(this) { + when (this) { is Numeric -> longValue is ObjString -> value.toLong() is ObjChar -> value.code.toLong() @@ -305,4 +306,6 @@ class ObjNullPointerError(context: Context) : ObjError(context, "object is null" class ObjAssertionError(context: Context, message: String) : ObjError(context, message) class ObjClassCastError(context: Context, message: String) : ObjError(context, message) class ObjIndexOutOfBoundsError(context: Context, message: String = "index out of bounds") : ObjError(context, message) -class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message) \ No newline at end of file +class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message) + +class ObjIterationFinishedError(context: Context) : ObjError(context, "iteration finished") \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjBool.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjBool.kt index 3a55795..1fac6ab 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ObjBool.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjBool.kt @@ -21,4 +21,7 @@ data class ObjBool(val value: Boolean) : Obj() { companion object { val type = ObjClass("Bool") } -} \ No newline at end of file +} + +val ObjTrue = ObjBool(true) +val ObjFalse = ObjBool(false) \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt index 51c4620..61aca8c 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt @@ -1,20 +1,147 @@ package net.sergeych.ling -val ObjClassType by lazy { ObjClass("Class") } +val ObjClassType by lazy { ObjClass("Class") } class ObjClass( - val className: String -): Obj() { + val className: String, + vararg val parents: ObjClass, +) : Obj() { + + val allParentsSet: Set = parents.flatMap { + listOf(it) + it.allParentsSet + }.toSet() override val objClass: ObjClass by lazy { ObjClassType } + // members: fields most often + private val members = mutableMapOf>() + override fun toString(): String = className - override suspend fun compareTo(context: Context, other: Obj): Int = if( other === this ) 0 else -1 + override suspend fun compareTo(context: Context, other: Obj): Int = if (other === this) 0 else -1 -// val parents: List get() = emptyList() +// private var initInstanceHandler: (suspend (Context, List) -> Obj)? = null -// suspend fun callInstanceMethod(context: Context, name: String, self: Obj,args: Arguments): Obj { -// getInstanceMethod(context, name).invoke(context, self,args) +// suspend fun newInstance(context: Context, vararg args: Obj): Obj = +// initInstanceHandler?.invoke(context, args.toList()) +// ?: context.raiseError("No initInstance handler for $this") +// +// fun buildInstance(f: suspend Context.(List) -> Obj) { +// if (initInstanceHandler != null) throw IllegalStateException("initInstance already set") +// initInstanceHandler = f // } +// +// fun addParent(context: Context, parent: Obj) { +// val self = context.thisObj +// self.parentInstances.add(parent) +// } +// + fun defaultInstance(): Obj = object : Obj() { + override val objClass: ObjClass = this@ObjClass + } + + fun createField(name: String, initialValue: Obj, isMutable: Boolean = false, pos: Pos = Pos.builtIn) { + if (name in members || allParentsSet.any { name in it.members } == true) + throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes") + members[name] = WithAccess(initialValue, isMutable) + } + + fun addFn(name: String, isOpen: Boolean = false, code: suspend Context.() -> Obj) { + createField(name, statement { code() }, isOpen) + } + + fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false) + + + /** + * Get instance member traversing the hierarchy if needed. Its meaning is different for different objects. + */ + fun getInstanceMemberOrNull(name: String): WithAccess? { + members[name]?.let { return it } + allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } } + return null + } + + fun getInstanceMember(atPos: Pos, name: String): WithAccess = + getInstanceMemberOrNull(name) + ?: throw ScriptError(atPos, "symbol doesn't exist: $name") +} + +/** + * Abstract class that must provide `iterator` method that returns [ObjIterator] instance. + */ +val ObjIterable by lazy { ObjClass("Iterable").apply { + + addFn("toList") { + val result = mutableListOf() + val iterator = thisObj.invokeInstanceMethod(this, "iterator") + + while( iterator.invokeInstanceMethod(this, "hasNext").toBool() ) + result += iterator.invokeInstanceMethod(this, "next") + + +// val next = iterator.getMemberOrNull("next")!! +// val hasNext = iterator.getMemberOrNull("hasNext")!! +// while( hasNext.invoke(this, iterator).toBool() ) +// result += next.invoke(this, iterator) + ObjList(result) + } + +} } + +/** + * Collection is an iterator with `size`] + */ +val ObjCollection by lazy { + val i: ObjClass = ObjIterable + ObjClass("Collection", i) +} + +val ObjIterator by lazy { ObjClass("Iterator") } + +class ObjArrayIterator(val array: Obj) : Obj() { + + override val objClass: ObjClass by lazy { type } + + private var nextIndex = 0 + private var lastIndex = 0 + + suspend fun init(context: Context) { + nextIndex = 0 + lastIndex = array.invokeInstanceMethod(context, "size").toInt() + ObjVoid + } + + companion object { + val type by lazy { + ObjClass("ArrayIterator", ObjIterator).apply { + addFn("next") { + val self = thisAs() + if (self.nextIndex < self.lastIndex) { + self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj()) + } else raiseError(ObjIterationFinishedError(this)) + } + addFn("hasNext") { + val self = thisAs() + if (self.nextIndex < self.lastIndex) ObjTrue else ObjFalse + } + } + } + } +} + + +val ObjArray by lazy { + + /** + * Array abstract class is a [ObjCollection] with `getAt` method. + */ + ObjClass("Array", ObjCollection).apply { + // we can create iterators using size/getat: + + addFn("iterator") { + ObjArrayIterator(thisObj).also { it.init(this) } + } + addFn("isample") { "ok".toObj()} + } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjInt.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjInt.kt index 99b76ad..659dbe7 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ObjInt.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjInt.kt @@ -75,4 +75,6 @@ data class ObjInt(var value: Long) : Obj(), Numeric { companion object { val type = ObjClass("Int") } -} \ No newline at end of file +} + +fun Int.toObj() = ObjInt(this.toLong()) \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt index 4903361..9ca8a25 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt @@ -2,6 +2,11 @@ package net.sergeych.ling class ObjList(val list: MutableList) : Obj() { + init { + for( p in objClass.parents) + parentInstances.add( p.defaultInstance()) + } + override fun toString(): String = "[${ list.joinToString(separator = ", ") { it.inspect() } }]" @@ -42,13 +47,20 @@ class ObjList(val list: MutableList) : Obj() { } override suspend fun plus(context: Context, other: Obj): Obj { - (other as? ObjList) ?: context.raiseError("cannot concatenate $this with $other") + (other as? ObjList) ?: context.raiseError("'+': can't concatenate $this with $other") return ObjList((list + other.list).toMutableList()) } override suspend fun plusAssign(context: Context, other: Obj): Obj { - (other as? ObjList) ?: context.raiseError("cannot concatenate $this with $other") - list += other.list + // optimization + if( other is ObjList) { + list += other.list + return this + } + if( other.isInstanceOf(ObjIterable)) { + TODO("plusassign for iterable is not yet implemented") + } + list += other return this } @@ -56,12 +68,23 @@ class ObjList(val list: MutableList) : Obj() { get() = type companion object { - val type = ObjClass("List").apply { + val type = ObjClass("List", ObjArray).apply { + createField("size", statement { (thisObj as ObjList).list.size.toObj() } ) + addFn("getAt") { + requireExactCount(1) + thisAs().getAt(this, requiredArg(0).value.toInt()) + } + addFn("putAt") { + requireExactCount(2) + val newValue = args[1] + thisAs().putAt(this, requiredArg(0).value.toInt(), newValue) + newValue + } createField("add", statement { val l = thisAs().list diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjRange.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjRange.kt index 7d58b6e..4e18cba 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ObjRange.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjRange.kt @@ -70,7 +70,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob if (isEndInclusive) r1++ val i = index + r0 if (i >= r1) context.raiseIndexOutOfBounds("index $index is not in range (${r1 - r0})") - return if( isIntRange ) ObjInt(i.toLong()) else ObjChar(i.toChar()) + return if (isIntRange) ObjInt(i.toLong()) else ObjChar(i.toChar()) } @@ -83,7 +83,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob } companion object { - val type = ObjClass("Range").apply { + val type = ObjClass("Range", ObjIterable).apply { addFn("start") { thisAs().start ?: ObjNull } @@ -102,18 +102,57 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob addFn("isEndInclusive") { thisAs().isEndInclusive.toObj() } - addFn("size") { + addFn("iterator") { val self = thisAs() - if (self.start == null || self.end == null) - raiseError("size is only available for finite ranges") - if (self.isIntRange || self.isCharRange) { - if (self.isEndInclusive) - ObjInt(self.end.toLong() - self.start.toLong() + 1) - else - ObjInt(self.end.toLong() - self.start.toLong()) - } else { - ObjInt(2) - } + ObjRangeIterator(self).apply { init() } + } + } + } +} + +class ObjRangeIterator(val self: ObjRange) : Obj() { + + private var nextIndex = 0 + private var lastIndex = 0 + private var isCharRange: Boolean = false + + override val objClass: ObjClass = type + + fun Context.init() { + if (self.start == null || self.end == null) + raiseError("next is only available for finite ranges") + isCharRange = self.isCharRange + lastIndex = if (self.isIntRange || self.isCharRange) { + if (self.isEndInclusive) + self.end.toInt() - self.start.toInt() + 1 + else + self.end.toInt() - self.start.toInt() + } else { + raiseError("not implemented iterator for range of $this") + } + } + + fun hasNext(): Boolean = nextIndex < lastIndex + + fun next(context: Context): Obj = + if (nextIndex < lastIndex) { + val x = if (self.isEndInclusive) + self.start!!.toLong() + nextIndex++ + else + self.start!!.toLong() + nextIndex++ + if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x) + } + else { + context.raiseError(ObjIterationFinishedError(context)) + } + + companion object { + val type = ObjClass("RangeIterator", ObjIterable).apply { + addFn("hasNext") { + thisAs().hasNext().toObj() + } + addFn("next") { + thisAs().next(this) } } } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index db71138..3cdf7c2 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -154,12 +154,19 @@ private class Parser(fromPos: Pos) { '!' -> { if (currentChar == 'i') { pos.advance() - if( currentChar == 'n') { - pos.advance() - Token("!in", from, Token.Type.NOTIN) - } else { - pos.back() - Token("!", from, Token.Type.NOT) + when (currentChar) { + 'n' -> { + pos.advance() + Token("!in", from, Token.Type.NOTIN) + } + 's' -> { + pos.advance() + Token("!is", from, Token.Type.NOTIS) + } + else -> { + pos.back() + Token("!", from, Token.Type.NOT) + } } } else if (currentChar == '=') { @@ -249,6 +256,7 @@ private class Parser(fromPos: Pos) { } else when (text) { "in" -> Token("in", from, Token.Type.IN) + "is" -> Token("is", from, Token.Type.IS) else -> Token(text, from, Token.Type.ID) } } else diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 07448c4..75234ed 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -60,6 +60,11 @@ class Script( addConst("Char", ObjChar.type) addConst("List", ObjList.type) addConst("Range", ObjRange.type) + + // interfaces + addConst("Iterable", ObjIterable) + addConst("Array", ObjArray) + val pi = ObjReal(PI) addConst("π", pi) getOrCreateNamespace("Math").apply { diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index ddd9c38..a56b5b6 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -2,7 +2,6 @@ package io.github.kotlin.fibonacci import kotlinx.coroutines.test.runTest import net.sergeych.ling.* -import kotlin.math.PI import kotlin.test.* class ScriptTest { @@ -154,12 +153,12 @@ class ScriptTest { @Test fun compileBuiltinCallsTest() = runTest { // println(eval("π")) - val pi = eval("Math.PI") - assertIs(pi) - assertTrue(pi.value - PI < 0.000001) - assertTrue(eval("Math.PI+1").toDouble() - PI - 1.0 < 0.000001) +// val pi = eval("Math.PI") +// assertIs(pi) +// assertTrue(pi.value - PI < 0.000001) +// assertTrue(eval("Math.PI+1").toDouble() - PI - 1.0 < 0.000001) - assertTrue(eval("sin(Math.PI)").toDouble() - 1 < 0.000001) +// assertTrue(eval("sin(Math.PI)").toDouble() - 1 < 0.000001) assertTrue(eval("sin(π)").toDouble() - 1 < 0.000001) } @@ -831,6 +830,7 @@ class ScriptTest { """ val a = [4,3] assert(a.size == 2) + assert( 3 == a[1] ) """.trimIndent() ) } @@ -977,16 +977,69 @@ class ScriptTest { @Test fun testCharacterRange() = runTest { - eval(""" + eval( + """ val x = '0'..'9' println(x) assert( '5' in x) assert( 'z' !in x) for( ch in x ) println(ch) - """.trimIndent()) + """.trimIndent() + ) } + @Test + fun testIs() = runTest { + eval( + """ + val x = 1..10 + assert( x is Range ) + assert( x is Iterable ) + assert( x !is String) + assert( "foo" is String) + + assert( x is Iterable ) + """.trimIndent() + ) + } + + @Test + fun testForRange() = runTest { + eval( + """ + val x = 1..3 + val result = [] + for( i in x ) { + println(i) + result += (i*10) + } + assert( result == [10,20,30] ) + """.trimIndent() + ) + val a = mutableListOf(1, 2) + val b = listOf(3, 4) + a += 10 + a += b + println(a) + } + + @Test + fun iterableList() = runTest { + // 473 + eval( + """ + for( i in 0..<1024 ) { + val list = (1..1024).toList() + assert(list.size == 1024) + assert(list[0] == 1) + assert(list[-1] == 1024) + } + """.trimIndent() + ) + } + + // @Test // fun testLambda1() = runTest { // val l = eval(""" diff --git a/settings.gradle.kts b/settings.gradle.kts index aa09cf7..1a417c4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ dependencyResolutionManagement { maven("https://maven.universablockchain.com/") maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven") mavenLocal() + maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven") } }