From f1003f5b9556d0a3c03dbcf0c0bf0fb0f23a91f5 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 3 Apr 2026 20:58:08 +0300 Subject: [PATCH] Add descending ranges and for-loop support --- docs/Range.md | 44 ++++++ docs/ai_language_reference.md | 4 +- docs/tutorial.md | 51 ++++++ .../kotlin/net/sergeych/lyng/Compiler.kt | 44 ++++-- .../kotlin/net/sergeych/lyng/Parser.kt | 2 + .../kotlin/net/sergeych/lyng/Token.kt | 2 +- .../lyng/bytecode/BytecodeCompiler.kt | 92 ++++++++--- .../net/sergeych/lyng/bytecode/CmdBuilder.kt | 8 +- .../sergeych/lyng/bytecode/CmdDisassembler.kt | 3 +- .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 16 +- .../lyng/highlight/SimpleLyngHighlighter.kt | 4 +- .../kotlin/net/sergeych/lyng/obj/ObjRange.kt | 149 ++++++++++++------ .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 1 + .../kotlin/net/sergeych/lyng/statements.kt | 6 +- lynglib/src/commonTest/kotlin/ScriptTest.kt | 83 ++++++++++ 15 files changed, 411 insertions(+), 98 deletions(-) diff --git a/docs/Range.md b/docs/Range.md index 4ad61bc..9cb5ac3 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -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? | diff --git a/docs/ai_language_reference.md b/docs/ai_language_reference.md index d4c8e6f..dab1ef2 100644 --- a/docs/ai_language_reference.md +++ b/docs/ai_language_reference.md @@ -50,8 +50,10 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T - Range literals: - inclusive: `a..b` - exclusive end: `a.. x + y }` - implicit `it`: `{ it + 1 }` diff --git a/docs/tutorial.md b/docs/tutorial.md index 281f1b4..5c922de 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index c7aa6e2..f350186 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt index d57d749..d0b8fae 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt index 74f7e7e..32ac4bc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt @@ -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, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 82bc1e2..f7e3fbc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt index 3c78487..fcffd93 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -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]) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt index 98f6829..b01aa70 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -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 ) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index 58e612c..ee42d92 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt index 893c186..b75d548 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt index c3c5761..c6f29a9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt @@ -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().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().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") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 8f73b60..76d95f8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -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() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index 1fa266f..3987570 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -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, diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 54a8c36..5aeb01a 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -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 { eval("""(0.0..1.0).toList()""") } + assertFailsWith { + eval("""(5 downTo 1 step -1).toList()""") + } assertFailsWith { eval("""(0..).toList()""") }