diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt index e4e03ae..205813b 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt @@ -1,17 +1,19 @@ package net.sergeych.ling -data class Arguments(val list: List ) { +data class Arguments(val callerPos: Pos,val list: List) { + + data class Info(val value: Obj,val pos: Pos) val size by list::size - operator fun get(index: Int): Obj = list[index] + operator fun get(index: Int): Obj = list[index].value fun firstAndOnly(): Obj { if( list.size != 1 ) throw IllegalArgumentException("Expected one argument, got ${list.size}") - return list.first() + return list.first().value } companion object { - val EMPTY = Arguments(emptyList()) + val EMPTY = Arguments("".toSource().startPos,emptyList()) } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index bd097d8..dc94254 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -48,10 +48,10 @@ type alias has target type name. So we have to have something that denotes a _ty class Compiler { fun compile(source: Source): Script { - val tokens = parseLing(source).listIterator() - val start = source.startPos - // at this level: "global" context: just script to execute or new function - // declaration forming closures + return parseScript(source.startPos, parseLing(source).listIterator()) + } + + private fun parseScript(start: Pos, tokens: ListIterator): Script { val statements = mutableListOf() while (parseStatement(tokens)?.also { statements += it @@ -61,7 +61,7 @@ class Compiler { } private fun parseStatement(tokens: ListIterator): Statement? { - while(true) { + while (true) { val t = tokens.next() return when (t.type) { Token.Type.ID -> { @@ -90,6 +90,11 @@ class Compiler { Token.Type.SEMICOLON -> continue + Token.Type.RBRACE -> { + tokens.previous() + return null + } + Token.Type.EOF -> null else -> { @@ -136,11 +141,11 @@ class Compiler { Token.Type.ID -> { parseVarAccess(t, tokens) } - // todoL: check if it's a function call - // todoL: check if it's a field access - // todoL: check if it's a var - // todoL: check if it's a const - // todoL: check if it's a type + // todoL: check if it's a function call + // todoL: check if it's a field access + // todoL: check if it's a var + // todoL: check if it's a const + // todoL: check if it's a type // "+" -> statement { parseNumber(true,tokens) }?????? // "-" -> statement { parseNumber(false,tokens) } @@ -167,51 +172,62 @@ class Compiler { } - fun parseVarAccess(id: Token, tokens: ListIterator,path: List = emptyList()): Statement { + fun parseVarAccess(id: Token, tokens: ListIterator, path: List = emptyList()): Statement { val nt = tokens.next() fun resolve(context: Context): Context { var targetContext = context - for( n in path) { + for (n in path) { val x = targetContext[n] ?: throw ScriptError(id.pos, "undefined symbol: $n") - (x.value as? ObjNamespace )?.let { targetContext = it.context } + (x.value as? ObjNamespace)?.let { targetContext = it.context } ?: throw ScriptError(id.pos, "Invalid symbolic path (wrong type of ${x.name}: ${x.value}") } return targetContext } - return when(nt.type) { + return when (nt.type) { Token.Type.DOT -> { // selector val t = tokens.next() - if( t.type== Token.Type.ID) { - parseVarAccess(t,tokens,path+id.value) - } - else - throw ScriptError(t.pos,"Expected identifier after '.'") + if (t.type == Token.Type.ID) { + parseVarAccess(t, tokens, path + id.value) + } else + throw ScriptError(t.pos, "Expected identifier after '.'") } + Token.Type.LPAREN -> { + // function call // Load arg list - val args = mutableListOf() + val args = mutableListOf() do { val t = tokens.next() - when(t.type) { + when (t.type) { Token.Type.RPAREN, Token.Type.COMMA -> {} else -> { tokens.previous() - parseExpression(tokens)?.let { args += it } + parseExpression(tokens)?.let { args += Arguments.Info(it, t.pos) } ?: throw ScriptError(t.pos, "Expecting arguments list") } } } while (t.type != Token.Type.RPAREN) + statement(id.pos) { context -> val v = resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: $id") - (v.value as? Statement)?.execute(context.copy(Arguments(args.map { it.execute(context) }))) + (v.value as? Statement)?.execute( + context.copy( + Arguments( + nt.pos, + args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) } + ) + ) + ) ?: throw ScriptError(id.pos, "Variable $id is not callable ($id)") } } + Token.Type.LBRACKET -> { TODO("indexing") } + else -> { // just access the var tokens.previous() @@ -247,32 +263,108 @@ class Compiler { * Parse keyword-starting statenment. * @return parsed statement or null if, for example. [id] is not among keywords */ - private fun parseKeywordStatement(id: Token, tokens: ListIterator): Statement? - = when (id.value) { + private fun parseKeywordStatement(id: Token, tokens: ListIterator): Statement? = when (id.value) { "val" -> parseVarDeclaration(id.value, false, tokens) "var" -> parseVarDeclaration(id.value, true, tokens) + "fn", "fun" -> parseFunctionDeclaration(tokens) else -> null } + data class FnParamDef( + val name: String, + val pos: Pos, + val defaultValue: Statement? = null + ) + + private fun parseFunctionDeclaration(tokens: ListIterator): Statement { + var t = tokens.next() + val start = t.pos + val name = if (t.type != Token.Type.ID) + throw ScriptError(t.pos, "Expected identifier after 'fn'") + else t.value + + t = tokens.next() + if (t.type != Token.Type.LPAREN) + throw ScriptError(t.pos, "Bad function definition: expected '(' after 'fn ${name}'") + val params = mutableListOf() + var defaultListStarted = false + do { + t = tokens.next() + if (t.type == Token.Type.RPAREN) + break + if (t.type != Token.Type.ID) + throw ScriptError(t.pos, "Expected identifier after '('") + val n = tokens.next() + val defaultValue = if (n.type == Token.Type.ASSIGN) { + parseExpression(tokens)?.also { defaultListStarted = true } + ?: throw ScriptError(n.pos, "Expected initialization expression") + } else { + if (defaultListStarted) + throw ScriptError(n.pos, "requires default value too") + if (n.type != Token.Type.COMMA) + tokens.previous() + null + } + params.add(FnParamDef(t.value, t.pos, defaultValue)) + } while (true) + + println("arglist: $params") + + // Here we should be at open body + val fnStatements = parseBlock(tokens) + + val fnBody = statement(t.pos) { context -> + // load params + for ((i, d) in params.withIndex()) { + if (i < context.args.size) + context.addItem(d.name, false, context.args.list[i].value) + else + context.addItem( + d.name, + false, + d.defaultValue?.execute(context) + ?: throw ScriptError(context.args.callerPos, "missing required argument #${1+i}: ${d.name}") + ) + } + + fnStatements.execute(context) + } + return statement(start) { context -> + context.addItem(name, false, fnBody) + fnBody + } + } + + private fun parseBlock(tokens: ListIterator): Statement { + val t = tokens.next() + if (t.type != Token.Type.LBRACE) + throw ScriptError(t.pos, "Expected block body start: {") + return parseScript(t.pos, tokens).also { + val t1 = tokens.next() + if (t1.type != Token.Type.RBRACE) + throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }") + } + } + private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: ListIterator): Statement { val nameToken = tokens.next() - if( nameToken.type != Token.Type.ID) + if (nameToken.type != Token.Type.ID) throw ScriptError(nameToken.pos, "Expected identifier after '$kind'") val name = nameToken.value val eqToken = tokens.next() var setNull = false - if( eqToken.type != Token.Type.ASSIGN) { - if( !mutable ) + if (eqToken.type != Token.Type.ASSIGN) { + if (!mutable) throw ScriptError(eqToken.pos, "Expected initializator: '=' after '$kind ${name}'") else { tokens.previous() setNull = true } } - val initialExpression = if( setNull ) null else parseExpression(tokens) + val initialExpression = if (setNull) null else parseExpression(tokens) ?: throw ScriptError(eqToken.pos, "Expected initializer expression") return statement(nameToken.pos) { context -> - if( context.containsLocal(name) ) + if (context.containsLocal(name)) throw ScriptError(nameToken.pos, "Variable $name is already defined") val initValue = initialExpression?.execute(context) ?: ObjNull context.addItem(name, mutable, initValue) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index c40b255..3604583 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -185,8 +185,4 @@ private class Parser(fromPos: Pos) { } private fun advance() = pos.advance() - - init { -// advance() - } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt index cefdbc8..be67758 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt @@ -39,8 +39,10 @@ class MutablePos(private val from: Pos) { return if (column+1 < current.length) { current[column++] } else { - line++ column = 0 + while( ++line < lines.size && lines[line].isEmpty() ) { + // skip empty lines + } if(line >= lines.size) null else lines[line][column] } @@ -59,4 +61,7 @@ class MutablePos(private val from: Pos) { override fun toString() = "($line:$column)" + init { + if( lines[0].isEmpty()) advance() + } } diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index cfc094c..c952095 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -122,14 +122,45 @@ class ScriptTest { assertEquals(10, context.eval("b").toInt()) } -// @Test -// fun functionTest() = runTest { -// val context = Context() -// context.eval(""" -// fun foo(a, b) { -// a + b -// } -// """.trimIndent()) -// } + @Test + fun functionTest() = runTest { + val context = Context() + context.eval( + """ + fun foo(a, b) { + a + b + } + """.trimIndent() + ) + assertEquals(17, context.eval("foo(3,14)").toInt()) + assertFailsWith { + assertEquals(17, context.eval("foo(3)").toInt()) + } + + context.eval(""" + fn bar(a, b=10) { + a + b + 1 + } + """.trimIndent()) + assertEquals(10, context.eval("bar(3, 6)").toInt()) + assertEquals(14, context.eval("bar(3)").toInt()) + } + + @Test + fun simpleClosureTest() = runTest { + val context = Context() + context.eval( + """ + var global = 10 + + fun foo(a, b) { + global + a + b + } + """.trimIndent() + ) + assertEquals(27, context.eval("foo(3,14)").toInt()) + context.eval("global = 20") + assertEquals(37, context.eval("foo(3,14)").toInt()) + } } \ No newline at end of file