Add descending ranges and for-loop support

This commit is contained in:
Sergey Chernov 2026-04-03 20:58:08 +03:00
parent d8c53c500e
commit f1003f5b95
15 changed files with 411 additions and 98 deletions

View File

@ -25,6 +25,23 @@ Exclusive end ranges are adopted from kotlin either:
assert(4 in r) assert(4 in r)
>>> void >>> void
Descending finite ranges are explicit too:
val r = 5 downTo 1
assert(r.isDescending)
assert(r.toList() == [5,4,3,2,1])
>>> void
Use `downUntil` when the lower bound should be excluded:
val r = 5 downUntil 1
assert(r.toList() == [5,4,3,2])
assert(1 !in r)
>>> void
This is explicit by design: `5..1` is not treated as a reverse range. It is an
ordinary ascending range with no values in it when iterated.
In any case, we can test an object to belong to using `in` and `!in` and In any case, we can test an object to belong to using `in` and `!in` and
access limits: access limits:
@ -73,6 +90,23 @@ but
>>> 2 >>> 2
>>> void >>> void
Descending ranges work in `for` loops exactly the same way:
for( i in 3 downTo 1 )
println(i)
>>> 3
>>> 2
>>> 1
>>> void
And with an exclusive lower bound:
for( i in 3 downUntil 1 )
println(i)
>>> 3
>>> 2
>>> void
### Stepped ranges ### Stepped ranges
Use `step` to change the iteration increment. The range bounds still define membership, Use `step` to change the iteration increment. The range bounds still define membership,
@ -80,9 +114,18 @@ so iteration ends when the next value is no longer in the range.
assert( [1,3,5] == (1..5 step 2).toList() ) assert( [1,3,5] == (1..5 step 2).toList() )
assert( [1,3] == (1..<5 step 2).toList() ) assert( [1,3] == (1..<5 step 2).toList() )
assert( [5,3,1] == (5 downTo 1 step 2).toList() )
assert( ['a','c','e'] == ('a'..'e' step 2).toList() ) assert( ['a','c','e'] == ('a'..'e' step 2).toList() )
>>> void >>> void
Descending ranges still use a positive `step`; the direction comes from
`downTo` / `downUntil`:
assert( ['e','c','a'] == ('e' downTo 'a' step 2).toList() )
>>> void
A negative step with `downTo` / `downUntil` is invalid.
Real ranges require an explicit step: Real ranges require an explicit step:
assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() ) assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() )
@ -119,6 +162,7 @@ Exclusive end char ranges are supported too:
|-----------------|------------------------------|---------------| |-----------------|------------------------------|---------------|
| contains(other) | used in `in` | Range, or Any | | contains(other) | used in `in` | Range, or Any |
| isEndInclusive | true for '..' | Bool | | isEndInclusive | true for '..' | Bool |
| isDescending | true for `downTo`/`downUntil`| 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 |
| step | explicit iteration step | Any? | | step | explicit iteration step | Any? |

View File

@ -50,8 +50,10 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
- Range literals: - Range literals:
- inclusive: `a..b` - inclusive: `a..b`
- exclusive end: `a..<b` - exclusive end: `a..<b`
- descending inclusive: `a downTo b`
- descending exclusive end: `a downUntil b`
- open-ended forms are supported (`a..`, `..b`, `..`). - open-ended forms are supported (`a..`, `..b`, `..`).
- optional step: `a..b step 2` - optional step: `a..b step 2`, `a downTo b step 2`
- Lambda literal: - Lambda literal:
- with params: `{ x, y -> x + y }` - with params: `{ x, y -> x + y }`
- implicit `it`: `{ it + 1 }` - implicit `it`: `{ it + 1 }`

View File

