Add stepped range iteration

This commit is contained in:
Sergey Chernov 2026-02-07 05:45:44 +03:00
parent 79bece94a5
commit 78d8e546d5
16 changed files with 297 additions and 243 deletions

View File

@ -45,10 +45,11 @@ are equal or within another, taking into account the end-inclusiveness:
assert( (1..<3) in (1..3) )
>>> void
## Finite Ranges are iterable
## Ranges are iterable
So given a range with both ends, you can assume it is [Iterable]. This automatically let
use finite ranges in loops and convert it to lists:
Finite ranges are [Iterable] and can be used in loops and list conversions.
Open-ended ranges are iterable only with an explicit `step`, and open-start
ranges are never iterable.
assert( [-2, -1, 0, 1] == (-2..1).toList() )
>>> void
@ -70,6 +71,26 @@ but
>>> 2
>>> void
### Stepped ranges
Use `step` to change the iteration increment. The range bounds still define membership,
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( ['a','c','e'] == ('a'..'e' step 2).toList() )
>>> void
Real ranges require an explicit step:
assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() )
>>> void
Open-ended ranges require an explicit step to iterate:
(0.. step 1).take(3).toList()
>>> [0,1,2]
## Character ranges
You can use Char as both ends of the closed range:
@ -98,6 +119,7 @@ Exclusive end char ranges are supported too:
| isEndInclusive | true for '..' | Bool |
| isOpen | at any end | Bool |
| isIntRange | both start and end are Int | Bool |
| step | explicit iteration step | Any? |
| start | | Any? |
| end | | Any? |
| size | for finite ranges, see above | Long |

View File

