more on classes

universal args declaration parser, lambda code rewritten to it
This commit is contained in:
Sergey Chernov 2025-06-08 13:35:51 +04:00
parent 4dc73b91c2
commit 71a2933066
10 changed files with 291 additions and 52 deletions

View File

@ -0,0 +1,98 @@
package net.sergeych.lyng
/**
* List of argument declarations in the __definition__ of the lambda, class constructor,
* function, etc. It is created by [Compiler.parseArgsDeclaration]
*/
data class ArgsDeclaration(val args: List<Item>, val endTokenType: Token.Type) {
init {
val i = args.count { it.isEllipsis }
if (i > 1) throw ScriptError(args[i].pos, "there can be only one argument")
}
/**
* parse args and create local vars in a given context
*/
suspend fun assignToContext(
context: Context,
fromArgs: Arguments = context.args,
defaultAccessType: Compiler.AccessType = Compiler.AccessType.Var
) {
fun assign(a: Item, value: Obj) {
context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value)
}
suspend fun processHead(index: Int): Int {
var i = index
while (i != args.size) {
val a = args[i]
if (a.isEllipsis) break
val value = when {
i < fromArgs.size -> fromArgs[i]
a.defaultValue != null -> a.defaultValue.execute(context)
else -> context.raiseArgumentError("too few arguments for the call")
}
assign(a, value)
i++
}
return i
}
suspend fun processTail(index: Int): Int {
var i = args.size - 1
var j = fromArgs.size - 1
while (i > index) {
val a = args[i]
if (a.isEllipsis) break
val value = when {
j >= index -> {
fromArgs[j--]
}
a.defaultValue != null -> a.defaultValue.execute(context)
else -> context.raiseArgumentError("too few arguments for the call")
}
assign(a, value)
i--
}
return j
}
fun processEllipsis(index: Int, toFromIndex: Int) {
val a = args[index]
val l = if (index > toFromIndex) ObjList()
else ObjList( fromArgs.values.subList(index, toFromIndex+1).toMutableList())
assign(a, l)
}
val leftIndex = processHead(0)
if (leftIndex < args.size) {
val end = processTail(leftIndex)
processEllipsis(leftIndex, end)
}
else {
if( leftIndex < fromArgs.size)
context.raiseArgumentError("too many arguments for the call")
}
}
/**
* Single argument declaration descriptor.
*
* @param defaultValue default value, if set, can't be an [Obj] as it can depend on the call site, call args, etc.
* If not null, could be executed on __caller context__ only.
*/
data class Item(
val name: String,
val type: TypeDecl = TypeDecl.Obj,
val pos: Pos = Pos.builtIn,
val isEllipsis: Boolean = false,
/**
* Default value, if set, can't be an [Obj] as it can depend on the call site, call args, etc.
* So it is a [Statement] that must be executed on __caller context__.
*/
val defaultValue: Statement? = null,
val accessType: Compiler.AccessType? = null,
val visibility: Compiler.Visibility? = null,
)
}

View File