@ -1359,6 +1359,41 @@ size and index access, like lists:
"total letters: "+letters "total letters: "+letters
>>> "total letters: 10" >>> "total letters: 10"
When you need a counting loop that goes backwards, use an explicit descending
range:
var sum = 0
for( i in 5 downTo 1 ) {
sum += i
}
sum
>>> 15
If the lower bound should be excluded, use `downUntil`:
val xs = []
for( i in 5 downUntil 1 ) {
xs.add(i)
}
xs
>>> [5,4,3,2]
This is intentionally explicit: `5..1` is an empty ascending range, not an
implicit reverse loop.
Descending loops also support `step`:
val xs = []
for( i in 10 downTo 1 step 3 ) {
xs.add(i)
}
xs
>>> [10,7,4,1]
For descending ranges, `step` stays positive. The direction comes from
`downTo` / `downUntil`, so `10 downTo 1 step 3` is valid, while
`10 downTo 1 step -3` is an error.
For loop support breaks the same as while loops above: For loop support breaks the same as while loops above:
fun search(haystack, needle) { fun search(haystack, needle) {
@ -1488,6 +1523,14 @@ It could be open and closed:
assert( 5 !in (1..<5) ) assert( 5 !in (1..<5) )
>>> void >>> void
Descending ranges are explicit too:
(5 downTo 1).toList()
>>> [5,4,3,2,1]
(5 downUntil 1).toList()
>>> [5,4,3,2]
Ranges could be inside other ranges: Ranges could be inside other ranges:
assert( (2..3) in (1..10) ) assert( (2..3) in (1..10) )
@ -1505,6 +1548,14 @@ and you can use ranges in for-loops:
>>> b >>> b
>>> void >>> void
Descending character ranges work the same way:
for( ch in 'e' downTo 'a' step 2 ) println(ch)
>>> e
>>> c
>>> a
>>> void
See [Ranges](Range.md) for detailed documentation on it. See [Ranges](Range.md) for detailed documentation on it.
# Time routines # Time routines

View File

@ -3058,9 +3058,10 @@ class Compiler(
} }
} }
Token.Type.DOTDOT, Token.Type.DOTDOTLT -> { Token.Type.DOTDOT, Token.Type.DOTDOTLT, Token.Type.DOWNTO, Token.Type.DOWNUNTIL -> {
// range operator // range operator
val isEndInclusive = t.type == Token.Type.DOTDOT val isEndInclusive = t.type == Token.Type.DOTDOT || t.type == Token.Type.DOWNTO
val isDescending = t.type == Token.Type.DOWNTO || t.type == Token.Type.DOWNUNTIL
val left = operand val left = operand
// if it is an open end range, then the end of line could be here that we do not want // if it is an open end range, then the end of line could be here that we do not want
// to skip in parseExpression: // to skip in parseExpression:
@ -3078,12 +3079,19 @@ class Compiler(
val lConst = constIntValueOrNull(left) val lConst = constIntValueOrNull(left)
val rConst = constIntValueOrNull(rightRef) val rConst = constIntValueOrNull(rightRef)
if (lConst != null && rConst != null) { if (lConst != null && rConst != null) {
operand = ConstRef(ObjRange(ObjInt.of(lConst), ObjInt.of(rConst), isEndInclusive).asReadonly) operand = ConstRef(
ObjRange(
ObjInt.of(lConst),
ObjInt.of(rConst),
isEndInclusive,
isDescending = isDescending
).asReadonly
)
} else { } else {
operand = RangeRef(left, rightRef, isEndInclusive) operand = RangeRef(left, rightRef, isEndInclusive, isDescending = isDescending)
} }
} else { } else {
operand = RangeRef(left, rightRef, isEndInclusive) operand = RangeRef(left, rightRef, isEndInclusive, isDescending = isDescending)
} }
} }
@ -3098,7 +3106,7 @@ class Compiler(
} }
val leftRef = range.start?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) } val leftRef = range.start?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
val rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) } val rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
RangeRef(leftRef, rightRef, range.isEndInclusive) RangeRef(leftRef, rightRef, range.isEndInclusive, isDescending = range.isDescending)
} }
else -> { else -> {
cc.previous() cc.previous()
@ -3108,7 +3116,13 @@ class Compiler(
if (rangeRef.step != null) throw ScriptError(t.pos, "step is already specified for this range") 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 stepExpr = parseExpression() ?: throw ScriptError(t.pos, "Expected step expression")
val stepRef = StatementRef(stepExpr) val stepRef = StatementRef(stepExpr)
operand = RangeRef(rangeRef.left, rangeRef.right, rangeRef.isEndInclusive, stepRef) operand = RangeRef(
rangeRef.left,
rangeRef.right,
rangeRef.isEndInclusive,
isDescending = rangeRef.isDescending,
step = stepRef
)
} }
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
@ -7828,15 +7842,23 @@ class Compiler(
if (range.step != null && !range.step.isNull) return null if (range.step != null && !range.step.isNull) return null
val start = range.start?.toLong() ?: return null val start = range.start?.toLong() ?: return null
val end = range.end?.toLong() ?: return null val end = range.end?.toLong() ?: return null
val endExclusive = if (range.isEndInclusive) end + 1 else end val stopBoundary = if (range.isDescending) {
return ConstIntRange(start, endExclusive) if (range.isEndInclusive) end - 1 else end
} else {
if (range.isEndInclusive) end + 1 else end
}
return ConstIntRange(start, stopBoundary, range.isDescending)
} }
is RangeRef -> { is RangeRef -> {
if (ref.step != null) return null if (ref.step != null) return null
val start = constIntValueOrNull(ref.left) ?: return null val start = constIntValueOrNull(ref.left) ?: return null
val end = constIntValueOrNull(ref.right) ?: return null val end = constIntValueOrNull(ref.right) ?: return null
val endExclusive = if (ref.isEndInclusive) end + 1 else end val stopBoundary = if (ref.isDescending) {
return ConstIntRange(start, endExclusive) if (ref.isEndInclusive) end - 1 else end
} else {
if (ref.isEndInclusive) end + 1 else end
}
return ConstIntRange(start, stopBoundary, ref.isDescending)
} }
else -> return null else -> return null
} }

View File

