From dcab60b7bb1335f3d4617de77eb0e9d078274428 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 31 May 2025 23:24:31 +0400 Subject: [PATCH] for in range, range size and indexing --- docs/Range.md | 62 ++++++++++++--- .../kotlin/net/sergeych/ling/Compiler.kt | 4 +- .../kotlin/net/sergeych/ling/Context.kt | 5 +- .../kotlin/net/sergeych/ling/Obj.kt | 77 ++++++++++++++----- .../kotlin/net/sergeych/ling/ObjList.kt | 6 +- library/src/commonTest/kotlin/ScriptTest.kt | 4 +- 6 files changed, 119 insertions(+), 39 deletions(-) diff --git a/docs/Range.md b/docs/Range.md index 6a0b9f7..07215c3 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -45,17 +45,57 @@ are equal or within another, taking into account the end-inclusiveness: assert( (1..<3) in (1..3) ) >>> void +## Range size and indexed access + +This might be confusing, but the range size and limits are used with for loops +so their meaning is special. + +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 ) + >>> void + +In spite of this you can use ranges in for loops: + + for( i in 1..3 ) + println(i) + >>> 1 + >>> 2 + >>> 3 + >>> void + +but + + for( i in 1..<3 ) + println(i) + >>> 1 + >>> 2 + >>> void # Instance members -| member | description | args | -|-----------------|----------------------------|---------------| -| contains(other) | used in `in` | Range, or Any | -| inclusiveEnd | true for '..' | Bool | -| isOpen | at any end | Bool | -| isIntRange | both start and end are Int | Bool | -| start | | Bool | -| end | | Bool | -| | | | -| | | | -| | | | +| member | description | args | +|-----------------|------------------------------|---------------| +| contains(other) | used in `in` | Range, or Any | +| isEndInclusive | true for '..' | Bool | +| isOpen | at any end | Bool | +| isIntRange | both start and end are Int | Bool | +| start | | Bool | +| end | | Bool | +| size | for finite ranges, see above | Long | +| [] | see above | | +| | | | diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 308e5c3..917afdd 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -303,14 +303,14 @@ class Compiler( Token.Type.DOTDOT, Token.Type.DOTDOTLT -> { // closed-range operator - val inclusiveEnd = t.type == Token.Type.DOTDOT + val isEndInclusive = t.type == Token.Type.DOTDOT val left = operand val right = parseStatement(cc) operand = Accessor { ObjRange( left?.getter?.invoke(it)?.value ?: ObjNull, right?.execute(it) ?: ObjNull, - inclusiveEnd = inclusiveEnd + isEndInclusive = isEndInclusive ).asReadonly } } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 1d30f50..b086085 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -16,7 +16,10 @@ class Context( @Suppress("unused") fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this)) - + fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing = + raiseError(ObjIndexOutOfBoundsError(this, message)) + fun raiseArgumentError(message: String = "Illegal argument error"): Nothing = + raiseError(ObjIllegalArgumentError(this, message)) fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg)) fun raiseError(message: String): Nothing { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index 5a840a6..fa5d295 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -52,9 +52,10 @@ sealed class Obj { getInstanceMemberOrNull(name) ?: throw ScriptError(atPos, "symbol doesn't exist: $name") - suspend fun callInstanceMethod(context: Context, - name: String, - args: Arguments = Arguments.EMPTY + suspend fun callInstanceMethod( + context: Context, + name: String, + args: Arguments = Arguments.EMPTY ): Obj = // note that getInstanceMember traverses the hierarchy objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args) @@ -193,7 +194,7 @@ sealed class Obj { members[name] = WithAccess(initialValue, isMutable) } - fun addFn(name: String, isOpen: Boolean = false, code: suspend Context.()->Obj) { + fun addFn(name: String, isOpen: Boolean = false, code: suspend Context.() -> Obj) { createField(name, statement { code() }, isOpen) } @@ -210,7 +211,6 @@ sealed class Obj { callOn(context.copy(atPos, args = args, newThisObj = thisObj)) - val asReadonly: WithAccess by lazy { WithAccess(this, false) } val asMutable: WithAccess by lazy { WithAccess(this, true) } @@ -287,36 +287,40 @@ fun Obj.toBool(): Boolean = (this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean $this") -class ObjRange(val start: Obj?, val end: Obj?,val inclusiveEnd: Boolean) : Obj() { +class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() { override val objClass: ObjClass = type override fun toString(): String { val result = StringBuilder() result.append("${start ?: '∞'} ..") - if( !inclusiveEnd) result.append('<') + if (!isEndInclusive) result.append('<') result.append(" ${end ?: '∞'}") return result.toString() } suspend fun containsRange(context: Context, other: ObjRange): Boolean { - if( start != null ) { + if (start != null) { // our start is not -∞ so other start should be GTE or is not contained: - if( other.start != null && start.compareTo(context, other.start) > 0) return false + if (other.start != null && start.compareTo(context, other.start) > 0) return false } - if( end != null ) { + if (end != null) { // same with the end: if it is open, it can't be contained in ours: - if( other.end == null ) return false + if (other.end == null) return false // both exists, now there could be 4 cases: return when { - other.inclusiveEnd && inclusiveEnd -> + other.isEndInclusive && isEndInclusive -> end.compareTo(context, other.end) >= 0 - !other.inclusiveEnd && !inclusiveEnd -> + + !other.isEndInclusive && !isEndInclusive -> end.compareTo(context, other.end) >= 0 - other.inclusiveEnd && !inclusiveEnd -> + + other.isEndInclusive && !isEndInclusive -> end.compareTo(context, other.end) > 0 - !other.inclusiveEnd && inclusiveEnd -> + + !other.isEndInclusive && isEndInclusive -> end.compareTo(context, other.end) >= 0 + else -> throw IllegalStateException("unknown comparison") } } @@ -325,7 +329,7 @@ class ObjRange(val start: Obj?, val end: Obj?,val inclusiveEnd: Boolean) : Obj() override suspend fun contains(context: Context, other: Obj): Boolean { - if( other is ObjRange) + if (other is ObjRange) return containsRange(context, other) if (start == null && end == null) return true @@ -334,18 +338,36 @@ class ObjRange(val start: Obj?, val end: Obj?,val inclusiveEnd: Boolean) : Obj() } if (end != null) { val cmp = end.compareTo(context, other) - if (inclusiveEnd && cmp < 0 || !inclusiveEnd && cmp <= 0) return false + if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false } return true } + override suspend fun getAt(context: Context, index: Int): Obj { + if( !isIntRange ) { + return when (index) { + 0 -> start ?: ObjNull + 1 -> end ?: ObjNull + else -> context.raiseIndexOutOfBounds("index out of range: $index for max of 2 for non-int ranges") + } + } + // int range, should be finite + val r0 = start?.toInt() ?: context.raiseArgumentError("start is not integer") + var r1 = end?.toInt() ?: context.raiseArgumentError("end is not integer") + if( isEndInclusive ) r1++ + val i = index + r0 + if( i >= r1 ) context.raiseIndexOutOfBounds("index $index is not in range (${r1-r0})") + return ObjInt(i.toLong()) + } + + val isIntRange: Boolean by lazy { start is ObjInt && end is ObjInt } companion object { val type = ObjClass("Range").apply { - addFn("start" ) { + addFn("start") { thisAs().start ?: ObjNull } addFn("end") { @@ -357,8 +379,21 @@ class ObjRange(val start: Obj?, val end: Obj?,val inclusiveEnd: Boolean) : Obj() addFn("isIntRange") { thisAs().isIntRange.toObj() } - addFn("inclusiveEnd") { - thisAs().inclusiveEnd.toObj() + addFn("isEndInclusive") { + thisAs().isEndInclusive.toObj() + } + addFn("size") { + val self = thisAs() + if (self.start == null || self.end == null) + raiseError("size is only available for finite ranges") + if (self.isIntRange) { + if (self.isEndInclusive) + ObjInt(self.end.toLong() - self.start.toLong() + 1) + else + ObjInt(self.end.toLong() - self.start.toLong()) + } else { + ObjInt(2) + } } } } @@ -378,3 +413,5 @@ 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 diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt index 23f992f..4903361 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjList.kt @@ -6,9 +6,9 @@ class ObjList(val list: MutableList) : Obj() { list.joinToString(separator = ", ") { it.inspect() } }]" - fun normalize(context: Context, index: Int, allowInclusiveEnd: Boolean = false): Int { + fun normalize(context: Context, index: Int, allowisEndInclusive: Boolean = false): Int { val i = if (index < 0) list.size + index else index - if (allowInclusiveEnd && i == list.size) return i + if (allowisEndInclusive && i == list.size) return i if (i !in list.indices) context.raiseError("index $index out of bounds for size ${list.size}") return i } @@ -75,7 +75,7 @@ class ObjList(val list: MutableList) : Obj() { val l = thisAs() var index = l.normalize( this, requiredArg(0).value.toInt(), - allowInclusiveEnd = true + allowisEndInclusive = true ) for (i in 1..