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) ) assert( (1..<3) in (1..3) )
>>> void >>> void
## Finite Ranges are iterable ## Ranges are iterable
So given a range with both ends, you can assume it is [Iterable]. This automatically let Finite ranges are [Iterable] and can be used in loops and list conversions.
use finite ranges in loops and convert it to lists: 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() ) assert( [-2, -1, 0, 1] == (-2..1).toList() )
>>> void >>> void
@ -70,6 +71,26 @@ but
>>> 2 >>> 2
>>> void >>> 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 ## Character ranges
You can use Char as both ends of the closed range: 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 | | isEndInclusive | true for '..' | Bool |
| isOpen | at any end | Bool | | isOpen | at any end | Bool |
| isIntRange | both start and end are Int | Bool | | isIntRange | both start and end are Int | Bool |
| step | explicit iteration step | Any? |
| start | | Any? | | start | | Any? |
| end | | Any? | | end | | Any? |
| size | for finite ranges, see above | Long | | size | for finite ranges, see above | Long |

View File

@ -36,7 +36,7 @@ class LyngLexer : LexerBase() {
"abstract", "closed", "override", "static", "extern", "open", "private", "protected", "abstract", "closed", "override", "static", "extern", "open", "private", "protected",
"if", "else", "for", "while", "return", "true", "false", "null", "if", "else", "for", "while", "return", "true", "false", "null",
"when", "in", "is", "break", "continue", "try", "catch", "finally", "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) { override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) {

View File

@ -703,6 +703,10 @@ class Compiler(
private suspend fun parseTypeAliasDeclaration(): Statement { private suspend fun parseTypeAliasDeclaration(): Statement {
val nameToken = cc.requireToken(Token.Type.ID, "type alias name expected") 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 declaredName = nameToken.value
val outerClassName = currentEnclosingClassName() val outerClassName = currentEnclosingClassName()
val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName
@ -719,13 +723,13 @@ class Compiler(
} }
val typeParamNames = uniqueParams val typeParamNames = uniqueParams
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.add(typeParamNames) if (typeParamNames.isNotEmpty()) pendingTypeParamStack.add(typeParamNames)
val body = try { val (body, bodyMini) = try {
cc.skipWsTokens() cc.skipWsTokens()
val eq = cc.nextNonWhitespace() val eq = cc.nextNonWhitespace()
if (eq.type != Token.Type.ASSIGN) { if (eq.type != Token.Type.ASSIGN) {
throw ScriptError(eq.pos, "type alias $qualifiedName expects '='") throw ScriptError(eq.pos, "type alias $qualifiedName expects '='")
} }
parseTypeExpressionWithMini().first parseTypeExpressionWithMini()
} finally { } finally {
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.removeLast() if (typeParamNames.isNotEmpty()) pendingTypeParamStack.removeLast()
} }
@ -738,7 +742,16 @@ class Compiler(
outerCtx?.classScopeMembers?.add(declaredName) outerCtx?.classScopeMembers?.add(declaredName)
registerClassScopeMember(outerClassName, 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 aliasExpr = net.sergeych.lyng.obj.TypeDeclRef(body, nameToken.pos)
val initStmt = ExpressionStatement(aliasExpr, nameToken.pos) val initStmt = ExpressionStatement(aliasExpr, nameToken.pos)
@ -2422,7 +2435,10 @@ class Compiler(
// to skip in parseExpression: // to skip in parseExpression:
val current = cc.current() val current = cc.current()
val right = 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 null
else else
parseExpression() 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 -> { Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
operand = operand?.let { left -> operand = operand?.let { left ->
// Trailing block-argument function call: the leading '{' is already consumed, // Trailing block-argument function call: the leading '{' is already consumed,
@ -6042,12 +6082,14 @@ class Compiler(
is ConstRef -> { is ConstRef -> {
val range = ref.constValue as? ObjRange ?: return null val range = ref.constValue as? ObjRange ?: return null
if (!range.isIntRange) return null if (!range.isIntRange) return null
if (range.step != null && !range.step.isNull) return null
val start = range.start?.toLong() ?: return null val start = range.start?.toLong() ?: return null
val end = range.end?.toLong() ?: return null val end = range.end?.toLong() ?: return null
val endExclusive = if (range.isEndInclusive) end + 1 else end val endExclusive = if (range.isEndInclusive) end + 1 else end
return ConstIntRange(start, endExclusive) return ConstIntRange(start, endExclusive)
} }
is RangeRef -> { is RangeRef -> {
if (ref.step != null) return null
val start = constIntValueOrNull(ref.left) ?: return null val start = constIntValueOrNull(ref.left) ?: return null
val end = constIntValueOrNull(ref.right) ?: return null val end = constIntValueOrNull(ref.right) ?: return null
val endExclusive = if (ref.isEndInclusive) end + 1 else end val 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) "in" -> Token("in", from, Token.Type.IN)
"is" -> Token("is", from, Token.Type.IS) "is" -> Token("is", from, Token.Type.IS)
"by" -> Token("by", from, Token.Type.BY) "by" -> Token("by", from, Token.Type.BY)
"step" -> Token("step", from, Token.Type.STEP)
"object" -> Token("object", from, Token.Type.OBJECT) "object" -> Token("object", from, Token.Type.OBJECT)
"as" -> { "as" -> {
// support both `as` and tight `as?` without spaces // support both `as` and tight `as?` without spaces

View File

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

View File

@ -2202,8 +2202,17 @@ class BytecodeCompiler(
val inclusiveSlot = allocSlot() val inclusiveSlot = allocSlot()
val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive)) val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive))
builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot) builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot)
val 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() 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) updateSlotType(dst, SlotType.OBJ)
slotObjClass[dst] = ObjRange.type slotObjClass[dst] = ObjRange.type
return CompiledValue(dst, SlotType.OBJ) return CompiledValue(dst, SlotType.OBJ)
@ -5508,7 +5517,8 @@ class BytecodeCompiler(
private fun extractRangeRef(source: Statement): RangeRef? { private fun extractRangeRef(source: Statement): RangeRef? {
val target = if (source is BytecodeStatement) source.original else source val target = if (source is BytecodeStatement) source.original else source
val expr = target as? ExpressionStatement ?: return null val expr = target as? ExpressionStatement ?: return null
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? { private fun extractDeclaredRange(stmt: Statement?): RangeRef? {
@ -5519,6 +5529,7 @@ class BytecodeCompiler(
if (ref is RangeRef) return ref if (ref is RangeRef) return ref
if (ref is ConstRef) { if (ref is ConstRef) {
val range = ref.constValue as? ObjRange ?: return null 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 start = range.start as? ObjInt ?: return null
val end = range.end as? ObjInt ?: return null val end = range.end as? ObjInt ?: return null
val left = ConstRef(start.asReadonly) val left = ConstRef(start.asReadonly)

View File

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

View File

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

View File

@ -171,13 +171,16 @@ class CmdMakeRange(
internal val startSlot: Int, internal val startSlot: Int,
internal val endSlot: Int, internal val endSlot: Int,
internal val inclusiveSlot: Int, internal val inclusiveSlot: Int,
internal val stepSlot: Int,
internal val dst: Int, internal val dst: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val start = frame.slotToObj(startSlot) val start = frame.slotToObj(startSlot)
val end = frame.slotToObj(endSlot) val end = frame.slotToObj(endSlot)
val inclusive = frame.slotToObj(inclusiveSlot).toBool() val inclusive = frame.slotToObj(inclusiveSlot).toBool()
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 return
} }
} }

View File

@ -41,9 +41,9 @@ private val fallbackKeywordIds = setOf(
// boolean operators // boolean operators
"and", "or", "not", "and", "or", "not",
// declarations & modifiers // 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", "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 // control flow and misc
"if", "else", "when", "while", "do", "for", "try", "catch", "finally", "if", "else", "when", "while", "do", "for", "try", "catch", "finally",
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset" "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 Type.COMMA, Type.SEMICOLON, Type.COLON -> HighlightKind.Punctuation
// textual control keywords // textual control keywords
Type.IN, Type.NOTIN, Type.IS, Type.NOTIS, Type.AS, Type.ASNULL, Type.BY, Type.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 Type.AND, Type.OR, Type.NOT -> HighlightKind.Keyword
// labels / annotations // 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.addPropertyDoc
import net.sergeych.lyng.miniast.type 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 isOpenStart by lazy { start == null || start.isNull }
val isOpenEnd by lazy { end == null || end.isNull } val isOpenEnd by lazy { end == null || end.isNull }
val hasExplicitStep: Boolean get() = step != null && !step.isNull
override val objClass: ObjClass get() = type 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) ?: '∞'} ..") result.append("${start?.inspect(scope) ?: '∞'} ..")
if (!isEndInclusive) result.append('<') if (!isEndInclusive) result.append('<')
result.append(" ${end?.inspect(scope) ?: '∞'}") result.append(" ${end?.inspect(scope) ?: '∞'}")
if (hasExplicitStep) {
result.append(" step ${step?.inspect(scope)}")
}
return ObjString(result.toString()) 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 { 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: // 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: // 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: // both exists, now there could be 4 cases:
return when { return when {
other.isEndInclusive && isEndInclusive -> other.isEndInclusive && isEndInclusive ->
end.compareTo(scope, other.end) >= 0 end!!.compareTo(scope, other.end!!) >= 0
!other.isEndInclusive && !isEndInclusive -> !other.isEndInclusive && !isEndInclusive ->
end.compareTo(scope, other.end) >= 0 end!!.compareTo(scope, other.end!!) >= 0
other.isEndInclusive && !isEndInclusive -> other.isEndInclusive && !isEndInclusive ->
end.compareTo(scope, other.end) > 0 end!!.compareTo(scope, other.end!!) > 0
!other.isEndInclusive && isEndInclusive -> !other.isEndInclusive && isEndInclusive ->
end.compareTo(scope, other.end) >= 0 end!!.compareTo(scope, other.end!!) >= 0
else -> throw IllegalStateException("unknown comparison") 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 (isOpenStart && isOpenEnd) return true
if (start != null) { if (!isOpenStart) {
if (start.compareTo(scope, other) > 0) return false if (start!!.compareTo(scope, other) > 0) return false
} }
if (end != null) { if (!isOpenEnd) {
val cmp = end.compareTo(scope, other) val cmp = end!!.compareTo(scope, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
} }
return true 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) { 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 s = start.value
val e = end.value val e = end.value
if (isEndInclusive) { if (isEndInclusive) {
@ -152,7 +161,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
if (!callback(ObjInt.of(i))) break 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 s = start.value
val e = end.value val e = end.value
if (isEndInclusive) { 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 { override suspend fun compareTo(scope: Scope, other: Obj): Int {
return (other as? ObjRange)?.let { 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 ?: -1
} }
@ -180,6 +193,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
var result = start?.hashCode() ?: 0 var result = start?.hashCode() ?: 0
result = 31 * result + (end?.hashCode() ?: 0) result = 31 * result + (end?.hashCode() ?: 0)
result = 31 * result + isEndInclusive.hashCode() result = 31 * result + isEndInclusive.hashCode()
result = 31 * result + (step?.hashCode() ?: 0)
return result 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 (start != other.start) return false
if (end != other.end) return false if (end != other.end) return false
if (isEndInclusive != other.isEndInclusive) return false if (isEndInclusive != other.isEndInclusive) return false
if (step != other.step) return false
return true return true
} }
@ -213,12 +228,19 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
moduleName = "lyng.stdlib", moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().end ?: ObjNull } 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( addPropertyDoc(
name = "isOpen", name = "isOpen",
doc = "Whether the range is open on either side (no start or no end).", doc = "Whether the range is open on either side (no start or no end).",
type = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib", moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj() } getter = { thisAs<ObjRange>().let { it.isOpenStart || it.isOpenEnd }.toObj() }
) )
addPropertyDoc( addPropertyDoc(
name = "isIntRange", name = "isIntRange",
@ -248,20 +270,78 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib"
) { ) {
val self = thisAs<ObjRange>() val self = thisAs<ObjRange>()
if (net.sergeych.lyng.PerfFlags.RANGE_FAST_ITER) { self.buildIterator(this)
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() }
} }
} }
} }
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 package net.sergeych.lyng.obj
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.Scope 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 current: Obj? = null
private var lastIndex = 0 private var initialized = false
private var isCharRange: Boolean = false
override val objClass: ObjClass get() = type override val objClass: ObjClass get() = type
fun Scope.init() { private fun ensureInit() {
val s = self.start if (!initialized) {
val e = self.end current = self.start ?: ObjNull
if (s is ObjInt && e is ObjInt) { initialized = true
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")
} }
} }
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 = suspend fun next(scope: Scope): Obj {
if (nextIndex < lastIndex) { if (!hasNext(scope)) {
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 {
scope.raiseError(ObjIterationFinishedException(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 { companion object {
val type = ObjClass("RangeIterator", ObjIterator).apply { val type = ObjClass("RangeIterator", ObjIterator).apply {
addFn("hasNext") { addFn("hasNext") {
thisAs<ObjRangeIterator>().hasNext().toObj() thisAs<ObjRangeIterator>().hasNext(this).toObj()
} }
addFn("next") { addFn("next") {
thisAs<ObjRangeIterator>().next(this) thisAs<ObjRangeIterator>().next(this)

View File

@ -2495,7 +2495,8 @@ class MapLiteralRef(private val entries: List<MapLiteralEntry>) : ObjRef {
class RangeRef( class RangeRef(
internal val left: ObjRef?, internal val left: ObjRef?,
internal val right: ObjRef?, internal val right: ObjRef?,
internal val isEndInclusive: Boolean internal val isEndInclusive: Boolean,
internal val step: ObjRef? = null
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
return evalValue(scope).asReadonly return evalValue(scope).asReadonly
@ -2504,7 +2505,8 @@ class RangeRef(
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
val l = left?.evalValue(scope) ?: ObjNull val l = left?.evalValue(scope) ?: ObjNull
val r = right?.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) 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( loopIntRange(
forContext, forContext,
sourceObj.start!!.toLong(), sourceObj.start!!.toLong(),

View File

@ -3457,6 +3457,26 @@ class ScriptTest {
println(y.list) 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 @Test
fun testMultilineStrings() = runTest { fun testMultilineStrings() = runTest {
assertEquals( assertEquals(

View File

@ -21,14 +21,11 @@
package net.sergeych.lyngweb package net.sergeych.lyngweb
import kotlinx.browser.window 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.HighlightKind
import net.sergeych.lyng.highlight.SimpleLyngHighlighter import net.sergeych.lyng.highlight.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.offsetOf 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 import org.w3c.dom.HTMLStyleElement
fun ensureBootstrapCodeBlocks(html: String): String { fun ensureBootstrapCodeBlocks(html: String): String {
@ -339,164 +336,34 @@ fun applyLyngHighlightToText(text: String): String {
*/ */
suspend fun applyLyngHighlightToTextAst(text: String): String { suspend fun applyLyngHighlightToTextAst(text: String): String {
return try { return try {
// Ensure CSS present
ensureLyngHighlightStyles() ensureLyngHighlightStyles()
val source = Source("<web>", text) val analysis = LyngLanguageTools.analyze(text, "<web>")
// Token baseline val source = analysis.source
val tokenSpans = SimpleLyngHighlighter().highlight(text) val tokenSpans = analysis.lexicalHighlights
if (tokenSpans.isEmpty()) return htmlEscape(text) 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>() val overrides = HashMap<Pair<Int, Int>, String>()
fun putName(startPos: net.sergeych.lyng.Pos, name: String, cls: String) { fun classForSemantic(kind: LyngSemanticKind): String? = when (kind) {
val s = source.offsetOf(startPos) LyngSemanticKind.Function -> "hl-fn"
val e = s + name.length LyngSemanticKind.Class, LyngSemanticKind.Enum, LyngSemanticKind.TypeAlias -> "hl-class"
if (s >= 0 && e <= text.length && s < e) overrides[s to e] = cls 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 -> LyngLanguageTools.semanticHighlights(analysis).forEach { s ->
when (d) { classForSemantic(s.kind)?.let { overrides[s.range.start to s.range.endExclusive] = it }
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")
}
} }
// Imports: color each segment as directive/path
mini?.imports?.forEach { imp -> analysis.mini?.imports?.forEach { imp ->
imp.segments.forEach { seg -> imp.segments.forEach { seg ->
val s = source.offsetOf(seg.range.start) val s = source.offsetOf(seg.range.start)
val e = source.offsetOf(seg.range.end) val e = source.offsetOf(seg.range.end)
if (s >= 0 && e <= text.length && s < e) overrides[s to e] = "hl-dir" 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 // Render merging overrides
val sb = StringBuilder(text.length + tokenSpans.size * 16) 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", "if", "else", "while", "do", "for", "when", "try", "catch", "finally",
"throw", "return", "break", "continue", "in", "is", "as", "as?", "not", "throw", "return", "break", "continue", "in", "is", "as", "as?", "not",
"true", "false", "null", "private", "protected", "abstract", "closed", "override", "open", "extern", "static", "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 { fun skipWs(idx0: Int): Int {
var idx = idx0 var idx = idx0