@ -448,6 +448,8 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
"is" -> Token("is", from, Token.Type.IS) "is" -> Token("is", from, Token.Type.IS)
"by" -> Token("by", from, Token.Type.BY) "by" -> Token("by", from, Token.Type.BY)
"step" -> Token("step", from, Token.Type.STEP) "step" -> Token("step", from, Token.Type.STEP)
"downTo" -> Token("downTo", from, Token.Type.DOWNTO)
"downUntil" -> Token("downUntil", from, Token.Type.DOWNUNTIL)
"object" -> Token("object", from, Token.Type.OBJECT) "object" -> Token("object", from, Token.Type.OBJECT)
"as" -> { "as" -> {
// support both `as` and tight `as?` without spaces // support both `as` and tight `as?` without spaces

View File

@ -36,7 +36,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
PLUS, MINUS, STAR, SLASH, PERCENT, PLUS, MINUS, STAR, SLASH, PERCENT,
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, IFNULLASSIGN, ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, IFNULLASSIGN,
PLUS2, MINUS2, PLUS2, MINUS2,
IN, NOTIN, IS, NOTIS, BY, STEP, IN, NOTIN, IS, NOTIS, BY, STEP, DOWNTO, DOWNUNTIL,
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH, EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
SHUTTLE, SHUTTLE,
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON, AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,

View File

@ -3617,6 +3617,9 @@ class BytecodeCompiler(
val inclusiveSlot = allocSlot() val inclusiveSlot = allocSlot()
val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive)) val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive))
builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot) builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot)
val descendingSlot = allocSlot()
val descendingId = builder.addConst(BytecodeConst.Bool(ref.isDescending))
builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
val stepSlot = if (ref.step != null) { val stepSlot = if (ref.step != null) {
val step = compileRefWithFallback(ref.step, null, Pos.builtIn) ?: return null val step = compileRefWithFallback(ref.step, null, Pos.builtIn) ?: return null
ensureObjSlot(step).slot ensureObjSlot(step).slot
@ -3627,7 +3630,7 @@ class BytecodeCompiler(
slot slot
} }
val dst = allocSlot() val dst = allocSlot()
builder.emit(Opcode.MAKE_RANGE, startSlot, endSlot, inclusiveSlot, stepSlot, dst) builder.emit(Opcode.MAKE_RANGE, startSlot, endSlot, inclusiveSlot, descendingSlot, stepSlot, dst)
updateSlotType(dst, SlotType.OBJ) updateSlotType(dst, SlotType.OBJ)
slotObjClass[dst] = ObjRange.type slotObjClass[dst] = ObjRange.type
return CompiledValue(dst, SlotType.OBJ) return CompiledValue(dst, SlotType.OBJ)
@ -6244,11 +6247,14 @@ class BytecodeCompiler(
val iSlot = loopSlotId val iSlot = loopSlotId
val endSlot = allocSlot() val endSlot = allocSlot()
val descendingSlot = allocSlot()
if (range != null) { if (range != null) {
val startId = builder.addConst(BytecodeConst.IntVal(range.start)) val startId = builder.addConst(BytecodeConst.IntVal(range.start))
val endId = builder.addConst(BytecodeConst.IntVal(range.endExclusive)) val endId = builder.addConst(BytecodeConst.IntVal(range.stopBoundary))
val descendingId = builder.addConst(BytecodeConst.Bool(range.isDescending))
builder.emit(Opcode.CONST_INT, startId, iSlot) builder.emit(Opcode.CONST_INT, startId, iSlot)
builder.emit(Opcode.CONST_INT, endId, endSlot) builder.emit(Opcode.CONST_INT, endId, endSlot)
builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
updateSlotType(iSlot, SlotType.INT) updateSlotType(iSlot, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
} else { } else {
@ -6258,9 +6264,15 @@ class BytecodeCompiler(
val startValue = compileRef(left) ?: return null val startValue = compileRef(left) ?: return null
val endValue = compileRef(right) ?: return null val endValue = compileRef(right) ?: return null
if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null
val descendingId = builder.addConst(BytecodeConst.Bool(rangeRef.isDescending))
emitMove(startValue, iSlot) emitMove(startValue, iSlot)
emitMove(endValue, endSlot) emitMove(endValue, endSlot)
if (rangeRef.isEndInclusive) { builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
if (rangeRef.isDescending) {
if (rangeRef.isEndInclusive) {
builder.emit(Opcode.DEC_INT, endSlot)
}
} else if (rangeRef.isEndInclusive) {
builder.emit(Opcode.INC_INT, endSlot) builder.emit(Opcode.INC_INT, endSlot)
} }
updateSlotType(iSlot, SlotType.INT) updateSlotType(iSlot, SlotType.INT)
@ -6270,7 +6282,7 @@ class BytecodeCompiler(
val rangeValue = compileRef(rangeLocal) ?: return null val rangeValue = compileRef(rangeLocal) ?: return null
val rangeObj = ensureObjSlot(rangeValue) val rangeObj = ensureObjSlot(rangeValue)
val okSlot = allocSlot() val okSlot = allocSlot()
builder.emit(Opcode.RANGE_INT_BOUNDS, rangeObj.slot, iSlot, endSlot, okSlot) builder.emit(Opcode.RANGE_INT_BOUNDS, rangeObj.slot, iSlot, endSlot, descendingSlot, okSlot)
val badRangeLabel = builder.label() val badRangeLabel = builder.label()
builder.emit( builder.emit(
Opcode.JMP_IF_FALSE, Opcode.JMP_IF_FALSE,
@ -6294,14 +6306,7 @@ class BytecodeCompiler(
val endLabel = builder.label() val endLabel = builder.label()
val doneLabel = builder.label() val doneLabel = builder.label()
builder.mark(loopLabel) builder.mark(loopLabel)
builder.emit( emitIntForLoopCheck(iSlot, endSlot, descendingSlot, endLabel)
Opcode.JMP_IF_GTE_INT,
listOf(
CmdBuilder.Operand.IntVal(iSlot),
CmdBuilder.Operand.IntVal(endSlot),
CmdBuilder.Operand.LabelRef(endLabel)
)
)
updateSlotType(iSlot, SlotType.INT) updateSlotType(iSlot, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
loopStack.addLast( loopStack.addLast(
@ -6324,7 +6329,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
} }
builder.mark(continueLabel) builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot) emitIntForLoopStep(iSlot, descendingSlot)
if (hasRealWiden) { if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots) emitLoopRealCoercions(realWidenSlots)
} }
@ -6377,14 +6382,7 @@ class BytecodeCompiler(
val continueLabel = builder.label() val continueLabel = builder.label()
val endLabel = builder.label() val endLabel = builder.label()
builder.mark(loopLabel) builder.mark(loopLabel)
builder.emit( emitIntForLoopCheck(iSlot, endSlot, descendingSlot, endLabel)
Opcode.JMP_IF_GTE_INT,
listOf(
CmdBuilder.Operand.IntVal(iSlot),
CmdBuilder.Operand.IntVal(endSlot),
CmdBuilder.Operand.LabelRef(endLabel)
)
)
updateSlotType(iSlot, SlotType.INT) updateSlotType(iSlot, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
loopStack.addLast( loopStack.addLast(
@ -6407,7 +6405,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
} }
builder.mark(continueLabel) builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot) emitIntForLoopStep(iSlot, descendingSlot)
if (hasRealWiden) { if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots) emitLoopRealCoercions(realWidenSlots)
} }
@ -8719,11 +8717,59 @@ class BytecodeCompiler(
val end = range.end as? ObjInt ?: return null val end = range.end as? ObjInt ?: return null
val left = ConstRef(start.asReadonly) val left = ConstRef(start.asReadonly)
val right = ConstRef(end.asReadonly) val right = ConstRef(end.asReadonly)
return RangeRef(left, right, range.isEndInclusive) return RangeRef(left, right, range.isEndInclusive, isDescending = range.isDescending)
} }
return null return null
} }
private fun emitIntForLoopCheck(iSlot: Int, stopSlot: Int, descendingSlot: Int, endLabel: CmdBuilder.Label) {
val descendingLabel = builder.label()
val afterCheckLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(
CmdBuilder.Operand.IntVal(descendingSlot),
CmdBuilder.Operand.LabelRef(descendingLabel)
)
)
builder.emit(
Opcode.JMP_IF_GTE_INT,
listOf(
CmdBuilder.Operand.IntVal(iSlot),
CmdBuilder.Operand.IntVal(stopSlot),
CmdBuilder.Operand.LabelRef(endLabel)
)
)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(afterCheckLabel)))
builder.mark(descendingLabel)
builder.emit(
Opcode.JMP_IF_LTE_INT,
listOf(
CmdBuilder.Operand.IntVal(iSlot),
CmdBuilder.Operand.IntVal(stopSlot),
CmdBuilder.Operand.LabelRef(endLabel)
)
)
builder.mark(afterCheckLabel)
}
private fun emitIntForLoopStep(iSlot: Int, descendingSlot: Int) {
val descendingLabel = builder.label()
val afterStepLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(
CmdBuilder.Operand.IntVal(descendingSlot),
CmdBuilder.Operand.LabelRef(descendingLabel)
)
)
builder.emit(Opcode.INC_INT, iSlot)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(afterStepLabel)))
builder.mark(descendingLabel)
builder.emit(Opcode.DEC_INT, iSlot)
builder.mark(afterStepLabel)
}
private fun extractRangeFromLocal(source: Statement): RangeRef? { private fun extractRangeFromLocal(source: Statement): RangeRef? {
val target = if (source is BytecodeStatement) source.original else source val target = if (source is BytecodeStatement) source.original else source
val expr = target as? ExpressionStatement ?: return null val expr = target as? ExpressionStatement ?: return null

View File

@ -147,7 +147,7 @@ class CmdBuilder {
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW -> Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RANGE_INT_BOUNDS -> Opcode.RANGE_INT_BOUNDS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RET_LABEL, Opcode.THROW -> Opcode.RET_LABEL, Opcode.THROW ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT -> Opcode.RESOLVE_SCOPE_SLOT ->
@ -228,7 +228,7 @@ class CmdBuilder {
Opcode.SET_INDEX -> Opcode.SET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.MAKE_RANGE -> Opcode.MAKE_RANGE ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_LITERAL -> Opcode.LIST_LITERAL ->
listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_MEMBER_SLOT -> Opcode.GET_MEMBER_SLOT ->
@ -311,10 +311,10 @@ class CmdBuilder {
} }
Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1]) Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1])
Opcode.GET_OBJ_CLASS -> CmdGetObjClass(operands[0], operands[1]) Opcode.GET_OBJ_CLASS -> CmdGetObjClass(operands[0], operands[1])
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3]) Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3], operands[4])
Opcode.LOAD_THIS -> CmdLoadThis(operands[0]) Opcode.LOAD_THIS -> CmdLoadThis(operands[0])
Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1]) Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1])
Opcode.MAKE_RANGE -> CmdMakeRange(operands[0], operands[1], operands[2], operands[3], operands[4]) Opcode.MAKE_RANGE -> CmdMakeRange(operands[0], operands[1], operands[2], operands[3], operands[4], operands[5])
Opcode.CHECK_IS -> CmdCheckIs(operands[0], operands[1], operands[2]) Opcode.CHECK_IS -> CmdCheckIs(operands[0], operands[1], operands[2])
Opcode.ASSERT_IS -> CmdAssertIs(operands[0], operands[1]) Opcode.ASSERT_IS -> CmdAssertIs(operands[0], operands[1])
Opcode.MAKE_QUALIFIED_VIEW -> CmdMakeQualifiedView(operands[0], operands[1], operands[2]) Opcode.MAKE_QUALIFIED_VIEW -> CmdMakeQualifiedView(operands[0], operands[1], operands[2])

