Add stepped range iteration

This commit is contained in:
sergeych committed 2026-02-07 05:45:44 +03:00
1 parent 79bece94a5
commit 78d8e546d5
16 files changed
+297 -243

No files matched your search

@@ -703,6 +703,10 @@ class Compiler(
private suspend fun parseTypeAliasDeclaration(): Statement {
val nameToken = cc.requireToken(Token.Type.ID, "type alias name expected")
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
pendingDeclStart = null
val declaredName = nameToken.value
val outerClassName = currentEnclosingClassName()
val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName
@@ -719,13 +723,13 @@ class Compiler(
}
val typeParamNames = uniqueParams
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.add(typeParamNames)
val body = try {
val (body, bodyMini) = try {
cc.skipWsTokens()
val eq = cc.nextNonWhitespace()
if (eq.type != Token.Type.ASSIGN) {
throw ScriptError(eq.pos, "type alias $qualifiedName expects '='")
}
parseTypeExpressionWithMini().first
parseTypeExpressionWithMini()
} finally {
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.removeLast()
}
@@ -738,7 +742,16 @@ class Compiler(
outerCtx?.classScopeMembers?.add(declaredName)
registerClassScopeMember(outerClassName, declaredName)
}
pendingDeclDoc = null
miniSink?.onTypeAliasDecl(
MiniTypeAliasDecl(
range = MiniRange(startPos, cc.currentPos()),
name = declaredName,
typeParams = typeParams.map { it.name },
target = bodyMini,
doc = doc,
nameStart = nameToken.pos
)
)
val aliasExpr = net.sergeych.lyng.obj.TypeDeclRef(body, nameToken.pos)
val initStmt = ExpressionStatement(aliasExpr, nameToken.pos)
@@ -2422,7 +2435,10 @@ class Compiler(
// to skip in parseExpression:
val current = cc.current()
val right =
if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINGLE_LINE_COMMENT)
if (current.type == Token.Type.NEWLINE ||
current.type == Token.Type.SINGLE_LINE_COMMENT ||
current.type == Token.Type.STEP
)
null
else
parseExpression()
@@ -2440,6 +2456,30 @@ class Compiler(
}
}
Token.Type.STEP -> {
val left = operand ?: throw ScriptError(t.pos, "step requires a range")
val rangeRef = when (left) {
is RangeRef -> left
is ConstRef -> {
val range = left.constValue as? ObjRange ?: run {
cc.previous()
return operand
}
val leftRef = range.start?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
val rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
RangeRef(leftRef, rightRef, range.isEndInclusive)
}
else -> {
cc.previous()
return operand
}
}
if (rangeRef.step != null) throw ScriptError(t.pos, "step is already specified for this range")
val stepExpr = parseExpression() ?: throw ScriptError(t.pos, "Expected step expression")
val stepRef = StatementRef(stepExpr)
operand = RangeRef(rangeRef.left, rangeRef.right, rangeRef.isEndInclusive, stepRef)
}
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
operand = operand?.let { left ->
// Trailing block-argument function call: the leading '{' is already consumed,
@@ -6042,12 +6082,14 @@ class Compiler(
is ConstRef -> {
val range = ref.constValue as? ObjRange ?: return null
if (!range.isIntRange) return null
if (range.step != null && !range.step.isNull) return null
val start = range.start?.toLong() ?: return null
val end = range.end?.toLong() ?: return null
val endExclusive = if (range.isEndInclusive) end + 1 else end
return ConstIntRange(start, endExclusive)
}
is RangeRef -> {
if (ref.step != null) return null
val start = constIntValueOrNull(ref.left) ?: return null
val end = constIntValueOrNull(ref.right) ?: return null
val endExclusive = if (ref.isEndInclusive) end + 1 else end
@@ -358,6 +358,7 @@ private class Parser(fromPos: Pos) {
"in" -> Token("in", from, Token.Type.IN)
"is" -> Token("is", from, Token.Type.IS)
"by" -> Token("by", from, Token.Type.BY)
"step" -> Token("step", from, Token.Type.STEP)
"object" -> Token("object", from, Token.Type.OBJECT)
"as" -> {
// support both `as` and tight `as?` without spaces
@@ -597,4 +598,4 @@ private class Parser(fromPos: Pos) {
loadToEndOfLine()
}
}
}
@@ -36,7 +36,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
PLUS, MINUS, STAR, SLASH, PERCENT,
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, IFNULLASSIGN,
PLUS2, MINUS2,
IN, NOTIN, IS, NOTIS, BY,
IN, NOTIN, IS, NOTIS, BY, STEP,
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
SHUTTLE,
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
@@ -59,4 +59,4 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
companion object {
// fun eof(parser: Parser) = Token("", parser.currentPos, Type.EOF)
}
}
}
@@ -2202,8 +2202,17 @@ class BytecodeCompiler(
val inclusiveSlot = allocSlot()
val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive))
builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot)
val stepSlot = if (ref.step != null) {
val step = compileRefWithFallback(ref.step, null, Pos.builtIn) ?: return null
ensureObjSlot(step).slot
} else {
val slot = allocSlot()
builder.emit(Opcode.CONST_NULL, slot)
updateSlotType(slot, SlotType.OBJ)
slot
}
val dst = allocSlot()
builder.emit(Opcode.MAKE_RANGE, startSlot, endSlot, inclusiveSlot, dst)
builder.emit(Opcode.MAKE_RANGE, startSlot, endSlot, inclusiveSlot, stepSlot, dst)
updateSlotType(dst, SlotType.OBJ)
slotObjClass[dst] = ObjRange.type
return CompiledValue(dst, SlotType.OBJ)
@@ -5508,7 +5517,8 @@ class BytecodeCompiler(
private fun extractRangeRef(source: Statement): RangeRef? {
val target = if (source is BytecodeStatement) source.original else source
val expr = target as? ExpressionStatement ?: return null
return expr.ref as? RangeRef
val ref = expr.ref as? RangeRef ?: return null
return if (ref.step != null) null else ref
}
private fun extractDeclaredRange(stmt: Statement?): RangeRef? {
@@ -5519,6 +5529,7 @@ class BytecodeCompiler(
if (ref is RangeRef) return ref
if (ref is ConstRef) {
val range = ref.constValue as? ObjRange ?: return null
if (range.step != null && !range.step.isNull) return null
val start = range.start as? ObjInt ?: return null
val end = range.end as? ObjInt ?: return null
val left = ConstRef(start.asReadonly)
@@ -183,7 +183,7 @@ class CmdBuilder {
Opcode.SET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.MAKE_RANGE ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_LITERAL ->
listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_MEMBER_SLOT ->
@@ -232,7 +232,7 @@ class CmdBuilder {
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3])
Opcode.LOAD_THIS -> CmdLoadThis(operands[0])
Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1])
Opcode.MAKE_RANGE -> CmdMakeRange(operands[0], operands[1], operands[2], operands[3])
Opcode.MAKE_RANGE -> CmdMakeRange(operands[0], operands[1], operands[2], operands[3], operands[4])
Opcode.CHECK_IS -> CmdCheckIs(operands[0], operands[1], operands[2])
Opcode.ASSERT_IS -> CmdAssertIs(operands[0], operands[1])
Opcode.MAKE_QUALIFIED_VIEW -> CmdMakeQualifiedView(operands[0], operands[1], operands[2])
@@ -76,7 +76,13 @@ object CmdDisassembler {
is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot)
is CmdMakeQualifiedView -> Opcode.MAKE_QUALIFIED_VIEW to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
is CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.okSlot)
is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf(cmd.startSlot, cmd.endSlot, cmd.inclusiveSlot, cmd.dst)
is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf(
cmd.startSlot,
cmd.endSlot,
cmd.inclusiveSlot,
cmd.stepSlot,
cmd.dst
)
is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot)
is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst)
is CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot)
@@ -216,8 +222,10 @@ object CmdDisassembler {
listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RANGE_INT_BOUNDS, Opcode.MAKE_RANGE ->
Opcode.RANGE_INT_BOUNDS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.MAKE_RANGE ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RET_LABEL, Opcode.THROW ->
listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT ->
@@ -171,13 +171,16 @@ class CmdMakeRange(
internal val startSlot: Int,
internal val endSlot: Int,
internal val inclusiveSlot: Int,
internal val stepSlot: Int,
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val start = frame.slotToObj(startSlot)
val end = frame.slotToObj(endSlot)
val inclusive = frame.slotToObj(inclusiveSlot).toBool()
frame.storeObjResult(dst, ObjRange(start, end, isEndInclusive = inclusive))
val stepObj = frame.slotToObj(stepSlot)
val step = if (stepObj.isNull) null else stepObj
frame.storeObjResult(dst, ObjRange(start, end, isEndInclusive = inclusive, step = step))
return
}
}
@@ -41,9 +41,9 @@ private val fallbackKeywordIds = setOf(
// boolean operators
"and", "or", "not",
// declarations & modifiers
"fun", "fn", "class", "interface", "enum", "val", "var", "import", "package",
"fun", "fn", "class", "interface", "enum", "val", "var", "type", "import", "package",
"abstract", "closed", "override", "public", "lazy", "dynamic",
"private", "protected", "static", "open", "extern", "init", "get", "set", "by",
"private", "protected", "static", "open", "extern", "init", "get", "set", "by", "step",
// control flow and misc
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset"
@@ -74,7 +74,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) {
Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation
// textual control keywords
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.OBJECT,
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.STEP, Type.OBJECT,
Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword
// labels / annotations
@@ -23,10 +23,16 @@ import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() {
class ObjRange(
val start: Obj?,
val end: Obj?,
val isEndInclusive: Boolean,
val step: Obj? = null
) : Obj() {
val isOpenStart by lazy { start == null || start.isNull }
val isOpenEnd by lazy { end == null || end.isNull }
val hasExplicitStep: Boolean get() = step != null && !step.isNull
override val objClass: ObjClass get() = type
@@ -35,6 +41,9 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
result.append("${start?.inspect(scope) ?: '∞'} ..")
if (!isEndInclusive) result.append('<')
result.append(" ${end?.inspect(scope) ?: '∞'}")
if (hasExplicitStep) {
result.append(" step ${step?.inspect(scope)}")
}
return ObjString(result.toString())
}
@@ -64,26 +73,26 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
}
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {
if (start != null) {
if (!isOpenStart) {
// our start is not -∞ so other start should be GTE or is not contained:
if (other.start != null && start.compareTo(scope, other.start) > 0) return false
if (!other.isOpenStart && start!!.compareTo(scope, other.start!!) > 0) return false
}
if (end != null) {
if (!isOpenEnd) {
// same with the end: if it is open, it can't be contained in ours:
if (other.end == null) return false
if (other.isOpenEnd) return false
// both exists, now there could be 4 cases:
return when {
other.isEndInclusive && isEndInclusive ->
end.compareTo(scope, other.end) >= 0
end!!.compareTo(scope, other.end!!) >= 0
!other.isEndInclusive && !isEndInclusive ->
end.compareTo(scope, other.end) >= 0
end!!.compareTo(scope, other.end!!) >= 0
other.isEndInclusive && !isEndInclusive ->
end.compareTo(scope, other.end) > 0
end!!.compareTo(scope, other.end!!) > 0
!other.isEndInclusive && isEndInclusive ->
end.compareTo(scope, other.end) >= 0
end!!.compareTo(scope, other.end!!) >= 0
else -> throw IllegalStateException("unknown comparison")
}
@@ -120,12 +129,12 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
}
}
if (start == null && end == null) return true
if (start != null) {
if (start.compareTo(scope, other) > 0) return false
if (isOpenStart && isOpenEnd) return true
if (!isOpenStart) {
if (start!!.compareTo(scope, other) > 0) return false
}
if (end != null) {
val cmp = end.compareTo(scope, other)
if (!isOpenEnd) {
val cmp = end!!.compareTo(scope, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
}
return true
@@ -140,7 +149,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
}
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
if (start is ObjInt && end is ObjInt) {
if (!hasExplicitStep && start is ObjInt && end is ObjInt) {
val s = start.value
val e = end.value
if (isEndInclusive) {
@@ -152,7 +161,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
if (!callback(ObjInt.of(i))) break
}
}
} else if (start is ObjChar && end is ObjChar) {
} else if (!hasExplicitStep && start is ObjChar && end is ObjChar) {
val s = start.value
val e = end.value
if (isEndInclusive) {
@@ -171,7 +180,11 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return (other as? ObjRange)?.let {
if( start == other.start && end == other.end ) 0 else -1
if (start == other.start &&
end == other.end &&
isEndInclusive == other.isEndInclusive &&
step == other.step
) 0 else -1
}
?: -1
}
@@ -180,6 +193,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
var result = start?.hashCode() ?: 0
result = 31 * result + (end?.hashCode() ?: 0)
result = 31 * result + isEndInclusive.hashCode()
result = 31 * result + (step?.hashCode() ?: 0)
return result
}
@@ -192,6 +206,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
if (start != other.start) return false
if (end != other.end) return false
if (isEndInclusive != other.isEndInclusive) return false
if (step != other.step) return false
return true
}
@@ -213,12 +228,19 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().end ?: ObjNull }
)
addPropertyDoc(
name = "step",
doc = "Explicit step for iteration, or null if implicit.",
type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().step ?: ObjNull }
)
addPropertyDoc(
name = "isOpen",
doc = "Whether the range is open on either side (no start or no end).",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj() }
getter = { thisAs<ObjRange>().let { it.isOpenStart || it.isOpenEnd }.toObj() }
)
addPropertyDoc(
name = "isIntRange",
@@ -248,20 +270,78 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
moduleName = "lyng.stdlib"
) {
val self = thisAs<ObjRange>()
if (net.sergeych.lyng.PerfFlags.RANGE_FAST_ITER) {
val s = self.start
val e = self.end
if (s is ObjInt && e is ObjInt) {
val start = s.value.toInt()
val endExclusive = (if (self.isEndInclusive) e.value.toInt() + 1 else e.value.toInt())
// Only for ascending simple ranges; fall back otherwise
if (start <= endExclusive) {
return@addFnDoc ObjFastIntRangeIterator(start, endExclusive)
}
}
}
ObjRangeIterator(self).apply { init() }
self.buildIterator(this)
}
}
}
private fun explicitStepOrNull(): Obj? = step?.takeUnless { it.isNull }
private suspend fun resolveStep(scope: Scope, explicitStep: Obj?): Obj {
val startObj = start ?: ObjNull
if (explicitStep != null) {
if (explicitStep is Numeric && explicitStep.doubleValue == 0.0) {
scope.raiseIllegalState("Range step cannot be zero")
}
if (startObj is ObjChar && explicitStep !is ObjInt) {
scope.raiseIllegalState("Char range step must be Int")
}
if (startObj is Numeric && explicitStep !is Numeric) {
scope.raiseIllegalState("Numeric range step must be numeric")
}
return explicitStep
}
if (startObj is ObjInt) {
val cmp = if (end == null || end.isNull) 0 else startObj.compareTo(scope, end)
val dir = if (cmp >= 0) -1 else 1
return ObjInt.of(dir.toLong())
}
if (startObj is ObjChar) {
val endChar = end as? ObjChar
?: scope.raiseIllegalState("Char range requires Char end to infer step")
val dir = if (startObj.value >= endChar.value) -1 else 1
return ObjInt.of(dir.toLong())
}
if (startObj is ObjReal) {
scope.raiseIllegalState("Real range requires explicit step")
}
scope.raiseIllegalState("Range of ${startObj.objClass.className} requires explicit step")
}
private suspend fun directionMismatch(scope: Scope, step: Obj): Boolean {
if (end == null || end.isNull) return false
val startObj = start ?: ObjNull
if (startObj is ObjChar && end !is ObjChar) return false
val cmp = startObj.compareTo(scope, end)
if (cmp == -2) return false
if (cmp == 0) return false
val stepSign = when {
startObj is ObjChar && step is ObjInt -> step.value.compareTo(0)
step is Numeric -> step.doubleValue.compareTo(0.0)
else -> return false
}
return (cmp < 0 && stepSign < 0) || (cmp > 0 && stepSign > 0)
}
suspend fun buildIterator(scope: Scope): Obj {
if (isOpenStart) scope.raiseIllegalState("Range with open start is not iterable")
val explicitStep = explicitStepOrNull()
if (isOpenEnd && explicitStep == null) {
scope.raiseIllegalState("Open-ended range requires explicit step to iterate")
}
val stepValue = resolveStep(scope, explicitStep)
val mismatch = directionMismatch(scope, stepValue)
if (net.sergeych.lyng.PerfFlags.RANGE_FAST_ITER) {
val s = start
val e = end
if (!mismatch && stepValue is ObjInt && stepValue.value == 1L && s is ObjInt && e is ObjInt) {
val startVal = s.value.toInt()
val endExclusive = (if (isEndInclusive) e.value.toInt() + 1 else e.value.toInt())
if (startVal <= endExclusive) {
return ObjFastIntRangeIterator(startVal, endExclusive)
}
}
}
return ObjRangeIterator(this, stepValue, mismatch)
}
}
@@ -17,57 +17,55 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.Scope
class ObjRangeIterator(val self: ObjRange) : Obj() {
class ObjRangeIterator(
private val self: ObjRange,
private val step: Obj,
private val directionMismatch: Boolean
) : Obj() {
private var nextIndex = 0
private var lastIndex = 0
private var isCharRange: Boolean = false
private var current: Obj? = null
private var initialized = false
override val objClass: ObjClass get() = type
fun Scope.init() {
val s = self.start
val e = self.end
if (s is ObjInt && e is ObjInt) {
lastIndex = if (self.isEndInclusive)
(e.value - s.value + 1).toInt()
else
(e.value - s.value).toInt()
} else if (s is ObjChar && e is ObjChar) {
isCharRange = true
lastIndex = if (self.isEndInclusive)
(e.value.code - s.value.code + 1)
else
(e.value.code - s.value.code)
} else {
raiseError("not implemented iterator for range of $this")
private fun ensureInit() {
if (!initialized) {
current = self.start ?: ObjNull
initialized = true
}
}
fun hasNext(): Boolean = nextIndex < lastIndex
suspend fun hasNext(scope: Scope): Boolean {
if (directionMismatch) return false
ensureInit()
val cur = current ?: return false
return self.contains(scope, cur)
}
fun next(scope: Scope): Obj =
if (nextIndex < lastIndex) {
val start = self.start
val x = if (start is ObjInt)
start.value + nextIndex++
else if (start is ObjChar)
start.value.code.toLong() + nextIndex++
else
scope.raiseError("iterator error: unsupported range start")
if (isCharRange) ObjChar(x.toInt().toChar()) else ObjInt.of(x)
}
else {
suspend fun next(scope: Scope): Obj {
if (!hasNext(scope)) {
scope.raiseError(ObjIterationFinishedException(scope))
}
val result = current ?: scope.raiseError("iterator error: missing current value")
current = advance(scope, result)
return result
}
private suspend fun advance(scope: Scope, value: Obj): Obj =
if (value is ObjChar) {
val delta = (step as? ObjInt)
?: scope.raiseIllegalState("Char range step must be Int")
ObjChar((value.value.code + delta.value.toInt()).toChar())
} else {
value.plus(scope, step)
}
companion object {
val type = ObjClass("RangeIterator", ObjIterator).apply {
addFn("hasNext") {
thisAs<ObjRangeIterator>().hasNext().toObj()
thisAs<ObjRangeIterator>().hasNext(this).toObj()
}
addFn("next") {
thisAs<ObjRangeIterator>().next(this)
@@ -2495,7 +2495,8 @@ class MapLiteralRef(private val entries: List<MapLiteralEntry>) : ObjRef {
class RangeRef(
internal val left: ObjRef?,
internal val right: ObjRef?,
internal val isEndInclusive: Boolean
internal val isEndInclusive: Boolean,
internal val step: ObjRef? = null
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
return evalValue(scope).asReadonly
@@ -2504,7 +2505,8 @@ class RangeRef(
override suspend fun evalValue(scope: Scope): Obj {
val l = left?.evalValue(scope) ?: ObjNull
val r = right?.evalValue(scope) ?: ObjNull
return ObjRange(l, r, isEndInclusive = isEndInclusive)
val st = step?.evalValue(scope)
return ObjRange(l, r, isEndInclusive = isEndInclusive, step = st)
}
}
@@ -125,7 +125,7 @@ class ForInStatement(
}
val sourceObj = source.execute(forContext)
return if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) {
return if (sourceObj is ObjRange && sourceObj.isIntRange && !sourceObj.hasExplicitStep && PerfFlags.PRIMITIVE_FASTOPS) {
loopIntRange(
forContext,
sourceObj.start!!.toLong(),
@@ -3457,6 +3457,26 @@ class ScriptTest {
println(y.list)
}
@Test
fun testRangeStepIteration() = runTest {
val ints = eval("""(1..5 step 2).toList()""") as ObjList
assertEquals(listOf(1, 3, 5), ints.list.map { it.toInt() })
val chars = eval("""('a'..'e' step 2).toList()""") as ObjList
assertEquals(listOf('a', 'c', 'e'), chars.list.map { it.toString().single() })
val reals = eval("""(0.0..1.0 step 0.25).toList()""") as ObjList
assertEquals(listOf(0.0, 0.25, 0.5, 0.75, 1.0), reals.list.map { it.toDouble() })
val empty = eval("""(5..1 step 1).toList()""") as ObjList
assertEquals(0, empty.list.size)
val openEnd = eval("""(0.. step 1).take(3).toList()""") as ObjList
assertEquals(listOf(0, 1, 2), openEnd.list.map { it.toInt() })
assertFailsWith<ExecutionError> {
eval("""(0.0..1.0).toList()""")
}
assertFailsWith<ExecutionError> {
eval("""(0..).toList()""")
}
}
@Test
fun testMultilineStrings() = runTest {
assertEquals(