@ -33,6 +33,7 @@ data class Arguments(val list: List<Info>) : Iterable<Obj> {
companion object { companion object {
val EMPTY = Arguments(emptyList()) val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.map { Info(it, Pos.UNKNOWN) })
} }
override fun iterator(): Iterator<Obj> { override fun iterator(): Iterator<Obj> {

View File

@ -347,22 +347,8 @@ class Compiler(
} }
context.addItem("it", false, itValue) context.addItem("it", false, itValue)
} else { } else {
// assign vars as declared // assign vars as declared the standard way
if (args.size != argsDeclaration.args.size && !argsDeclaration.args.last().isEllipsis) argsDeclaration.assignToContext(context)
raiseArgumentError("Too many arguments : called with ${args.size}, lambda accepts only ${argsDeclaration.args.size}")
for ((n, a) in argsDeclaration.args.withIndex()) {
if (n >= args.size) {
if (a.initialValue != null)
context.addItem(a.name, false, a.initialValue.execute(context))
else throw ScriptError(a.pos, "argument $n is out of scope")
} else {
val value = if (a.isEllipsis) {
ObjList(args.values.subList(n, args.values.size).toMutableList())
} else
args[n]
context.addItem(a.name, false, value)
}
}
} }
body.execute(context) body.execute(context)
} }
@ -411,29 +397,12 @@ class Compiler(
} }
} }
enum class AccessType { enum class AccessType(val isMutable: Boolean) {
Val, Var, Default Val(false), Var(true), Initialization(false)
} }
enum class Visibility { enum class Visibility {
Default, Public, Private, Protected, Internal Public, Private, Protected, Internal
}
data class ArgVar(
val name: String,
val type: TypeDecl = TypeDecl.Obj,
val pos: Pos,
val isEllipsis: Boolean,
val initialValue: Statement? = null,
val accessType: AccessType = AccessType.Default,
val visibility: Visibility = Visibility.Default
)
data class ArgsDeclaration(val args: List<ArgVar>, val endTokenType: Token.Type) {
init {
val i = args.indexOfFirst { it.isEllipsis }
if (i >= 0 && i != args.lastIndex) throw ScriptError(args[i].pos, "ellipsis argument must be last")
}
} }
/** /**
@ -441,7 +410,7 @@ class Compiler(
* @return declaration or null if there is no valid list of arguments * @return declaration or null if there is no valid list of arguments
*/ */
private fun parseArgsDeclaration(cc: CompilerContext, isClassDeclaration: Boolean = false): ArgsDeclaration? { private fun parseArgsDeclaration(cc: CompilerContext, isClassDeclaration: Boolean = false): ArgsDeclaration? {
val result = mutableListOf<ArgVar>() val result = mutableListOf<ArgsDeclaration.Item>()
var endTokenType: Token.Type? = null var endTokenType: Token.Type? = null
val startPos = cc.savePos() val startPos = cc.savePos()
@ -484,7 +453,7 @@ class Compiler(
Visibility.Public Visibility.Public
} }
else -> Visibility.Default else -> null
} }
// val/var? // val/var?
val access = when (t.value) { val access = when (t.value) {
@ -504,7 +473,7 @@ class Compiler(
AccessType.Var AccessType.Var
} }
else -> AccessType.Default else -> null
} }
var defaultValue: Statement? = null var defaultValue: Statement? = null
@ -514,7 +483,15 @@ class Compiler(
// type information // type information
val typeInfo = parseTypeDeclaration(cc) val typeInfo = parseTypeDeclaration(cc)
val isEllipsis = cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true) val isEllipsis = cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true)
result += ArgVar(t.value, typeInfo, t.pos, isEllipsis, defaultValue, access, visibility) result += ArgsDeclaration.Item(
t.value,
typeInfo,
t.pos,
isEllipsis,
defaultValue,
access,
visibility
)
// important: valid argument list continues with ',' and ends with '->' or ')' // important: valid argument list continues with ',' and ends with '->' or ')'
// otherwise it is not an argument list: // otherwise it is not an argument list:
@ -714,24 +691,45 @@ class Compiler(
private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement { private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement {
val nameToken = cc.requireToken(Token.Type.ID) val nameToken = cc.requireToken(Token.Type.ID)
val parsedArgs = parseArgsDeclaration(cc) val parsedArgs = parseArgsDeclaration(cc)
if( parsedArgs != null && parsedArgs.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) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val t = cc.next() val t = cc.next()
if (t.type == Token.Type.LBRACE) {
var extraInit: Statement? = null
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
// parse body // parse body
} TODO("parse body")
} else null
// create class // create class
val className = nameToken.value val className = nameToken.value
lateinit var classContext: Context
// val constructorCode = statement { val defaultAccess = if (isStruct) AccessType.Var else AccessType.Initialization
// val classContext = copy() val defaultVisibility = Visibility.Public
// }
// create instance constructor
// create custom objClass with all fields and instance constructor
val newClass = ObjClass(className, parsedArgs?.args ?: emptyList()) val constructorCode = statement {
// statement { // constructor code is registered with class instance and is called over
// addConst(nameToken.value, ) // new `thisObj` already set by class to ObjInstance
// } 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
// parsedArgs?.let { pa ->
// pa.extractArgs { (def, value) ->
// val access = def.accessType ?: defaultAccess
// val visibility = def.visibility ?: defaultVisibility
// addItem(def.name, access.isMutable, value)
// }
//
thisObj
}
TODO() TODO()
} }

View File

