for in range, range size and indexing

This commit is contained in:
Sergey Chernov 2025-05-31 23:24:31 +04:00
parent 2b3b0b298c
commit dcab60b7bb
6 changed files with 119 additions and 39 deletions

View File

@ -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 |
| 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 | |
| | | |

View File

@ -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
}
}

View File

@ -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 {

View File

@ -52,7 +52,8 @@ sealed class Obj {
getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
suspend fun callInstanceMethod(context: Context,
suspend fun callInstanceMethod(
context: Context,
name: String,
args: Arguments = Arguments.EMPTY
): Obj =
@ -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<Obj> by lazy { WithAccess(this, false) }
val asMutable: WithAccess<Obj> 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<ObjRange>().start ?: ObjNull
}
addFn("end") {
@ -357,8 +379,21 @@ class ObjRange(val start: Obj?, val end: Obj?,val inclusiveEnd: Boolean) : Obj()
addFn("isIntRange") {
thisAs<ObjRange>().isIntRange.toObj()
}
addFn("inclusiveEnd") {
thisAs<ObjRange>().inclusiveEnd.toObj()
addFn("isEndInclusive") {
thisAs<ObjRange>().isEndInclusive.toObj()
}
addFn("size") {
val self = thisAs<ObjRange>()
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)

View File

@ -6,9 +6,9 @@ class ObjList(val list: MutableList<Obj>) : 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>) : Obj() {
val l = thisAs<ObjList>()
var index = l.normalize(
this, requiredArg<ObjInt>(0).value.toInt(),
allowInclusiveEnd = true
allowisEndInclusive = true
)
for (i in 1..<args.size) l.list.add(index++, args[i])
ObjVoid

View File

@ -904,7 +904,7 @@ class ScriptTest {
assert(r.isOpen == false)
assert(r.start == 10)
assert(r.end == 20)
assert(r.inclusiveEnd == true)
assert(r.isEndInclusive == true)
assert(r.isIntRange)
assert(12 in r)
@ -938,7 +938,7 @@ class ScriptTest {
assert(r.isOpen == false)
assert(r.start == 10)
assert(r.end == 20)
assert(r.inclusiveEnd == false)
assert(r.isEndInclusive == false)
assert(r.isIntRange)
assert(12 in r)