@ -36,7 +36,7 @@ class LyngLexer : LexerBase() {
"abstract", "closed", "override", "static", "extern", "open", "private", "protected",
"if", "else", "for", "while", "return", "true", "false", "null",
"when", "in", "is", "break", "continue", "try", "catch", "finally",
"get", "set", "object", "enum", "init", "by", "property", "constructor"
"get", "set", "object", "enum", "init", "by", "step", "property", "constructor"
)
override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) {

View File

@ -703,6 +703,10 @@ class Compiler(
private suspend fun parseTypeAliasDeclaration(): Statement {
val nameToken = cc.requireToken(Token.Type.ID, "type alias name expected")
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
pendingDeclStart = null
val declaredName = nameToken.value
val outerClassName = currentEnclosingClassName()
val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName
@ -719,13 +723,13 @@ class Compiler(
}
val typeParamNames = uniqueParams
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.add(typeParamNames)
val body = try {
val (body, bodyMini) = try {
cc.skipWsTokens()
val eq = cc.nextNonWhitespace()
if (eq.type != Token.Type.ASSIGN) {
throw ScriptError(eq.pos, "type alias $qualifiedName expects '='")
}
parseTypeExpressionWithMini().first
parseTypeExpressionWithMini()
} finally {
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.removeLast()
}
@ -738,7 +742,16 @@ class Compiler(
outerCtx?.classScopeMembers?.add(declaredName)
registerClassScopeMember(outerClassName, declaredName)
}
pendingDeclDoc = null
miniSink?.onTypeAliasDecl(
MiniTypeAliasDecl(
range = MiniRange(startPos, cc.currentPos()),
name = declaredName,
typeParams = typeParams.map { it.name },
target = bodyMini,
doc = doc,
nameStart = nameToken.pos
)
)
val aliasExpr = net.sergeych.lyng.obj.TypeDeclRef(body, nameToken.pos)
val initStmt = ExpressionStatement(aliasExpr, nameToken.pos)
@ -2422,7 +2435,10 @@ class Compiler(
// to skip in parseExpression:
val current = cc.current()
val right =
if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINGLE_LINE_COMMENT)
if (current.type == Token.Type.NEWLINE ||
current.type == Token.Type.SINGLE_LINE_COMMENT ||
current.type == Token.Type.STEP
)
null
else
parseExpression()
@ -2440,6 +2456,30 @@ class Compiler(
}
}
Token.Type.STEP -> {
val left = operand ?: throw ScriptError(t.pos, "step requires a range")
val rangeRef = when (left) {
is RangeRef -> left
is ConstRef -> {
val range = left.constValue as? ObjRange ?: run {
cc.previous()
return operand
}
val leftRef = range.start?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
val rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
RangeRef(leftRef, rightRef, range.isEndInclusive)
}
else -> {
cc.previous()
return operand
}
}
if (rangeRef.step != null) throw ScriptError(t.pos, "step is already specified for this range")
val stepExpr = parseExpression() ?: throw ScriptError(t.pos, "Expected step expression")
val stepRef = StatementRef(stepExpr)
operand = RangeRef(rangeRef.left, rangeRef.right, rangeRef.isEndInclusive, stepRef)
}
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
operand = operand?.let { left ->
// Trailing block-argument function call: the leading '{' is already consumed,
@ -6042,12 +6082,14 @@ class Compiler(
is ConstRef -> {
val range = ref.constValue as? ObjRange ?: return null
if (!range.isIntRange) return null
if (range.step != null && !range.step.isNull) return null
val start = range.start?.toLong() ?: return null
val end = range.end?.toLong() ?: return null
val endExclusive = if (range.isEndInclusive) end + 1 else end
return ConstIntRange(start, endExclusive)
}
is RangeRef -> {
if (ref.step != null) return null
val start = constIntValueOrNull(ref.left) ?: return null
val end = constIntValueOrNull(ref.right) ?: return null
val endExclusive = if (ref.isEndInclusive) end + 1 else end

View File

@ -358,6 +358,7 @@ private class Parser(fromPos: Pos) {
"in" -> Token("in", from, Token.Type.IN)
"is" -> Token("is", from, Token.Type.IS)
"by" -> Token("by", from, Token.Type.BY)
"step" -> Token("step", from, Token.Type.STEP)
"object" -> Token("object", from, Token.Type.OBJECT)
"as" -> {
// support both `as` and tight `as?` without spaces

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,
IN, NOTIN, IS, NOTIS, BY, STEP,
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
SHUTTLE,
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,

View File

@ -2202,8 +2202,17 @@ class BytecodeCompiler(
val inclusiveSlot = allocSlot()
val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive))
builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot)
val stepSlot = if (ref.step != null) {
val step = compileRefWithFallback(ref.step, null, Pos.builtIn) ?: return null
ensureObjSlot(step).slot
} else {
val slot = allocSlot()
builder.emit(Opcode.CONST_NULL, slot)
updateSlotType(slot, SlotType.OBJ)
slot
}
val dst = allocSlot()
builder.emit(Opcode.MAKE_RANGE, startSlot, endSlot, inclusiveSlot, dst)
builder.emit(Opcode.MAKE_RANGE, startSlot, endSlot, inclusiveSlot, stepSlot, dst)
updateSlotType(dst, SlotType.OBJ)
slotObjClass[dst] = ObjRange.type
return CompiledValue(dst, SlotType.OBJ)
@ -5508,7 +5517,8 @@ class BytecodeCompiler(
private fun extractRangeRef(source: Statement): RangeRef? {
val target = if (source is BytecodeStatement) source.original else source
val expr = target as? ExpressionStatement ?: return null
return expr.ref as? RangeRef
val ref = expr.ref as? RangeRef ?: return null
return if (ref.step != null) null else ref
}
private fun extractDeclaredRange(stmt: Statement?): RangeRef? {
@ -5519,6 +5529,7 @@ class BytecodeCompiler(
if (ref is RangeRef) return ref
if (ref is ConstRef) {
val range = ref.constValue as? ObjRange ?: return null
if (range.step != null && !range.step.isNull) return null
val start = range.start as? ObjInt ?: return null
val end = range.end as? ObjInt ?: return null
val left = ConstRef(start.asReadonly)

View File

@ -183,7 +183,7 @@ class CmdBuilder {
Opcode.SET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.MAKE_RANGE ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_LITERAL ->
listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_MEMBER_SLOT ->
@ -232,7 +232,7 @@ class CmdBuilder {
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3])
Opcode.LOAD_THIS -> CmdLoadThis(operands[0])
Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1])
Opcode.MAKE_RANGE -> CmdMakeRange(operands[0], operands[1], operands[2], operands[3])
Opcode.MAKE_RANGE -> CmdMakeRange(operands[0], operands[1], operands[2], operands[3], operands[4])
Opcode.CHECK_IS -> CmdCheckIs(operands[0], operands[1], operands[2])
Opcode.ASSERT_IS -> CmdAssertIs(operands[0], operands[1])
Opcode.MAKE_QUALIFIED_VIEW -> CmdMakeQualifiedView(operands[0], operands[1], operands[2])

View File

@ -76,7 +76,13 @@ object CmdDisassembler {
is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot)
is CmdMakeQualifiedView -> Opcode.MAKE_QUALIFIED_VIEW to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
is CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.okSlot)
is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf(cmd.startSlot, cmd.endSlot, cmd.inclusiveSlot, cmd.dst)
is CmdMakeRange -> Opcode.MAKE_RANGE to intArrayOf(
cmd.startSlot,
cmd.endSlot,
cmd.inclusiveSlot,
cmd.stepSlot,
cmd.dst
)
is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot)
is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst)
is CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot)
@ -216,8 +222,10 @@ object CmdDisassembler {
listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RANGE_INT_BOUNDS, Opcode.MAKE_RANGE ->
Opcode.RANGE_INT_BOUNDS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.MAKE_RANGE ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RET_LABEL, Opcode.THROW ->
listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT ->

View File

@ -171,13 +171,16 @@ class CmdMakeRange(
internal val startSlot: Int,
internal val endSlot: Int,
internal val inclusiveSlot: Int,
internal val stepSlot: Int,
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val start = frame.slotToObj(startSlot)
val end = frame.slotToObj(endSlot)
val inclusive = frame.slotToObj(inclusiveSlot).toBool()
frame.storeObjResult(dst, ObjRange(start, end, isEndInclusive = inclusive))
val stepObj = frame.slotToObj(stepSlot)
val step = if (stepObj.isNull) null else stepObj
frame.storeObjResult(dst, ObjRange(start, end, isEndInclusive = inclusive, step = step))
return
}
}

View File

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

View File

@ -23,10 +23,16 @@ import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() {
class ObjRange(
val start: Obj?,
val end: Obj?,
val isEndInclusive: Boolean,
val step: Obj? = null
) : Obj() {
val isOpenStart by lazy { start == null || start.isNull }
val isOpenEnd by lazy { end == null || end.isNull }
val hasExplicitStep: Boolean get() = step != null && !step.isNull
override val objClass: ObjClass get() = type
@ -35,6 +41,9 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
result.append("${start?.inspect(scope) ?: '∞'} ..")
if (!isEndInclusive) result.append('<')
result.append(" ${end?.inspect(scope) ?: '∞'}")
if (hasExplicitStep) {
result.append(" step ${step?.inspect(scope)}")
}
return ObjString(result.toString())
}
@ -64,26 +73,26 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
}
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {
if (start != null) {
if (!isOpenStart) {
// our start is not -∞ so other start should be GTE or is not contained:
if (other.start != null && start.compareTo(scope, other.start) > 0) return false
if (!other.isOpenStart && start!!.compareTo(scope, other.start!!) > 0) return false
}
if (end != null) {
if (!isOpenEnd) {
// same with the end: if it is open, it can't be contained in ours:
if (other.end == null) return false
if (other.isOpenEnd) return false
// both exists, now there could be 4 cases:
return when {
other.isEndInclusive && isEndInclusive ->
end.compareTo(scope, other.end) >= 0
end!!.compareTo(scope, other.end!!) >= 0
!other.isEndInclusive && !isEndInclusive ->
end.compareTo(scope, other.end) >= 0
end!!.compareTo(scope, other.end!!) >= 0
other.isEndInclusive && !isEndInclusive ->
end.compareTo(scope, other.end) > 0
end!!.compareTo(scope, other.end!!) > 0
!other.isEndInclusive && isEndInclusive ->
end.compareTo(scope, other.end) >= 0
end!!.compareTo(scope, other.end!!) >= 0
else -> throw IllegalStateException("unknown comparison")
}
@ -120,12 +129,12 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
}
}
if (start == null && end == null) return true
if (start != null) {
if (start.compareTo(scope, other) > 0) return false
if (isOpenStart && isOpenEnd) return true
if (!isOpenStart) {
if (start!!.compareTo(scope, other) > 0) return false
}
if (end != null) {
val cmp = end.compareTo(scope, other)
if (!isOpenEnd) {
val cmp = end!!.compareTo(scope, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
}
return true
@ -140,7 +149,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
}
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
if (start is ObjInt && end is ObjInt) {
if (!hasExplicitStep && start is ObjInt && end is ObjInt) {
val s = start.value
val e = end.value
if (isEndInclusive) {
@ -152,7 +161,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
if (!callback(ObjInt.of(i))) break
}
}
} else if (start is ObjChar && end is ObjChar) {
} else if (!hasExplicitStep && start is ObjChar && end is ObjChar) {
val s = start.value
val e = end.value
if (isEndInclusive) {
@ -171,7 +180,11 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return (other as? ObjRange)?.let {
if( start == other.start && end == other.end ) 0 else -1
if (start == other.start &&
end == other.end &&
isEndInclusive == other.isEndInclusive &&
step == other.step
) 0 else -1
}
?: -1
}
@ -180,6 +193,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
var result = start?.hashCode() ?: 0
result = 31 * result + (end?.hashCode() ?: 0)
result = 31 * result + isEndInclusive.hashCode()
result = 31 * result + (step?.hashCode() ?: 0)
return result
}
@ -192,6 +206,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
if (start != other.start) return false
if (end != other.end) return false
if (isEndInclusive != other.isEndInclusive) return false
if (step != other.step) return false
return true
}
@ -213,12 +228,19 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().end ?: ObjNull }
)
addPropertyDoc(
name = "step",
doc = "Explicit step for iteration, or null if implicit.",
type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().step ?: ObjNull }
)
addPropertyDoc(
name = "isOpen",
doc = "Whether the range is open on either side (no start or no end).",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj() }
getter = { thisAs<ObjRange>().let { it.isOpenStart || it.isOpenEnd }.toObj() }
)
addPropertyDoc(
name = "isIntRange",
@ -248,20 +270,78 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
moduleName = "lyng.stdlib"
) {
val self = thisAs<ObjRange>()
if (net.sergeych.lyng.PerfFlags.RANGE_FAST_ITER) {
val s = self.start
val e = self.end
if (s is ObjInt && e is ObjInt) {
val start = s.value.toInt()
val endExclusive = (if (self.isEndInclusive) e.value.toInt() + 1 else e.value.toInt())
// Only for ascending simple ranges; fall back otherwise
if (start <= endExclusive) {
return@addFnDoc ObjFastIntRangeIterator(start, endExclusive)
}
}
}
ObjRangeIterator(self).apply { init() }
self.buildIterator(this)
}
}
}
private fun explicitStepOrNull(): Obj? = step?.takeUnless { it.isNull }
private suspend fun resolveStep(scope: Scope, explicitStep: Obj?): Obj {
val startObj = start ?: ObjNull
if (explicitStep != null) {
if (explicitStep is Numeric && explicitStep.doubleValue == 0.0) {
scope.raiseIllegalState("Range step cannot be zero")
}
if (startObj is ObjChar && explicitStep !is ObjInt) {
scope.raiseIllegalState("Char range step must be Int")
}
if (startObj is Numeric && explicitStep !is Numeric) {
scope.raiseIllegalState("Numeric range step must be numeric")
}
return explicitStep
}
if (startObj is ObjInt) {
val cmp = if (end == null || end.isNull) 0 else startObj.compareTo(scope, end)
val dir = if (cmp >= 0) -1 else 1
return ObjInt.of(dir.toLong())
}
if (startObj is ObjChar) {
val endChar = end as? ObjChar
?: scope.raiseIllegalState("Char range requires Char end to infer step")
val dir = if (startObj.value >= endChar.value) -1 else 1
return ObjInt.of(dir.toLong())
}
if (startObj is ObjReal) {
scope.raiseIllegalState("Real range requires explicit step")
}
scope.raiseIllegalState("Range of ${startObj.objClass.className} requires explicit step")
}
private suspend fun directionMismatch(scope: Scope, step: Obj): Boolean {
if (end == null || end.isNull) return false
val startObj = start ?: ObjNull
if (startObj is ObjChar && end !is ObjChar) return false
val cmp = startObj.compareTo(scope, end)
if (cmp == -2) return false
if (cmp == 0) return false
val stepSign = when {
startObj is ObjChar && step is ObjInt -> step.value.compareTo(0)
step is Numeric -> step.doubleValue.compareTo(0.0)
else -> return false
}
return (cmp < 0 && stepSign < 0) || (cmp > 0 && stepSign > 0)
}
suspend fun buildIterator(scope: Scope): Obj {
if (isOpenStart) scope.raiseIllegalState("Range with open start is not iterable")
val explicitStep = explicitStepOrNull()
if (isOpenEnd && explicitStep == null) {
scope.raiseIllegalState("Open-ended range requires explicit step to iterate")
}
val stepValue = resolveStep(scope, explicitStep)
val mismatch = directionMismatch(scope, stepValue)
if (net.sergeych.lyng.PerfFlags.RANGE_FAST_ITER) {
val s = start
val e = end
if (!mismatch && stepValue is ObjInt && stepValue.value == 1L && s is ObjInt && e is ObjInt) {
val startVal = s.value.toInt()
val endExclusive = (if (isEndInclusive) e.value.toInt() + 1 else e.value.toInt())
if (startVal <= endExclusive) {
return ObjFastIntRangeIterator(startVal, endExclusive)
}
}
}
return ObjRangeIterator(this, stepValue, mismatch)
}
}

