Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be4f2c7f45 | ||
|
|
7cc80e2433 | ||
|
|
dacdcd7faa | ||
|
|
59a76efdce | ||
|
|
bb862e6cb5 | ||
|
|
aea819b89a | ||
|
|
88974e0f2d |
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -149,7 +142,7 @@ class Compiler(
|
||||
v.invokeInstanceMethod(
|
||||
context,
|
||||
next.value,
|
||||
args.toArguments(context,false)
|
||||
args.toArguments(context, false)
|
||||
), isMutable = false
|
||||
)
|
||||
}
|
||||
@@ -157,7 +150,7 @@ class Compiler(
|
||||
|
||||
Token.Type.LBRACE -> {
|
||||
// single lambda arg, like assertTrows { ... }
|
||||
cc.next()
|
||||
cc.next()
|
||||
isCall = true
|
||||
val lambda =
|
||||
parseExpression(cc) ?: throw ScriptError(t.pos, "expected valid lambda here")
|
||||
@@ -170,7 +163,7 @@ class Compiler(
|
||||
v.invokeInstanceMethod(
|
||||
context,
|
||||
next.value,
|
||||
Arguments(listOf(lambda),true)
|
||||
Arguments(listOf(lambda), true)
|
||||
), isMutable = false
|
||||
)
|
||||
}
|
||||
@@ -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,16 +709,141 @@ 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 {
|
||||
val throwStatement = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "throw object expected")
|
||||
return statement {
|
||||
var errorObject = throwStatement.execute(this)
|
||||
if( errorObject is ObjString )
|
||||
if (errorObject is ObjString)
|
||||
errorObject = ObjException(this, errorObject.value)
|
||||
if( errorObject is ObjException )
|
||||
if (errorObject is ObjException)
|
||||
raiseError(errorObject)
|
||||
else raiseError("this is not an exception object: $errorObject")
|
||||
}
|
||||
@@ -745,7 +860,7 @@ class Compiler(
|
||||
val catches = mutableListOf<CatchBlockData>()
|
||||
cc.skipTokens(Token.Type.NEWLINE)
|
||||
var t = cc.next()
|
||||
while( t.value == "catch" ) {
|
||||
while (t.value == "catch") {
|
||||
|
||||
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
|
||||
t = cc.next()
|
||||
@@ -785,45 +900,47 @@ 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" ) {
|
||||
val finallyClause = if (t.value == "finally") {
|
||||
parseBlock(cc)
|
||||
} else {
|
||||
cc.previous()
|
||||
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) {
|
||||
val objException = when (e) {
|
||||
is ExecutionError -> e.errorObject
|
||||
else -> ObjUnknownException(this, e.message ?: e.toString())
|
||||
}
|
||||
// let's see if we should catch it:
|
||||
var isCaught = false
|
||||
for( cdata in catches ) {
|
||||
for (cdata in catches) {
|
||||
var exceptionObject: ObjException? = null
|
||||
for (exceptionClassName in cdata.classNames) {
|
||||
val exObj = ObjException.getErrorClass(exceptionClassName)
|
||||
?: raiseSymbolNotFound("error clas not exists: $exceptionClassName")
|
||||
if( objException.isInstanceOf(exObj) ) {
|
||||
if (objException.isInstanceOf(exObj)) {
|
||||
exceptionObject = objException
|
||||
break
|
||||
}
|
||||
}
|
||||
if( exceptionObject != null ) {
|
||||
if (exceptionObject != null) {
|
||||
val catchContext = this.copy(pos = cdata.catchVar.pos)
|
||||
catchContext.addItem(cdata.catchVar.value, false, objException)
|
||||
result = cdata.block.execute(catchContext)
|
||||
@@ -832,10 +949,9 @@ class Compiler(
|
||||
}
|
||||
}
|
||||
// rethrow if not caught this exception
|
||||
if( !isCaught )
|
||||
if (!isCaught)
|
||||
throw e
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
// finally clause does not alter result!
|
||||
finallyClause?.execute(this)
|
||||
}
|
||||
@@ -1080,8 +1196,8 @@ class Compiler(
|
||||
cc.skipTokens(Token.Type.NEWLINE)
|
||||
|
||||
val t = cc.next()
|
||||
if( t.type != Token.Type.ID && t.value != "while" )
|
||||
cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here")
|
||||
if (t.type != Token.Type.ID && t.value != "while")
|
||||
cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here")
|
||||
|
||||
val conditionStart = ensureLparen(cc)
|
||||
val condition =
|
||||
@@ -1105,10 +1221,9 @@ class Compiler(
|
||||
doContext = it.copy().apply { skipContextCreation = true }
|
||||
try {
|
||||
result = body.execute(doContext)
|
||||
}
|
||||
catch( e: LoopBreakContinueException) {
|
||||
if( e.label == label || e.label == null ) {
|
||||
if( e.doContinue ) continue
|
||||
} catch (e: LoopBreakContinueException) {
|
||||
if (e.label == label || e.label == null) {
|
||||
if (e.doContinue) continue
|
||||
else {
|
||||
result = e.result
|
||||
wasBroken = true
|
||||
@@ -1117,8 +1232,8 @@ class Compiler(
|
||||
}
|
||||
throw e
|
||||
}
|
||||
} while( condition.execute(doContext).toBool() )
|
||||
if( !wasBroken ) elseStatement?.let { s -> result = s.execute(it) }
|
||||
} while (condition.execute(doContext).toBool())
|
||||
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -1328,7 +1445,7 @@ class Compiler(
|
||||
val block = parseScript(startPos, cc)
|
||||
return statement(startPos) {
|
||||
// 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 {
|
||||
val t1 = cc.next()
|
||||
if (t1.type != Token.Type.RBRACE)
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
* Scan backwards as deep as specified looking for visibility token. Does not change position.
|
||||
*/
|
||||
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 -> {}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
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()
|
||||
// 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()
|
||||
// }
|
||||
/**
|
||||
* Skip newlines and comments. Returns (and reads) first non-whitespace token.
|
||||
* Note that [Token.Type.EOF] is not considered a whitespace token.
|
||||
*/
|
||||
fun skipWsTokens(): Token {
|
||||
while( current().type in wstokens ) next()
|
||||
return next()
|
||||
}
|
||||
|
||||
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,14 +137,18 @@ private class Parser(fromPos: Pos) {
|
||||
if (currentChar == '.') {
|
||||
pos.advance()
|
||||
// .. already parsed:
|
||||
if (currentChar == '.') {
|
||||
pos.advance()
|
||||
Token("...", from, Token.Type.ELLIPSIS)
|
||||
} else if (currentChar == '<') {
|
||||
pos.advance()
|
||||
Token("..<", from, Token.Type.DOTDOTLT)
|
||||
} else {
|
||||
Token("..", from, Token.Type.DOTDOT)
|
||||
when (currentChar) {
|
||||
'.' -> {
|
||||
pos.advance()
|
||||
Token("...", from, Token.Type.ELLIPSIS)
|
||||
}
|
||||
'<' -> {
|
||||
pos.advance()
|
||||
Token("..<", from, Token.Type.DOTDOTLT)
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -1750,8 +1750,9 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThrowExisting()= runTest {
|
||||
eval("""
|
||||
fun testThrowExisting() = runTest {
|
||||
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("""
|
||||
fun testCatchShort1() = runTest {
|
||||
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("""
|
||||
fun testCatchShort2() = runTest {
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user