Compare commits

..
7 Commits
12 changed files with 665 additions and 127 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 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. 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 # Conveying data with exceptions
The simplest way is to provide exception string and `Exception` class: The simplest way is to provide exception string and `Exception` class:
+83
View File
@@ -552,6 +552,89 @@ Or, more neat:
>>> just 3 >>> just 3
>>> void >>> 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 ## while
Regular pre-condition while loop, as expression, loop returns the last expression as everything else: 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 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "0.5.1-SNAPSHOT" version = "0.6.0-SNAPSHOT"
buildscript { buildscript {
repositories { 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 -> { Token.Type.PLUS2, Token.Type.MINUS2 -> {
cc.previous() cc.previous()
parseExpression(cc) parseExpression(cc)
@@ -149,7 +142,7 @@ class Compiler(
v.invokeInstanceMethod( v.invokeInstanceMethod(
context, context,
next.value, next.value,
args.toArguments(context,false) args.toArguments(context, false)
), isMutable = false ), isMutable = false
) )
} }
@@ -157,7 +150,7 @@ class Compiler(
Token.Type.LBRACE -> { Token.Type.LBRACE -> {
// single lambda arg, like assertTrows { ... } // single lambda arg, like assertTrows { ... }
cc.next() cc.next()
isCall = true isCall = true
val lambda = val lambda =
parseExpression(cc) ?: throw ScriptError(t.pos, "expected valid lambda here") parseExpression(cc) ?: throw ScriptError(t.pos, "expected valid lambda here")
@@ -170,7 +163,7 @@ class Compiler(
v.invokeInstanceMethod( v.invokeInstanceMethod(
context, context,
next.value, next.value,
Arguments(listOf(lambda),true) Arguments(listOf(lambda), true)
), isMutable = false ), isMutable = false
) )
} }
@@ -458,17 +451,14 @@ class Compiler(
} }
Token.Type.NEWLINE -> {} Token.Type.NEWLINE -> {}
Token.Type.PROTECTED, Token.Type.PRIVATE -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
}
Token.Type.ID -> { Token.Type.ID -> {
// visibility // visibility
val visibility = if (isClassDeclaration) val visibility = if (isClassDeclaration && t.value == "private") {
cc.getVisibility(Visibility.Public) t = cc.next()
else Visibility.Public Visibility.Private
} else Visibility.Public
// val/var? // val/var?
val access = when (t.value) { val access = when (t.value) {
"val" -> { "val" -> {
@@ -660,7 +650,7 @@ class Compiler(
"void" -> Accessor { ObjVoid.asReadonly } "void" -> Accessor { ObjVoid.asReadonly }
"null" -> Accessor { ObjNull.asReadonly } "null" -> Accessor { ObjNull.asReadonly }
"true" -> Accessor { ObjBool(true).asReadonly } "true" -> Accessor { ObjBool(true).asReadonly }
"false" -> Accessor { ObjBool(false).asReadonly } "false" -> Accessor { ObjFalse.asReadonly }
else -> { else -> {
Accessor({ Accessor({
it.pos = t.pos it.pos = t.pos
@@ -707,8 +697,8 @@ class Compiler(
* @return parsed statement or null if, for example. [id] is not among keywords * @return parsed statement or null if, for example. [id] is not among keywords
*/ */
private fun parseKeywordStatement(id: Token, cc: CompilerContext): Statement? = when (id.value) { private fun parseKeywordStatement(id: Token, cc: CompilerContext): Statement? = when (id.value) {
"val" -> parseVarDeclaration(id.value, false, cc) "val" -> parseVarDeclaration(false, Visibility.Public, cc)
"var" -> parseVarDeclaration(id.value, true, cc) "var" -> parseVarDeclaration(true, Visibility.Public, cc)
"while" -> parseWhileStatement(cc) "while" -> parseWhileStatement(cc)
"do" -> parseDoWhileStatement(cc) "do" -> parseDoWhileStatement(cc)
"for" -> parseForStatement(cc) "for" -> parseForStatement(cc)
@@ -719,16 +709,141 @@ class Compiler(
"class" -> parseClassDeclaration(cc, false) "class" -> parseClassDeclaration(cc, false)
"try" -> parseTryStatement(cc) "try" -> parseTryStatement(cc)
"throw" -> parseThrowStatement(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 { private fun parseThrowStatement(cc: CompilerContext): Statement {
val throwStatement = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "throw object expected") val throwStatement = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "throw object expected")
return statement { return statement {
var errorObject = throwStatement.execute(this) var errorObject = throwStatement.execute(this)
if( errorObject is ObjString ) if (errorObject is ObjString)
errorObject = ObjException(this, errorObject.value) errorObject = ObjException(this, errorObject.value)
if( errorObject is ObjException ) if (errorObject is ObjException)
raiseError(errorObject) raiseError(errorObject)
else raiseError("this is not an exception object: $errorObject") else raiseError("this is not an exception object: $errorObject")
} }
@@ -745,7 +860,7 @@ class Compiler(
val catches = mutableListOf<CatchBlockData>() val catches = mutableListOf<CatchBlockData>()
cc.skipTokens(Token.Type.NEWLINE) cc.skipTokens(Token.Type.NEWLINE)
var t = cc.next() var t = cc.next()
while( t.value == "catch" ) { while (t.value == "catch") {
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) { if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
t = cc.next() t = cc.next()
@@ -785,45 +900,47 @@ class Compiler(
} else { } else {
// no (e: Exception) block: should be shortest variant `catch { ... }` // no (e: Exception) block: should be shortest variant `catch { ... }`
cc.skipTokenOfType(Token.Type.LBRACE, "expected catch(...) or catch { ... } here") cc.skipTokenOfType(Token.Type.LBRACE, "expected catch(...) or catch { ... } here")
catches += CatchBlockData(Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"), catches += CatchBlockData(
parseBlock(cc,true)) Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"),
parseBlock(cc, true)
)
t = cc.next() t = cc.next()
} }
} }
if( catches.isEmpty() ) val finallyClause = if (t.value == "finally") {
throw ScriptError(cc.currentPos(), "try block must have at least one catch clause")
val finallyClause = if( t.value == "finally" ) {
parseBlock(cc) parseBlock(cc)
} else { } else {
cc.previous() cc.previous()
null null
} }
if (catches.isEmpty() && finallyClause == null)
throw ScriptError(cc.currentPos(), "try block must have either catch or finally clause or both")
return statement { return statement {
var result: Obj = ObjVoid var result: Obj = ObjVoid
try { try {
// body is a parsed block, it already has separate context // body is a parsed block, it already has separate context
result = body.execute(this) result = body.execute(this)
} } catch (e: Exception) {
catch (e: Exception) {
// convert to appropriate exception // convert to appropriate exception
val objException = when(e) { val objException = when (e) {
is ExecutionError -> e.errorObject is ExecutionError -> e.errorObject
else -> ObjUnknownException(this, e.message ?: e.toString()) else -> ObjUnknownException(this, e.message ?: e.toString())
} }
// let's see if we should catch it: // let's see if we should catch it:
var isCaught = false var isCaught = false
for( cdata in catches ) { for (cdata in catches) {
var exceptionObject: ObjException? = null var exceptionObject: ObjException? = null
for (exceptionClassName in cdata.classNames) { for (exceptionClassName in cdata.classNames) {
val exObj = ObjException.getErrorClass(exceptionClassName) val exObj = ObjException.getErrorClass(exceptionClassName)
?: raiseSymbolNotFound("error clas not exists: $exceptionClassName") ?: raiseSymbolNotFound("error clas not exists: $exceptionClassName")
if( objException.isInstanceOf(exObj) ) { if (objException.isInstanceOf(exObj)) {
exceptionObject = objException exceptionObject = objException
break break
} }
} }
if( exceptionObject != null ) { if (exceptionObject != null) {
val catchContext = this.copy(pos = cdata.catchVar.pos) val catchContext = this.copy(pos = cdata.catchVar.pos)
catchContext.addItem(cdata.catchVar.value, false, objException) catchContext.addItem(cdata.catchVar.value, false, objException)
result = cdata.block.execute(catchContext) result = cdata.block.execute(catchContext)
@@ -832,10 +949,9 @@ class Compiler(
} }
} }
// rethrow if not caught this exception // rethrow if not caught this exception
if( !isCaught ) if (!isCaught)
throw e throw e
} } finally {
finally {
// finally clause does not alter result! // finally clause does not alter result!
finallyClause?.execute(this) finallyClause?.execute(this)
} }
@@ -1080,8 +1196,8 @@ class Compiler(
cc.skipTokens(Token.Type.NEWLINE) cc.skipTokens(Token.Type.NEWLINE)
val t = cc.next() val t = cc.next()
if( t.type != Token.Type.ID && t.value != "while" ) if (t.type != Token.Type.ID && t.value != "while")
cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here") cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here")
val conditionStart = ensureLparen(cc) val conditionStart = ensureLparen(cc)
val condition = val condition =
@@ -1105,10 +1221,9 @@ class Compiler(
doContext = it.copy().apply { skipContextCreation = true } doContext = it.copy().apply { skipContextCreation = true }
try { try {
result = body.execute(doContext) result = body.execute(doContext)
} } catch (e: LoopBreakContinueException) {
catch( e: LoopBreakContinueException) { if (e.label == label || e.label == null) {
if( e.label == label || e.label == null ) { if (e.doContinue) continue
if( e.doContinue ) continue
else { else {
result = e.result result = e.result
wasBroken = true wasBroken = true
@@ -1117,8 +1232,8 @@ class Compiler(
} }
throw e throw e
} }
} while( condition.execute(doContext).toBool() ) } while (condition.execute(doContext).toBool())
if( !wasBroken ) elseStatement?.let { s -> result = s.execute(it) } if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
result result
} }
} }
@@ -1272,9 +1387,11 @@ class Compiler(
} }
} }
private fun parseFunctionDeclaration(tokens: CompilerContext): Statement { private fun parseFunctionDeclaration(
val visibility = tokens.getVisibility() tokens: CompilerContext,
visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false
): Statement {
var t = tokens.next() var t = tokens.next()
val start = t.pos val start = t.pos
val name = if (t.type != Token.Type.ID) val name = if (t.type != Token.Type.ID)
@@ -1328,7 +1445,7 @@ class Compiler(
val block = parseScript(startPos, cc) val block = parseScript(startPos, cc)
return statement(startPos) { return statement(startPos) {
// block run on inner context: // block run on inner context:
block.execute(if( it.skipContextCreation ) it else it.copy(startPos)) block.execute(if (it.skipContextCreation) it else it.copy(startPos))
}.also { }.also {
val t1 = cc.next() val t1 = cc.next()
if (t1.type != Token.Type.RBRACE) if (t1.type != Token.Type.RBRACE)
@@ -1336,26 +1453,22 @@ class Compiler(
} }
} }
private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: CompilerContext): Statement { private fun parseVarDeclaration(
// we are just after var/val, visibility if exists is 2 steps behind isMutable: Boolean,
val visibility = when (tokens.atOffset(-2)?.type) { visibility: Visibility,
Token.Type.PRIVATE -> tokens: CompilerContext,
Visibility.Private @Suppress("UNUSED_PARAMETER") isOpen: Boolean = false
): Statement {
Token.Type.PROTECTED -> Visibility.Protected
else -> Visibility.Public
}
val nameToken = tokens.next() val nameToken = tokens.next()
val start = nameToken.pos val start = nameToken.pos
if (nameToken.type != Token.Type.ID) 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 name = nameToken.value
val eqToken = tokens.next() val eqToken = tokens.next()
var setNull = false var setNull = false
if (eqToken.type != Token.Type.ASSIGN) { if (eqToken.type != Token.Type.ASSIGN) {
if (!mutable) if (!isMutable)
throw ScriptError(start, "val must be initialized") throw ScriptError(start, "val must be initialized")
else { else {
tokens.previous() tokens.previous()
@@ -1374,7 +1487,7 @@ class Compiler(
// create a separate copy: // create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, mutable, initValue, visibility) context.addItem(name, isMutable, initValue, visibility)
initValue initValue
} }
} }
@@ -1518,7 +1631,8 @@ class Compiler(
/** /**
* The keywords that stop processing of expression term * 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 hasNext() = currentIndex < tokens.size
fun hasPrevious() = currentIndex > 0 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 previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun savePos() = currentIndex fun savePos() = currentIndex
@@ -47,9 +51,7 @@ internal class CompilerContext(val tokens: List<Token>) {
throw ScriptError(at, message) throw ScriptError(at, message)
} }
fun currentPos() = fun currentPos(): Pos = tokens[currentIndex].pos
if (hasNext()) next().pos.also { previous() }
else previous().pos.also { next() }
/** /**
* Skips next token if its type is `tokenType`, returns `true` if so. * 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. * Return value of the next token if it is an identifier, null otherwise.
* Does not change position. * Does not change position.
*/ */
@Suppress("unused")
fun nextIdValue(): String? { fun nextIdValue(): String? {
return if (hasNext()) { return if (hasNext()) {
val nt = tokens[currentIndex] 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. * If the token at current position plus offset (could be negative) exists, returns it, otherwise returns null.
*/ */
@Suppress("unused")
fun atOffset(offset: Int): Token? = fun atOffset(offset: Int): Token? =
if (currentIndex + offset in tokens.indices) tokens[currentIndex + offset] else null if (currentIndex + offset in tokens.indices) tokens[currentIndex + offset] else null
/** fun matchQualifiers(keyword: String, vararg qualifiers: String): Boolean {
* Scan backwards as deep as specified looking for visibility token. Does not change position. val pos = savePos()
*/ var count = 0
fun getVisibility(default: Visibility = Visibility.Public, depths: Int = 2): Visibility { while( count < qualifiers.size) {
for( i in -depths .. -1) { val t = next()
when( atOffset(i)?.type) { when(t.type) {
Token.Type.PROTECTED -> return Visibility.Protected Token.Type.ID -> {
Token.Type.PRIVATE -> return Visibility.Private if( t.value in qualifiers ) count++
else -> {} else { restorePos(pos); return false }
}
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT, Token.Type.NEWLINE -> {}
else -> { restorePos(pos); return false }
} }
} }
return default val t = next()
if( t.type == Token.Type.ID && t.value == keyword ) {
return true
} else {
restorePos(pos)
return false
}
} }
// fun expectKeyword(vararg keyword: String): String { /**
// val t = next() * Skip newlines and comments. Returns (and reads) first non-whitespace token.
// if (t.type != Token.Type.ID && t.value !in keyword) { * Note that [Token.Type.EOF] is not considered a whitespace token.
// throw ScriptError(t.pos, "expected one of ${keyword.joinToString()}") */
// fun skipWsTokens(): Token {
// } while( current().type in wstokens ) next()
return next()
// 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 { open suspend fun contains(context: Context, other: Obj): Boolean {
context.raiseNotImplemented() return invokeInstanceMethod(context, "contains", other).toBool()
} }
open val asStr: ObjString by lazy { open val asStr: ObjString by lazy {
@@ -42,7 +42,8 @@ open class ObjClass(
visibility: Visibility = Visibility.Public, visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn 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") throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility) members[name] = ObjRecord(initialValue, isMutable, visibility)
} }
@@ -97,7 +98,19 @@ val ObjIterable by lazy {
*/ */
val ObjCollection by lazy { val ObjCollection by lazy {
val i: ObjClass = ObjIterable 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") } val ObjIterator by lazy { ObjClass("Iterator") }
@@ -145,6 +158,15 @@ val ObjArray by lazy {
addFn("iterator") { addFn("iterator") {
ObjArrayIterator(thisObj).also { it.init(this) } 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() } addFn("isample") { "ok".toObj() }
} }
} }
@@ -75,6 +75,10 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
return this return this
} }
override suspend fun contains(context: Context, other: Obj): Boolean {
return list.contains(other)
}
override val objClass: ObjClass override val objClass: ObjClass
get() = type get() = type
@@ -31,6 +31,14 @@ data class ObjString(val value: String) : Obj() {
return ObjChar(value[index]) 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 { companion object {
val type = ObjClass("String").apply { val type = ObjClass("String").apply {
addConst("startsWith", addConst("startsWith",
@@ -137,14 +137,18 @@ private class Parser(fromPos: Pos) {
if (currentChar == '.') { if (currentChar == '.') {
pos.advance() pos.advance()
// .. already parsed: // .. already parsed:
if (currentChar == '.') { when (currentChar) {
pos.advance() '.' -> {
Token("...", from, Token.Type.ELLIPSIS) pos.advance()
} else if (currentChar == '<') { Token("...", from, Token.Type.ELLIPSIS)
pos.advance() }
Token("..<", from, Token.Type.DOTDOTLT) '<' -> {
} else { pos.advance()
Token("..", from, Token.Type.DOTDOT) Token("..<", from, Token.Type.DOTDOTLT)
}
else -> {
Token("..", from, Token.Type.DOTDOT)
}
} }
} else } else
Token(".", from, Token.Type.DOT) Token(".", from, Token.Type.DOT)
@@ -280,8 +284,6 @@ private class Parser(fromPos: Pos) {
when (text) { when (text) {
"in" -> Token("in", from, Token.Type.IN) "in" -> Token("in", from, Token.Type.IN)
"is" -> Token("is", from, Token.Type.IS) "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 -> Token(text, from, Token.Type.ID)
} }
} else } else
@@ -338,7 +340,7 @@ private class Parser(fromPos: Pos) {
// could be integer, also hex: // could be integer, also hex:
if (currentChar == 'x' && p1 == "0") { if (currentChar == 'x' && p1 == "0") {
pos.advance() 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()) if (currentChar.isLetter())
raise("invalid hex literal") 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, AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT, SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label 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, //PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
ELLIPSIS, DOTDOT, DOTDOTLT, ELLIPSIS, DOTDOT, DOTDOTLT,
NEWLINE, NEWLINE,
+298 -18
View File
@@ -298,9 +298,9 @@ class ScriptTest {
@Test @Test
fun eqNeqTest() = runTest { fun eqNeqTest() = runTest {
assertEquals(ObjBool(true), eval("val x = 2; x == 2")) 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(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("1 == 1").toBool() }
assertTrue { eval("true == true").toBool() } assertTrue { eval("true == true").toBool() }
@@ -313,17 +313,17 @@ class ScriptTest {
@Test @Test
fun logicTest() = runTest { fun logicTest() = runTest {
assertEquals(ObjBool(false), eval("true && false")) assertEquals(ObjFalse, eval("true && false"))
assertEquals(ObjBool(false), eval("false && false")) assertEquals(ObjFalse, eval("false && false"))
assertEquals(ObjBool(false), eval("false && true")) assertEquals(ObjFalse, eval("false && true"))
assertEquals(ObjBool(true), eval("true && true")) assertEquals(ObjBool(true), eval("true && true"))
assertEquals(ObjBool(true), eval("true || false")) 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("false || true"))
assertEquals(ObjBool(true), eval("true || true")) assertEquals(ObjBool(true), eval("true || true"))
assertEquals(ObjBool(false), eval("!true")) assertEquals(ObjFalse, eval("!true"))
assertEquals(ObjBool(true), eval("!false")) assertEquals(ObjBool(true), eval("!false"))
} }
@@ -1750,8 +1750,9 @@ class ScriptTest {
} }
@Test @Test
fun testThrowExisting()= runTest { fun testThrowExisting() = runTest {
eval(""" eval(
"""
val x = IllegalArgumentException("test") val x = IllegalArgumentException("test")
assert( x is Exception ) assert( x is Exception )
@@ -1773,12 +1774,14 @@ class ScriptTest {
} }
assertEquals(3, t) assertEquals(3, t)
assert(finallyCaught) assert(finallyCaught)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testCatchShort1()= runTest { fun testCatchShort1() = runTest {
eval(""" eval(
"""
val x = IllegalArgumentException("test") val x = IllegalArgumentException("test")
var t = 0 var t = 0
var finallyCaught = false var finallyCaught = false
@@ -1795,12 +1798,14 @@ class ScriptTest {
} }
assertEquals(31, t) assertEquals(31, t)
assert(finallyCaught) assert(finallyCaught)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testCatchShort2()= runTest { fun testCatchShort2() = runTest {
eval(""" eval(
"""
val x = IllegalArgumentException("test") val x = IllegalArgumentException("test")
var caught = null var caught = null
try { try {
@@ -1810,12 +1815,14 @@ class ScriptTest {
caught = it caught = it
} }
assert( caught is IllegalArgumentException ) assert( caught is IllegalArgumentException )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testAccessEHData() = runTest { fun testAccessEHData() = runTest {
eval(""" eval(
"""
val x = IllegalArgumentException("test") val x = IllegalArgumentException("test")
val m = try { val m = try {
throw x throw x
@@ -1830,7 +1837,280 @@ class ScriptTest {
} }
println(m) println(m)
assert( m == "test" ) 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()
)
}
} }