From 71a2933066bdc3457a4408079d14b0a5a55f5a7f Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 8 Jun 2025 13:35:51 +0400 Subject: [PATCH] more on classes universal args declaration parser, lambda code rewritten to it --- .../net/sergeych/lyng/ArgsDeclaration.kt | 98 +++++++++++++ .../kotlin/net/sergeych/lyng/Arguments.kt | 1 + .../kotlin/net/sergeych/lyng/Compiler.kt | 98 +++++++------ .../kotlin/net/sergeych/lyng/Context.kt | 3 + .../kotlin/net/sergeych/lyng/ObjClass.kt | 2 +- .../kotlin/net/sergeych/lyng/ObjList.kt | 2 +- .../kotlin/net/sergeych/lyng/Pos.kt | 1 + .../kotlin/net/sergeych/lyng/Script.kt | 7 + .../kotlin/net/sergeych/lyng/Source.kt | 1 + library/src/commonTest/kotlin/ScriptTest.kt | 130 ++++++++++++++++++ 10 files changed, 291 insertions(+), 52 deletions(-) create mode 100644 library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt new file mode 100644 index 0000000..215b751 --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -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, 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, + ) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 0d0fef8..ee79626 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -33,6 +33,7 @@ data class Arguments(val list: List) : Iterable { companion object { val EMPTY = Arguments(emptyList()) + fun from(values: Collection) = Arguments(values.map { Info(it, Pos.UNKNOWN) }) } override fun iterator(): Iterator { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index b7fb582..b155cad 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -347,22 +347,8 @@ class Compiler( } context.addItem("it", false, itValue) } else { - // assign vars as declared - if (args.size != argsDeclaration.args.size && !argsDeclaration.args.last().isEllipsis) - 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) - } - } + // assign vars as declared the standard way + argsDeclaration.assignToContext(context) } body.execute(context) } @@ -411,29 +397,12 @@ class Compiler( } } - enum class AccessType { - Val, Var, Default + enum class AccessType(val isMutable: Boolean) { + Val(false), Var(true), Initialization(false) } enum class Visibility { - Default, 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, 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") - } + Public, Private, Protected, Internal } /** @@ -441,7 +410,7 @@ class Compiler( * @return declaration or null if there is no valid list of arguments */ private fun parseArgsDeclaration(cc: CompilerContext, isClassDeclaration: Boolean = false): ArgsDeclaration? { - val result = mutableListOf() + val result = mutableListOf() var endTokenType: Token.Type? = null val startPos = cc.savePos() @@ -484,7 +453,7 @@ class Compiler( Visibility.Public } - else -> Visibility.Default + else -> null } // val/var? val access = when (t.value) { @@ -504,7 +473,7 @@ class Compiler( AccessType.Var } - else -> AccessType.Default + else -> null } var defaultValue: Statement? = null @@ -514,7 +483,15 @@ class Compiler( // type information val typeInfo = parseTypeDeclaration(cc) 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 ')' // otherwise it is not an argument list: @@ -714,24 +691,45 @@ class Compiler( private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement { val nameToken = cc.requireToken(Token.Type.ID) 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) 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 - } + TODO("parse body") + } else null + // create class val className = nameToken.value + lateinit var classContext: Context -// val constructorCode = statement { -// val classContext = copy() -// } + val defaultAccess = if (isStruct) AccessType.Var else AccessType.Initialization + val defaultVisibility = Visibility.Public + // create instance constructor + // create custom objClass with all fields and instance constructor - val newClass = ObjClass(className, parsedArgs?.args ?: emptyList()) -// statement { -// addConst(nameToken.value, ) -// } -// } + val constructorCode = statement { + // constructor code is registered with class instance and is called over + // 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() } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt index 67b1231..9d99bea 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt @@ -65,6 +65,9 @@ class Context( fun copy(pos: Pos, args: Arguments = Arguments.EMPTY,newThisObj: Obj? = null): Context = 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 addItem(name: String, isMutable: Boolean, value: Obj?): StoredObj { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt index 05b435f..c5e92b5 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt @@ -4,7 +4,7 @@ val ObjClassType by lazy { ObjClass("Class") } class ObjClass( val className: String, - val constructorArgs: List = emptyList(), + val constructorArgs: List = emptyList(), vararg val parents: ObjClass, ) : Obj() { constructor( diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt index 560a524..f3f57b6 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjList.kt @@ -1,6 +1,6 @@ package net.sergeych.lyng -class ObjList(val list: MutableList) : Obj() { +class ObjList(val list: MutableList = mutableListOf()) : Obj() { init { for (p in objClass.parents) diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt index babe4ea..84da4f3 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt @@ -16,6 +16,7 @@ data class Pos(val source: Source, val line: Int, val column: Int) { companion object { val builtIn = Pos(Source.builtIn, 0, 0) + val UNKNOWN = Pos(Source.UNKNOWN, -1, -1) } } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 5b019c7..47b85ca 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -119,6 +119,13 @@ class Script( raiseError(ObjAssertionError(this,"Assertion failed")) } + addVoidFn("assertEquals") { + val a = requiredArg(0) + val b = requiredArg(1) + if( a.compareTo(this, b) != 0 ) + raiseError(ObjAssertionError(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}")) + } + addVoidFn("delay") { delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong()) } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Source.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Source.kt index b339ce4..a0da033 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Source.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Source.kt @@ -6,6 +6,7 @@ class Source(val fileName: String, text: String) { companion object { val builtIn: Source by lazy { Source("built-in", "") } + val UNKNOWN: Source by lazy { Source("UNKNOWN", "") } } val startPos: Pos = Pos(this, 0, 0) diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index e214509..e6b97f9 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -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 { + 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 { + 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 { + pa.assignToContext(c) + } + } + @Test fun testWhileBlockIsolation1() = runTest { eval(