View File

@ -17,57 +17,55 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.Scope
class ObjRangeIterator(val self: ObjRange) : Obj() {
class ObjRangeIterator(
private val self: ObjRange,
private val step: Obj,
private val directionMismatch: Boolean
) : Obj() {
private var nextIndex = 0
private var lastIndex = 0
private var isCharRange: Boolean = false
private var current: Obj? = null
private var initialized = false
override val objClass: ObjClass get() = type
fun Scope.init() {
val s = self.start
val e = self.end
if (s is ObjInt && e is ObjInt) {
lastIndex = if (self.isEndInclusive)
(e.value - s.value + 1).toInt()
else
(e.value - s.value).toInt()
} else if (s is ObjChar && e is ObjChar) {
isCharRange = true
lastIndex = if (self.isEndInclusive)
(e.value.code - s.value.code + 1)
else
(e.value.code - s.value.code)
} else {
raiseError("not implemented iterator for range of $this")
private fun ensureInit() {
if (!initialized) {
current = self.start ?: ObjNull
initialized = true
}
}
fun hasNext(): Boolean = nextIndex < lastIndex
suspend fun hasNext(scope: Scope): Boolean {
if (directionMismatch) return false
ensureInit()
val cur = current ?: return false
return self.contains(scope, cur)
}
fun next(scope: Scope): Obj =
if (nextIndex < lastIndex) {
val start = self.start
val x = if (start is ObjInt)
start.value + nextIndex++
else if (start is ObjChar)
start.value.code.toLong() + nextIndex++
else
scope.raiseError("iterator error: unsupported range start")
if (isCharRange) ObjChar(x.toInt().toChar()) else ObjInt.of(x)
}
else {
suspend fun next(scope: Scope): Obj {
if (!hasNext(scope)) {
scope.raiseError(ObjIterationFinishedException(scope))
}
val result = current ?: scope.raiseError("iterator error: missing current value")
current = advance(scope, result)
return result
}
private suspend fun advance(scope: Scope, value: Obj): Obj =
if (value is ObjChar) {
val delta = (step as? ObjInt)
?: scope.raiseIllegalState("Char range step must be Int")
ObjChar((value.value.code + delta.value.toInt()).toChar())
} else {
value.plus(scope, step)
}
companion object {
val type = ObjClass("RangeIterator", ObjIterator).apply {
addFn("hasNext") {
thisAs<ObjRangeIterator>().hasNext().toObj()
thisAs<ObjRangeIterator>().hasNext(this).toObj()
}
addFn("next") {
thisAs<ObjRangeIterator>().next(this)

View File

@ -2495,7 +2495,8 @@ class MapLiteralRef(private val entries: List<MapLiteralEntry>) : ObjRef {
class RangeRef(
internal val left: ObjRef?,
internal val right: ObjRef?,
internal val isEndInclusive: Boolean
internal val isEndInclusive: Boolean,
internal val step: ObjRef? = null
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
return evalValue(scope).asReadonly
@ -2504,7 +2505,8 @@ class RangeRef(
override suspend fun evalValue(scope: Scope): Obj {
val l = left?.evalValue(scope) ?: ObjNull
val r = right?.evalValue(scope) ?: ObjNull
return ObjRange(l, r, isEndInclusive = isEndInclusive)
val st = step?.evalValue(scope)
return ObjRange(l, r, isEndInclusive = isEndInclusive, step = st)
}
}

View File

@ -125,7 +125,7 @@ class ForInStatement(
}
val sourceObj = source.execute(forContext)
return if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) {
return if (sourceObj is ObjRange && sourceObj.isIntRange && !sourceObj.hasExplicitStep && PerfFlags.PRIMITIVE_FASTOPS) {
loopIntRange(
forContext,
sourceObj.start!!.toLong(),

View File

@ -3457,6 +3457,26 @@ class ScriptTest {
println(y.list)
}
@Test
fun testRangeStepIteration() = runTest {
val ints = eval("""(1..5 step 2).toList()""") as ObjList
assertEquals(listOf(1, 3, 5), ints.list.map { it.toInt() })
val chars = eval("""('a'..'e' step 2).toList()""") as ObjList
assertEquals(listOf('a', 'c', 'e'), chars.list.map { it.toString().single() })
val reals = eval("""(0.0..1.0 step 0.25).toList()""") as ObjList
assertEquals(listOf(0.0, 0.25, 0.5, 0.75, 1.0), reals.list.map { it.toDouble() })
val empty = eval("""(5..1 step 1).toList()""") as ObjList
assertEquals(0, empty.list.size)
val openEnd = eval("""(0.. step 1).take(3).toList()""") as ObjList
assertEquals(listOf(0, 1, 2), openEnd.list.map { it.toInt() })
assertFailsWith<ExecutionError> {
eval("""(0.0..1.0).toList()""")
}
assertFailsWith<ExecutionError> {
eval("""(0..).toList()""")
}
}
@Test
fun testMultilineStrings() = runTest {
assertEquals(

View File

@ -21,14 +21,11 @@
package net.sergeych.lyngweb
import kotlinx.browser.window
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Source
import net.sergeych.lyng.binding.Binder
import net.sergeych.lyng.binding.SymbolKind
import net.sergeych.lyng.highlight.HighlightKind
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.tools.LyngLanguageTools
import net.sergeych.lyng.tools.LyngSemanticKind
import org.w3c.dom.HTMLStyleElement
fun ensureBootstrapCodeBlocks(html: String): String {
@ -339,164 +336,34 @@ fun applyLyngHighlightToText(text: String): String {
*/
suspend fun applyLyngHighlightToTextAst(text: String): String {
return try {
// Ensure CSS present
ensureLyngHighlightStyles()
val source = Source("<web>", text)
// Token baseline
val tokenSpans = SimpleLyngHighlighter().highlight(text)
val analysis = LyngLanguageTools.analyze(text, "<web>")
val source = analysis.source
val tokenSpans = analysis.lexicalHighlights
if (tokenSpans.isEmpty()) return htmlEscape(text)
// Build Mini-AST
val sink = MiniAstBuilder()
Compiler.compileWithMini(text, sink)
val mini = sink.build()
// Collect overrides from AST and Binding with precise offsets
val overrides = HashMap<Pair<Int, Int>, String>()
fun putName(startPos: net.sergeych.lyng.Pos, name: String, cls: String) {
val s = source.offsetOf(startPos)
val e = s + name.length
if (s >= 0 && e <= text.length && s < e) overrides[s to e] = cls
fun classForSemantic(kind: LyngSemanticKind): String? = when (kind) {
LyngSemanticKind.Function -> "hl-fn"
LyngSemanticKind.Class, LyngSemanticKind.Enum, LyngSemanticKind.TypeAlias -> "hl-class"
LyngSemanticKind.Value -> "hl-val"
LyngSemanticKind.Variable -> "hl-var"
LyngSemanticKind.Parameter -> "hl-param"
LyngSemanticKind.TypeRef -> "hl-ty"
LyngSemanticKind.EnumConstant -> "hl-enumc"
}
// Declarations
mini?.declarations?.forEach { d ->
when (d) {
is MiniFunDecl -> putName(d.nameStart, d.name, "hl-fn")
is MiniClassDecl -> putName(d.nameStart, d.name, "hl-class")
is net.sergeych.lyng.miniast.MiniValDecl -> putName(d.nameStart, d.name, if (d.mutable) "hl-var" else "hl-val")
is MiniEnumDecl -> putName(d.nameStart, d.name, "hl-class")
}
LyngLanguageTools.semanticHighlights(analysis).forEach { s ->
classForSemantic(s.kind)?.let { overrides[s.range.start to s.range.endExclusive] = it }
}
// Imports: color each segment as directive/path
mini?.imports?.forEach { imp ->
analysis.mini?.imports?.forEach { imp ->
imp.segments.forEach { seg ->
val s = source.offsetOf(seg.range.start)
val e = source.offsetOf(seg.range.end)
if (s >= 0 && e <= text.length && s < e) overrides[s to e] = "hl-dir"
}
}
// Parameters
mini?.declarations?.filterIsInstance<MiniFunDecl>()?.forEach { fn ->
fn.params.forEach { p -> putName(p.nameStart, p.name, "hl-param") }
}
// Type name segments
fun addTypeSegments(t: net.sergeych.lyng.miniast.MiniTypeRef?) {
when (t) {
is MiniTypeName -> t.segments.forEach { seg ->
val s = source.offsetOf(seg.range.start)
val e = s + seg.name.length
if (s >= 0 && e <= text.length && s < e) overrides[s to e] = "hl-ty"
}
is net.sergeych.lyng.miniast.MiniGenericType -> {
addTypeSegments(t.base)
t.args.forEach { addTypeSegments(it) }
}
else -> {}
}
}
mini?.declarations?.forEach { d ->
when (d) {
is MiniFunDecl -> {
addTypeSegments(d.returnType)
d.params.forEach { addTypeSegments(it.type) }
}
is net.sergeych.lyng.miniast.MiniValDecl -> addTypeSegments(d.type)
is MiniClassDecl -> {}
is MiniEnumDecl -> {}
}
}
// Apply binder results to mark usages by semantic kind (params, locals, top-level, functions, classes)
try {
if (mini != null) {
val binding = Binder.bind(text, mini)
val symbolsById = binding.symbols.associateBy { it.id }
// Map decl ranges to avoid overriding declarations
val declKeys = HashSet<Pair<Int, Int>>()
for (sym in binding.symbols) {
declKeys += (sym.declStart to sym.declEnd)
}
fun classForKind(k: SymbolKind): String? = when (k) {
SymbolKind.Function -> "hl-fn"
SymbolKind.Class, SymbolKind.Enum -> "hl-class"
SymbolKind.Parameter -> "hl-param"
SymbolKind.Value -> "hl-val"
SymbolKind.Variable -> "hl-var"
}
for (ref in binding.references) {
val key = ref.start to ref.end
if (declKeys.contains(key)) continue
if (!overrides.containsKey(key)) {
val sym = symbolsById[ref.symbolId]
val cls = sym?.let { classForKind(it.kind) }
if (cls != null) overrides[key] = cls
}
}
}
} catch (_: Throwable) {
// Binder is best-effort; ignore on any failure
}
fun isFollowedByParen(rangeEnd: Int): Boolean {
var i = rangeEnd
while (i < text.length) {
val ch = text[i]
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { i++; continue }
return ch == '('
}
return false
}
fun isFollowedByBlock(rangeEnd: Int): Boolean {
var i = rangeEnd
while (i < text.length) {
val ch = text[i]
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { i++; continue }
return ch == '{'
}
return false
}
// First: mark function call-sites (identifier immediately followed by '('), best-effort.
// Do this before vars/params so it takes precedence where both could match.
run {
for (s in tokenSpans) {
if (s.kind == HighlightKind.Identifier) {
val key = s.range.start to s.range.endExclusive
if (!overrides.containsKey(key)) {
if (isFollowedByParen(s.range.endExclusive) || isFollowedByBlock(s.range.endExclusive)) {
overrides[key] = "hl-fn"
}
}
}
}
}
// Highlight usages of top-level vals/vars and parameters (best-effort, no binder yet)
val nameRoleMap = HashMap<String, String>(8)
mini?.declarations?.forEach { d ->
when (d) {
is net.sergeych.lyng.miniast.MiniValDecl -> nameRoleMap[d.name] = if (d.mutable) "hl-var" else "hl-val"
is MiniFunDecl -> d.params.forEach { p -> nameRoleMap[p.name] = "hl-param" }
else -> {}
}
}
// For every identifier token not already overridden, apply role based on known names
for (s in tokenSpans) {
if (s.kind == HighlightKind.Identifier) {
val key = s.range.start to s.range.endExclusive
if (!overrides.containsKey(key)) {
val ident = text.substring(s.range.start, s.range.endExclusive)
val cls = nameRoleMap[ident]
if (cls != null) {
// Avoid marking function call sites as vars/params
if (!isFollowedByParen(s.range.endExclusive)) {
overrides[key] = cls
}
}
}
}
}
// Render merging overrides
val sb = StringBuilder(text.length + tokenSpans.size * 16)
@ -534,7 +401,7 @@ private fun detectDeclarationAndParamOverrides(text: String): Map<Pair<Int, Int>
"if", "else", "while", "do", "for", "when", "try", "catch", "finally",
"throw", "return", "break", "continue", "in", "is", "as", "as?", "not",
"true", "false", "null", "private", "protected", "abstract", "closed", "override", "open", "extern", "static",
"init", "get", "set", "Unset", "by"
"init", "get", "set", "Unset", "by", "step"
)
fun skipWs(idx0: Int): Int {
var idx = idx0