+fn definitions and calls, closures

This commit is contained in:
Sergey Chernov 2025-05-18 14:41:01 +04:00
parent 408543d191
commit 166b1fa0d5
5 changed files with 174 additions and 48 deletions

View File

@ -1,17 +1,19 @@
package net.sergeych.ling package net.sergeych.ling
data class Arguments(val list: List<Obj> ) { data class Arguments(val callerPos: Pos,val list: List<Info>) {
data class Info(val value: Obj,val pos: Pos)
val size by list::size 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 { fun firstAndOnly(): Obj {
if( list.size != 1 ) throw IllegalArgumentException("Expected one argument, got ${list.size}") if( list.size != 1 ) throw IllegalArgumentException("Expected one argument, got ${list.size}")
return list.first() return list.first().value
} }
companion object { companion object {
val EMPTY = Arguments(emptyList()) val EMPTY = Arguments("".toSource().startPos,emptyList())
} }
} }

View File

@ -48,10 +48,10 @@ type alias has target type name. So we have to have something that denotes a _ty
class Compiler { class Compiler {
fun compile(source: Source): Script { fun compile(source: Source): Script {
val tokens = parseLing(source).listIterator() return parseScript(source.startPos, parseLing(source).listIterator())
val start = source.startPos }
// at this level: "global" context: just script to execute or new function
// declaration forming closures private fun parseScript(start: Pos, tokens: ListIterator<Token>): Script {
val statements = mutableListOf<Statement>() val statements = mutableListOf<Statement>()
while (parseStatement(tokens)?.also { while (parseStatement(tokens)?.also {
statements += it statements += it
@ -61,7 +61,7 @@ class Compiler {
} }
private fun parseStatement(tokens: ListIterator<Token>): Statement? { private fun parseStatement(tokens: ListIterator<Token>): Statement? {
while(true) { while (true) {
val t = tokens.next() val t = tokens.next()
return when (t.type) { return when (t.type) {
Token.Type.ID -> { Token.Type.ID -> {
@ -90,6 +90,11 @@ class Compiler {
Token.Type.SEMICOLON -> continue Token.Type.SEMICOLON -> continue
Token.Type.RBRACE -> {
tokens.previous()
return null
}
Token.Type.EOF -> null Token.Type.EOF -> null
else -> { else -> {
@ -136,11 +141,11 @@ class Compiler {
Token.Type.ID -> { Token.Type.ID -> {
parseVarAccess(t, tokens) parseVarAccess(t, tokens)
} }
// todoL: check if it's a function call // todoL: check if it's a function call
// todoL: check if it's a field access // todoL: check if it's a field access
// todoL: check if it's a var // todoL: check if it's a var
// todoL: check if it's a const // todoL: check if it's a const
// todoL: check if it's a type // todoL: check if it's a type
// "+" -> statement { parseNumber(true,tokens) }?????? // "+" -> statement { parseNumber(true,tokens) }??????
// "-" -> statement { parseNumber(false,tokens) } // "-" -> statement { parseNumber(false,tokens) }
@ -167,51 +172,62 @@ class Compiler {
} }
fun parseVarAccess(id: Token, tokens: ListIterator<Token>,path: List<String> = emptyList()): Statement { fun parseVarAccess(id: Token, tokens: ListIterator<Token>, path: List<String> = emptyList()): Statement {
val nt = tokens.next() val nt = tokens.next()
fun resolve(context: Context): Context { fun resolve(context: Context): Context {
var targetContext = context var targetContext = context
for( n in path) { for (n in path) {
val x = targetContext[n] ?: throw ScriptError(id.pos, "undefined symbol: $n") 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}") ?: throw ScriptError(id.pos, "Invalid symbolic path (wrong type of ${x.name}: ${x.value}")
} }
return targetContext return targetContext
} }
return when(nt.type) { return when (nt.type) {
Token.Type.DOT -> { Token.Type.DOT -> {
// selector // selector
val t = tokens.next() val t = tokens.next()
if( t.type== Token.Type.ID) { if (t.type == Token.Type.ID) {
parseVarAccess(t,tokens,path+id.value) parseVarAccess(t, tokens, path + id.value)
} } else
else throw ScriptError(t.pos, "Expected identifier after '.'")
throw ScriptError(t.pos,"Expected identifier after '.'")
} }
Token.Type.LPAREN -> { Token.Type.LPAREN -> {
// function call
// Load arg list // Load arg list
val args = mutableListOf<Statement>() val args = mutableListOf<Arguments.Info>()
do { do {
val t = tokens.next() val t = tokens.next()
when(t.type) { when (t.type) {
Token.Type.RPAREN, Token.Type.COMMA -> {} Token.Type.RPAREN, Token.Type.COMMA -> {}
else -> { else -> {
tokens.previous() tokens.previous()
parseExpression(tokens)?.let { args += it } parseExpression(tokens)?.let { args += Arguments.Info(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list") ?: throw ScriptError(t.pos, "Expecting arguments list")
} }
} }
} while (t.type != Token.Type.RPAREN) } while (t.type != Token.Type.RPAREN)
statement(id.pos) { context -> statement(id.pos) { context ->
val v = resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: $id") 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)") ?: throw ScriptError(id.pos, "Variable $id is not callable ($id)")
} }
} }
Token.Type.LBRACKET -> { Token.Type.LBRACKET -> {
TODO("indexing") TODO("indexing")
} }
else -> { else -> {
// just access the var // just access the var
tokens.previous() tokens.previous()
@ -247,32 +263,108 @@ class Compiler {
* Parse keyword-starting statenment. * Parse keyword-starting statenment.
* @return parsed statement or null if, for example. [id] is not among keywords * @return parsed statement or null if, for example. [id] is not among keywords
*/ */
private fun parseKeywordStatement(id: Token, tokens: ListIterator<Token>): Statement? private fun parseKeywordStatement(id: Token, tokens: ListIterator<Token>): Statement? = when (id.value) {
= when (id.value) {
"val" -> parseVarDeclaration(id.value, false, tokens) "val" -> parseVarDeclaration(id.value, false, tokens)
"var" -> parseVarDeclaration(id.value, true, tokens) "var" -> parseVarDeclaration(id.value, true, tokens)
"fn", "fun" -> parseFunctionDeclaration(tokens)
else -> null else -> null
} }
data class FnParamDef(
val name: String,
val pos: Pos,
val defaultValue: Statement? = null
)
private fun parseFunctionDeclaration(tokens: ListIterator<Token>): 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<FnParamDef>()
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<Token>): 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<Token>): Statement { private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: ListIterator<Token>): Statement {
val nameToken = tokens.next() val nameToken = tokens.next()
if( nameToken.type != Token.Type.ID) if (nameToken.type != Token.Type.ID)
throw ScriptError(nameToken.pos, "Expected identifier after '$kind'") throw ScriptError(nameToken.pos, "Expected identifier after '$kind'")
val name = nameToken.value val name = nameToken.value
val eqToken = tokens.next() val eqToken = tokens.next()
var setNull = false var setNull = false
if( eqToken.type != Token.Type.ASSIGN) { if (eqToken.type != Token.Type.ASSIGN) {
if( !mutable ) if (!mutable)
throw ScriptError(eqToken.pos, "Expected initializator: '=' after '$kind ${name}'") throw ScriptError(eqToken.pos, "Expected initializator: '=' after '$kind ${name}'")
else { else {
tokens.previous() tokens.previous()
setNull = true 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") ?: throw ScriptError(eqToken.pos, "Expected initializer expression")
return statement(nameToken.pos) { context -> return statement(nameToken.pos) { context ->
if( context.containsLocal(name) ) if (context.containsLocal(name))
throw ScriptError(nameToken.pos, "Variable $name is already defined") throw ScriptError(nameToken.pos, "Variable $name is already defined")
val initValue = initialExpression?.execute(context) ?: ObjNull val initValue = initialExpression?.execute(context) ?: ObjNull
context.addItem(name, mutable, initValue) context.addItem(name, mutable, initValue)

View File

@ -185,8 +185,4 @@ private class Parser(fromPos: Pos) {
} }
private fun advance() = pos.advance() private fun advance() = pos.advance()
init {
// advance()
}
} }

View File

@ -39,8 +39,10 @@ class MutablePos(private val from: Pos) {
return if (column+1 < current.length) { return if (column+1 < current.length) {
current[column++] current[column++]
} else { } else {
line++
column = 0 column = 0
while( ++line < lines.size && lines[line].isEmpty() ) {
// skip empty lines
}
if(line >= lines.size) null if(line >= lines.size) null
else lines[line][column] else lines[line][column]
} }
@ -59,4 +61,7 @@ class MutablePos(private val from: Pos) {
override fun toString() = "($line:$column)" override fun toString() = "($line:$column)"
init {
if( lines[0].isEmpty()) advance()
}
} }

View File

@ -122,14 +122,45 @@ class ScriptTest {
assertEquals(10, context.eval("b").toInt()) assertEquals(10, context.eval("b").toInt())
} }
// @Test @Test
// fun functionTest() = runTest { fun functionTest() = runTest {
// val context = Context() val context = Context()
// context.eval(""" context.eval(
// fun foo(a, b) { """
// a + b fun foo(a, b) {
// } a + b
// """.trimIndent()) }
// } """.trimIndent()
)
assertEquals(17, context.eval("foo(3,14)").toInt())
assertFailsWith<ScriptError> {
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())
}
} }