diff --git a/docs/Range.md b/docs/Range.md index 909d0e3..644711e 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -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 | @@ -105,4 +127,4 @@ Exclusive end char ranges are supported too: Ranges are also used with the `clamp(value, range)` function and the `value.clamp(range)` extension method to limit values within boundaries. -[Iterable]: Iterable.md \ No newline at end of file +[Iterable]: Iterable.md diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt index b2f02a7..bd14f57 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt @@ -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) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index a4b9bfc..30c8aac 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt index e52772e..7171862 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt @@ -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 @@ -597,4 +598,4 @@ private class Parser(fromPos: Pos) { loadToEndOfLine() } -} \ No newline at end of file +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt index 19dcf32..74f7e7e 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, + 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, @@ -59,4 +59,4 @@ data class Token(val value: String, val pos: Pos, val type: Type) { companion object { // fun eof(parser: Parser) = Token("", parser.currentPos, Type.EOF) } -} \ No newline at end of file +} 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 76b30b7..6c4c1dd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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) 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 59dbd59..6558a8e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -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]) 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 f126bf9..9b30177 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -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 -> 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 1db8b39..56c8d33 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -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 } } 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 b29938a..956e187 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt @@ -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 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 dc61c66..1ddb898 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt @@ -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().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().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().let { it.start == null || it.end == null }.toObj() } + getter = { thisAs().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() - 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) + } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt index 5a38285..d4ef07a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt @@ -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().hasNext().toObj() + thisAs().hasNext(this).toObj() } addFn("next") { thisAs().next(this) 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 dd4e0e0..8c7bb50 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -2495,7 +2495,8 @@ class MapLiteralRef(private val entries: List) : 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) } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index 782ea9a..92e9308 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -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(), diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 69759e7..1f0bcba 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -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 { + eval("""(0.0..1.0).toList()""") + } + assertFailsWith { + eval("""(0..).toList()""") + } + } + @Test fun testMultilineStrings() = runTest { assertEquals( diff --git a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt index bfff5aa..07adca7 100644 --- a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt +++ b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt @@ -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("", text) - // Token baseline - val tokenSpans = SimpleLyngHighlighter().highlight(text) + val analysis = LyngLanguageTools.analyze(text, "") + 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, 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()?.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>() - 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(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 "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