2025-08-23 09:41:23 +03:00

2061 lines
82 KiB
Kotlin

/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import ObjEnumClass
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportProvider
/**
* The LYNG compiler.
*/
class Compiler(
val cc: CompilerContext,
val importManager: ImportProvider,
@Suppress("UNUSED_PARAMETER")
settings: Settings = Settings()
) {
var packageName: String? = null
class Settings
private val initStack = mutableListOf<MutableList<Statement>>()
val currentInitScope: MutableList<Statement>
get() =
initStack.lastOrNull() ?: cc.syntaxError("no initialization scope exists here")
private fun pushInitScope(): MutableList<Statement> = mutableListOf<Statement>().also { initStack.add(it) }
private fun popInitScope(): MutableList<Statement> = initStack.removeLast()
private val codeContexts = mutableListOf<CodeContext>(CodeContext.Module(null))
private suspend fun <T> inCodeContext(context: CodeContext, f: suspend () -> T): T {
return try {
codeContexts.add(context)
f()
} finally {
codeContexts.removeLast()
}
}
private suspend fun parseScript(): Script {
val statements = mutableListOf<Statement>()
val start = cc.currentPos()
// val returnScope = cc.startReturnScope()
// package level declarations
do {
val t = cc.current()
if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINLGE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) {
cc.next()
continue
}
if (t.type == Token.Type.ID) {
when (t.value) {
"package" -> {
cc.next()
val name = loadQualifiedName()
if (name.isEmpty()) throw ScriptError(cc.currentPos(), "Expecting package name here")
if (packageName != null) throw ScriptError(
cc.currentPos(),
"package name redefined, already set to $packageName"
)
packageName = name
continue
}
"import" -> {
cc.next()
val pos = cc.currentPos()
val name = loadQualifiedName()
val module = importManager.prepareImport(pos, name, null)
statements += statement {
module.importInto(this, null)
ObjVoid
}
continue
}
}
}
val s = parseStatement(braceMeansLambda = true)?.also {
statements += it
}
if (s == null) {
when (t.type) {
Token.Type.RBRACE, Token.Type.EOF, Token.Type.SEMICOLON -> {}
else ->
throw ScriptError(t.pos, "unexpeced `${t.value}` here")
}
break
}
} while (true)
return Script(start, statements)//returnScope.needCatch)
}
fun loadQualifiedName(): String {
val result = StringBuilder()
var t = cc.next()
while (t.type == Token.Type.ID) {
result.append(t.value)
t = cc.next()
if (t.type == Token.Type.DOT) {
result.append('.')
t = cc.next()
}
}
cc.previous()
return result.toString()
}
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
lastAnnotation = null
while (true) {
val t = cc.next()
return when (t.type) {
Token.Type.ID -> {
parseKeywordStatement(t)
?: run {
cc.previous()
parseExpression()
}
}
Token.Type.PLUS2, Token.Type.MINUS2 -> {
cc.previous()
parseExpression()
}
Token.Type.ATLABEL -> {
lastAnnotation = parseAnnotation(t)
continue
}
Token.Type.LABEL -> continue
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue
Token.Type.NEWLINE -> continue
Token.Type.SEMICOLON -> continue
Token.Type.LBRACE -> {
cc.previous()
if (braceMeansLambda)
parseExpression()
else
parseBlock()
}
Token.Type.RBRACE, Token.Type.RBRACKET -> {
cc.previous()
return null
}
Token.Type.EOF -> null
else -> {
// could be expression
cc.previous()
parseExpression()
}
}
}
}
private suspend fun parseExpression(): Statement? {
val pos = cc.currentPos()
return parseExpressionLevel()?.let { a -> statement(pos) { a.getter(it).value } }
}
private suspend fun parseExpressionLevel(level: Int = 0): Accessor? {
if (level == lastLevel)
return parseTerm()
var lvalue: Accessor? = parseExpressionLevel(level + 1) ?: return null
while (true) {
val opToken = cc.next()
val op = byLevel[level][opToken.type]
if (op == null) {
cc.previous()
break
}
val rvalue = parseExpressionLevel(level + 1)
?: throw ScriptError(opToken.pos, "Expecting expression")
lvalue = op.generate(opToken.pos, lvalue!!, rvalue)
}
return lvalue
}
private suspend fun parseTerm(): Accessor? {
var operand: Accessor? = null
// newlines _before_
cc.skipWsTokens()
while (true) {
val t = cc.next()
val startPos = t.pos
when (t.type) {
// Token.Type.NEWLINE, Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT-> {
// continue
// }
// very special case chained calls: call()<NL>.call2 {}.call3()
Token.Type.NEWLINE -> {
val saved = cc.savePos()
if (cc.peekNextNonWhitespace().type == Token.Type.DOT) {
// chained call continue from it
continue
} else {
// restore position and stop parsing as a term:
cc.restorePos(saved)
cc.previous()
return operand
}
}
Token.Type.SEMICOLON, Token.Type.EOF, Token.Type.RBRACE, Token.Type.COMMA -> {
cc.previous()
return operand
}
Token.Type.NOT -> {
if (operand != null) throw ScriptError(t.pos, "unexpected operator not '!'")
val op = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
operand = Accessor { op.getter(it).value.logicalNot(it).asReadonly }
}
Token.Type.DOT, Token.Type.NULL_COALESCE -> {
val isOptional = t.type == Token.Type.NULL_COALESCE
operand?.let { left ->
// dot call: calling method on the operand, if next is ID, "("
var isCall = false
val next = cc.next()
if (next.type == Token.Type.ID) {
// could be () call or obj.method {} call
val nt = cc.current()
when (nt.type) {
Token.Type.LPAREN -> {
cc.next()
// instance method call
val args = parseArgs().first
isCall = true
operand = Accessor { context ->
context.pos = next.pos
val v = left.getter(context).value
if (v == ObjNull && isOptional)
ObjNull.asReadonly
else
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
args.toArguments(context, false)
), isMutable = false
)
}
}
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
// single lambda arg, like assertTrows { ... }
cc.next()
isCall = true
val lambda =
parseLambdaExpression()
operand = Accessor { context ->
context.pos = next.pos
val v = left.getter(context).value
if (v == ObjNull && isOptional)
ObjNull.asReadonly
else
ObjRecord(
v.invokeInstanceMethod(
context,
next.value,
Arguments(listOf(lambda.getter(context).value), true)
), isMutable = false
)
}
}
else -> {}
}
}
if (!isCall) {
operand = Accessor({ context ->
val x = left.getter(context).value
if (x == ObjNull && isOptional) ObjNull.asReadonly
else x.readField(context, next.value)
}) { cxt, newValue ->
left.getter(cxt).value.writeField(cxt, next.value, newValue)
}
}
}
?: throw ScriptError(t.pos, "Expecting expression before dot")
}
Token.Type.COLONCOLON -> {
operand = parseScopeOperator(operand)
}
Token.Type.LPAREN, Token.Type.NULL_COALESCE_INVOKE -> {
operand?.let { left ->
// this is function call from <left>
operand = parseFunctionCall(
left,
false,
t.type == Token.Type.NULL_COALESCE_INVOKE
)
} ?: run {
// Expression in parentheses
val statement = parseStatement() ?: throw ScriptError(t.pos, "Expecting expression")
operand = Accessor {
statement.execute(it).asReadonly
}
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
cc.skipTokenOfType(Token.Type.RPAREN, "missing ')'")
}
}
Token.Type.LBRACKET, Token.Type.NULL_COALESCE_INDEX -> {
operand?.let { left ->
// array access
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
val index = parseStatement() ?: throw ScriptError(t.pos, "Expecting index expression")
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
operand = Accessor({ cxt ->
val i = index.execute(cxt)
val x = left.getter(cxt).value
if (x == ObjNull && isOptional) ObjNull.asReadonly
else x.getAt(cxt, i).asMutable
}) { cxt, newValue ->
left.getter(cxt).value.putAt(cxt, index.execute(cxt), newValue)
}
} ?: run {
// array literal
val entries = parseArrayLiteral()
// if it didn't throw, ot parsed ot and consumed it all
operand = Accessor { cxt ->
val list = mutableListOf<Obj>()
for (e in entries) {
when (e) {
is ListEntry.Element -> {
list += e.accessor.getter(cxt).value
}
is ListEntry.Spread -> {
val elements = e.accessor.getter(cxt).value
when {
elements is ObjList -> list.addAll(elements.list)
else -> cxt.raiseError("Spread element must be list")
}
}
}
}
ObjList(list).asReadonly
}
}
}
Token.Type.ID -> {
// there could be terminal operators or keywords:// variable to read or like
when (t.value) {
in stopKeywords -> {
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
cc.previous()
val s = parseStatement() ?: throw ScriptError(t.pos, "Expecting valid statement")
operand = Accessor { s.execute(it).asReadonly }
}
"else", "break", "continue" -> {
cc.previous()
return operand
}
"throw" -> {
val s = parseThrowStatement()
operand = Accessor {
s.execute(it).asReadonly
}
}
else -> operand?.let { left ->
// selector: <lvalue>, '.' , <id>
// we replace operand with selector code, that
// is RW:
operand = Accessor({
it.pos = t.pos
left.getter(it).value.readField(it, t.value)
}) { cxt, newValue ->
cxt.pos = t.pos
left.getter(cxt).value.writeField(cxt, t.value, newValue)
}
} ?: run {
// variable to read or like
cc.previous()
operand = parseAccessor()
}
}
}
Token.Type.PLUS2 -> {
// note: post-increment result is not assignable (truly lvalue)
operand?.let { left ->
// post increment
left.setter(startPos)
operand = Accessor { cxt ->
val x = left.getter(cxt)
if (x.isMutable) {
if (x.value.isConst) {
x.value.plus(cxt, ObjInt.One).also {
left.setter(startPos)(cxt, it)
}.asReadonly
} else
x.value.getAndIncrement(cxt).asReadonly
} else cxt.raiseError("Cannot increment immutable value")
}
} ?: run {
// no lvalue means pre-increment, expression to increment follows
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
operand = Accessor { ctx ->
val x = next.getter(ctx).also {
if (!it.isMutable) ctx.raiseError("Cannot increment immutable value")
}.value
if (x.isConst) {
next.setter(startPos)(ctx, x.plus(ctx, ObjInt.One))
x.asReadonly
} else x.incrementAndGet(ctx).asReadonly
}
}
}
Token.Type.MINUS2 -> {
// note: post-decrement result is not assignable (truly lvalue)
operand?.let { left ->
// post decrement
left.setter(startPos)
operand = Accessor { cxt ->
val x = left.getter(cxt)
if (!x.isMutable) cxt.raiseError("Cannot decrement immutable value")
if (x.value.isConst) {
x.value.minus(cxt, ObjInt.One).also {
left.setter(startPos)(cxt, it)
}.asReadonly
} else
x.value.getAndDecrement(cxt).asReadonly
}
} ?: run {
// no lvalue means pre-decrement, expression to decrement follows
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
operand = Accessor { cxt ->
val x = next.getter(cxt)
if (!x.isMutable) cxt.raiseError("Cannot decrement immutable value")
if (x.value.isConst) {
x.value.minus(cxt, ObjInt.One).also {
next.setter(startPos)(cxt, it)
}.asReadonly
} else
x.value.decrementAndGet(cxt).asReadonly
}
}
}
Token.Type.DOTDOT, Token.Type.DOTDOTLT -> {
// range operator
val isEndInclusive = t.type == Token.Type.DOTDOT
val left = operand
// if it is an open end range, then the end of line could be here that we do not want
// to skip in parseExpression:
val current = cc.current()
val right =
if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINLGE_LINE_COMMENT)
null
else
parseExpression()
operand = Accessor {
ObjRange(
left?.getter?.invoke(it)?.value ?: ObjNull,
right?.execute(it) ?: ObjNull,
isEndInclusive = isEndInclusive
).asReadonly
}
}
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
operand = operand?.let { left ->
cc.previous()
parseFunctionCall(
left,
blockArgument = true,
t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
)
} ?: parseLambdaExpression()
}
Token.Type.RBRACKET, Token.Type.RPAREN -> {
cc.previous()
return operand
}
else -> {
cc.previous()
operand?.let { return it }
operand = parseAccessor() ?: return null //throw ScriptError(t.pos, "Expecting expression")
}
}
}
}
/**
* Parse lambda expression, leading '{' is already consumed
*/
private suspend fun parseLambdaExpression(): Accessor {
// lambda args are different:
val startPos = cc.currentPos()
val argsDeclaration = parseArgsDeclaration()
if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW)
throw ScriptError(
startPos,
"lambda must have either valid arguments declaration with '->' or no arguments"
)
val body = parseBlock(skipLeadingBrace = true)
var closure: Scope? = null
val callStatement = statement {
// and the source closure of the lambda which might have other thisObj.
val context = this.applyClosure(closure!!)
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = args.list
val itValue: Obj = when (l.size) {
// no args: it == void
0 -> ObjVoid
// one args: it is this arg
1 -> l[0]
// more args: it is a list of args
else -> ObjList(l.toMutableList())
}
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
} else {
// assign vars as declared the standard way
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
}
body.execute(context)
}
return Accessor { x ->
closure = x
callStatement.asReadonly
}
}
private suspend fun parseArrayLiteral(): List<ListEntry> {
// it should be called after Token.Type.LBRACKET is consumed
val entries = mutableListOf<ListEntry>()
while (true) {
val t = cc.next()
when (t.type) {
Token.Type.COMMA -> {
// todo: check commas sequences like [,] [,,] before, after or instead of expressions
}
Token.Type.RBRACKET -> return entries
Token.Type.ELLIPSIS -> {
parseExpressionLevel()?.let { entries += ListEntry.Spread(it) }
}
else -> {
cc.previous()
parseExpressionLevel()?.let { entries += ListEntry.Element(it) }
?: throw ScriptError(t.pos, "invalid list literal: expecting expression")
}
}
}
}
private fun parseScopeOperator(operand: Accessor?): Accessor {
// implement global scope maybe?
if (operand == null) throw ScriptError(cc.next().pos, "Expecting expression before ::")
val t = cc.next()
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "Expecting ID after ::")
return when (t.value) {
"class" -> Accessor {
operand.getter(it).value.objClass.asReadonly
}
else -> throw ScriptError(t.pos, "Unknown scope operation: ${t.value}")
}
}
/**
* Parse argument declaration, used in lambda (and later in fn too)
* @return declaration or null if there is no valid list of arguments
*/
private suspend fun parseArgsDeclaration(isClassDeclaration: Boolean = false): ArgsDeclaration? {
val result = mutableListOf<ArgsDeclaration.Item>()
var endTokenType: Token.Type? = null
val startPos = cc.savePos()
cc.skipWsTokens()
while (endTokenType == null) {
var t = cc.next()
when (t.type) {
Token.Type.RPAREN, Token.Type.ARROW -> {
// empty list?
endTokenType = t.type
}
Token.Type.NEWLINE -> {}
Token.Type.ID -> {
// visibility
val visibility = if (isClassDeclaration && t.value == "private") {
t = cc.next()
Visibility.Private
} else Visibility.Public
// val/var?
val access = when (t.value) {
"val" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
AccessType.Val
}
"var" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
AccessType.Var
}
else -> null
}
var defaultValue: Statement? = null
cc.ifNextIs(Token.Type.ASSIGN) {
defaultValue = parseExpression()
}
// type information
val typeInfo = parseTypeDeclaration()
val isEllipsis = cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true)
result += ArgsDeclaration.Item(
t.value,
typeInfo,
t.pos,
isEllipsis,
defaultValue,
access,
visibility
)
// important: valid argument list continues with ',' and ends with '->' or ')'
// otherwise it is not an argument list:
when (val tt = cc.nextNonWhitespace().type) {
Token.Type.RPAREN -> {
// end of arguments
endTokenType = tt
}
Token.Type.ARROW -> {
// end of arguments too
endTokenType = tt
}
Token.Type.COMMA -> {
// next argument, OK
}
else -> {
// this is not a valid list of arguments:
cc.restorePos(startPos) // for the current
return null
}
}
}
else -> {
// if we get here. there os also no valid list of arguments:
cc.restorePos(startPos)
return null
}
}
}
return ArgsDeclaration(result, endTokenType)
}
private fun parseTypeDeclaration(): TypeDecl {
return if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
val tt = cc.requireToken(Token.Type.ID, "type name or type expression required")
val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
TypeDecl.Simple(tt.value, isNullable)
} else TypeDecl.TypeAny
}
/**
* Parse arguments list during the call and detect last block argument
* _following the parenthesis_ call: `(1,2) { ... }`
*/
private suspend fun parseArgs(): Pair<List<ParsedArgument>, Boolean> {
val args = mutableListOf<ParsedArgument>()
do {
val t = cc.next()
when (t.type) {
Token.Type.NEWLINE,
Token.Type.RPAREN, Token.Type.COMMA -> {
}
Token.Type.ELLIPSIS -> {
parseStatement()?.let { args += ParsedArgument(it, t.pos, isSplat = true) }
?: throw ScriptError(t.pos, "Expecting arguments list")
}
else -> {
cc.previous()
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
if (cc.current().type == Token.Type.COLON)
parseTypeDeclaration()
// Here should be a valid termination:
}
}
} while (t.type != Token.Type.RPAREN)
// block after?
val pos = cc.savePos()
val end = cc.next()
var lastBlockArgument = false
if (end.type == Token.Type.LBRACE) {
// last argument - callable
val callableAccessor = parseLambdaExpression()
args += ParsedArgument(
// transform accessor to the callable:
statement {
callableAccessor.getter(this).value
},
end.pos
)
lastBlockArgument = true
} else
cc.restorePos(pos)
return args to lastBlockArgument
}
private suspend fun parseFunctionCall(
left: Accessor,
blockArgument: Boolean,
isOptional: Boolean
): Accessor {
// insofar, functions always return lvalue
var detectedBlockArgument = blockArgument
val args = if (blockArgument) {
val blockArg = ParsedArgument(
parseExpression()
?: throw ScriptError(cc.currentPos(), "lambda body expected"), cc.currentPos()
)
listOf(blockArg)
} else {
val r = parseArgs()
detectedBlockArgument = r.second
r.first
}
return Accessor { context ->
val v = left.getter(context)
if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly
v.value.callOn(
context.copy(
context.pos,
args.toArguments(context, detectedBlockArgument)
// Arguments(
// args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) }
// ),
)
).asReadonly
}
}
private suspend fun parseAccessor(): Accessor? {
// could be: literal
val t = cc.next()
return when (t.type) {
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
cc.previous()
val n = parseNumber(true)
Accessor {
n.asReadonly
}
}
Token.Type.STRING -> Accessor { ObjString(t.value).asReadonly }
Token.Type.CHAR -> Accessor { ObjChar(t.value[0]).asReadonly }
Token.Type.PLUS -> {
val n = parseNumber(true)
Accessor { n.asReadonly }
}
Token.Type.MINUS -> {
parseNumberOrNull(false)?.let { n ->
Accessor { n.asReadonly }
} ?: run {
val n = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression after unary minus")
Accessor {
n.getter.invoke(it).value.negate(it).asReadonly
}
}
}
Token.Type.ID -> {
when (t.value) {
"void" -> Accessor { ObjVoid.asReadonly }
"null" -> Accessor { ObjNull.asReadonly }
"true" -> Accessor { ObjBool(true).asReadonly }
"false" -> Accessor { ObjFalse.asReadonly }
else -> {
Accessor({
it.pos = t.pos
it[t.value]
?: it.raiseError("symbol not defined: '${t.value}'")
}) { ctx, newValue ->
ctx[t.value]?.let { stored ->
ctx.pos = t.pos
if (stored.isMutable)
stored.value = newValue
else
ctx.raiseError("Cannot assign to immutable value")
} ?: ctx.raiseError("symbol not defined: '${t.value}'")
}
}
}
}
else -> null
}
}
private fun parseNumberOrNull(isPlus: Boolean): Obj? {
val pos = cc.savePos()
val t = cc.next()
return when (t.type) {
Token.Type.INT, Token.Type.HEX -> {
val n = t.value.replace("_", "").toLong(if (t.type == Token.Type.HEX) 16 else 10)
if (isPlus) ObjInt(n) else ObjInt(-n)
}
Token.Type.REAL -> {
val d = t.value.toDouble()
if (isPlus) ObjReal(d) else ObjReal(-d)
}
else -> {
cc.restorePos(pos)
null
}
}
}
private fun parseNumber(isPlus: Boolean): Obj {
return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number")
}
suspend fun parseAnnotation(t: Token): (suspend (Scope, ObjString, Statement) -> Statement) {
val extraArgs = parseArgsOrNull()
// println("annotation ${t.value}: args: $extraArgs")
return { scope, name, body ->
val extras = extraArgs?.first?.toArguments(scope, extraArgs.second)?.list
val required = listOf(name, body)
val args = extras?.let { required + it } ?: required
val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}")
if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}")
(fn.execute(scope.copy(Arguments(args))) as? Statement)
?: scope.raiseClassCastError("function annotation must return callable")
}
}
suspend fun parseArgsOrNull(): Pair<List<ParsedArgument>, Boolean>? =
if (cc.skipNextIf(Token.Type.LPAREN))
parseArgs()
else
null
/**
* Parse keyword-starting statement.
* @return parsed statement or null if, for example. [id] is not among keywords
*/
private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) {
"val" -> parseVarDeclaration(false, Visibility.Public)
"var" -> parseVarDeclaration(true, Visibility.Public)
"while" -> parseWhileStatement()
"do" -> parseDoWhileStatement()
"for" -> parseForStatement()
"break" -> parseBreakStatement(id.pos)
"continue" -> parseContinueStatement(id.pos)
"if" -> parseIfStatement()
"class" -> parseClassDeclaration()
"enum" -> parseEnumDeclaration()
"try" -> parseTryStatement()
"throw" -> parseThrowStatement()
"when" -> parseWhenStatement()
else -> {
// triples
cc.previous()
val isExtern = cc.skipId("extern")
when {
cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration(
Visibility.Private,
isExtern,
isStatic = true
)
cc.matchQualifiers("fun", "static") -> parseFunctionDeclaration(
Visibility.Public,
isExtern,
isStatic = true
)
cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fun") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
cc.matchQualifiers("fn") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
cc.matchQualifiers("val", "private", "static") -> parseVarDeclaration(
false,
Visibility.Private,
isStatic = true
)
cc.matchQualifiers("val", "static") -> parseVarDeclaration(false, Visibility.Public, isStatic = true)
cc.matchQualifiers("val", "private") -> parseVarDeclaration(false, Visibility.Private)
cc.matchQualifiers("var", "static") -> parseVarDeclaration(true, Visibility.Public, isStatic = true)
cc.matchQualifiers("var", "static", "private") -> parseVarDeclaration(
true,
Visibility.Private,
isStatic = true
)
cc.matchQualifiers("var", "private") -> parseVarDeclaration(true, Visibility.Private)
cc.matchQualifiers("val", "open") -> parseVarDeclaration(false, Visibility.Private, true)
cc.matchQualifiers("var", "open") -> parseVarDeclaration(true, Visibility.Private, true)
else -> {
cc.next()
null
}
}
}
}
data class WhenCase(val condition: Statement, val block: Statement)
private suspend fun parseWhenStatement(): Statement {
// has a value, when(value) ?
var t = cc.nextNonWhitespace()
return if (t.type == Token.Type.LPAREN) {
// when(value)
val value = parseStatement() ?: 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.nextNonWhitespace()
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() ?: 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() ?: 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() ?: throw ScriptError(
cc.currentPos(),
"when else block expected"
)
skipParseBody = true
} else {
cc.previous()
val x = parseExpression()
?: 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() ?: 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 suspend fun parseThrowStatement(): Statement {
val throwStatement = parseStatement() ?: throw ScriptError(cc.currentPos(), "throw object expected")
return statement {
var errorObject = throwStatement.execute(this)
if (errorObject is ObjString)
errorObject = ObjException(this, errorObject.value)
if (errorObject is ObjException)
raiseError(errorObject)
else raiseError("this is not an exception object: $errorObject")
}
}
private data class CatchBlockData(
val catchVar: Token,
val classNames: List<String>,
val block: Statement
)
private suspend fun parseTryStatement(): Statement {
val body = parseBlock()
val catches = mutableListOf<CatchBlockData>()
cc.skipTokens(Token.Type.NEWLINE)
var t = cc.next()
while (t.value == "catch") {
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
t = cc.next()
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "expected catch variable")
val catchVar = t
val exClassNames = mutableListOf<String>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
// load list of exception classes
do {
t = cc.next()
if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "expected exception class name")
exClassNames += t.value
t = cc.next()
when (t.type) {
Token.Type.COMMA -> {
continue
}
Token.Type.RPAREN -> {
break
}
else -> throw ScriptError(t.pos, "syntax error: expected ',' or ')'")
}
} while (true)
} else {
// no type!
exClassNames += "Exception"
cc.skipTokenOfType(Token.Type.RPAREN)
}
val block = parseBlock()
catches += CatchBlockData(catchVar, exClassNames, block)
cc.skipTokens(Token.Type.NEWLINE)
t = cc.next()
} else {
// no (e: Exception) block: should be the 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(true)
)
t = cc.next()
}
}
val finallyClause = if (t.value == "finally") {
parseBlock()
} 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) {
// convert to appropriate exception
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) {
var exceptionObject: ObjException? = null
for (exceptionClassName in cdata.classNames) {
val exObj = ObjException.getErrorClass(exceptionClassName)
?: raiseSymbolNotFound("error clas not exists: $exceptionClassName")
if (objException.isInstanceOf(exObj)) {
exceptionObject = objException
break
}
}
if (exceptionObject != null) {
val catchContext = this.copy(pos = cdata.catchVar.pos)
catchContext.addItem(cdata.catchVar.value, false, objException)
result = cdata.block.execute(catchContext)
isCaught = true
break
}
}
// rethrow if not caught this exception
if (!isCaught)
throw e
} finally {
// finally clause does not alter result!
finallyClause?.execute(this)
}
result
}
}
private fun parseEnumDeclaration(): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
// so far only simplest enums:
val names = mutableListOf<String>()
// skip '{'
cc.skipTokenOfType(Token.Type.LBRACE)
do {
val t = cc.nextNonWhitespace()
when (t.type) {
Token.Type.ID -> {
names += t.value
val t1 = cc.nextNonWhitespace()
when (t1.type) {
Token.Type.COMMA ->
continue
Token.Type.RBRACE -> break
else -> {
t1.raiseSyntax("unexpected token")
}
}
}
else -> t.raiseSyntax("expected enum entry name")
}
} while (true)
return statement {
ObjEnumClass.createSimpleEnum(nameToken.value, names).also {
addItem(nameToken.value, false, it, recordType = ObjRecord.Type.Enum)
}
}
}
private suspend fun parseClassDeclaration(): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
return inCodeContext(CodeContext.ClassBody(nameToken.value)) {
val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(isClassDeclaration = true)
else null
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
nameToken.pos,
"Bad class declaration: expected ')' at the end of the primary constructor"
)
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val t = cc.next()
pushInitScope()
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
// parse body
parseScript().also {
cc.skipTokens(Token.Type.RBRACE)
}
} else {
cc.previous()
null
}
val initScope = popInitScope()
// create class
val className = nameToken.value
// @Suppress("UNUSED_VARIABLE") val defaultAccess = if (isStruct) AccessType.Var else AccessType.Initialization
// @Suppress("UNUSED_VARIABLE") val defaultVisibility = Visibility.Public
// create instance constructor
// create custom objClass with all fields and instance constructor
val constructorCode = statement {
// constructor code is registered with class instance and is called over
// new `thisObj` already set by class to ObjInstance.instanceContext
thisObj as ObjInstance
// the context now is a "class creation context", we must use its args to initialize
// fields. Note that 'this' is already set by class
constructorArgsDeclaration?.assignToContext(this)
bodyInit?.execute(this)
thisObj
}
// inheritance must alter this code:
val newClass = ObjInstanceClass(className).apply {
instanceConstructor = constructorCode
constructorMeta = constructorArgsDeclaration
}
statement {
// the main statement should create custom ObjClass instance with field
// accessors, constructor registration, etc.
addItem(className, false, newClass)
if (initScope.isNotEmpty()) {
val classScope = copy(newThisObj = newClass)
newClass.classScope = classScope
for (s in initScope)
s.execute(classScope)
}
newClass
}
}
}
private fun getLabel(maxDepth: Int = 2): String? {
var cnt = 0
var found: String? = null
while (cc.hasPrevious() && cnt < maxDepth) {
val t = cc.previous()
cnt++
if (t.type == Token.Type.LABEL) {
found = t.value
break
}
}
while (cnt-- > 0) cc.next()
return found
}
private suspend fun parseForStatement(): Statement {
val label = getLabel()?.also { cc.labels += it }
val start = ensureLparen()
val tVar = cc.next()
if (tVar.type != Token.Type.ID)
throw ScriptError(tVar.pos, "Bad for statement: expected loop variable")
val tOp = cc.next()
if (tOp.value == "in") {
// in loop
val source = parseStatement() ?: throw ScriptError(start, "Bad for statement: expected expression")
ensureRparen()
val (canBreak, body) = cc.parseLoop {
parseStatement() ?: throw ScriptError(start, "Bad for statement: expected loop body")
}
// possible else clause
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
parseStatement()
} else {
cc.previous()
null
}
return statement(body.pos) { cxt ->
val forContext = cxt.copy(start)
// loop var: StoredObject
val loopSO = forContext.addItem(tVar.value, true, ObjNull)
// insofar we suggest source object is enumerable. Later we might need to add checks
val sourceObj = source.execute(forContext)
if (sourceObj is ObjRange && sourceObj.isIntRange) {
loopIntRange(
forContext,
sourceObj.start!!.toInt(),
if (sourceObj.isEndInclusive) sourceObj.end!!.toInt() + 1 else sourceObj.end!!.toInt(),
loopSO, body, elseStatement, label, canBreak
)
} else if (sourceObj.isInstanceOf(ObjIterable)) {
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
} else {
val size = runCatching { sourceObj.invokeInstanceMethod(forContext, "size").toInt() }
.getOrElse {
throw ScriptError(
tOp.pos,
"object is not enumerable: no size in $sourceObj",
it
)
}
var result: Obj = ObjVoid
var breakCaught = false
if (size > 0) {
var current = runCatching { sourceObj.getAt(forContext, ObjInt(0)) }
.getOrElse {
throw ScriptError(
tOp.pos,
"object is not enumerable: no index access for ${sourceObj.inspect(cxt)}",
it
)
}
var index = 0
while (true) {
loopSO.value = current
if (canBreak) {
try {
result = body.execute(forContext)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
breakCaught = true
if (lbe.doContinue) continue
else {
result = lbe.result
break
}
} else
throw lbe
}
} else result = body.execute(forContext)
if (++index >= size) break
current = sourceObj.getAt(forContext, ObjInt(index.toLong()))
}
}
if (!breakCaught && elseStatement != null) {
result = elseStatement.execute(cxt)
}
result
}
}
} else {
// maybe other loops?
throw ScriptError(tOp.pos, "Unsupported for-loop syntax")
}
}
private suspend fun loopIntRange(
forScope: Scope, start: Int, end: Int, loopVar: ObjRecord,
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
): Obj {
var result: Obj = ObjVoid
val iVar = ObjInt(0)
loopVar.value = iVar
if (catchBreak) {
for (i in start..<end) {
iVar.value = i.toLong()
try {
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
return lbe.result
}
throw lbe
}
}
} else {
for (i in start.toLong()..<end.toLong()) {
iVar.value = i
result = body.execute(forScope)
}
}
return elseStatement?.execute(forScope) ?: result
}
private suspend fun loopIterable(
forScope: Scope, sourceObj: Obj, loopVar: ObjRecord,
body: Statement, elseStatement: Statement?, label: String?,
catchBreak: Boolean
): Obj {
val iterObj = sourceObj.invokeInstanceMethod(forScope, "iterator")
var result: Obj = ObjVoid
while (iterObj.invokeInstanceMethod(forScope, "hasNext").toBool()) {
if (catchBreak)
try {
loopVar.value = iterObj.invokeInstanceMethod(forScope, "next")
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
return lbe.result
}
throw lbe
}
else {
loopVar.value = iterObj.invokeInstanceMethod(forScope, "next")
result = body.execute(forScope)
}
}
return elseStatement?.execute(forScope) ?: result
}
@Suppress("UNUSED_VARIABLE")
private suspend fun parseDoWhileStatement(): Statement {
val label = getLabel()?.also { cc.labels += it }
val (breakFound, body) = cc.parseLoop {
parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad while statement: expected statement")
}
label?.also { cc.labels -= it }
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")
val conditionStart = ensureLparen()
val condition =
parseExpression() ?: throw ScriptError(conditionStart, "Bad while statement: expected expression")
ensureRparen()
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
parseStatement()
} else {
cc.previous()
null
}
return statement(body.pos) {
var wasBroken = false
var result: Obj = ObjVoid
lateinit var doScope: Scope
do {
doScope = it.copy().apply { skipScopeCreation = true }
try {
result = body.execute(doScope)
} catch (e: LoopBreakContinueException) {
if (e.label == label || e.label == null) {
if (e.doContinue) continue
else {
result = e.result
wasBroken = true
break
}
}
throw e
}
} while (condition.execute(doScope).toBool())
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
result
}
}
private suspend fun parseWhileStatement(): Statement {
val label = getLabel()?.also { cc.labels += it }
val start = ensureLparen()
val condition =
parseExpression() ?: throw ScriptError(start, "Bad while statement: expected expression")
ensureRparen()
val body = parseStatement() ?: throw ScriptError(start, "Bad while statement: expected statement")
label?.also { cc.labels -= it }
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
parseStatement()
} else {
cc.previous()
null
}
return statement(body.pos) {
var result: Obj = ObjVoid
var wasBroken = false
while (condition.execute(it).toBool()) {
try {
// we don't need to create new context here: if body is a block,
// parse block will do it, otherwise single statement doesn't need it:
result = body.execute(it)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
else {
result = lbe.result
wasBroken = true
break
}
} else
throw lbe
}
}
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
result
}
}
private suspend fun parseBreakStatement(start: Pos): Statement {
var t = cc.next()
val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) {
cc.previous()
null
} else {
t.value
}?.also {
// check that label is defined
cc.ensureLabelIsValid(start, it)
}
// expression?
t = cc.next()
cc.previous()
val resultExpr = if (t.pos.line == start.line && (!t.isComment &&
t.type != Token.Type.SEMICOLON &&
t.type != Token.Type.NEWLINE)
) {
// we have something on this line, could be expression
parseStatement()
} else null
cc.addBreak()
return statement(start) {
val returnValue = resultExpr?.execute(it)// ?: ObjVoid
throw LoopBreakContinueException(
doContinue = false,
label = label,
result = returnValue ?: ObjVoid
)
}
}
private fun parseContinueStatement(start: Pos): Statement {
val t = cc.next()
val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) {
cc.previous()
null
} else {
t.value
}?.also {
// check that label is defined
cc.ensureLabelIsValid(start, it)
}
cc.addBreak()
return statement(start) {
throw LoopBreakContinueException(
doContinue = true,
label = label,
)
}
}
private fun ensureRparen(): Pos {
val t = cc.next()
if (t.type != Token.Type.RPAREN)
throw ScriptError(t.pos, "expected ')'")
return t.pos
}
private fun ensureLparen(): Pos {
val t = cc.next()
if (t.type != Token.Type.LPAREN)
throw ScriptError(t.pos, "expected '('")
return t.pos
}
private suspend fun parseIfStatement(): Statement {
val start = ensureLparen()
val condition = parseExpression()
?: throw ScriptError(start, "Bad if statement: expected expression")
val pos = ensureRparen()
val ifBody = parseStatement() ?: throw ScriptError(pos, "Bad if statement: expected statement")
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
// could be else block:
val t2 = cc.next()
// we generate different statements: optimization
return if (t2.type == Token.Type.ID && t2.value == "else") {
val elseBody =
parseStatement() ?: throw ScriptError(pos, "Bad else statement: expected statement")
return statement(start) {
if (condition.execute(it).toBool())
ifBody.execute(it)
else
elseBody.execute(it)
}
} else {
cc.previous()
statement(start) {
if (condition.execute(it).toBool())
ifBody.execute(it)
else
ObjVoid
}
}
}
private suspend fun parseFunctionDeclaration(
visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isExtern: Boolean = false,
isStatic: Boolean = false,
): Statement {
var t = cc.next()
val start = t.pos
var extTypeName: String? = null
var name = if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "Expected identifier after 'fun'")
else t.value
val annotation = lastAnnotation
val parentContext = codeContexts.last()
t = cc.next()
// Is extension?
if (t.type == Token.Type.DOT) {
extTypeName = name
t = cc.next()
if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "illegal extension format: expected function name")
name = t.value
t = cc.next()
}
if (t.type != Token.Type.LPAREN)
throw ScriptError(t.pos, "Bad function definition: expected '(' after 'fn ${name}'")
val argsDeclaration = parseArgsDeclaration()
if (argsDeclaration == null || argsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
t.pos,
"Bad function definition: expected valid argument declaration or () after 'fn ${name}'"
)
if (cc.current().type == Token.Type.COLON) parseTypeDeclaration()
return inCodeContext(CodeContext.Function(name)) {
// Here we should be at open body
val fnStatements = if (isExtern)
statement { raiseError("extern function not provided: $name") }
else
parseBlock()
var closure: Scope? = null
val fnBody = statement(t.pos) { callerContext ->
callerContext.pos = start
// restore closure where the function was defined, and making a copy of it
// for local space. If there is no closure, we are in, say, class context where
// the closure is in the class initialization and we needn't more:
val context = closure?.let { ClosureScope(callerContext, it) }
?: callerContext
// load params from caller context
argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val)
if (extTypeName != null) {
context.thisObj = callerContext.thisObj
}
fnStatements.execute(context)
}
val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure
// for the function, unless we're in the class scope:
if (isStatic || parentContext !is CodeContext.ClassBody)
closure = context
val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody)
?: fnBody
extTypeName?.let { typeName ->
// class extension method
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
type.addFn(name, isOpen = true) {
// ObjInstance has a fixed instance scope, so we need to build a closure
(thisObj as? ObjInstance)?.let { i ->
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
}
// other classes can create one-time scope for this rare case:
?: annotatedFnBody.execute(thisObj.autoInstanceScope(this))
}
}
// regular function/method
?: context.addItem(name, false, annotatedFnBody, visibility)
// as the function can be called from anywhere, we have
// saved the proper context in the closure
annotatedFnBody
}
if (isStatic) {
currentInitScope += fnCreateStatement
NopStatement
} else
fnCreateStatement
}
}
private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement {
val startPos = cc.currentPos()
if (!skipLeadingBrace) {
val t = cc.next()
if (t.type != Token.Type.LBRACE)
throw ScriptError(t.pos, "Expected block body start: {")
}
val block = parseScript()
return statement(startPos) {
// block run on inner context:
block.execute(if (it.skipScopeCreation) it else it.copy(startPos))
}.also {
val t1 = cc.next()
if (t1.type != Token.Type.RBRACE)
throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }")
}
}
private suspend fun parseVarDeclaration(
isMutable: Boolean,
visibility: Visibility,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isStatic: Boolean = false
): Statement {
val nameToken = cc.next()
val start = nameToken.pos
if (nameToken.type != Token.Type.ID)
throw ScriptError(nameToken.pos, "Expected identifier here")
val name = nameToken.value
val eqToken = cc.next()
var setNull = false
val isDelegate = if (eqToken.isId("by")) {
true
} else {
if (eqToken.type != Token.Type.ASSIGN) {
if (!isMutable)
throw ScriptError(start, "val must be initialized")
else {
cc.previous()
setNull = true
}
}
false
}
val initialExpression = if (setNull) null
else parseStatement(true)
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
if (isStatic) {
// find objclass instance: this is tricky: this code executes in object initializer,
// when creating instance, but we need to execute it in the class initializer which
// is missing as for now. Add it to the compiler context?
// if (isDelegate) throw ScriptError(start, "static delegates are not yet implemented")
currentInitScope += statement {
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, pos)
addItem(name, isMutable, initValue, visibility, ObjRecord.Type.Field)
ObjVoid
}
return NopStatement
}
return statement(nameToken.pos) { context ->
if (context.containsLocal(name))
throw ScriptError(nameToken.pos, "Variable $name is already defined")
if (isDelegate) {
TODO()
// println("initial expr = $initialExpression")
// val initValue =
// (initialExpression?.execute(context.copy(Arguments(ObjString(name)))) as? Statement)
// ?.execute(context.copy(Arguments(ObjString(name))))
// ?: context.raiseError("delegate initialization required")
// println("delegate init: $initValue")
// if (!initValue.isInstanceOf(ObjArray))
// context.raiseIllegalArgument("delegate initialized must be an array")
// val s = initValue.getAt(context, 1)
// val setter = if (s == ObjNull) statement { raiseNotImplemented("setter is not provided") }
// else (s as? Statement) ?: context.raiseClassCastError("setter must be a callable")
// ObjDelegate(
// (initValue.getAt(context, 0) as? Statement)
// ?: context.raiseClassCastError("getter must be a callable"), setter
// ).also {
// context.addItem(name, isMutable, it, visibility, recordType = ObjRecord.Type.Field)
// }
} else {
// init value could be a val; when we initialize by-value type var with it, we need to
// create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
initValue
}
}
}
data class Operator(
val tokenType: Token.Type,
val priority: Int, val arity: Int = 2,
val generate: (Pos, Accessor, Accessor) -> Accessor
) {
// fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND
companion object {
fun simple(tokenType: Token.Type, priority: Int, f: suspend (Scope, Obj, Obj) -> Obj): Operator =
Operator(tokenType, priority, 2) { _: Pos, a: Accessor, b: Accessor ->
Accessor { f(it, a.getter(it).value, b.getter(it).value).asReadonly }
}
}
}
companion object {
suspend fun compile(source: Source, importManager: ImportProvider): Script {
return Compiler(CompilerContext(parseLyng(source)), importManager).parseScript()
}
private var lastPriority = 0
val allOps = listOf(
// assignments, lowest priority
Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b ->
Accessor {
val value = b.getter(it).value
val access = a.getter(it)
if (!access.isMutable) throw ScriptError(pos, "cannot assign to immutable variable")
if (access.value.assign(it, value) == null)
a.setter(pos)(it, value)
value.asReadonly
}
},
Operator(Token.Type.PLUSASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
(x.plusAssign(it, y) ?: run {
val result = x.plus(it, y)
a.setter(pos)(it, result)
result
}).asReadonly
}
},
Operator(Token.Type.MINUSASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
(x.minusAssign(it, y) ?: run {
val result = x.minus(it, y)
a.setter(pos)(it, result)
result
}).asReadonly
}
},
Operator(Token.Type.STARASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
(x.mulAssign(it, y) ?: run {
val result = x.mul(it, y)
a.setter(pos)(it, result)
result
}).asReadonly
}
},
Operator(Token.Type.SLASHASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
(x.divAssign(it, y) ?: run {
val result = x.div(it, y)
a.setter(pos)(it, result)
result
}).asReadonly
}
},
Operator(Token.Type.PERCENTASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
(x.modAssign(it, y) ?: run {
val result = x.mod(it, y)
a.setter(pos)(it, result)
result
}).asReadonly
}
},
// logical 1
Operator.simple(Token.Type.OR, ++lastPriority) { ctx, a, b -> a.logicalOr(ctx, b) },
// logical 2
Operator.simple(Token.Type.AND, ++lastPriority) { ctx, a, b -> a.logicalAnd(ctx, b) },
// bitwise or 2
// bitwise and 3
// equality/not equality 4
Operator.simple(Token.Type.EQARROW, ++lastPriority) { _, a, b -> ObjMapEntry(a, b) },
//
Operator.simple(Token.Type.EQ, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) == 0) },
Operator.simple(Token.Type.NEQ, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
Operator.simple(Token.Type.REF_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
Operator.simple(Token.Type.REF_NEQ, lastPriority) { _, a, b -> ObjBool(a !== b) },
// relational <=,... 5
Operator.simple(Token.Type.LTE, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
Operator.simple(Token.Type.LT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) },
Operator.simple(Token.Type.GTE, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) >= 0) },
Operator.simple(Token.Type.GT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) > 0) },
// in, is:
Operator.simple(Token.Type.IN, lastPriority) { c, a, b -> ObjBool(b.contains(c, a)) },
Operator.simple(Token.Type.NOTIN, lastPriority) { c, a, b -> ObjBool(!b.contains(c, a)) },
Operator.simple(Token.Type.IS, lastPriority) { _, a, b -> ObjBool(a.isInstanceOf(b)) },
Operator.simple(Token.Type.NOTIS, lastPriority) { _, a, b -> ObjBool(!a.isInstanceOf(b)) },
Operator(Token.Type.ELVIS, ++lastPriority, 2) { _: Pos, a: Accessor, b: Accessor ->
Accessor {
val aa = a.getter(it).value
(
if (aa != ObjNull) aa
else b.getter(it).value
).asReadonly
}
},
// shuttle <=> 6
Operator.simple(Token.Type.SHUTTLE, ++lastPriority) { c, a, b ->
ObjInt(a.compareTo(c, b).toLong())
},
// bit shifts 7
Operator.simple(Token.Type.PLUS, ++lastPriority) { ctx, a, b -> a.plus(ctx, b) },
Operator.simple(Token.Type.MINUS, lastPriority) { ctx, a, b -> a.minus(ctx, b) },
Operator.simple(Token.Type.STAR, ++lastPriority) { ctx, a, b -> a.mul(ctx, b) },
Operator.simple(Token.Type.SLASH, lastPriority) { ctx, a, b -> a.div(ctx, b) },
Operator.simple(Token.Type.PERCENT, lastPriority) { ctx, a, b -> a.mod(ctx, b) },
)
// private val assigner = allOps.first { it.tokenType == Token.Type.ASSIGN }
//
// fun performAssignment(context: Context, left: Accessor, right: Accessor) {
// assigner.generate(context.pos, left, right)
// }
val lastLevel = lastPriority + 1
val byLevel: List<Map<Token.Type, Operator>> = (0..<lastLevel).map { l ->
allOps.filter { it.priority == l }.associateBy { it.tokenType }
}
suspend fun compile(code: String): Script = compile(Source("<eval>", code), Script.defaultImportManager)
/**
* The keywords that stop processing of expression term
*/
val stopKeywords =
setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class")
}
}
suspend fun eval(code: String) = Compiler.compile(code).execute()