Compare commits

..
7 Commits
12 changed files with 639 additions and 101 deletions

No files matched your search

+16
View File
@@ -81,6 +81,22 @@ to catch all exceptions to, then you can write it even shorter:
You can even check the type of the `it` and create more convenient and sophisticated processing logic. Such approach is
used, for example, in Scala.
## finally block
If `finally` block present, it will be executed after body (until first exception)
and catch block, if any will match. finally statement is executed even if the
exception will be thrown and not caught locally. It does not alter try/catch block result:
try {
}
finally {
println("called finally")
}
>>> called finally
>>> void
- and yes, there could be try-finally block, no catching, but perform some guaranteed cleanup.
# Conveying data with exceptions
The simplest way is to provide exception string and `Exception` class:
+83
View File
@@ -552,6 +552,89 @@ Or, more neat:
>>> just 3
>>> void
## When
It is very much like the kotlin's:
fun type(x) {
when(x) {
in 'a'..'z', in 'A'..'Z' -> "letter"
in '0'..'9' -> "digit"
'$' -> "dollar"
"EUR" -> "crap"
in ['@', '#', '^'] -> "punctuation1"
in "*&.," -> "punctuation2"
else -> "unknown"
}
}
assertEquals("digit", type('3'))
assertEquals("dollar", type('$'))
assertEquals("crap", type("EUR"))
>>> void
Notice, several conditions can be grouped with a comma.
Also, you can check the type too:
fun type(x) {
when(x) {
"42", 42 -> "answer to the great question"
is Real, is Int -> "number"
is String -> {
for( d in x ) {
if( d !in '0'..'9' )
break "unknown"
}
else "number"
}
}
}
assertEquals("number", type(5))
assertEquals("number", type("153"))
assertEquals("number", type(π/2))
assertEquals("unknown", type("12%"))
assertEquals("answer to the great question", type(42))
assertEquals("answer to the great question", type("42"))
>>> void
### supported when conditions:
#### Contains:
You can thest that _when expression_ is _contained_, or not contained, in some object using `in container` and `!in container`. The container is any object that provides `contains` method, otherwise the runtime exception will be thrown.
Typical builtin types that are containers (e.g. support `conain`):
| class | notes |
|------------|--------------------------------------------|
| Collection | contains an element (1) |
| Array | faster maybe that Collection's |
| List | faster than Array's |
| String | character in string or substring in string |
| Range | object is included in the range (2) |
(1)
: Iterable is not the container as it can be infinite
(2)
: Depending on the inclusivity and open/closed range parameters. BE careful here: String range is allowed, but it is usually not what you expect of it:
assert( "more" in "a".."z") // string range ok
assert( 'x' !in "a".."z") // char in string range: probably error
assert( 'x' in 'a'..'z') // character range: ok
assert( "x" !in 'a'..'z') // string in character range: could be error
>>> void
So we recommend not to mix characters and string ranges; use `ch in str` that works
as expected:
"foo" in "foobar"
>>> true
and also character inclusion:
'o' in "foobar"
>>> true
## while
Regular pre-condition while loop, as expression, loop returns the last expression as everything else:
+1 -1
View File
@@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "0.5.1-SNAPSHOT"
version = "0.6.0-SNAPSHOT"
buildscript {
repositories {
@@ -39,13 +39,6 @@ class Compiler(
}
}
Token.Type.PRIVATE, Token.Type.PROTECTED -> {
if (cc.nextIdValue() in setOf("var", "val", "class", "fun", "fn")) {
continue
} else
throw ScriptError(t.pos, "unexpected keyword ${t.value}")
}
Token.Type.PLUS2, Token.Type.MINUS2 -> {
cc.previous()
parseExpression(cc)
@@ -458,17 +451,14 @@ class Compiler(
}
Token.Type.NEWLINE -> {}
Token.Type.PROTECTED, Token.Type.PRIVATE -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
}
Token.Type.ID -> {
// visibility
val visibility = if (isClassDeclaration)
cc.getVisibility(Visibility.Public)
else Visibility.Public
val visibility = if (isClassDeclaration && t.value == "private") {
t = cc.next()
Visibility.Private
} else Visibility.Public
// val/var?
val access = when (t.value) {
"val" -> {
@@ -660,7 +650,7 @@ class Compiler(
"void" -> Accessor { ObjVoid.asReadonly }
"null" -> Accessor { ObjNull.asReadonly }
"true" -> Accessor { ObjBool(true).asReadonly }
"false" -> Accessor { ObjBool(false).asReadonly }
"false" -> Accessor { ObjFalse.asReadonly }
else -> {
Accessor({
it.pos = t.pos
@@ -707,8 +697,8 @@ class Compiler(
* @return parsed statement or null if, for example. [id] is not among keywords
*/
private fun parseKeywordStatement(id: Token, cc: CompilerContext): Statement? = when (id.value) {
"val" -> parseVarDeclaration(id.value, false, cc)
"var" -> parseVarDeclaration(id.value, true, cc)
"val" -> parseVarDeclaration(false, Visibility.Public, cc)
"var" -> parseVarDeclaration(true, Visibility.Public, cc)
"while" -> parseWhileStatement(cc)
"do" -> parseDoWhileStatement(cc)
"for" -> parseForStatement(cc)
@@ -719,7 +709,132 @@ class Compiler(
"class" -> parseClassDeclaration(cc, false)
"try" -> parseTryStatement(cc)
"throw" -> parseThrowStatement(cc)
else -> null
"when" -> parseWhenStatement(cc)
else -> {
// triples
cc.previous()
when {
cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(cc, Visibility.Private)
cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(cc, Visibility.Private)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(cc, isOpen = true)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(cc, isOpen = true)
cc.matchQualifiers("val", "private") -> parseVarDeclaration(false, Visibility.Private, cc)
cc.matchQualifiers("var", "private") -> parseVarDeclaration(true, Visibility.Private, cc)
cc.matchQualifiers("val", "open") -> parseVarDeclaration(false, Visibility.Private, cc, true)
cc.matchQualifiers("var", "open") -> parseVarDeclaration(true, Visibility.Private, cc, true)
else -> {
cc.next()
null
}
}
}
}
data class WhenCase(val condition: Statement, val block: Statement)
private fun parseWhenStatement(cc: CompilerContext): Statement {
// has a value, when(value) ?
var t = cc.skipWsTokens()
return if (t.type == Token.Type.LPAREN) {
// when(value)
val value = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when(value) expected")
cc.skipTokenOfType(Token.Type.RPAREN)
t = cc.next()
if (t.type != Token.Type.LBRACE) throw ScriptError(t.pos, "when { ... } expected")
val cases = mutableListOf<WhenCase>()
var elseCase: Statement? = null
lateinit var whenValue: Obj
// there could be 0+ then clauses
// condition could be a value, in and is clauses:
// parse several conditions for one then clause
// loop cases
outer@ while (true) {
var skipParseBody = false
val currentCondition = mutableListOf<Statement>()
// loop conditions
while (true) {
t = cc.skipWsTokens()
when (t.type) {
Token.Type.IN,
Token.Type.NOTIN -> {
// we need a copy in the closure:
val isIn = t.type == Token.Type.IN
val container = parseExpression(cc) ?: throw ScriptError(cc.currentPos(), "type expected")
currentCondition += statement {
val r = container.execute(this).contains(this, whenValue)
ObjBool(if (isIn) r else !r)
}
}
Token.Type.IS, Token.Type.NOTIS -> {
// we need a copy in the closure:
val isIn = t.type == Token.Type.IS
val caseType = parseExpression(cc) ?: throw ScriptError(cc.currentPos(), "type expected")
currentCondition += statement {
val r = whenValue.isInstanceOf(caseType.execute(this))
ObjBool(if (isIn) r else !r)
}
}
Token.Type.COMMA ->
continue
Token.Type.ARROW ->
break
Token.Type.RBRACE ->
break@outer
else -> {
if (t.value == "else") {
cc.skipTokens(Token.Type.ARROW)
if (elseCase != null) throw ScriptError(
cc.currentPos(),
"when else block already defined"
)
elseCase =
parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when else block expected")
skipParseBody = true
} else {
cc.previous()
val x = parseExpression(cc)
?: throw ScriptError(cc.currentPos(), "when case condition expected")
currentCondition += statement {
ObjBool(x.execute(this).compareTo(this, whenValue) == 0)
}
}
}
}
}
// parsed conditions?
if (!skipParseBody) {
val block = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "when case block expected")
for (c in currentCondition) cases += WhenCase(c, block)
}
}
statement {
var result: Obj = ObjVoid
// in / is and like uses whenValue from closure:
whenValue = value.execute(this)
var found = false
for (c in cases)
if (c.condition.execute(this).toBool()) {
result = c.block.execute(this)
found = true
break
}
if (!found && elseCase != null) result = elseCase.execute(this)
result
}
} else {
// when { cond -> ... }
TODO("when without object is not yet implemented")
}
}
private fun parseThrowStatement(cc: CompilerContext): Statement {
@@ -785,13 +900,13 @@ class Compiler(
} else {
// no (e: Exception) block: should be shortest variant `catch { ... }`
cc.skipTokenOfType(Token.Type.LBRACE, "expected catch(...) or catch { ... } here")
catches += CatchBlockData(Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"),
parseBlock(cc,true))
catches += CatchBlockData(
Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"),
parseBlock(cc, true)
)
t = cc.next()
}
}
if( catches.isEmpty() )
throw ScriptError(cc.currentPos(), "try block must have at least one catch clause")
val finallyClause = if (t.value == "finally") {
parseBlock(cc)
} else {
@@ -799,13 +914,15 @@ class Compiler(
null
}
if (catches.isEmpty() && finallyClause == null)
throw ScriptError(cc.currentPos(), "try block must have either catch or finally clause or both")
return statement {
var result: Obj = ObjVoid
try {
// body is a parsed block, it already has separate context
result = body.execute(this)
}
catch (e: Exception) {
} catch (e: Exception) {
// convert to appropriate exception
val objException = when (e) {
is ExecutionError -> e.errorObject
@@ -834,8 +951,7 @@ class Compiler(
// rethrow if not caught this exception
if (!isCaught)
throw e
}
finally {
} finally {
// finally clause does not alter result!
finallyClause?.execute(this)
}
@@ -1105,8 +1221,7 @@ class Compiler(
doContext = it.copy().apply { skipContextCreation = true }
try {
result = body.execute(doContext)
}
catch( e: LoopBreakContinueException) {
} catch (e: LoopBreakContinueException) {
if (e.label == label || e.label == null) {
if (e.doContinue) continue
else {
@@ -1272,9 +1387,11 @@ class Compiler(
}
}
private fun parseFunctionDeclaration(tokens: CompilerContext): Statement {
val visibility = tokens.getVisibility()
private fun parseFunctionDeclaration(
tokens: CompilerContext,
visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false
): Statement {
var t = tokens.next()
val start = t.pos
val name = if (t.type != Token.Type.ID)
@@ -1336,26 +1453,22 @@ class Compiler(
}
}
private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: CompilerContext): Statement {
// we are just after var/val, visibility if exists is 2 steps behind
val visibility = when (tokens.atOffset(-2)?.type) {
Token.Type.PRIVATE ->
Visibility.Private
Token.Type.PROTECTED -> Visibility.Protected
else -> Visibility.Public
}
private fun parseVarDeclaration(
isMutable: Boolean,
visibility: Visibility,
tokens: CompilerContext,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false
): Statement {
val nameToken = tokens.next()
val start = nameToken.pos
if (nameToken.type != Token.Type.ID)
throw ScriptError(nameToken.pos, "Expected identifier after '$kind'")
throw ScriptError(nameToken.pos, "Expected identifier here")
val name = nameToken.value
val eqToken = tokens.next()
var setNull = false
if (eqToken.type != Token.Type.ASSIGN) {
if (!mutable)
if (!isMutable)
throw ScriptError(start, "val must be initialized")
else {
tokens.previous()
@@ -1374,7 +1487,7 @@ class Compiler(
// create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, mutable, initValue, visibility)
context.addItem(name, isMutable, initValue, visibility)
initValue
}
}
@@ -1518,7 +1631,8 @@ class Compiler(
/**
* The keywords that stop processing of expression term
*/
val stopKeywords = setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class", "struct")
val stopKeywords =
setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class", "struct")
}
}
@@ -21,7 +21,11 @@ internal class CompilerContext(val tokens: List<Token>) {
fun hasNext() = currentIndex < tokens.size
fun hasPrevious() = currentIndex > 0
fun next() = tokens.getOrElse(currentIndex) { throw IllegalStateException("No next token") }.also { currentIndex++ }
fun next() =
if( currentIndex < tokens.size ) tokens[currentIndex++]
else Token("", tokens.last().pos, Token.Type.EOF)
// throw IllegalStateException("No more tokens")
fun previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun savePos() = currentIndex
@@ -47,9 +51,7 @@ internal class CompilerContext(val tokens: List<Token>) {
throw ScriptError(at, message)
}
fun currentPos() =
if (hasNext()) next().pos.also { previous() }
else previous().pos.also { next() }
fun currentPos(): Pos = tokens[currentIndex].pos
/**
* Skips next token if its type is `tokenType`, returns `true` if so.
@@ -102,6 +104,7 @@ internal class CompilerContext(val tokens: List<Token>) {
* Return value of the next token if it is an identifier, null otherwise.
* Does not change position.
*/
@Suppress("unused")
fun nextIdValue(): String? {
return if (hasNext()) {
val nt = tokens[currentIndex]
@@ -117,36 +120,43 @@ internal class CompilerContext(val tokens: List<Token>) {
/**
* If the token at current position plus offset (could be negative) exists, returns it, otherwise returns null.
*/
@Suppress("unused")
fun atOffset(offset: Int): Token? =
if (currentIndex + offset in tokens.indices) tokens[currentIndex + offset] else null
fun matchQualifiers(keyword: String, vararg qualifiers: String): Boolean {
val pos = savePos()
var count = 0
while( count < qualifiers.size) {
val t = next()
when(t.type) {
Token.Type.ID -> {
if( t.value in qualifiers ) count++
else { restorePos(pos); return false }
}
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT, Token.Type.NEWLINE -> {}
else -> { restorePos(pos); return false }
}
}
val t = next()
if( t.type == Token.Type.ID && t.value == keyword ) {
return true
} else {
restorePos(pos)
return false
}
}
/**
* Scan backwards as deep as specified looking for visibility token. Does not change position.
* Skip newlines and comments. Returns (and reads) first non-whitespace token.
* Note that [Token.Type.EOF] is not considered a whitespace token.
*/
fun getVisibility(default: Visibility = Visibility.Public, depths: Int = 2): Visibility {
for( i in -depths .. -1) {
when( atOffset(i)?.type) {
Token.Type.PROTECTED -> return Visibility.Protected
Token.Type.PRIVATE -> return Visibility.Private
else -> {}
}
}
return default
fun skipWsTokens(): Token {
while( current().type in wstokens ) next()
return next()
}
// fun expectKeyword(vararg keyword: String): String {
// val t = next()
// if (t.type != Token.Type.ID && t.value !in keyword) {
// throw ScriptError(t.pos, "expected one of ${keyword.joinToString()}")
//
// }
// data class ReturnScope(val needCatch: Boolean = false)
// private val
// fun startReturnScope(): ReturnScope {
// return ReturnScope()
// }
companion object {
val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT)
}
}
@@ -89,7 +89,7 @@ open class Obj {
}
open suspend fun contains(context: Context, other: Obj): Boolean {
context.raiseNotImplemented()
return invokeInstanceMethod(context, "contains", other).toBool()
}
open val asStr: ObjString by lazy {
@@ -42,7 +42,8 @@ open class ObjClass(
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
if (name in members || allParentsSet.any { name in it.members })
val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] }
if( existing?.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
@@ -97,7 +98,19 @@ val ObjIterable by lazy {
*/
val ObjCollection by lazy {
val i: ObjClass = ObjIterable
ObjClass("Collection", i)
ObjClass("Collection", i).apply {
// it is not effective, but it is open:
addFn("contains", isOpen = true) {
val obj = args.firstAndOnly()
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
if( obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0 )
return@addFn ObjTrue
}
ObjFalse
}
}
}
val ObjIterator by lazy { ObjClass("Iterator") }
@@ -145,6 +158,15 @@ val ObjArray by lazy {
addFn("iterator") {
ObjArrayIterator(thisObj).also { it.init(this) }
}
addFn("contains", isOpen = true) {
val obj = args.firstAndOnly()
for( i in 0..< thisObj.invokeInstanceMethod(this, "size").toInt()) {
if( thisObj.getAt(this, i).compareTo(this, obj) == 0 ) return@addFn ObjTrue
}
ObjFalse
}
addFn("isample") { "ok".toObj() }
}
}
@@ -75,6 +75,10 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
return this
}
override suspend fun contains(context: Context, other: Obj): Boolean {
return list.contains(other)
}
override val objClass: ObjClass
get() = type
@@ -31,6 +31,14 @@ data class ObjString(val value: String) : Obj() {
return ObjChar(value[index])
}
override suspend fun contains(context: Context, other: Obj): Boolean {
return if (other is ObjString)
value.contains(other.value)
else if (other is ObjChar)
value.contains(other.value)
else context.raiseArgumentError("String.contains can't take $other")
}
companion object {
val type = ObjClass("String").apply {
addConst("startsWith",
@@ -137,15 +137,19 @@ private class Parser(fromPos: Pos) {
if (currentChar == '.') {
pos.advance()
// .. already parsed:
if (currentChar == '.') {
when (currentChar) {
'.' -> {
pos.advance()
Token("...", from, Token.Type.ELLIPSIS)
} else if (currentChar == '<') {
}
'<' -> {
pos.advance()
Token("..<", from, Token.Type.DOTDOTLT)
} else {
}
else -> {
Token("..", from, Token.Type.DOTDOT)
}
}
} else
Token(".", from, Token.Type.DOT)
}
@@ -280,8 +284,6 @@ private class Parser(fromPos: Pos) {
when (text) {
"in" -> Token("in", from, Token.Type.IN)
"is" -> Token("is", from, Token.Type.IS)
"protected" -> Token("protected", from, Token.Type.PROTECTED)
"private" -> Token("private", from, Token.Type.PRIVATE)
else -> Token(text, from, Token.Type.ID)
}
} else
@@ -338,7 +340,7 @@ private class Parser(fromPos: Pos) {
// could be integer, also hex:
if (currentChar == 'x' && p1 == "0") {
pos.advance()
Token(loadChars({ it in hexDigits }), start, Token.Type.HEX).also {
Token(loadChars { it in hexDigits }, start, Token.Type.HEX).also {
if (currentChar.isLetter())
raise("invalid hex literal")
}
@@ -17,7 +17,6 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label
PRIVATE, PROTECTED,
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
ELLIPSIS, DOTDOT, DOTDOTLT,
NEWLINE,
+295 -15
View File
@@ -298,9 +298,9 @@ class ScriptTest {
@Test
fun eqNeqTest() = runTest {
assertEquals(ObjBool(true), eval("val x = 2; x == 2"))
assertEquals(ObjBool(false), eval("val x = 3; x == 2"))
assertEquals(ObjFalse, eval("val x = 3; x == 2"))
assertEquals(ObjBool(true), eval("val x = 3; x != 2"))
assertEquals(ObjBool(false), eval("val x = 3; x != 3"))
assertEquals(ObjFalse, eval("val x = 3; x != 3"))
assertTrue { eval("1 == 1").toBool() }
assertTrue { eval("true == true").toBool() }
@@ -313,17 +313,17 @@ class ScriptTest {
@Test
fun logicTest() = runTest {
assertEquals(ObjBool(false), eval("true && false"))
assertEquals(ObjBool(false), eval("false && false"))
assertEquals(ObjBool(false), eval("false && true"))
assertEquals(ObjFalse, eval("true && false"))
assertEquals(ObjFalse, eval("false && false"))
assertEquals(ObjFalse, eval("false && true"))
assertEquals(ObjBool(true), eval("true && true"))
assertEquals(ObjBool(true), eval("true || false"))
assertEquals(ObjBool(false), eval("false || false"))
assertEquals(ObjFalse, eval("false || false"))
assertEquals(ObjBool(true), eval("false || true"))
assertEquals(ObjBool(true), eval("true || true"))
assertEquals(ObjBool(false), eval("!true"))
assertEquals(ObjFalse, eval("!true"))
assertEquals(ObjBool(true), eval("!false"))
}
@@ -1751,7 +1751,8 @@ class ScriptTest {
@Test
fun testThrowExisting() = runTest {
eval("""
eval(
"""
val x = IllegalArgumentException("test")
assert( x is Exception )
@@ -1773,12 +1774,14 @@ class ScriptTest {
}
assertEquals(3, t)
assert(finallyCaught)
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testCatchShort1() = runTest {
eval("""
eval(
"""
val x = IllegalArgumentException("test")
var t = 0
var finallyCaught = false
@@ -1795,12 +1798,14 @@ class ScriptTest {
}
assertEquals(31, t)
assert(finallyCaught)
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testCatchShort2() = runTest {
eval("""
eval(
"""
val x = IllegalArgumentException("test")
var caught = null
try {
@@ -1810,12 +1815,14 @@ class ScriptTest {
caught = it
}
assert( caught is IllegalArgumentException )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testAccessEHData() = runTest {
eval("""
eval(
"""
val x = IllegalArgumentException("test")
val m = try {
throw x
@@ -1830,7 +1837,280 @@ class ScriptTest {
}
println(m)
assert( m == "test" )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testTryFinally() = runTest {
val c = Context()
assertFails {
c.eval(
"""
var resource = "used"
try {
throw "test"
}
finally {
resource = "freed"
}
""".trimIndent()
)
}
c.eval(
"""
assertEquals("freed", resource)
""".trimIndent()
)
}
@Test
fun testThrowFromKotlin() = runTest {
val c = Context()
c.addFn("callThrow") {
raiseArgumentError("fromKotlin")
}
c.eval(
"""
val result = try {
callThrow()
"fail"
}
catch(e) {
println("caught:"+e)
println(e.message)
assert( e is IllegalArgumentException )
assertEquals("fromKotlin", e.message)
"ok"
}
assertEquals(result, "ok")
""".trimIndent()
)
}
@Test
fun testReturnValue1() = runTest {
val r = eval(
"""
class Point(x,y) {
println("1")
fun length() { sqrt(d2()) }
println("2")
private fun d2() {x*x + y*y}
println("3")
}
println("Helluva")
val p = Point(3,4)
// assertEquals( 5, p.length() )
// assertThrows { p.d2() }
"111"
""".trimIndent()
)
assertEquals("111", r.toString())
}
@Test
fun doWhileValuesTest() = runTest {
eval(
"""
var count = 0
val result = do {
count++
if( count < 10 )
continue
if( count % 2 == 1 )
break "found "+count
} while(count < 100)
else "no"
assertEquals("found 11", result)
""".trimIndent()
)
eval(
"""
var count = 0
val result = do {
count++
if( count < 10 )
continue
if( count % 2 == 1 )
break "found "+count
} while(count < 5)
else "no"
assertEquals("no", result)
""".trimIndent()
)
eval(
"""
var count = 0
val result = do {
count++
if( count % 2 == 3 )
break "found "+count
"proc "+count
} while(count < 5)
assertEquals("proc 5", result)
""".trimIndent()
)
}
@Test
fun doWhileValuesLabelTest() = runTest {
eval(
"""
var count = 0
var count2 = 0
var count3 = 0
val result = outer@ do {
count2++
count = 0
do {
count++
if( count < 10 || count2 < 5 ) {
continue
}
if( count % 2 == 1 )
break@outer "found "+count + "/" + count2
} while(count < 14)
count3++
} while( count2 < 100 )
else "no"
assertEquals("found 11/5", result)
assertEquals( 4, count3)
""".trimIndent()
)
}
@Test
fun testSimpleWhen() = runTest {
eval(
"""
var result = when("a") {
"a" -> "ok"
else -> "fail"
}
assertEquals(result, "ok")
result = when(5) {
3 -> "fail1"
4 -> "fail2"
else -> "ok2"
}
assert(result == "ok2")
result = when(5) {
3 -> "fail"
4 -> "fail2"
}
assert(result == void)
""".trimIndent()
)
}
@Test
fun testWhenIs() = runTest {
eval(
"""
var result = when("a") {
is Int -> "fail2"
is String -> "ok"
else -> "fail"
}
assertEquals(result, "ok")
result = when(5) {
3 -> "fail1"
4 -> "fail2"
else -> "ok2"
}
assert(result == "ok2")
result = when(5) {
3 -> "fail"
4 -> "fail2"
}
assert(result == void)
result = when(5) {
!is String -> "ok"
4 -> "fail2"
}
assert(result == "ok")
""".trimIndent()
)
}
@Test
fun testWhenIn() = runTest {
eval(
"""
var result = when('e') {
in 'a'..'c' -> "fail2"
in 'a'..'z' -> "ok"
else -> "fail"
}
// assertEquals(result, "ok")
result = when(5) {
in [1,2,3,4,6] -> "fail1"
in [7, 0, 9] -> "fail2"
else -> "ok2"
}
assert(result == "ok2")
result = when(5) {
in [1,2,3,4,6] -> "fail1"
in [7, 0, 9] -> "fail2"
in [-1, 5, 11] -> "ok3"
else -> "fail3"
}
assert(result == "ok3")
result = when(5) {
!in [1,2,3,4,6, 5] -> "fail1"
!in [7, 0, 9, 5] -> "fail2"
!in [-1, 15, 11] -> "ok4"
else -> "fail3"
}
assert(result == "ok4")
result = when(5) {
in [1,3] -> "fail"
in 2..4 -> "fail2"
}
assert(result == void)
""".trimIndent()
)
}
@Test
fun testWhenSample1() = runTest {
eval(
"""
fun type(x) {
when(x) {
in 'a'..'z', in 'A'..'Z' -> "letter"
in '0'..'9' -> "digit"
in "$%&" -> "hate char"
else -> "unknown"
}
}
assertEquals("digit", type('3'))
assertEquals("letter", type('E'))
assertEquals("hate char", type('%'))
""".trimIndent()
)
}
@Test
fun testWhenSample2() = runTest {
eval(
"""
fun type(x) {
when(x) {
"42", 42 -> "answer to the great question"
is Real, is Int -> "number"
is String -> {
for( d in x ) {
if( d !in '0'..'9' )
break "unknown"
}
else "number"
}
}
}
assertEquals("number", type(5))
""".trimIndent()
)
}
}