View File

@ -96,11 +96,12 @@ object CmdDisassembler {
is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst) is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot) 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 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 CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.descendingSlot, cmd.okSlot)
is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf( is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf(
cmd.startSlot, cmd.startSlot,
cmd.endSlot, cmd.endSlot,
cmd.inclusiveSlot, cmd.inclusiveSlot,
cmd.descendingSlot,
cmd.stepSlot, cmd.stepSlot,
cmd.dst cmd.dst
) )

View File

@ -273,6 +273,7 @@ class CmdMakeRange(
internal val startSlot: Int, internal val startSlot: Int,
internal val endSlot: Int, internal val endSlot: Int,
internal val inclusiveSlot: Int, internal val inclusiveSlot: Int,
internal val descendingSlot: Int,
internal val stepSlot: Int, internal val stepSlot: Int,
internal val dst: Int, internal val dst: Int,
) : Cmd() { ) : Cmd() {
@ -280,9 +281,10 @@ class CmdMakeRange(
val start = frame.slotToObj(startSlot) val start = frame.slotToObj(startSlot)
val end = frame.slotToObj(endSlot) val end = frame.slotToObj(endSlot)
val inclusive = frame.slotToObj(inclusiveSlot).toBool() val inclusive = frame.slotToObj(inclusiveSlot).toBool()
val descending = frame.slotToObj(descendingSlot).toBool()
val stepObj = frame.slotToObj(stepSlot) val stepObj = frame.slotToObj(stepSlot)
val step = if (stepObj.isNull) null else stepObj val step = if (stepObj.isNull) null else stepObj
frame.storeObjResult(dst, ObjRange(start, end, isEndInclusive = inclusive, step = step)) frame.storeObjResult(dst, ObjRange(start, end, isEndInclusive = inclusive, isDescending = descending, step = step))
return return
} }
} }
@ -430,6 +432,7 @@ class CmdRangeIntBounds(
internal val src: Int, internal val src: Int,
internal val startSlot: Int, internal val startSlot: Int,
internal val endSlot: Int, internal val endSlot: Int,
internal val descendingSlot: Int,
internal val okSlot: Int, internal val okSlot: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
@ -439,10 +442,19 @@ class CmdRangeIntBounds(
frame.setBool(okSlot, false) frame.setBool(okSlot, false)
return return
} }
if (range.isDescending) {
frame.setBool(okSlot, false)
return
}
val start = (range.start as ObjInt).value val start = (range.start as ObjInt).value
val end = (range.end as ObjInt).value val end = (range.end as ObjInt).value
frame.setInt(startSlot, start) frame.setInt(startSlot, start)
frame.setInt(endSlot, if (range.isEndInclusive) end + 1 else end) frame.setInt(endSlot, if (range.isDescending) {
if (range.isEndInclusive) end - 1 else end
} else {
if (range.isEndInclusive) end + 1 else end
})
frame.setBool(descendingSlot, range.isDescending)
frame.setBool(okSlot, true) frame.setBool(okSlot, true)
return return
} }

