+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
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
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())
}
}

View File

@ -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<Token>): Script {
val statements = mutableListOf<Statement>()
while (parseStatement(tokens)?.also {
statements += it
@ -90,6 +90,11 @@ class Compiler {
Token.Type.SEMICOLON -> continue
Token.Type.RBRACE -> {
tokens.previous()
return null
}
Token.Type.EOF -> null
else -> {
@ -185,33 +190,44 @@ class Compiler {
val t = tokens.next()
if (t.type == Token.Type.ID) {
parseVarAccess(t, tokens, path + id.value)
}
else
} else
throw ScriptError(t.pos, "Expected identifier after '.'")
}
Token.Type.LPAREN -> {
// function call
// Load arg list
val args = mutableListOf<Statement>()
val args = mutableListOf<Arguments.Info>()
do {
val t = tokens.next()
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,13 +263,89 @@ 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<Token>): Statement?
= when (id.value) {
private fun parseKeywordStatement(id: Token, tokens: ListIterator<Token>): 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<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 {
val nameToken = tokens.next()
if (nameToken.type != Token.Type.ID)

View File

@ -185,8 +185,4 @@ private class Parser(fromPos: Pos) {
}
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) {
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()
}
}

View File

@ -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<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())
}
}