fix #49 regular expressions, operator match and docs
This commit is contained in:
1 parent
ead2f7168e
commit
f45fa7f7a0
11 files changed
+304
-45
No files matched your search
@@ -1000,6 +1000,7 @@ class Compiler(
|
||||
// condition could be a value, in and is clauses:
|
||||
// parse several conditions for one then clause
|
||||
|
||||
|
||||
// loop cases
|
||||
outer@ while (true) {
|
||||
|
||||
@@ -1466,7 +1467,7 @@ class Compiler(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i in start ..< end) {
|
||||
for (i in start..<end) {
|
||||
iVar.value = i
|
||||
result = body.execute(forScope)
|
||||
}
|
||||
@@ -2007,6 +2008,8 @@ class Compiler(
|
||||
Operator.simple(Token.Type.NEQ, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
|
||||
Operator.simple(Token.Type.REF_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
|
||||
Operator.simple(Token.Type.REF_NEQ, lastPriority) { _, a, b -> ObjBool(a !== b) },
|
||||
Operator.simple(Token.Type.MATCH, lastPriority) { s, a, b -> a.operatorMatch(s,b) },
|
||||
Operator.simple(Token.Type.NOTMATCH, lastPriority) { s, a, b -> a.operatorNotMatch(s,b) },
|
||||
// relational <=,... 5
|
||||
Operator.simple(Token.Type.LTE, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
|
||||
Operator.simple(Token.Type.LT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) },
|
||||
|
||||
@@ -20,10 +20,10 @@ package net.sergeych.lyng
|
||||
val digitsSet = ('0'..'9').toSet()
|
||||
val digits = { d: Char -> d in digitsSet }
|
||||
val hexDigits = digitsSet + ('a'..'f') + ('A'..'F')
|
||||
val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit() }
|
||||
val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit() || d == '$' || d == '~' }
|
||||
|
||||
@Suppress("unused")
|
||||
val idFirstChars = { d: Char -> d.isLetter() || d == '_' }
|
||||
val idFirstChars = { d: Char -> d.isLetter() || d == '_' || d == '$' }
|
||||
|
||||
fun parseLyng(source: Source): List<Token> {
|
||||
val p = Parser(fromPos = source.startPos)
|
||||
@@ -67,13 +67,16 @@ private class Parser(fromPos: Pos) {
|
||||
pos.advance()
|
||||
Token("===", from, Token.Type.REF_EQ)
|
||||
}
|
||||
|
||||
else -> Token("==", from, Token.Type.EQ)
|
||||
}
|
||||
} else if( currentChar == '>' ) {
|
||||
} else if (currentChar == '>') {
|
||||
pos.advance()
|
||||
Token("=>", from, Token.Type.EQARROW)
|
||||
}
|
||||
else
|
||||
} else if (currentChar == '~') {
|
||||
pos.advance()
|
||||
Token("=~", from, Token.Type.MATCH)
|
||||
} else
|
||||
Token("=", from, Token.Type.ASSIGN)
|
||||
}
|
||||
|
||||
@@ -227,6 +230,9 @@ private class Parser(fromPos: Pos) {
|
||||
Token("!==", from, Token.Type.REF_NEQ)
|
||||
} else
|
||||
Token("!=", from, Token.Type.NEQ)
|
||||
} else if (currentChar == '~') {
|
||||
pos.advance()
|
||||
Token("!~", from, Token.Type.NOTMATCH)
|
||||
} else
|
||||
Token("!", from, Token.Type.NOT)
|
||||
}
|
||||
@@ -267,7 +273,7 @@ private class Parser(fromPos: Pos) {
|
||||
|
||||
in digitsSet -> {
|
||||
pos.back()
|
||||
decodeNumber(loadChars { it in digitsSet || it == '_'}, from)
|
||||
decodeNumber(loadChars { it in digitsSet || it == '_' }, from)
|
||||
}
|
||||
|
||||
'\'' -> {
|
||||
@@ -291,7 +297,7 @@ private class Parser(fromPos: Pos) {
|
||||
}
|
||||
|
||||
'?' -> {
|
||||
when(currentChar.also { pos.advance() }) {
|
||||
when (currentChar.also { pos.advance() }) {
|
||||
':' -> Token("??", from, Token.Type.ELVIS)
|
||||
'?' -> Token("??", from, Token.Type.ELVIS)
|
||||
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
|
||||
@@ -310,7 +316,7 @@ private class Parser(fromPos: Pos) {
|
||||
// Labels processing is complicated!
|
||||
// some@ statement: label 'some', ID 'statement'
|
||||
// statement@some: ID 'statement', LABEL 'some'!
|
||||
if (ch.isLetter() || ch == '_') {
|
||||
if (idNextChars(ch)) {
|
||||
val text = ch + loadChars(idNextChars)
|
||||
if (currentChar == '@') {
|
||||
pos.advance()
|
||||
@@ -395,25 +401,24 @@ private class Parser(fromPos: Pos) {
|
||||
private fun fixMultilineStringLiteral(source: String): String {
|
||||
val sizes = mutableListOf<Int>()
|
||||
val lines = source.lines().toMutableList()
|
||||
if( lines.size == 0 ) return ""
|
||||
if( lines[0].isBlank() ) lines.removeFirst()
|
||||
if( lines.isEmpty()) return ""
|
||||
if( lines.last().isBlank() ) lines.removeLast()
|
||||
if (lines.size == 0) return ""
|
||||
if (lines[0].isBlank()) lines.removeFirst()
|
||||
if (lines.isEmpty()) return ""
|
||||
if (lines.last().isBlank()) lines.removeLast()
|
||||
|
||||
val normalized = lines.map { l ->
|
||||
if( l.isBlank() ) {
|
||||
if (l.isBlank()) {
|
||||
sizes.add(-1)
|
||||
""
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
val margin = leftMargin(l)
|
||||
sizes += margin
|
||||
" ".repeat(margin) + l.trim()
|
||||
}
|
||||
}
|
||||
val commonMargin = sizes.filter { it >= 0 }.min()
|
||||
val fixed = if( commonMargin < 1 ) lines else normalized.map {
|
||||
if( it.isBlank() ) "" else it.drop(commonMargin)
|
||||
val fixed = if (commonMargin < 1) lines else normalized.map {
|
||||
if (it.isBlank()) "" else it.drop(commonMargin)
|
||||
}
|
||||
return fixed.joinToString("\n")
|
||||
}
|
||||
@@ -433,11 +438,26 @@ private class Parser(fromPos: Pos) {
|
||||
'\\' -> {
|
||||
pos.advance() ?: raise("unterminated string")
|
||||
when (currentChar) {
|
||||
'n' -> {sb.append('\n'); pos.advance()}
|
||||
'r' -> {sb.append('\r'); pos.advance()}
|
||||
't' -> {sb.append('\t'); pos.advance()}
|
||||
'"' -> {sb.append('"'); pos.advance()}
|
||||
'\\' -> {sb.append('\\'); pos.advance()}
|
||||
'n' -> {
|
||||
sb.append('\n'); pos.advance()
|
||||
}
|
||||
|
||||
'r' -> {
|
||||
sb.append('\r'); pos.advance()
|
||||
}
|
||||
|
||||
't' -> {
|
||||
sb.append('\t'); pos.advance()
|
||||
}
|
||||
|
||||
'"' -> {
|
||||
sb.append('"'); pos.advance()
|
||||
}
|
||||
|
||||
'\\' -> {
|
||||
sb.append('\\'); pos.advance()
|
||||
}
|
||||
|
||||
else -> {
|
||||
sb.append('\\').append(currentChar)
|
||||
pos.advance()
|
||||
@@ -445,7 +465,7 @@ private class Parser(fromPos: Pos) {
|
||||
}
|
||||
}
|
||||
|
||||
'\n', '\r'-> {
|
||||
'\n', '\r' -> {
|
||||
newlineDetected = true
|
||||
sb.append(currentChar)
|
||||
pos.advance()
|
||||
@@ -459,7 +479,7 @@ private class Parser(fromPos: Pos) {
|
||||
}
|
||||
pos.advance()
|
||||
|
||||
val result = sb.toString().let { if( newlineDetected ) fixMultilineStringLiteral(it) else it }
|
||||
val result = sb.toString().let { if (newlineDetected) fixMultilineStringLiteral(it) else it }
|
||||
|
||||
return Token(result, start, Token.Type.STRING)
|
||||
}
|
||||
@@ -538,7 +558,7 @@ private class Parser(fromPos: Pos) {
|
||||
|
||||
init {
|
||||
// skip shebang
|
||||
if( pos.readFragment("#!") )
|
||||
if (pos.readFragment("#!"))
|
||||
loadToEndOfLine()
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
||||
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
||||
PLUS2, MINUS2,
|
||||
IN, NOTIN, IS, NOTIS,
|
||||
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ,
|
||||
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
|
||||
SHUTTLE,
|
||||
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||
|
||||
@@ -178,6 +178,14 @@ open class Obj {
|
||||
scope.raiseNotImplemented()
|
||||
}
|
||||
|
||||
open suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
|
||||
scope.raiseNotImplemented()
|
||||
}
|
||||
|
||||
open suspend fun operatorNotMatch(scope: Scope, other: Obj): Obj {
|
||||
return operatorMatch(scope,other).logicalNot(scope)
|
||||
}
|
||||
|
||||
open suspend fun assign(scope: Scope, other: Obj): Obj? = null
|
||||
|
||||
open fun getValue(scope: Scope) = this
|
||||
@@ -301,6 +309,18 @@ open class Obj {
|
||||
return scope
|
||||
}
|
||||
|
||||
inline fun <reified R: Obj> cast(scope: Scope): R {
|
||||
castOrNull<R>()?.let { return it }
|
||||
scope.raiseClassCastError("can't cast ${this::class.simpleName} to ${R::class.simpleName}")
|
||||
|
||||
}
|
||||
|
||||
inline fun <reified R: Obj> castOrNull(): R? {
|
||||
(this as? R)?.let { return it }
|
||||
// todo: check for subclasses
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val rootObjectType = ObjClass("Obj").apply {
|
||||
|
||||
@@ -22,11 +22,20 @@ import net.sergeych.lyng.Scope
|
||||
class ObjRegex(val regex: Regex) : Obj() {
|
||||
override val objClass = type
|
||||
|
||||
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
|
||||
return regex.find(other.cast<ObjString>(scope).value)?.let {
|
||||
scope.addConst("$~", ObjRegexMatch(it))
|
||||
ObjTrue
|
||||
} ?: ObjFalse
|
||||
}
|
||||
|
||||
fun find(s: ObjString): Obj =
|
||||
regex.find(s.value)?.let { ObjRegexMatch(it) } ?: ObjNull
|
||||
|
||||
companion object {
|
||||
val type by lazy {
|
||||
object : ObjClass("Regex") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
println(scope.requireOnlyArg<ObjString>().value)
|
||||
return ObjRegex(
|
||||
scope.requireOnlyArg<ObjString>().value.toRegex()
|
||||
)
|
||||
@@ -36,8 +45,7 @@ class ObjRegex(val regex: Regex) : Obj() {
|
||||
ObjBool(args.firstAndOnly().toString().matches(thisAs<ObjRegex>().regex))
|
||||
}
|
||||
addFn("find") {
|
||||
val s = requireOnlyArg<ObjString>().value
|
||||
thisAs<ObjRegex>().regex.find(s)?.let { ObjRegexMatch(it) } ?: ObjNull
|
||||
thisAs<ObjRegex>().find(requireOnlyArg<ObjString>())
|
||||
}
|
||||
addFn("findAll") {
|
||||
val s = requireOnlyArg<ObjString>().value
|
||||
@@ -61,6 +69,7 @@ class ObjRegexMatch(val match: MatchResult) : Obj() {
|
||||
|
||||
val objRange: ObjRange by lazy {
|
||||
val r = match.range
|
||||
|
||||
ObjRange(
|
||||
ObjInt(r.first.toLong()),
|
||||
ObjInt(r.last.toLong()),
|
||||
@@ -68,6 +77,19 @@ class ObjRegexMatch(val match: MatchResult) : Obj() {
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun toString(scope: Scope,calledFromLyng: Boolean): ObjString {
|
||||
return ObjString("RegexMath(${objRange.toString(scope)},${objGroups.toString(scope)})")
|
||||
}
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
return objGroups.getAt(scope, index)
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if( other === this) return 0
|
||||
return -2
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type by lazy {
|
||||
object : ObjClass("RegexMatch") {
|
||||
|
||||
@@ -56,16 +56,23 @@ data class ObjString(val value: String) : Obj() {
|
||||
}
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
if (index is ObjInt) return ObjChar(value[index.toInt()])
|
||||
if (index is ObjRange) {
|
||||
val start = if (index.start == null || index.start.isNull) 0 else index.start.toInt()
|
||||
val end = if (index.end == null || index.end.isNull) value.length else {
|
||||
val e = index.end.toInt()
|
||||
if (index.isEndInclusive) e + 1 else e
|
||||
when (index) {
|
||||
is ObjInt -> return ObjChar(value[index.toInt()])
|
||||
is ObjRange -> {
|
||||
val start = if (index.start == null || index.start.isNull) 0 else index.start.toInt()
|
||||
val end = if (index.end == null || index.end.isNull) value.length else {
|
||||
val e = index.end.toInt()
|
||||
if (index.isEndInclusive) e + 1 else e
|
||||
}
|
||||
return ObjString(value.substring(start, end))
|
||||
}
|
||||
return ObjString(value.substring(start, end))
|
||||
|
||||
is ObjRegex -> {
|
||||
return index.find(this)
|
||||
}
|
||||
|
||||
else -> scope.raiseIllegalArgument("String index must be Int, Regex or Range")
|
||||
}
|
||||
scope.raiseIllegalArgument("String index must be Int or Range")
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -96,6 +103,11 @@ data class ObjString(val value: String) : Obj() {
|
||||
return value == other.value
|
||||
}
|
||||
|
||||
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
|
||||
val re = other.cast<ObjRegex>(scope)
|
||||
return re.operatorMatch(scope, this)
|
||||
}
|
||||
|
||||
override suspend fun lynonType(): LynonType = LynonType.String
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
@@ -108,8 +120,9 @@ data class ObjString(val value: String) : Obj() {
|
||||
ObjString(decoder.unpackBinaryData().decodeToString())
|
||||
}.apply {
|
||||
addFn("toInt") {
|
||||
ObjInt(thisAs<ObjString>().value.toLongOrNull()
|
||||
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
||||
ObjInt(
|
||||
thisAs<ObjString>().value.toLongOrNull()
|
||||
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
||||
)
|
||||
}
|
||||
addFn("startsWith") {
|
||||
@@ -160,6 +173,22 @@ data class ObjString(val value: String) : Obj() {
|
||||
addFn("trim") {
|
||||
thisAs<ObjString>().value.trim().let(::ObjString)
|
||||
}
|
||||
addFn("matches") {
|
||||
val s = requireOnlyArg<Obj>()
|
||||
val self = thisAs<ObjString>().value
|
||||
ObjBool(
|
||||
when (s) {
|
||||
is ObjRegex -> self.matches(s.regex)
|
||||
is ObjString -> {
|
||||
if (s.value == ".*") true
|
||||
else self.matches(s.value.toRegex())
|
||||
}
|
||||
|
||||
else ->
|
||||
raiseIllegalArgument("can't match ${s.objClass.className}: required Regex or String")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user