Compare commits
No commits in common. "master" and "0.8.15" have entirely different histories.
27
README.md
27
README.md
@ -134,19 +134,7 @@ Designed to add scripting to kotlin multiplatform application in easy and effici
|
||||
|
||||
# Language Roadmap
|
||||
|
||||
```mermaid
|
||||
flowchart
|
||||
v0([the idea])
|
||||
v1([v1: make it happen])
|
||||
v2([v1.5: testmake it fasted and rich featured])
|
||||
v3([v2: ideal script language])
|
||||
v0 --code MVP--> v1
|
||||
v1 --refine and extend--> v2
|
||||
v2 -- optimize --> v3
|
||||
|
||||
```
|
||||
|
||||
## v1.0.0 "Make it happen"
|
||||
## v1.0.0
|
||||
|
||||
Planned autumn 2025. Complete dynamic language with sufficient standard library:
|
||||
|
||||
@ -174,28 +162,23 @@ Ready features:
|
||||
|
||||
### Under way:
|
||||
|
||||
- [ ] regular exceptions + extended `when`
|
||||
- [ ] regular exceptions
|
||||
- [ ] multiple inheritance for user classes
|
||||
- [ ] site with integrated interpreter to give a try
|
||||
- [ ] kotlin part public API good docs, integration focused
|
||||
|
||||
## plan: v1.0 - v1.5 "Rich and stable"
|
||||
|
||||
Estimated spring of 2026
|
||||
## v1.1+
|
||||
|
||||
Planned features.
|
||||
|
||||
- [ ] type specifications
|
||||
- [ ] language server or compose-based lyng-aware editor
|
||||
- [ ] source docs and maybe lyng.md to a standard
|
||||
- [ ] metadata first class access from lyng
|
||||
|
||||
## After 1.5 "Ideal scripting"
|
||||
|
||||
Estimated winter 2027
|
||||
Further
|
||||
|
||||
- [ ] client with GUI support based on compose multiplatform somehow
|
||||
- [ ] notebook - style workbooks with graphs, formulae, etc.
|
||||
- [ ] aggressive optimizations
|
||||
- [ ] language server or compose-based lyng-aware editor
|
||||
|
||||
[parallelism]: docs/parallelism.md
|
@ -1,91 +0,0 @@
|
||||
# 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,8 +275,6 @@ Logical operation could be used the same
|
||||
| === | | Any | (2) |
|
||||
| !== | | Any | (2) |
|
||||
| != | | Any | (1) |
|
||||
| =~ | | | (3) |
|
||||
| !~ | | | (3) |
|
||||
| ++a, a++ | | Int | |
|
||||
| --a, a-- | | Int | |
|
||||
|
||||
@ -288,9 +286,6 @@ Logical operation could be used the same
|
||||
singleton object, like `null`, are referentially equal too, while string different literals even being equal are most
|
||||
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:
|
||||
|
||||
assert( null == null) // singletons
|
||||
@ -1189,30 +1184,6 @@ See also [math operations](math.md)
|
||||
|
||||
The type for the character objects is `Char`.
|
||||
|
||||
### String literal escapes
|
||||
|
||||
| escape | ASCII value |
|
||||
|--------|-----------------------|
|
||||
| \n | 0x10, newline |
|
||||
| \r | 0x13, carriage return |
|
||||
| \t | 0x07, tabulation |
|
||||
| \\ | \ slash character |
|
||||
| \" | " double quote |
|
||||
|
||||
Other `\c` combinations, where c is any char except mentioned above, are left intact, e.g.:
|
||||
|
||||
val s = "\a"
|
||||
assert(s[0] == '\')
|
||||
assert(s[1] == 'a')
|
||||
>>> void
|
||||
|
||||
same as:
|
||||
|
||||
val s = "\\a"
|
||||
assert(s[0] == '\')
|
||||
assert(s[1] == 'a')
|
||||
>>> void
|
||||
|
||||
### Char literal escapes
|
||||
|
||||
Are the same as in string literals with little difference:
|
||||
@ -1220,7 +1191,6 @@ Are the same as in string literals with little difference:
|
||||
| escape | ASCII value |
|
||||
|--------|-------------------|
|
||||
| \n | 0x10, newline |
|
||||
| \r | 0x13, carriage return |
|
||||
| \t | 0x07, tabulation |
|
||||
| \\ | \ slash character |
|
||||
| \' | ' apostrophe |
|
||||
@ -1290,26 +1260,9 @@ Open-ended ranges could be used to get start and end too:
|
||||
assertEquals( "pult", "catapult"[ 4.. ])
|
||||
>>> void
|
||||
|
||||
|
||||
### String operations
|
||||
|
||||
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
|
||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
||||
|
||||
Typical set of String functions includes:
|
||||
|
||||
@ -1327,24 +1280,17 @@ Typical set of String functions includes:
|
||||
| size | size in characters like `length` because String is [Array] |
|
||||
| (args...) | sprintf-like formatting, see [string formatting] |
|
||||
| [index] | character at index |
|
||||
| [Range] | substring at range (2) |
|
||||
| [Regex] | find first match of regex, like [Regex.find] (2) |
|
||||
| [Range] | substring at range |
|
||||
| s1 + s2 | concatenation |
|
||||
| s1 += s2 | self-modifying concatenation |
|
||||
| toReal() | attempts to parse string as a Real value |
|
||||
| toInt() | parse string to Int value |
|
||||
| characters() | create [List] of characters (1) |
|
||||
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
||||
| matches(re) | matches the regular expression (2) |
|
||||
| | |
|
||||
|
||||
|
||||
(1)
|
||||
: List is mutable therefore a new copy is created on each call.
|
||||
|
||||
(2)
|
||||
: See [Regex]
|
||||
|
||||
### Literals
|
||||
|
||||
String literal could be multiline:
|
||||
@ -1417,8 +1363,4 @@ See [math functions](math.md). Other general purpose functions are:
|
||||
|
||||
[RingBuffer]: RingBuffer.md
|
||||
|
||||
[Collection]: Collection.md
|
||||
|
||||
[Array]: Array.md
|
||||
|
||||
[Regex]: Regex.md
|
||||
[Collection]: Collection.md
|
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.9.0-SNAPSHOT"
|
||||
version = "0.8.15-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
|
@ -1000,7 +1000,6 @@ class Compiler(
|
||||
// condition could be a value, in and is clauses:
|
||||
// parse several conditions for one then clause
|
||||
|
||||
|
||||
// loop cases
|
||||
outer@ while (true) {
|
||||
|
||||
@ -1377,16 +1376,9 @@ class Compiler(
|
||||
if (sourceObj is ObjRange && sourceObj.isIntRange) {
|
||||
loopIntRange(
|
||||
forContext,
|
||||
sourceObj.start!!.toLong(),
|
||||
if (sourceObj.isEndInclusive)
|
||||
sourceObj.end!!.toLong() + 1
|
||||
else
|
||||
sourceObj.end!!.toLong(),
|
||||
loopSO,
|
||||
body,
|
||||
elseStatement,
|
||||
label,
|
||||
canBreak
|
||||
sourceObj.start!!.toInt(),
|
||||
if (sourceObj.isEndInclusive) sourceObj.end!!.toInt() + 1 else sourceObj.end!!.toInt(),
|
||||
loopSO, body, elseStatement, label, canBreak
|
||||
)
|
||||
} else if (sourceObj.isInstanceOf(ObjIterable)) {
|
||||
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
|
||||
@ -1447,7 +1439,7 @@ class Compiler(
|
||||
}
|
||||
|
||||
private suspend fun loopIntRange(
|
||||
forScope: Scope, start: Long, end: Long, loopVar: ObjRecord,
|
||||
forScope: Scope, start: Int, end: Int, loopVar: ObjRecord,
|
||||
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
|
||||
): Obj {
|
||||
var result: Obj = ObjVoid
|
||||
@ -1455,7 +1447,7 @@ class Compiler(
|
||||
loopVar.value = iVar
|
||||
if (catchBreak) {
|
||||
for (i in start..<end) {
|
||||
iVar.value = i//.toLong()
|
||||
iVar.value = i.toLong()
|
||||
try {
|
||||
result = body.execute(forScope)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
@ -1467,7 +1459,7 @@ class Compiler(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i in start..<end) {
|
||||
for (i in start.toLong()..<end.toLong()) {
|
||||
iVar.value = i
|
||||
result = body.execute(forScope)
|
||||
}
|
||||
@ -2008,8 +2000,6 @@ 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() || d == '$' || d == '~' }
|
||||
val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit() }
|
||||
|
||||
@Suppress("unused")
|
||||
val idFirstChars = { d: Char -> d.isLetter() || d == '_' || d == '$' }
|
||||
val idFirstChars = { d: Char -> d.isLetter() || d == '_' }
|
||||
|
||||
fun parseLyng(source: Source): List<Token> {
|
||||
val p = Parser(fromPos = source.startPos)
|
||||
@ -67,16 +67,13 @@ 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 if (currentChar == '~') {
|
||||
pos.advance()
|
||||
Token("=~", from, Token.Type.MATCH)
|
||||
} else
|
||||
}
|
||||
else
|
||||
Token("=", from, Token.Type.ASSIGN)
|
||||
}
|
||||
|
||||
@ -230,9 +227,6 @@ 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)
|
||||
}
|
||||
@ -273,7 +267,7 @@ private class Parser(fromPos: Pos) {
|
||||
|
||||
in digitsSet -> {
|
||||
pos.back()
|
||||
decodeNumber(loadChars { it in digitsSet || it == '_' }, from)
|
||||
decodeNumber(loadChars { it in digitsSet || it == '_'}, from)
|
||||
}
|
||||
|
||||
'\'' -> {
|
||||
@ -297,7 +291,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)
|
||||
@ -316,7 +310,7 @@ private class Parser(fromPos: Pos) {
|
||||
// Labels processing is complicated!
|
||||
// some@ statement: label 'some', ID 'statement'
|
||||
// statement@some: ID 'statement', LABEL 'some'!
|
||||
if (idNextChars(ch)) {
|
||||
if (ch.isLetter() || ch == '_') {
|
||||
val text = ch + loadChars(idNextChars)
|
||||
if (currentChar == '@') {
|
||||
pos.advance()
|
||||
@ -401,24 +395,25 @@ 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")
|
||||
}
|
||||
@ -438,34 +433,15 @@ 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()
|
||||
}
|
||||
|
||||
else -> {
|
||||
sb.append('\\').append(currentChar)
|
||||
pos.advance()
|
||||
}
|
||||
'n' -> {sb.append('\n'); pos.advance()}
|
||||
'r' -> {sb.append('\r'); pos.advance()}
|
||||
't' -> {sb.append('\t'); pos.advance()}
|
||||
'"' -> {sb.append('"'); pos.advance()}
|
||||
else -> sb.append('\\').append(currentChar)
|
||||
}
|
||||
}
|
||||
|
||||
'\n', '\r' -> {
|
||||
'\n', '\r'-> {
|
||||
newlineDetected = true
|
||||
sb.append(currentChar)
|
||||
pos.advance()
|
||||
@ -479,7 +455,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)
|
||||
}
|
||||
@ -558,7 +534,7 @@ private class Parser(fromPos: Pos) {
|
||||
|
||||
init {
|
||||
// skip shebang
|
||||
if (pos.readFragment("#!"))
|
||||
if( pos.readFragment("#!") )
|
||||
loadToEndOfLine()
|
||||
}
|
||||
|
||||
|
@ -136,6 +136,11 @@ open class Scope(
|
||||
?: thisObj.objClass
|
||||
.getInstanceMemberOrNull(name)
|
||||
)
|
||||
// ?.also {
|
||||
// if( name == "predicate") {
|
||||
// println("got predicate $it")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||
|
@ -241,8 +241,6 @@ class Script(
|
||||
addConst("CompletableDeferred", ObjCompletableDeferred.type)
|
||||
addConst("Mutex", ObjMutex.type)
|
||||
|
||||
addConst("Regex", ObjRegex.type)
|
||||
|
||||
addFn("launch") {
|
||||
val callable = requireOnlyArg<Statement>()
|
||||
ObjDeferred(globalDefer {
|
||||
|
@ -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, MATCH, NOTMATCH,
|
||||
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ,
|
||||
SHUTTLE,
|
||||
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||
|
@ -21,11 +21,15 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.sergeych.bintools.encodeToHex
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
import net.sergeych.mptools.CachedExpression
|
||||
import net.sergeych.synctools.ProtectedOp
|
||||
import net.sergeych.synctools.withLock
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
|
||||
open class Obj {
|
||||
|
||||
@ -178,14 +182,6 @@ 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
|
||||
@ -309,18 +305,6 @@ 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 {
|
||||
@ -516,4 +500,195 @@ data class ObjNamespace(val name: String) : Obj() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* note on [getStackTrace]. If [useStackTrace] is not null, it is used instead. Otherwise, it is calculated
|
||||
* from the current scope which is treated as exception scope. It is used to restore serialized
|
||||
* exception with stack trace; the scope of the de-serialized exception is not valid
|
||||
* for stack unwinding.
|
||||
*/
|
||||
open class ObjException(
|
||||
val exceptionClass: ExceptionClass,
|
||||
val scope: Scope,
|
||||
val message: ObjString,
|
||||
@Suppress("unused") val extraData: Obj = ObjNull,
|
||||
val useStackTrace: ObjList? = null
|
||||
) : Obj() {
|
||||
constructor(name: String, scope: Scope, message: String) : this(
|
||||
getOrCreateExceptionClass(name),
|
||||
scope,
|
||||
ObjString(message)
|
||||
)
|
||||
|
||||
private val cachedStackTrace = CachedExpression(initialValue = useStackTrace)
|
||||
|
||||
suspend fun getStackTrace(): ObjList {
|
||||
return cachedStackTrace.get {
|
||||
val result = ObjList()
|
||||
val cls = scope.get("StackTraceEntry")!!.value as ObjClass
|
||||
var s: Scope? = scope
|
||||
var lastPos: Pos? = null
|
||||
while (s != null) {
|
||||
val pos = s.pos
|
||||
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
||||
result.list += cls.callWithArgs(
|
||||
scope,
|
||||
pos.source.objSourceName,
|
||||
ObjInt(pos.line.toLong()),
|
||||
ObjInt(pos.column.toLong()),
|
||||
ObjString(pos.currentLine)
|
||||
)
|
||||
}
|
||||
s = s.parent
|
||||
lastPos = pos
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
constructor(scope: Scope, message: String) : this(Root, scope, ObjString(message))
|
||||
|
||||
fun raise(): Nothing {
|
||||
throw ExecutionError(this)
|
||||
}
|
||||
|
||||
override val objClass: ObjClass = exceptionClass
|
||||
|
||||
override suspend fun toString(scope: Scope,calledFromLyng: Boolean): ObjString {
|
||||
val at = getStackTrace().list.firstOrNull()?.toString(scope)
|
||||
?: ObjString("(unknown)")
|
||||
return ObjString("${objClass.className}: $message at $at")
|
||||
}
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
encoder.encodeAny(scope, exceptionClass.classNameObj)
|
||||
encoder.encodeAny(scope, message)
|
||||
encoder.encodeAny(scope, extraData)
|
||||
encoder.encodeAny(scope, getStackTrace())
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val message = scope.args.getOrNull(0)?.toString(scope) ?: ObjString(name)
|
||||
return ObjException(this, scope, message)
|
||||
}
|
||||
|
||||
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
|
||||
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||
return try {
|
||||
val lyngClass = decoder.decodeAnyAs<ObjString>(scope).value.let {
|
||||
((scope[it] ?: scope.raiseIllegalArgument("Unknown exception class: $it"))
|
||||
.value as? ExceptionClass)
|
||||
?: scope.raiseIllegalArgument("Not an exception class: $it")
|
||||
}
|
||||
ObjException(
|
||||
lyngClass,
|
||||
scope,
|
||||
decoder.decodeAnyAs<ObjString>(scope),
|
||||
decoder.decodeAny(scope),
|
||||
decoder.decodeAnyAs<ObjList>(scope)
|
||||
)
|
||||
} catch (e: ScriptError) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
scope.raiseIllegalArgument("Failed to deserialize exception: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Root = ExceptionClass("Throwable").apply {
|
||||
addConst("message", statement {
|
||||
(thisObj as ObjException).message.toObj()
|
||||
})
|
||||
addFn("stackTrace") {
|
||||
(thisObj as ObjException).getStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private val op = ProtectedOp()
|
||||
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
|
||||
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
|
||||
return op.withLock {
|
||||
existingErrorClasses.getOrPut(name) {
|
||||
ExceptionClass(name, Root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get [ObjClass] for error class by name if exists.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun getErrorClass(name: String): ObjClass? = op.withLock {
|
||||
existingErrorClasses[name]
|
||||
}
|
||||
|
||||
fun addExceptionsToContext(scope: Scope) {
|
||||
scope.addConst("Exception", Root)
|
||||
existingErrorClasses["Exception"] = Root
|
||||
for (name in listOf(
|
||||
"NullReferenceException",
|
||||
"AssertionFailedException",
|
||||
"ClassCastException",
|
||||
"IndexOutOfBoundsException",
|
||||
"IllegalArgumentException",
|
||||
"NoSuchElementException",
|
||||
"IllegalAssignmentException",
|
||||
"SymbolNotDefinedException",
|
||||
"IterationEndException",
|
||||
"AccessException",
|
||||
"UnknownException",
|
||||
"NotFoundException"
|
||||
)) {
|
||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ObjNullReferenceException(scope: Scope) : ObjException("NullReferenceException", scope, "object is null")
|
||||
|
||||
class ObjAssertionFailedException(scope: Scope, message: String) :
|
||||
ObjException("AssertionFailedException", scope, message)
|
||||
|
||||
class ObjClassCastException(scope: Scope, message: String) : ObjException("ClassCastException", scope, message)
|
||||
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
|
||||
ObjException("IndexOutOfBoundsException", scope, message)
|
||||
|
||||
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
|
||||
ObjException("IllegalArgumentException", scope, message)
|
||||
|
||||
class ObjIllegalStateException(scope: Scope, message: String = "illegal state") :
|
||||
ObjException("IllegalStateException", scope, message)
|
||||
|
||||
@Suppress("unused")
|
||||
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
|
||||
ObjException("IllegalArgumentException", scope, message)
|
||||
|
||||
class ObjIllegalAssignmentException(scope: Scope, message: String = "illegal assignment") :
|
||||
ObjException("NoSuchElementException", scope, message)
|
||||
|
||||
class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is not defined") :
|
||||
ObjException("SymbolNotDefinedException", scope, message)
|
||||
|
||||
class ObjIterationFinishedException(scope: Scope) :
|
||||
ObjException("IterationEndException", scope, "iteration finished")
|
||||
|
||||
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
|
||||
ObjException("AccessException", scope, message)
|
||||
|
||||
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
|
||||
ObjException("UnknownException", scope, message)
|
||||
|
||||
class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") :
|
||||
ObjException("IllegalOperationException", scope, message)
|
||||
|
||||
class ObjNotFoundException(scope: Scope, message: String = "not found") :
|
||||
ObjException("NotFoundException", scope, message)
|
||||
|
@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.bintools.encodeToHex
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
import net.sergeych.mptools.CachedExpression
|
||||
import net.sergeych.synctools.ProtectedOp
|
||||
import net.sergeych.synctools.withLock
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
|
||||
/**
|
||||
* note on [getStackTrace]. If [useStackTrace] is not null, it is used instead. Otherwise, it is calculated
|
||||
* from the current scope which is treated as exception scope. It is used to restore serialized
|
||||
* exception with stack trace; the scope of the de-serialized exception is not valid
|
||||
* for stack unwinding.
|
||||
*/
|
||||
open class ObjException(
|
||||
val exceptionClass: ExceptionClass,
|
||||
val scope: Scope,
|
||||
val message: ObjString,
|
||||
@Suppress("unused") val extraData: Obj = ObjNull,
|
||||
val useStackTrace: ObjList? = null
|
||||
) : Obj() {
|
||||
constructor(name: String, scope: Scope, message: String) : this(
|
||||
getOrCreateExceptionClass(name),
|
||||
scope,
|
||||
ObjString(message)
|
||||
)
|
||||
|
||||
private val cachedStackTrace = CachedExpression(initialValue = useStackTrace)
|
||||
|
||||
suspend fun getStackTrace(): ObjList {
|
||||
return cachedStackTrace.get {
|
||||
val result = ObjList()
|
||||
val cls = scope.get("StackTraceEntry")!!.value as ObjClass
|
||||
var s: Scope? = scope
|
||||
var lastPos: Pos? = null
|
||||
while (s != null) {
|
||||
val pos = s.pos
|
||||
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
||||
result.list += cls.callWithArgs(
|
||||
scope,
|
||||
pos.source.objSourceName,
|
||||
ObjInt(pos.line.toLong()),
|
||||
ObjInt(pos.column.toLong()),
|
||||
ObjString(pos.currentLine)
|
||||
)
|
||||
}
|
||||
s = s.parent
|
||||
lastPos = pos
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
constructor(scope: Scope, message: String) : this(Root, scope, ObjString(message))
|
||||
|
||||
fun raise(): Nothing {
|
||||
throw ExecutionError(this)
|
||||
}
|
||||
|
||||
override val objClass: ObjClass = exceptionClass
|
||||
|
||||
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
||||
val at = getStackTrace().list.firstOrNull()?.toString(scope)
|
||||
?: ObjString("(unknown)")
|
||||
return ObjString("${objClass.className}: $message at $at")
|
||||
}
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
encoder.encodeAny(scope, exceptionClass.classNameObj)
|
||||
encoder.encodeAny(scope, message)
|
||||
encoder.encodeAny(scope, extraData)
|
||||
encoder.encodeAny(scope, getStackTrace())
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val message = scope.args.getOrNull(0)?.toString(scope) ?: ObjString(name)
|
||||
return ObjException(this, scope, message)
|
||||
}
|
||||
|
||||
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
|
||||
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||
return try {
|
||||
val lyngClass = decoder.decodeAnyAs<ObjString>(scope).value.let {
|
||||
((scope[it] ?: scope.raiseIllegalArgument("Unknown exception class: $it"))
|
||||
.value as? ExceptionClass)
|
||||
?: scope.raiseIllegalArgument("Not an exception class: $it")
|
||||
}
|
||||
ObjException(
|
||||
lyngClass,
|
||||
scope,
|
||||
decoder.decodeAnyAs<ObjString>(scope),
|
||||
decoder.decodeAny(scope),
|
||||
decoder.decodeAnyAs<ObjList>(scope)
|
||||
)
|
||||
} catch (e: ScriptError) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
scope.raiseIllegalArgument("Failed to deserialize exception: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Root = ExceptionClass("Throwable").apply {
|
||||
addConst("message", statement {
|
||||
(thisObj as ObjException).message.toObj()
|
||||
})
|
||||
addFn("stackTrace") {
|
||||
(thisObj as ObjException).getStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private val op = ProtectedOp()
|
||||
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
|
||||
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
|
||||
return op.withLock {
|
||||
existingErrorClasses.getOrPut(name) {
|
||||
ExceptionClass(name, Root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get [ObjClass] for error class by name if exists.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun getErrorClass(name: String): ObjClass? = op.withLock {
|
||||
existingErrorClasses[name]
|
||||
}
|
||||
|
||||
fun addExceptionsToContext(scope: Scope) {
|
||||
scope.addConst("Exception", Root)
|
||||
existingErrorClasses["Exception"] = Root
|
||||
for (name in listOf(
|
||||
"NullReferenceException",
|
||||
"AssertionFailedException",
|
||||
"ClassCastException",
|
||||
"IndexOutOfBoundsException",
|
||||
"IllegalArgumentException",
|
||||
"NoSuchElementException",
|
||||
"IllegalAssignmentException",
|
||||
"SymbolNotDefinedException",
|
||||
"IterationEndException",
|
||||
"AccessException",
|
||||
"UnknownException",
|
||||
"NotFoundException"
|
||||
)) {
|
||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ObjNullReferenceException(scope: Scope) : ObjException("NullReferenceException", scope, "object is null")
|
||||
|
||||
class ObjAssertionFailedException(scope: Scope, message: String) :
|
||||
ObjException("AssertionFailedException", scope, message)
|
||||
|
||||
class ObjClassCastException(scope: Scope, message: String) : ObjException("ClassCastException", scope, message)
|
||||
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
|
||||
ObjException("IndexOutOfBoundsException", scope, message)
|
||||
|
||||
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
|
||||
ObjException("IllegalArgumentException", scope, message)
|
||||
|
||||
class ObjIllegalStateException(scope: Scope, message: String = "illegal state") :
|
||||
ObjException("IllegalStateException", scope, message)
|
||||
|
||||
@Suppress("unused")
|
||||
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
|
||||
ObjException("IllegalArgumentException", scope, message)
|
||||
|
||||
class ObjIllegalAssignmentException(scope: Scope, message: String = "illegal assignment") :
|
||||
ObjException("NoSuchElementException", scope, message)
|
||||
|
||||
class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is not defined") :
|
||||
ObjException("SymbolNotDefinedException", scope, message)
|
||||
|
||||
class ObjIterationFinishedException(scope: Scope) :
|
||||
ObjException("IterationEndException", scope, "iteration finished")
|
||||
|
||||
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
|
||||
ObjException("AccessException", scope, message)
|
||||
|
||||
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
|
||||
ObjException("UnknownException", scope, message)
|
||||
|
||||
class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") :
|
||||
ObjException("IllegalOperationException", scope, message)
|
||||
|
||||
class ObjNotFoundException(scope: Scope, message: String = "not found") :
|
||||
ObjException("NotFoundException", scope, message)
|
@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
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 {
|
||||
return ObjRegex(
|
||||
scope.requireOnlyArg<ObjString>().value.toRegex()
|
||||
)
|
||||
}
|
||||
}.apply {
|
||||
addFn("matches") {
|
||||
ObjBool(args.firstAndOnly().toString().matches(thisAs<ObjRegex>().regex))
|
||||
}
|
||||
addFn("find") {
|
||||
thisAs<ObjRegex>().find(requireOnlyArg<ObjString>())
|
||||
}
|
||||
addFn("findAll") {
|
||||
val s = requireOnlyArg<ObjString>().value
|
||||
ObjList(thisAs<ObjRegex>().regex.findAll(s).map { ObjRegexMatch(it) }.toMutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ObjRegexMatch(val match: MatchResult) : Obj() {
|
||||
override val objClass = type
|
||||
|
||||
val objGroups: ObjList by lazy {
|
||||
ObjList(
|
||||
match.groups.map { it?.let { ObjString(it.value) } ?: ObjNull }.toMutableList()
|
||||
)
|
||||
}
|
||||
|
||||
val objValue by lazy { ObjString(match.value) }
|
||||
|
||||
val objRange: ObjRange by lazy {
|
||||
val r = match.range
|
||||
|
||||
ObjRange(
|
||||
ObjInt(r.first.toLong()),
|
||||
ObjInt(r.last.toLong()),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
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") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
scope.raiseError("RegexMatch can't be constructed directly")
|
||||
}
|
||||
}.apply {
|
||||
addFn("groups") {
|
||||
thisAs<ObjRegexMatch>().objGroups
|
||||
}
|
||||
addFn("value") {
|
||||
thisAs<ObjRegexMatch>().objValue
|
||||
}
|
||||
addFn("range") {
|
||||
thisAs<ObjRegexMatch>().objRange
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -56,23 +56,16 @@ data class ObjString(val value: String) : Obj() {
|
||||
}
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
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))
|
||||
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
|
||||
}
|
||||
|
||||
is ObjRegex -> {
|
||||
return index.find(this)
|
||||
}
|
||||
|
||||
else -> scope.raiseIllegalArgument("String index must be Int, Regex or Range")
|
||||
return ObjString(value.substring(start, end))
|
||||
}
|
||||
scope.raiseIllegalArgument("String index must be Int or Range")
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@ -103,11 +96,6 @@ 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?) {
|
||||
@ -120,9 +108,8 @@ 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") {
|
||||
@ -173,22 +160,6 @@ 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")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -165,8 +165,6 @@ fun Exception.printStackTrace() {
|
||||
}
|
||||
}
|
||||
|
||||
fun String.re() { Regex(this) }
|
||||
|
||||
|
||||
""".trimIndent()
|
||||
|
||||
|
@ -2242,45 +2242,6 @@ 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
|
||||
fun testWhenSample1() = runTest {
|
||||
eval(
|
||||
@ -3281,45 +3242,7 @@ class ScriptTest {
|
||||
result.insertAt(-i-1, x)
|
||||
}
|
||||
assertEquals( src.sorted(), result )
|
||||
""".trimIndent()
|
||||
)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
|
||||
// @Test
|
||||
fun testMinimumOptimization() = runTest {
|
||||
val x = Scope().eval(
|
||||
"""
|
||||
fun naiveCountHappyNumbers() {
|
||||
var count = 0
|
||||
for( n1 in 0..9 )
|
||||
for( n2 in 0..9 )
|
||||
for( n3 in 0..9 )
|
||||
for( n4 in 0..9 )
|
||||
for( n5 in 0..9 )
|
||||
for( n6 in 0..9 )
|
||||
if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
|
||||
count
|
||||
}
|
||||
naiveCountHappyNumbers()
|
||||
""".trimIndent()
|
||||
).toInt()
|
||||
assertEquals(55252, x)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRegex1() = runTest {
|
||||
eval(
|
||||
"""
|
||||
assert( ! "123".re.matches("abs123def") )
|
||||
assert( ".*123.*".re.matches("abs123def") )
|
||||
// assertEquals( "123", "123".re.find("abs123def")?.value )
|
||||
// assertEquals( "123", "[0-9]{3}".re.find("abs123def")?.value )
|
||||
assertEquals( "123", "\d{3}".re.find("abs123def")?.value )
|
||||
assertEquals( "123", "\\d{3}".re.find("abs123def")?.value )
|
||||
assertEquals( [1,2,3], "\d".re.findAll("abs123def").map { it.value.toInt() } )
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
@ -332,8 +332,4 @@ class BookTest {
|
||||
runDocTests("../docs/Array.md")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRegex() = runBlocking {
|
||||
runDocTests("../docs/Regex.md")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user