View File

@ -43,7 +43,7 @@ private val fallbackKeywordIds = setOf(
// declarations & modifiers // declarations & modifiers
"fun", "fn", "class", "interface", "enum", "val", "var", "type", "import", "package", "fun", "fn", "class", "interface", "enum", "val", "var", "type", "import", "package",
"abstract", "closed", "override", "public", "lazy", "dynamic", "abstract", "closed", "override", "public", "lazy", "dynamic",
"private", "protected", "static", "open", "extern", "init", "get", "set", "by", "step", "private", "protected", "static", "open", "extern", "init", "get", "set", "by", "step", "downTo", "downUntil",
// control flow and misc // control flow and misc
"if", "else", "when", "while", "do", "for", "try", "catch", "finally", "if", "else", "when", "while", "do", "for", "try", "catch", "finally",
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset", "void" "throw", "return", "break", "continue", "this", "null", "true", "false", "unset", "void"
@ -74,7 +74,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) {
Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation
// textual control keywords // textual control keywords
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.STEP, Type.OBJECT, Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.STEP, Type.DOWNTO, Type.DOWNUNTIL, Type.OBJECT,
Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword
// labels / annotations // labels / annotations

View File

@ -28,6 +28,7 @@ class ObjRange(
val start: Obj?, val start: Obj?,
val end: Obj?, val end: Obj?,
val isEndInclusive: Boolean, val isEndInclusive: Boolean,
val isDescending: Boolean = false,
val step: Obj? = null val step: Obj? = null
) : Obj() { ) : Obj() {
@ -39,15 +40,38 @@ class ObjRange(
override suspend fun defaultToString(scope: Scope): ObjString { override suspend fun defaultToString(scope: Scope): ObjString {
val result = StringBuilder() val result = StringBuilder()
result.append("${start?.inspect(scope) ?: '∞'} ..") result.append(start?.inspect(scope) ?: "")
if (!isEndInclusive) result.append('<') when {
result.append(" ${end?.inspect(scope) ?: '∞'}") isDescending && isEndInclusive -> result.append(" downTo ")
isDescending && !isEndInclusive -> result.append(" downUntil ")
else -> {
result.append(" ..")
if (!isEndInclusive) result.append('<')
result.append(' ')
}
}
result.append(end?.inspect(scope) ?: "")
if (hasExplicitStep) { if (hasExplicitStep) {
result.append(" step ${step?.inspect(scope)}") result.append(" step ${step?.inspect(scope)}")
} }
return ObjString(result.toString()) return ObjString(result.toString())
} }
private data class NormalizedLowerBound(val value: Obj, val inclusive: Boolean)
private data class NormalizedUpperBound(val value: Obj, val inclusive: Boolean)
private fun normalizedLowerBound(): NormalizedLowerBound? =
when {
isDescending -> end?.takeUnless { it.isNull }?.let { NormalizedLowerBound(it, isEndInclusive) }
else -> start?.takeUnless { it.isNull }?.let { NormalizedLowerBound(it, true) }
}
private fun normalizedUpperBound(): NormalizedUpperBound? =
when {
isDescending -> start?.takeUnless { it.isNull }?.let { NormalizedUpperBound(it, true) }
else -> end?.takeUnless { it.isNull }?.let { NormalizedUpperBound(it, isEndInclusive) }
}
/** /**
* IF end is open (null/ObjNull), returns null * IF end is open (null/ObjNull), returns null
* Otherwise, return correct value for the exclusive end * Otherwise, return correct value for the exclusive end
@ -74,29 +98,21 @@ class ObjRange(
} }
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean { suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {
if (!isOpenStart) { val ourLower = normalizedLowerBound()
// our start is not -∞ so other start should be GTE or is not contained: val otherLower = other.normalizedLowerBound()
if (!other.isOpenStart && start!!.compareTo(scope, other.start!!) > 0) return false if (ourLower != null) {
if (otherLower == null) return false
val cmp = ourLower.value.compareTo(scope, otherLower.value)
if (cmp == -2 || cmp > 0) return false
if (cmp == 0 && otherLower.inclusive && !ourLower.inclusive) return false
} }
if (!isOpenEnd) { val ourUpper = normalizedUpperBound()
// same with the end: if it is open, it can't be contained in ours: val otherUpper = other.normalizedUpperBound()
if (other.isOpenEnd) return false if (ourUpper != null) {
// both exists, now there could be 4 cases: if (otherUpper == null) return false
return when { val cmp = ourUpper.value.compareTo(scope, otherUpper.value)
other.isEndInclusive && isEndInclusive -> if (cmp == -2 || cmp < 0) return false
end!!.compareTo(scope, other.end!!) >= 0 if (cmp == 0 && otherUpper.inclusive && !ourUpper.inclusive) return false
!other.isEndInclusive && !isEndInclusive ->
end!!.compareTo(scope, other.end!!) >= 0
other.isEndInclusive && !isEndInclusive ->
end!!.compareTo(scope, other.end!!) > 0
!other.isEndInclusive && isEndInclusive ->
end!!.compareTo(scope, other.end!!) >= 0
else -> throw IllegalStateException("unknown comparison")
}
} }
return true return true
} }
@ -108,35 +124,38 @@ class ObjRange(
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
if (start is ObjInt && end is ObjInt && other is ObjInt) { if (start is ObjInt && end is ObjInt && other is ObjInt) {
val s = start.value val lower = if (isDescending) end.value else start.value
val e = end.value val upper = if (isDescending) start.value else end.value
val v = other.value val v = other.value
if (v < s) return false if (v < lower || v > upper) return false
return if (isEndInclusive) v <= e else v < e return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
} }
if (start is ObjChar && end is ObjChar && other is ObjChar) { if (start is ObjChar && end is ObjChar && other is ObjChar) {
val s = start.value val lower = if (isDescending) end.value else start.value
val e = end.value val upper = if (isDescending) start.value else end.value
val v = other.value val v = other.value
if (v < s) return false if (v < lower || v > upper) return false
return if (isEndInclusive) v <= e else v < e return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
} }
if (start is ObjString && end is ObjString && other is ObjString) { if (start is ObjString && end is ObjString && other is ObjString) {
val s = start.value val lower = if (isDescending) end.value else start.value
val e = end.value val upper = if (isDescending) start.value else end.value
val v = other.value val v = other.value
if (v < s) return false if (v < lower || v > upper) return false
return if (isEndInclusive) v <= e else v < e return if (isDescending) v != lower || isEndInclusive else v != upper || isEndInclusive
} }
} }
if (isOpenStart && isOpenEnd) return true val lower = normalizedLowerBound()
if (!isOpenStart) { val upper = normalizedUpperBound()
if (start!!.compareTo(scope, other) > 0) return false if (lower == null && upper == null) return true
if (lower != null) {
val cmp = lower.value.compareTo(scope, other)
if (cmp == -2 || cmp > 0 || (!lower.inclusive && cmp == 0)) return false
} }
if (!isOpenEnd) { if (upper != null) {
val cmp = end!!.compareTo(scope, other) val cmp = upper.value.compareTo(scope, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false if (cmp == -2 || cmp < 0 || (!upper.inclusive && cmp == 0)) return false
} }
return true return true
} }
@ -153,7 +172,12 @@ class ObjRange(
if (!hasExplicitStep && start is ObjInt && end is ObjInt) { if (!hasExplicitStep && start is ObjInt && end is ObjInt) {
val s = start.value val s = start.value
val e = end.value val e = end.value
if (isEndInclusive) { if (isDescending) {
val last = if (isEndInclusive) e else e + 1
for (i in s downTo last) {
if (!callback(ObjInt.of(i))) break
}
} else if (isEndInclusive) {
for (i in s..e) { for (i in s..e) {
if (!callback(ObjInt.of(i))) break if (!callback(ObjInt.of(i))) break
} }
@ -165,7 +189,14 @@ class ObjRange(
} else if (!hasExplicitStep && start is ObjChar && end is ObjChar) { } else if (!hasExplicitStep && start is ObjChar && end is ObjChar) {
val s = start.value val s = start.value
val e = end.value val e = end.value
if (isEndInclusive) { if (isDescending) {
var c = s.code
val last = if (isEndInclusive) e.code else e.code + 1
while (c >= last) {
if (!callback(ObjChar(c.toChar()))) break
c--
}
} else if (isEndInclusive) {
for (c in s..e) { for (c in s..e) {
if (!callback(ObjChar(c))) break if (!callback(ObjChar(c))) break
} }
@ -184,6 +215,7 @@ class ObjRange(
if (start == other.start && if (start == other.start &&
end == other.end && end == other.end &&
isEndInclusive == other.isEndInclusive && isEndInclusive == other.isEndInclusive &&
isDescending == other.isDescending &&
step == other.step step == other.step
) 0 else -1 ) 0 else -1
} }
@ -194,6 +226,7 @@ class ObjRange(
var result = start?.hashCode() ?: 0 var result = start?.hashCode() ?: 0
result = 31 * result + (end?.hashCode() ?: 0) result = 31 * result + (end?.hashCode() ?: 0)
result = 31 * result + isEndInclusive.hashCode() result = 31 * result + isEndInclusive.hashCode()
result = 31 * result + isDescending.hashCode()
result = 31 * result + (step?.hashCode() ?: 0) result = 31 * result + (step?.hashCode() ?: 0)
return result return result
} }
@ -207,6 +240,7 @@ class ObjRange(
if (start != other.start) return false if (start != other.start) return false
if (end != other.end) return false if (end != other.end) return false
if (isEndInclusive != other.isEndInclusive) return false if (isEndInclusive != other.isEndInclusive) return false
if (isDescending != other.isDescending) return false
if (step != other.step) return false if (step != other.step) return false
return true return true
@ -264,6 +298,13 @@ class ObjRange(
moduleName = "lyng.stdlib", moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().isEndInclusive.toObj() } getter = { thisAs<ObjRange>().isEndInclusive.toObj() }
) )
addPropertyDoc(
name = "isDescending",
doc = "Whether the range iterates from the start bound down toward the end bound.",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().isDescending.toObj() }
)
addFnDoc( addFnDoc(
name = "iterator", name = "iterator",
doc = "Iterator over elements in this range (optimized for Int ranges).", doc = "Iterator over elements in this range (optimized for Int ranges).",
@ -290,18 +331,22 @@ class ObjRange(
if (startObj is Numeric && explicitStep !is Numeric) { if (startObj is Numeric && explicitStep !is Numeric) {
scope.raiseIllegalState("Numeric range step must be numeric") scope.raiseIllegalState("Numeric range step must be numeric")
} }
if (isDescending) {
val sign = when (explicitStep) {
is ObjInt -> explicitStep.value.compareTo(0)
is Numeric -> explicitStep.doubleValue.compareTo(0.0)
else -> 1
}
if (sign < 0) scope.raiseIllegalState("Descending range step must be positive")
return explicitStep.negate(scope)
}
return explicitStep return explicitStep
} }
if (startObj is ObjInt) { if (startObj is ObjInt) {
val cmp = if (end == null || end.isNull) 0 else startObj.compareTo(scope, end) return ObjInt.of(if (isDescending) -1 else 1)
val dir = if (cmp >= 0) -1 else 1
return ObjInt.of(dir.toLong())
} }
if (startObj is ObjChar) { if (startObj is ObjChar) {
val endChar = end as? ObjChar return ObjInt.of(if (isDescending) -1 else 1)
?: 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) { if (startObj is ObjReal) {
scope.raiseIllegalState("Real range requires explicit step") scope.raiseIllegalState("Real range requires explicit step")

View File

@ -1028,6 +1028,7 @@ class RangeRef(
internal val left: ObjRef?, internal val left: ObjRef?,
internal val right: ObjRef?, internal val right: ObjRef?,
internal val isEndInclusive: Boolean, internal val isEndInclusive: Boolean,
internal val isDescending: Boolean = false,
internal val step: ObjRef? = null internal val step: ObjRef? = null
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord = scope.raiseObjRefEvalDisabled() override suspend fun get(scope: Scope): ObjRecord = scope.raiseObjRefEvalDisabled()

View File

@ -77,7 +77,11 @@ class IfStatement(
} }
} }
data class ConstIntRange(val start: Long, val endExclusive: Long) data class ConstIntRange(
val start: Long,
val stopBoundary: Long,
val isDescending: Boolean,
)
class ForInStatement( class ForInStatement(
val loopVarName: String, val loopVarName: String,

View File

@ -294,6 +294,18 @@ class ScriptTest {
assertEquals(Token.Type.INT, tt[0].type) assertEquals(Token.Type.INT, tt[0].type)
assertEquals(Token.Type.DOTDOTLT, tt[1].type) assertEquals(Token.Type.DOTDOTLT, tt[1].type)
assertEquals(Token.Type.INT, tt[2].type) assertEquals(Token.Type.INT, tt[2].type)
tt = parseLyng("5 downTo 4".toSource())
assertEquals(Token.Type.INT, tt[0].type)
assertEquals(Token.Type.DOWNTO, tt[1].type)
assertEquals(Token.Type.INT, tt[2].type)
tt = parseLyng("5 downUntil 4".toSource())
assertEquals(Token.Type.INT, tt[0].type)
assertEquals(Token.Type.DOWNUNTIL, tt[1].type)
assertEquals(Token.Type.INT, tt[2].type)
} }
@Test @Test
@ -1280,6 +1292,36 @@ class ScriptTest {
assertTrue(convIndex > incIndex, "INT_TO_REAL should appear after INC_INT") assertTrue(convIndex > incIndex, "INT_TO_REAL should appear after INC_INT")
} }
@Test
fun testDescendingForLoopDisasm() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun countDown() {
var acc = 0
for (i in 5 downTo 1) {
acc += i
}
}
fun countDownVar() {
var acc = 0
val r = 5 downTo 1
for (i in r) {
acc += i
}
}
""".trimIndent()
)
val constDisasm = scope.disassembleSymbol("countDown")
val varDisasm = scope.disassembleSymbol("countDownVar")
assertTrue("DEC_INT" in constDisasm, "expected DEC_INT in descending for-loop disasm")
assertTrue("JMP_IF_LTE_INT" in constDisasm, "expected JMP_IF_LTE_INT in descending for-loop disasm")
assertTrue("CALL_MEMBER_SLOT" !in constDisasm, "descending literal range should avoid iterator fallback")
assertTrue("DEC_INT" in varDisasm, "expected DEC_INT in descending range-variable for-loop disasm")
assertTrue("JMP_IF_LTE_INT" in varDisasm, "expected descending comparison in range-variable for-loop disasm")
assertTrue("CALL_MEMBER_SLOT" !in varDisasm, "descending range-variable loop should avoid iterator fallback")
}
@Test @Test
fun testIntClosedRangeInclusive() = runTest { fun testIntClosedRangeInclusive() = runTest {
eval( eval(
@ -3485,17 +3527,58 @@ class ScriptTest {
fun testRangeStepIteration() = runTest { fun testRangeStepIteration() = runTest {
val ints = eval("""(1..5 step 2).toList()""") as ObjList val ints = eval("""(1..5 step 2).toList()""") as ObjList
assertEquals(listOf(1, 3, 5), ints.list.map { it.toInt() }) assertEquals(listOf(1, 3, 5), ints.list.map { it.toInt() })
val descending = eval("""(5 downTo 1).toList()""") as ObjList
assertEquals(listOf(5, 4, 3, 2, 1), descending.list.map { it.toInt() })
val descendingExclusive = eval("""(5 downUntil 1).toList()""") as ObjList
assertEquals(listOf(5, 4, 3, 2), descendingExclusive.list.map { it.toInt() })
val descendingStep = eval("""(10 downTo 1 step 3).toList()""") as ObjList
assertEquals(listOf(10, 7, 4, 1), descendingStep.list.map { it.toInt() })
val descendingChars = eval("""('e' downTo 'a' step 2).toList()""") as ObjList
assertEquals(listOf('e', 'c', 'a'), descendingChars.list.map { it.toString().single() })
val chars = eval("""('a'..'e' step 2).toList()""") as ObjList val chars = eval("""('a'..'e' step 2).toList()""") as ObjList
assertEquals(listOf('a', 'c', 'e'), chars.list.map { it.toString().single() }) assertEquals(listOf('a', 'c', 'e'), chars.list.map { it.toString().single() })
val reals = eval("""(0.0..1.0 step 0.25).toList()""") as ObjList 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() }) 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 val empty = eval("""(5..1 step 1).toList()""") as ObjList
assertEquals(0, empty.list.size) assertEquals(0, empty.list.size)
val plainDescending = eval("""(5..1).toList()""") as ObjList
assertEquals(0, plainDescending.list.size)
val openEnd = eval("""(0.. step 1).take(3).toList()""") as ObjList val openEnd = eval("""(0.. step 1).take(3).toList()""") as ObjList
assertEquals(listOf(0, 1, 2), openEnd.list.map { it.toInt() }) assertEquals(listOf(0, 1, 2), openEnd.list.map { it.toInt() })
assertEquals(
true,
eval(
"""
val r = 10 downTo 1
r.isDescending && r.isEndInclusive && (10 in r) && (1 in r) && (0 !in r)
""".trimIndent()
).toBool()
)
assertEquals(
true,
eval(
"""
val r = 10 downUntil 1
r.isDescending && !r.isEndInclusive && (10 in r) && (1 !in r) && ((8 downTo 3) in r)
""".trimIndent()
).toBool()
)
assertEquals(
15,
(eval(
"""
var s = 0
for (i in 5 downTo 1) s += i
s
""".trimIndent()
) as ObjInt).toInt()
)
assertFailsWith<ExecutionError> { assertFailsWith<ExecutionError> {
eval("""(0.0..1.0).toList()""") eval("""(0.0..1.0).toList()""")
} }
assertFailsWith<ExecutionError> {
eval("""(5 downTo 1 step -1).toList()""")
}
assertFailsWith<ExecutionError> { assertFailsWith<ExecutionError> {
eval("""(0..).toList()""") eval("""(0..).toList()""")
} }