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) ) assert( (1..<3) in (1..3) )
>>> void >>> 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 # Instance members
| member | description | args | | member | description | args |
|-----------------|----------------------------|---------------| |-----------------|------------------------------|---------------|
| contains(other) | used in `in` | Range, or Any | | contains(other) | used in `in` | Range, or Any |
| inclusiveEnd | true for '..' | Bool | | isEndInclusive | true for '..' | Bool |
| isOpen | at any end | Bool | | isOpen | at any end | Bool |
| isIntRange | both start and end are Int | Bool | | isIntRange | both start and end are Int | Bool |
| start | | Bool | | start | | Bool |
| end | | 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 -> { Token.Type.DOTDOT, Token.Type.DOTDOTLT -> {
// closed-range operator // closed-range operator
val inclusiveEnd = t.type == Token.Type.DOTDOT val isEndInclusive = t.type == Token.Type.DOTDOT
val left = operand val left = operand
val right = parseStatement(cc) val right = parseStatement(cc)
operand = Accessor { operand = Accessor {
ObjRange( ObjRange(
left?.getter?.invoke(it)?.value ?: ObjNull, left?.getter?.invoke(it)?.value ?: ObjNull,
right?.execute(it) ?: ObjNull, right?.execute(it) ?: ObjNull,
inclusiveEnd = inclusiveEnd isEndInclusive = isEndInclusive
).asReadonly ).asReadonly
} }
} }

View File

@ -16,7 +16,10 @@ class Context(
@Suppress("unused") @Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this)) 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 raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg))
fun raiseError(message: String): Nothing { fun raiseError(message: String): Nothing {

View File

@ -52,9 +52,10 @@ sealed class Obj {
getInstanceMemberOrNull(name) getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name") ?: throw ScriptError(atPos, "symbol doesn't exist: $name")
suspend fun callInstanceMethod(context: Context, suspend fun callInstanceMethod(
name: String, context: Context,
args: Arguments = Arguments.EMPTY name: String,
args: Arguments = Arguments.EMPTY
): Obj = ): Obj =
// note that getInstanceMember traverses the hierarchy // note that getInstanceMember traverses the hierarchy
objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args) objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args)
@ -193,7 +194,7 @@ sealed class Obj {
members[name] = WithAccess(initialValue, isMutable) 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) createField(name, statement { code() }, isOpen)
} }
@ -210,7 +211,6 @@ sealed class Obj {
callOn(context.copy(atPos, args = args, newThisObj = thisObj)) callOn(context.copy(atPos, args = args, newThisObj = thisObj))
val asReadonly: WithAccess<Obj> by lazy { WithAccess(this, false) } val asReadonly: WithAccess<Obj> by lazy { WithAccess(this, false) }
val asMutable: WithAccess<Obj> by lazy { WithAccess(this, true) } 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") (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 val objClass: ObjClass = type
override fun toString(): String { override fun toString(): String {
val result = StringBuilder() val result = StringBuilder()
result.append("${start ?: '∞'} ..") result.append("${start ?: '∞'} ..")
if( !inclusiveEnd) result.append('<') if (!isEndInclusive) result.append('<')
result.append(" ${end ?: '∞'}") result.append(" ${end ?: '∞'}")
return result.toString() return result.toString()
} }
suspend fun containsRange(context: Context, other: ObjRange): Boolean { 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: // 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: // 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: // both exists, now there could be 4 cases:
return when { return when {
other.inclusiveEnd && inclusiveEnd -> other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0 end.compareTo(context, other.end) >= 0
!other.inclusiveEnd && !inclusiveEnd ->
!other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) >= 0 end.compareTo(context, other.end) >= 0
other.inclusiveEnd && !inclusiveEnd ->
other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) > 0 end.compareTo(context, other.end) > 0
!other.inclusiveEnd && inclusiveEnd ->
!other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0 end.compareTo(context, other.end) >= 0
else -> throw IllegalStateException("unknown comparison") 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 { override suspend fun contains(context: Context, other: Obj): Boolean {
if( other is ObjRange) if (other is ObjRange)
return containsRange(context, other) return containsRange(context, other)
if (start == null && end == null) return true 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) { if (end != null) {
val cmp = end.compareTo(context, other) 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 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 { val isIntRange: Boolean by lazy {
start is ObjInt && end is ObjInt start is ObjInt && end is ObjInt
} }
companion object { companion object {
val type = ObjClass("Range").apply { val type = ObjClass("Range").apply {
addFn("start" ) { addFn("start") {
thisAs<ObjRange>().start ?: ObjNull thisAs<ObjRange>().start ?: ObjNull
} }
addFn("end") { addFn("end") {
@ -357,8 +379,21 @@ class ObjRange(val start: Obj?, val end: Obj?,val inclusiveEnd: Boolean) : Obj()
addFn("isIntRange") { addFn("isIntRange") {
thisAs<ObjRange>().isIntRange.toObj() thisAs<ObjRange>().isIntRange.toObj()
} }
addFn("inclusiveEnd") { addFn("isEndInclusive") {
thisAs<ObjRange>().inclusiveEnd.toObj() 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 ObjAssertionError(context: Context, message: String) : ObjError(context, message)
class ObjClassCastError(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() } 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 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}") if (i !in list.indices) context.raiseError("index $index out of bounds for size ${list.size}")
return i return i
} }
@ -75,7 +75,7 @@ class ObjList(val list: MutableList<Obj>) : Obj() {
val l = thisAs<ObjList>() val l = thisAs<ObjList>()
var index = l.normalize( var index = l.normalize(
this, requiredArg<ObjInt>(0).value.toInt(), this, requiredArg<ObjInt>(0).value.toInt(),
allowInclusiveEnd = true allowisEndInclusive = true
) )
for (i in 1..<args.size) l.list.add(index++, args[i]) for (i in 1..<args.size) l.list.add(index++, args[i])
ObjVoid ObjVoid

View File

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