Add stepped range iteration
This commit is contained in:
parent
79bece94a5
commit
78d8e546d5
@ -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 |
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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 ->
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>()
|
||||
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 = 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
ObjRangeIterator(self).apply { init() }
|
||||
}
|
||||
}
|
||||
return ObjRangeIterator(this, stepValue, mismatch)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
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)
|
||||
suspend fun hasNext(scope: Scope): Boolean {
|
||||
if (directionMismatch) return false
|
||||
ensureInit()
|
||||
val cur = current ?: return false
|
||||
return self.contains(scope, cur)
|
||||
}
|
||||
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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user