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)
>>> 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
access limits:
@ -73,6 +90,23 @@ but
>>> 2
>>> 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
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] == (1..<5 step 2).toList() )
assert( [5,3,1] == (5 downTo 1 step 2).toList() )
assert( ['a','c','e'] == ('a'..'e' step 2).toList() )
>>> 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:
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 |
| isEndInclusive | true for '..' | Bool |
| isDescending | true for `downTo`/`downUntil`| Bool |
| isOpen | at any end | Bool |
| isIntRange | both start and end are Int | Bool |
| 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:
- inclusive: `a..b`
- exclusive end: `a..<b`
- descending inclusive: `a downTo b`
- descending exclusive end: `a downUntil 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:
- with params: `{ x, y -> x + y }`
- implicit `it`: `{ it + 1 }`

View File

@ -1359,6 +1359,41 @@ size and index access, like lists:
"total letters: "+letters
>>> "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:
fun search(haystack, needle) {
@ -1488,6 +1523,14 @@ It could be open and closed:
assert( 5 !in (1..<5) )
>>> 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:
assert( (2..3) in (1..10) )
@ -1505,6 +1548,14 @@ and you can use ranges in for-loops:
>>> b
>>> 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.
# 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
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
// if it is an open end range, then the end of line could be here that we do not want
// to skip in parseExpression:
@ -3078,12 +3079,19 @@ class Compiler(
val lConst = constIntValueOrNull(left)
val rConst = constIntValueOrNull(rightRef)
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 {
operand = RangeRef(left, rightRef, isEndInclusive)
operand = RangeRef(left, rightRef, isEndInclusive, isDescending = isDescending)
}
} 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 rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
RangeRef(leftRef, rightRef, range.isEndInclusive)
RangeRef(leftRef, rightRef, range.isEndInclusive, isDescending = range.isDescending)
}
else -> {
cc.previous()
@ -3108,7 +3116,13 @@ class Compiler(
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)
operand = RangeRef(
rangeRef.left,
rangeRef.right,
rangeRef.isEndInclusive,
isDescending = rangeRef.isDescending,
step = stepRef
)
}
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
@ -7828,15 +7842,23 @@ class Compiler(
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)
val stopBoundary = if (range.isDescending) {
if (range.isEndInclusive) end - 1 else end
} else {
if (range.isEndInclusive) end + 1 else end
}
return ConstIntRange(start, stopBoundary, range.isDescending)
}
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
return ConstIntRange(start, endExclusive)
val stopBoundary = if (ref.isDescending) {
if (ref.isEndInclusive) end - 1 else end
} else {
if (ref.isEndInclusive) end + 1 else end
}
return ConstIntRange(start, stopBoundary, ref.isDescending)
}
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)
"by" -> Token("by", from, Token.Type.BY)
"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)
"as" -> {
// 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,
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, IFNULLASSIGN,
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,
SHUTTLE,
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,

View File

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

View File

@ -147,7 +147,7 @@ class CmdBuilder {
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
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 ->
listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT ->
@ -228,7 +228,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, OperandKind.SLOT)
listOf(OperandKind.SLOT, 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 ->
@ -311,10 +311,10 @@ class CmdBuilder {
}
Opcode.OBJ_TO_BOOL -> CmdObjToBool(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_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.ASSERT_IS -> CmdAssertIs(operands[0], operands[1])
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 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 CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.descendingSlot, cmd.okSlot)
is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf(
cmd.startSlot,
cmd.endSlot,
cmd.inclusiveSlot,
cmd.descendingSlot,
cmd.stepSlot,
cmd.dst
)

View File

@ -273,6 +273,7 @@ class CmdMakeRange(
internal val startSlot: Int,
internal val endSlot: Int,
internal val inclusiveSlot: Int,
internal val descendingSlot: Int,
internal val stepSlot: Int,
internal val dst: Int,
) : Cmd() {
@ -280,9 +281,10 @@ class CmdMakeRange(
val start = frame.slotToObj(startSlot)
val end = frame.slotToObj(endSlot)
val inclusive = frame.slotToObj(inclusiveSlot).toBool()
val descending = frame.slotToObj(descendingSlot).toBool()
val stepObj = frame.slotToObj(stepSlot)
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
}
}
@ -430,6 +432,7 @@ class CmdRangeIntBounds(
internal val src: Int,
internal val startSlot: Int,
internal val endSlot: Int,
internal val descendingSlot: Int,
internal val okSlot: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
@ -439,10 +442,19 @@ class CmdRangeIntBounds(
frame.setBool(okSlot, false)
return
}
if (range.isDescending) {
frame.setBool(okSlot, false)
return
}
val start = (range.start as ObjInt).value
val end = (range.end as ObjInt).value
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)
return
}

View File

@ -43,7 +43,7 @@ private val fallbackKeywordIds = setOf(
// declarations & modifiers
"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", "step",
"private", "protected", "static", "open", "extern", "init", "get", "set", "by", "step", "downTo", "downUntil",
// control flow and misc
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
"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
// 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
// labels / annotations

View File

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

View File

@ -1028,6 +1028,7 @@ class RangeRef(
internal val left: ObjRef?,
internal val right: ObjRef?,
internal val isEndInclusive: Boolean,
internal val isDescending: Boolean = false,
internal val step: ObjRef? = null
) : ObjRef {
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(
val loopVarName: String,

View File

@ -294,6 +294,18 @@ class ScriptTest {
assertEquals(Token.Type.INT, tt[0].type)
assertEquals(Token.Type.DOTDOTLT, tt[1].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
@ -1280,6 +1292,36 @@ class ScriptTest {
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
fun testIntClosedRangeInclusive() = runTest {
eval(
@ -3485,17 +3527,58 @@ class ScriptTest {
fun testRangeStepIteration() = runTest {
val ints = eval("""(1..5 step 2).toList()""") as ObjList
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
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 plainDescending = eval("""(5..1).toList()""") as ObjList
assertEquals(0, plainDescending.list.size)
val openEnd = eval("""(0.. step 1).take(3).toList()""") as ObjList
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> {
eval("""(0.0..1.0).toList()""")
}
assertFailsWith<ExecutionError> {
eval("""(5 downTo 1 step -1).toList()""")
}
assertFailsWith<ExecutionError> {
eval("""(0..).toList()""")
}