fix #49 regular expressions, operator match and docs
This commit is contained in:
parent
ead2f7168e
commit
f45fa7f7a0
91
docs/Regex.md
Normal file
91
docs/Regex.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Regular expressions
|
||||||
|
|
||||||
|
In lyng, you create regular expressions using class `Regex` or `String.re` methods:
|
||||||
|
|
||||||
|
assert( "\d*".re is Regex )
|
||||||
|
assert( Regex("\d*") is Regex )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
We plan to add slash syntax at some point.
|
||||||
|
|
||||||
|
To check that some string matches as whole to some regex:
|
||||||
|
|
||||||
|
assert( "123".matches("\d{3}".re) )
|
||||||
|
assert( !"123".matches("\d{4}".re) )
|
||||||
|
assert( !"1234".matches("\d".re) )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
To check that _part of the string_ matches some regular expession, use _match operator_ `=~` just like in Ruby, and its
|
||||||
|
counterpart, _not match_ operator `!~`:
|
||||||
|
|
||||||
|
assert( "abc123def" =~ "\d\d\d".re )
|
||||||
|
assert( "abc" !~ "\d\d\d".re )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
When you need to find groups, and more detailed match information, use `Regex.find`:
|
||||||
|
|
||||||
|
val result = Regex("abc(\d)(\d)(\d)").find( "bad456 good abc123")
|
||||||
|
assert( result != null )
|
||||||
|
assertEquals( 12 .. 17, result.range )
|
||||||
|
assertEquals( "abc123", result[0] )
|
||||||
|
assertEquals( "1", result[1] )
|
||||||
|
assertEquals( "2", result[2] )
|
||||||
|
assertEquals( "3", result[3] )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Note that the object `RegexMatch`, returned by [Regex.find], behaves much like in many other languages: it provides the
|
||||||
|
index range and groups matches as indexes.
|
||||||
|
|
||||||
|
Match operator actually also provides `RegexMatch` in `$~` reserved variable (borrowed from Ruby too):
|
||||||
|
|
||||||
|
assert( "bad456 good abc123" =~ "abc(\d)(\d)(\d)".re )
|
||||||
|
assertEquals( 12 .. 17, $~.range )
|
||||||
|
assertEquals( "abc123", $~[0] )
|
||||||
|
assertEquals( "1", $~[1] )
|
||||||
|
assertEquals( "2", $~[2] )
|
||||||
|
assertEquals( "3", $~[3] )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
This is often more readable than calling `find`.
|
||||||
|
|
||||||
|
Note that `=~` and `!~` operators against strings and regular expressions are commutative, e.g. regular expression and a
|
||||||
|
string can be either left or right operator, but not both:
|
||||||
|
|
||||||
|
assert( "abc" =~ "\wc".re )
|
||||||
|
assert( "abc" !~ "\w1c".re )
|
||||||
|
assert( "a\wc".re =~ "abcd" )
|
||||||
|
assert( "a[a-z]c".re !~ "a2cd" )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Also, string indexing is Regex-aware, and works like `Regex.find` (_not findall!_):
|
||||||
|
|
||||||
|
assert( "cd" == "abcdef"[ "c.".re ].value )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
|
||||||
|
# Regex class reference
|
||||||
|
|
||||||
|
| name | description | notes |
|
||||||
|
|--------------|-------------------------------------|-------|
|
||||||
|
| matches(str) | true if the whole `str` matches | |
|
||||||
|
| find(str) | find first match in `str` or null | (1) |
|
||||||
|
| findAll(str) | find all matches in `str` as [List] | (1) |
|
||||||
|
|
||||||
|
(1)
|
||||||
|
:: See `RegexMatch` class description below
|
||||||
|
|
||||||
|
# RegexMatch
|
||||||
|
|
||||||
|
| name | description | notes |
|
||||||
|
|-------|-------------------------------------------|-------|
|
||||||
|
| range | the [Range] of the match in source string | |
|
||||||
|
| value | the value that matches | |
|
||||||
|
| [n] | [List] of group matches | (1) |
|
||||||
|
|
||||||
|
(1)
|
||||||
|
:: the [0] element is always value, [1] is group 1 match of any, etc.
|
||||||
|
|
||||||
|
[List]: List.md
|
||||||
|
|
||||||
|
[Range]: Range.md
|
||||||
|
|
@ -275,6 +275,8 @@ Logical operation could be used the same
|
|||||||
| === | | Any | (2) |
|
| === | | Any | (2) |
|
||||||
| !== | | Any | (2) |
|
| !== | | Any | (2) |
|
||||||
| != | | Any | (1) |
|
| != | | Any | (1) |
|
||||||
|
| =~ | | | (3) |
|
||||||
|
| !~ | | | (3) |
|
||||||
| ++a, a++ | | Int | |
|
| ++a, a++ | | Int | |
|
||||||
| --a, a-- | | Int | |
|
| --a, a-- | | Int | |
|
||||||
|
|
||||||
@ -286,6 +288,9 @@ Logical operation could be used the same
|
|||||||
singleton object, like `null`, are referentially equal too, while string different literals even being equal are most
|
singleton object, like `null`, are referentially equal too, while string different literals even being equal are most
|
||||||
likely referentially not equal
|
likely referentially not equal
|
||||||
|
|
||||||
|
(3)
|
||||||
|
: Implemented now in String and Regex as regular expression match and not match, see [Regex].
|
||||||
|
|
||||||
Reference quality and object equality example:
|
Reference quality and object equality example:
|
||||||
|
|
||||||
assert( null == null) // singletons
|
assert( null == null) // singletons
|
||||||
@ -1285,9 +1290,26 @@ Open-ended ranges could be used to get start and end too:
|
|||||||
assertEquals( "pult", "catapult"[ 4.. ])
|
assertEquals( "pult", "catapult"[ 4.. ])
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
|
||||||
### String operations
|
### String operations
|
||||||
|
|
||||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
||||||
|
[Regex] support for strings, see the link, for example, whole string match:
|
||||||
|
|
||||||
|
assert( !"123".matches( "\d\d".re ) )
|
||||||
|
assert( "123".matches( "\d\d\d".re ) )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Extraction:
|
||||||
|
|
||||||
|
"abcd42def"[ "\d+".re ].value
|
||||||
|
>>> "42"
|
||||||
|
|
||||||
|
Part match:
|
||||||
|
|
||||||
|
assert( "abc foo def" =~ "f[oO]+".re )
|
||||||
|
assert( "foo" == $~.value )
|
||||||
|
>>> void
|
||||||
|
|
||||||
Typical set of String functions includes:
|
Typical set of String functions includes:
|
||||||
|
|
||||||
@ -1305,17 +1327,24 @@ Typical set of String functions includes:
|
|||||||
| size | size in characters like `length` because String is [Array] |
|
| size | size in characters like `length` because String is [Array] |
|
||||||
| (args...) | sprintf-like formatting, see [string formatting] |
|
| (args...) | sprintf-like formatting, see [string formatting] |
|
||||||
| [index] | character at index |
|
| [index] | character at index |
|
||||||
| [Range] | substring at range |
|
| [Range] | substring at range (2) |
|
||||||
|
| [Regex] | find first match of regex, like [Regex.find] (2) |
|
||||||
| s1 + s2 | concatenation |
|
| s1 + s2 | concatenation |
|
||||||
| s1 += s2 | self-modifying concatenation |
|
| s1 += s2 | self-modifying concatenation |
|
||||||
| toReal() | attempts to parse string as a Real value |
|
| toReal() | attempts to parse string as a Real value |
|
||||||
| toInt() | parse string to Int value |
|
| toInt() | parse string to Int value |
|
||||||
| characters() | create [List] of characters (1) |
|
| characters() | create [List] of characters (1) |
|
||||||
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
||||||
|
| matches(re) | matches the regular expression (2) |
|
||||||
|
| | |
|
||||||
|
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
: List is mutable therefore a new copy is created on each call.
|
: List is mutable therefore a new copy is created on each call.
|
||||||
|
|
||||||
|
(2)
|
||||||
|
: See [Regex]
|
||||||
|
|
||||||
### Literals
|
### Literals
|
||||||
|
|
||||||
String literal could be multiline:
|
String literal could be multiline:
|
||||||
@ -1391,3 +1420,5 @@ See [math functions](math.md). Other general purpose functions are:
|
|||||||
[Collection]: Collection.md
|
[Collection]: Collection.md
|
||||||
|
|
||||||
[Array]: Array.md
|
[Array]: Array.md
|
||||||
|
|
||||||
|
[Regex]: Regex.md
|
||||||
|
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.8.15-SNAPSHOT"
|
version = "0.9.0-SNAPSHOT"
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -1000,6 +1000,7 @@ class Compiler(
|
|||||||
// condition could be a value, in and is clauses:
|
// condition could be a value, in and is clauses:
|
||||||
// parse several conditions for one then clause
|
// parse several conditions for one then clause
|
||||||
|
|
||||||
|
|
||||||
// loop cases
|
// loop cases
|
||||||
outer@ while (true) {
|
outer@ while (true) {
|
||||||
|
|
||||||
@ -1466,7 +1467,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (i in start ..< end) {
|
for (i in start..<end) {
|
||||||
iVar.value = i
|
iVar.value = i
|
||||||
result = body.execute(forScope)
|
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.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_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
|
||||||
Operator.simple(Token.Type.REF_NEQ, 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
|
// relational <=,... 5
|
||||||
Operator.simple(Token.Type.LTE, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
|
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) },
|
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 digitsSet = ('0'..'9').toSet()
|
||||||
val digits = { d: Char -> d in digitsSet }
|
val digits = { d: Char -> d in digitsSet }
|
||||||
val hexDigits = digitsSet + ('a'..'f') + ('A'..'F')
|
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")
|
@Suppress("unused")
|
||||||
val idFirstChars = { d: Char -> d.isLetter() || d == '_' }
|
val idFirstChars = { d: Char -> d.isLetter() || d == '_' || d == '$' }
|
||||||
|
|
||||||
fun parseLyng(source: Source): List<Token> {
|
fun parseLyng(source: Source): List<Token> {
|
||||||
val p = Parser(fromPos = source.startPos)
|
val p = Parser(fromPos = source.startPos)
|
||||||
@ -67,13 +67,16 @@ private class Parser(fromPos: Pos) {
|
|||||||
pos.advance()
|
pos.advance()
|
||||||
Token("===", from, Token.Type.REF_EQ)
|
Token("===", from, Token.Type.REF_EQ)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Token("==", from, Token.Type.EQ)
|
else -> Token("==", from, Token.Type.EQ)
|
||||||
}
|
}
|
||||||
} else if( currentChar == '>' ) {
|
} else if (currentChar == '>') {
|
||||||
pos.advance()
|
pos.advance()
|
||||||
Token("=>", from, Token.Type.EQARROW)
|
Token("=>", from, Token.Type.EQARROW)
|
||||||
}
|
} else if (currentChar == '~') {
|
||||||
else
|
pos.advance()
|
||||||
|
Token("=~", from, Token.Type.MATCH)
|
||||||
|
} else
|
||||||
Token("=", from, Token.Type.ASSIGN)
|
Token("=", from, Token.Type.ASSIGN)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,6 +230,9 @@ private class Parser(fromPos: Pos) {
|
|||||||
Token("!==", from, Token.Type.REF_NEQ)
|
Token("!==", from, Token.Type.REF_NEQ)
|
||||||
} else
|
} else
|
||||||
Token("!=", from, Token.Type.NEQ)
|
Token("!=", from, Token.Type.NEQ)
|
||||||
|
} else if (currentChar == '~') {
|
||||||
|
pos.advance()
|
||||||
|
Token("!~", from, Token.Type.NOTMATCH)
|
||||||
} else
|
} else
|
||||||
Token("!", from, Token.Type.NOT)
|
Token("!", from, Token.Type.NOT)
|
||||||
}
|
}
|
||||||
@ -267,7 +273,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
|
|
||||||
in digitsSet -> {
|
in digitsSet -> {
|
||||||
pos.back()
|
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.ELVIS)
|
'?' -> Token("??", from, Token.Type.ELVIS)
|
||||||
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
|
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
|
||||||
@ -310,7 +316,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
// Labels processing is complicated!
|
// Labels processing is complicated!
|
||||||
// some@ statement: label 'some', ID 'statement'
|
// some@ statement: label 'some', ID 'statement'
|
||||||
// statement@some: ID 'statement', LABEL 'some'!
|
// statement@some: ID 'statement', LABEL 'some'!
|
||||||
if (ch.isLetter() || ch == '_') {
|
if (idNextChars(ch)) {
|
||||||
val text = ch + loadChars(idNextChars)
|
val text = ch + loadChars(idNextChars)
|
||||||
if (currentChar == '@') {
|
if (currentChar == '@') {
|
||||||
pos.advance()
|
pos.advance()
|
||||||
@ -395,25 +401,24 @@ private class Parser(fromPos: Pos) {
|
|||||||
private fun fixMultilineStringLiteral(source: String): String {
|
private fun fixMultilineStringLiteral(source: String): String {
|
||||||
val sizes = mutableListOf<Int>()
|
val sizes = mutableListOf<Int>()
|
||||||
val lines = source.lines().toMutableList()
|
val lines = source.lines().toMutableList()
|
||||||
if( lines.size == 0 ) return ""
|
if (lines.size == 0) return ""
|
||||||
if( lines[0].isBlank() ) lines.removeFirst()
|
if (lines[0].isBlank()) lines.removeFirst()
|
||||||
if( lines.isEmpty()) return ""
|
if (lines.isEmpty()) return ""
|
||||||
if( lines.last().isBlank() ) lines.removeLast()
|
if (lines.last().isBlank()) lines.removeLast()
|
||||||
|
|
||||||
val normalized = lines.map { l ->
|
val normalized = lines.map { l ->
|
||||||
if( l.isBlank() ) {
|
if (l.isBlank()) {
|
||||||
sizes.add(-1)
|
sizes.add(-1)
|
||||||
""
|
""
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
val margin = leftMargin(l)
|
val margin = leftMargin(l)
|
||||||
sizes += margin
|
sizes += margin
|
||||||
" ".repeat(margin) + l.trim()
|
" ".repeat(margin) + l.trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonMargin = sizes.filter { it >= 0 }.min()
|
val commonMargin = sizes.filter { it >= 0 }.min()
|
||||||
val fixed = if( commonMargin < 1 ) lines else normalized.map {
|
val fixed = if (commonMargin < 1) lines else normalized.map {
|
||||||
if( it.isBlank() ) "" else it.drop(commonMargin)
|
if (it.isBlank()) "" else it.drop(commonMargin)
|
||||||
}
|
}
|
||||||
return fixed.joinToString("\n")
|
return fixed.joinToString("\n")
|
||||||
}
|
}
|
||||||
@ -433,11 +438,26 @@ private class Parser(fromPos: Pos) {
|
|||||||
'\\' -> {
|
'\\' -> {
|
||||||
pos.advance() ?: raise("unterminated string")
|
pos.advance() ?: raise("unterminated string")
|
||||||
when (currentChar) {
|
when (currentChar) {
|
||||||
'n' -> {sb.append('\n'); pos.advance()}
|
'n' -> {
|
||||||
'r' -> {sb.append('\r'); pos.advance()}
|
sb.append('\n'); pos.advance()
|
||||||
't' -> {sb.append('\t'); pos.advance()}
|
}
|
||||||
'"' -> {sb.append('"'); pos.advance()}
|
|
||||||
'\\' -> {sb.append('\\'); pos.advance()}
|
'r' -> {
|
||||||
|
sb.append('\r'); pos.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
't' -> {
|
||||||
|
sb.append('\t'); pos.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
'"' -> {
|
||||||
|
sb.append('"'); pos.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
'\\' -> {
|
||||||
|
sb.append('\\'); pos.advance()
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
sb.append('\\').append(currentChar)
|
sb.append('\\').append(currentChar)
|
||||||
pos.advance()
|
pos.advance()
|
||||||
@ -445,7 +465,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
'\n', '\r'-> {
|
'\n', '\r' -> {
|
||||||
newlineDetected = true
|
newlineDetected = true
|
||||||
sb.append(currentChar)
|
sb.append(currentChar)
|
||||||
pos.advance()
|
pos.advance()
|
||||||
@ -459,7 +479,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
}
|
}
|
||||||
pos.advance()
|
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)
|
return Token(result, start, Token.Type.STRING)
|
||||||
}
|
}
|
||||||
@ -538,7 +558,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
// skip shebang
|
// skip shebang
|
||||||
if( pos.readFragment("#!") )
|
if (pos.readFragment("#!"))
|
||||||
loadToEndOfLine()
|
loadToEndOfLine()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
|
||||||
PLUS2, MINUS2,
|
PLUS2, MINUS2,
|
||||||
IN, NOTIN, IS, NOTIS,
|
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,
|
SHUTTLE,
|
||||||
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
||||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||||
|
@ -178,6 +178,14 @@ open class Obj {
|
|||||||
scope.raiseNotImplemented()
|
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 suspend fun assign(scope: Scope, other: Obj): Obj? = null
|
||||||
|
|
||||||
open fun getValue(scope: Scope) = this
|
open fun getValue(scope: Scope) = this
|
||||||
@ -301,6 +309,18 @@ open class Obj {
|
|||||||
return scope
|
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 {
|
companion object {
|
||||||
|
|
||||||
val rootObjectType = ObjClass("Obj").apply {
|
val rootObjectType = ObjClass("Obj").apply {
|
||||||
|
@ -22,11 +22,20 @@ import net.sergeych.lyng.Scope
|
|||||||
class ObjRegex(val regex: Regex) : Obj() {
|
class ObjRegex(val regex: Regex) : Obj() {
|
||||||
override val objClass = type
|
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 {
|
companion object {
|
||||||
val type by lazy {
|
val type by lazy {
|
||||||
object : ObjClass("Regex") {
|
object : ObjClass("Regex") {
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
println(scope.requireOnlyArg<ObjString>().value)
|
|
||||||
return ObjRegex(
|
return ObjRegex(
|
||||||
scope.requireOnlyArg<ObjString>().value.toRegex()
|
scope.requireOnlyArg<ObjString>().value.toRegex()
|
||||||
)
|
)
|
||||||
@ -36,8 +45,7 @@ class ObjRegex(val regex: Regex) : Obj() {
|
|||||||
ObjBool(args.firstAndOnly().toString().matches(thisAs<ObjRegex>().regex))
|
ObjBool(args.firstAndOnly().toString().matches(thisAs<ObjRegex>().regex))
|
||||||
}
|
}
|
||||||
addFn("find") {
|
addFn("find") {
|
||||||
val s = requireOnlyArg<ObjString>().value
|
thisAs<ObjRegex>().find(requireOnlyArg<ObjString>())
|
||||||
thisAs<ObjRegex>().regex.find(s)?.let { ObjRegexMatch(it) } ?: ObjNull
|
|
||||||
}
|
}
|
||||||
addFn("findAll") {
|
addFn("findAll") {
|
||||||
val s = requireOnlyArg<ObjString>().value
|
val s = requireOnlyArg<ObjString>().value
|
||||||
@ -61,6 +69,7 @@ class ObjRegexMatch(val match: MatchResult) : Obj() {
|
|||||||
|
|
||||||
val objRange: ObjRange by lazy {
|
val objRange: ObjRange by lazy {
|
||||||
val r = match.range
|
val r = match.range
|
||||||
|
|
||||||
ObjRange(
|
ObjRange(
|
||||||
ObjInt(r.first.toLong()),
|
ObjInt(r.first.toLong()),
|
||||||
ObjInt(r.last.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 {
|
companion object {
|
||||||
val type by lazy {
|
val type by lazy {
|
||||||
object : ObjClass("RegexMatch") {
|
object : ObjClass("RegexMatch") {
|
||||||
|
@ -56,8 +56,9 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||||
if (index is ObjInt) return ObjChar(value[index.toInt()])
|
when (index) {
|
||||||
if (index is ObjRange) {
|
is ObjInt -> return ObjChar(value[index.toInt()])
|
||||||
|
is ObjRange -> {
|
||||||
val start = if (index.start == null || index.start.isNull) 0 else index.start.toInt()
|
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 end = if (index.end == null || index.end.isNull) value.length else {
|
||||||
val e = index.end.toInt()
|
val e = index.end.toInt()
|
||||||
@ -65,7 +66,13 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
}
|
}
|
||||||
return ObjString(value.substring(start, end))
|
return ObjString(value.substring(start, end))
|
||||||
}
|
}
|
||||||
scope.raiseIllegalArgument("String index must be Int or Range")
|
|
||||||
|
is ObjRegex -> {
|
||||||
|
return index.find(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> scope.raiseIllegalArgument("String index must be Int, Regex or Range")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@ -96,6 +103,11 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
return value == other.value
|
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 lynonType(): LynonType = LynonType.String
|
||||||
|
|
||||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||||
@ -108,7 +120,8 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
ObjString(decoder.unpackBinaryData().decodeToString())
|
ObjString(decoder.unpackBinaryData().decodeToString())
|
||||||
}.apply {
|
}.apply {
|
||||||
addFn("toInt") {
|
addFn("toInt") {
|
||||||
ObjInt(thisAs<ObjString>().value.toLongOrNull()
|
ObjInt(
|
||||||
|
thisAs<ObjString>().value.toLongOrNull()
|
||||||
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
?: raiseIllegalArgument("can't convert to int: $thisObj")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -160,6 +173,22 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
addFn("trim") {
|
addFn("trim") {
|
||||||
thisAs<ObjString>().value.trim().let(::ObjString)
|
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")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2242,6 +2242,45 @@ class ScriptTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseSpecialVars() {
|
||||||
|
val l = parseLyng("$~".toSource("test$~"))
|
||||||
|
println(l)
|
||||||
|
assertEquals(Token.Type.ID, l[0].type)
|
||||||
|
assertEquals("$~", l[0].value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMatchOperator() = runTest {
|
||||||
|
eval("""
|
||||||
|
assert( "abc123".matches(".*\d{3}") )
|
||||||
|
assert( ".*\d{3}".re =~ "abc123" )
|
||||||
|
assert( "abc123" =~ ".*\d{3}".re )
|
||||||
|
assert( "abc123" !~ ".*\d{4}".re )
|
||||||
|
|
||||||
|
"abc123" =~ ".*(\d)(\d)(\d)$".re
|
||||||
|
println($~)
|
||||||
|
assertEquals("1", $~[1])
|
||||||
|
assertEquals("2", $~[2])
|
||||||
|
assertEquals("3", $~[3])
|
||||||
|
assertEquals("abc123", $~[0])
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// fun testWhenMatch() = runTest {
|
||||||
|
// eval(
|
||||||
|
// """
|
||||||
|
// when("abc123") {
|
||||||
|
// ".*(\d)(\d)(\d)".re -> { x ->
|
||||||
|
// assertEquals("123", x[0])
|
||||||
|
// }
|
||||||
|
// else -> assert(false)
|
||||||
|
// }
|
||||||
|
// """.trimIndent()
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testWhenSample1() = runTest {
|
fun testWhenSample1() = runTest {
|
||||||
eval(
|
eval(
|
||||||
@ -3247,7 +3286,7 @@ class ScriptTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
fun testMinimumOptimization() = runTest {
|
fun testMinimumOptimization() = runTest {
|
||||||
val x = Scope().eval(
|
val x = Scope().eval(
|
||||||
"""
|
"""
|
||||||
|
@ -332,4 +332,8 @@ class BookTest {
|
|||||||
runDocTests("../docs/Array.md")
|
runDocTests("../docs/Array.md")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRegex() = runBlocking {
|
||||||
|
runDocTests("../docs/Regex.md")
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user