@ -65,6 +65,9 @@ class Context(
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY,newThisObj: Obj? = null): Context = fun copy(pos: Pos, args: Arguments = Arguments.EMPTY,newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj) Context(this, args, pos, newThisObj ?: thisObj)
fun copy(args: Arguments = Arguments.EMPTY,newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj)
fun copy() = Context(this, args, pos, thisObj) fun copy() = Context(this, args, pos, thisObj)
fun addItem(name: String, isMutable: Boolean, value: Obj?): StoredObj { fun addItem(name: String, isMutable: Boolean, value: Obj?): StoredObj {

View File

@ -4,7 +4,7 @@ val ObjClassType by lazy { ObjClass("Class") }
class ObjClass( class ObjClass(
val className: String, val className: String,
val constructorArgs: List<Compiler.ArgVar> = emptyList(), val constructorArgs: List<ArgsDeclaration.Item> = emptyList(),
vararg val parents: ObjClass, vararg val parents: ObjClass,
) : Obj() { ) : Obj() {
constructor( constructor(

View File

@ -1,6 +1,6 @@
package net.sergeych.lyng package net.sergeych.lyng
class ObjList(val list: MutableList<Obj>) : Obj() { class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
init { init {
for (p in objClass.parents) for (p in objClass.parents)

View File

@ -16,6 +16,7 @@ data class Pos(val source: Source, val line: Int, val column: Int) {
companion object { companion object {
val builtIn = Pos(Source.builtIn, 0, 0) val builtIn = Pos(Source.builtIn, 0, 0)
val UNKNOWN = Pos(Source.UNKNOWN, -1, -1)
} }
} }

View File

@ -119,6 +119,13 @@ class Script(
raiseError(ObjAssertionError(this,"Assertion failed")) raiseError(ObjAssertionError(this,"Assertion failed"))
} }
addVoidFn("assertEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionError(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
}
addVoidFn("delay") { addVoidFn("delay") {
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong()) delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
} }

View File

@ -6,6 +6,7 @@ class Source(val fileName: String, text: String) {
companion object { companion object {
val builtIn: Source by lazy { Source("built-in", "") } val builtIn: Source by lazy { Source("built-in", "") }
val UNKNOWN: Source by lazy { Source("UNKNOWN", "") }
} }
val startPos: Pos = Pos(this, 0, 0) val startPos: Pos = Pos(this, 0, 0)

View File

@ -486,6 +486,136 @@ class ScriptTest {
) )
} }
@Test
fun testAssignArgumentsNoEllipsis() = runTest {
// equal args, no ellipsis, no defaults, ok
val ttEnd = Token.Type.RBRACE
var pa = ArgsDeclaration(listOf(
ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"),
), ttEnd)
var c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1,2,3).map { it.toObj() }))
pa.assignToContext(c)
assertEquals( ObjInt(1), c["a"]?.value)
assertEquals( ObjInt(2), c["b"]?.value)
assertEquals( ObjInt(3), c["c"]?.value)
// less args: error
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1,2).map { it.toObj() }))
assertFailsWith<ScriptError> {
pa.assignToContext(c)
}
// less args, no ellipsis, defaults, ok
pa = ArgsDeclaration(listOf(
ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c", defaultValue = statement { ObjInt(100) }),
), ttEnd)
pa.assignToContext(c)
assertEquals( ObjInt(1), c["a"]?.value)
assertEquals( ObjInt(2), c["b"]?.value)
assertEquals( ObjInt(100), c["c"]?.value)
// enough args. default value is ignored:
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(10, 2, 5).map { it.toObj() }))
pa.assignToContext(c)
assertEquals( ObjInt(10), c["a"]?.value)
assertEquals( ObjInt(2), c["b"]?.value)
assertEquals( ObjInt(5), c["c"]?.value)
}
@Test
fun testAssignArgumentsEndEllipsis() = runTest {
// equal args,
// less args, no ellipsis, defaults, ok
val ttEnd = Token.Type.RBRACE
var pa = ArgsDeclaration(listOf(
ArgsDeclaration.Item("a"),
ArgsDeclaration.Item("b", isEllipsis = true),
), ttEnd)
var c = Context(args = Arguments.from(listOf(1,2,3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assert( a == 1 ); println(b)")
c.eval("assert( b == [2,3] )")
c = Context(args = Arguments.from(listOf(1,2).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a, 1 ); println(b)")
c.eval("assertEquals( b, [2] )")
c = Context(args = Arguments.from(listOf(1).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assert( a == 1 ); println(b)")
c.eval("assert( b == [] )")
}
@Test
fun testAssignArgumentsStartEllipsis() = runTest {
val ttEnd = Token.Type.RBRACE
var pa = ArgsDeclaration(listOf(
ArgsDeclaration.Item("a", isEllipsis = true),
ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"),
), ttEnd)
var c = Context(args = Arguments.from(listOf(0,1,2,3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[0,1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(1,2,3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(2,3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( a,[] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(3).map { it.toObj() }))
assertFailsWith<ExecutionError> {
pa.assignToContext(c)
}
}
@Test
fun testAssignArgumentsmiddleEllipsis() = runTest {
val ttEnd = Token.Type.RBRACE
var pa = ArgsDeclaration(listOf(
ArgsDeclaration.Item("i"),
ArgsDeclaration.Item("a", isEllipsis = true),
ArgsDeclaration.Item("b"),
ArgsDeclaration.Item("c"),
), ttEnd)
var c = Context(args = Arguments.from(listOf(-1,0,1,2,3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, -1 )")
c.eval("assertEquals( a,[0,1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(0, 1,2,3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, 0 )")
c.eval("assertEquals( a,[1] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(1,2,3).map { it.toObj() }))
pa.assignToContext(c)
c.eval("assertEquals( i, 1)")
c.eval("assertEquals( a,[] )")
c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(2,3).map { it.toObj() }))
assertFailsWith<ExecutionError> {
pa.assignToContext(c)
}
}
@Test @Test
fun testWhileBlockIsolation1() = runTest { fun testWhileBlockIsolation1() = runTest {
eval( eval(