From 83825a9272999380ddee1371286d9945312677cc Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 27 Nov 2025 08:11:09 +0100 Subject: [PATCH] fixed call arg precedence bug in last arg callable scenario --- docs/samples/sum.lyng | 2 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 246 ++++++++++++++---- lynglib/src/commonTest/kotlin/ScriptTest.kt | 34 +++ 3 files changed, 224 insertions(+), 58 deletions(-) mode change 100644 => 100755 docs/samples/sum.lyng diff --git a/docs/samples/sum.lyng b/docs/samples/sum.lyng old mode 100644 new mode 100755 index 58dc1ab..97dd585 --- a/docs/samples/sum.lyng +++ b/docs/samples/sum.lyng @@ -1,7 +1,7 @@ +#!/bin/env lyng /* Calculate the limit of Sum( f(n) ) until it reaches asymptotic limit 0.00001% change - return null or found limit */ fun findSumLimit(f) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 2d78927..f6211db 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -35,8 +35,8 @@ class Compiler( // Stack of parameter-to-slot plans for current function being parsed (by declaration index) private val paramSlotPlanStack = mutableListOf>() - private val currentParamSlotPlan: Map? - get() = paramSlotPlanStack.lastOrNull() +// private val currentParamSlotPlan: Map? +// get() = paramSlotPlanStack.lastOrNull() // Track identifiers known to be locals/parameters in the current function for fast local emission private val localNamesStack = mutableListOf>() @@ -50,7 +50,11 @@ class Compiler( private inline fun withLocalNames(names: Set, block: () -> T): T { localNamesStack.add(names.toMutableSet()) - return try { block() } finally { localNamesStack.removeLast() } + return try { + block() + } finally { + localNamesStack.removeLast() + } } private fun declareLocalName(name: String) { @@ -86,6 +90,7 @@ class Compiler( if (t.startsWith("*")) t.removePrefix("*").trimStart() else line } } + else -> raw } } @@ -158,6 +163,7 @@ class Compiler( // A standalone newline not immediately following a comment resets doc buffer if (!prevWasComment) clearPendingDoc() else prevWasComment = false } + else -> {} } cc.next() @@ -191,12 +197,15 @@ class Compiler( val start = Pos(pos.source, pos.line, col) val end = Pos(pos.source, pos.line, col + p.length) col += p.length + 1 // account for following '.' between segments - net.sergeych.lyng.miniast.MiniImport.Segment(p, net.sergeych.lyng.miniast.MiniRange(start, end)) + MiniImport.Segment( + p, + MiniRange(start, end) + ) } val lastEnd = segs.last().range.end miniSink?.onImport( - net.sergeych.lyng.miniast.MiniImport( - net.sergeych.lyng.miniast.MiniRange(pos, lastEnd), + MiniImport( + MiniRange(pos, lastEnd), segs ) ) @@ -241,7 +250,10 @@ class Compiler( Script(start, statements) }.also { // Best-effort script end notification (use current position) - miniSink?.onScriptEnd(cc.currentPos(), net.sergeych.lyng.miniast.MiniScript(MiniRange(start, cc.currentPos()))) + miniSink?.onScriptEnd( + cc.currentPos(), + MiniScript(MiniRange(start, cc.currentPos())) + ) } } @@ -327,9 +339,8 @@ class Compiler( var lvalue: ObjRef? = parseExpressionLevel(level + 1) ?: return null while (true) { - val opToken = cc.next() - val op = byLevel[level][opToken.type] + val op = byLevel[level][opToken.type] if (op == null) { // handle ternary conditional at the top precedence level only: a ? b : c if (opToken.type == Token.Type.QUESTION && level == 0) { @@ -424,7 +435,7 @@ class Compiler( // single lambda arg, like assertThrows { ... } cc.next() isCall = true - val lambda = parseLambdaExpression() + val lambda = parseLambdaExpression() val argStmt = statement { lambda.get(this).value } val args = listOf(ParsedArgument(argStmt, next.pos)) operand = MethodCallRef(left, next.value, args, true, isOptional) @@ -552,11 +563,14 @@ class Compiler( Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { operand = operand?.let { left -> - cc.previous() + // Trailing block-argument function call: the leading '{' is already consumed, + // and the lambda must be parsed as a single argument BEFORE any following + // selectors like ".foo" are considered. Do NOT rewind here, otherwise + // the expression parser may capture ".foo" as part of the lambda expression. parseFunctionCall( left, blockArgument = true, - t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE + isOptional = t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE ) } ?: parseLambdaExpression() } @@ -778,7 +792,11 @@ class Compiler( val typeStart = cc.currentPos() var lastEnd = typeStart while (true) { - val idTok = if (first) cc.requireToken(Token.Type.ID, "type name or type expression required") else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type") + val idTok = + if (first) cc.requireToken(Token.Type.ID, "type name or type expression required") else cc.requireToken( + Token.Type.ID, + "identifier expected after '.' in type" + ) first = false segments += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos)) lastEnd = cc.currentPos() @@ -796,8 +814,11 @@ class Compiler( // Helper to build MiniTypeRef (base or generic) fun buildBaseRef(rangeEnd: Pos, args: List?, nullable: Boolean): MiniTypeRef { val base = MiniTypeName(MiniRange(typeStart, rangeEnd), segments.toList(), nullable = false) - return if (args == null || args.isEmpty()) base.copy(range = MiniRange(typeStart, rangeEnd), nullable = nullable) - else net.sergeych.lyng.miniast.MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable) + return if (args == null || args.isEmpty()) base.copy( + range = MiniRange(typeStart, rangeEnd), + nullable = nullable + ) + else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable) } // Optional generic arguments: '<' Type (',' Type)* '>' — single-level only (no nested generics for now) @@ -811,12 +832,17 @@ class Compiler( var argFirst = true val argStart = cc.currentPos() while (true) { - val idTok = if (argFirst) cc.requireToken(Token.Type.ID, "type argument name expected") else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type argument") + val idTok = if (argFirst) cc.requireToken( + Token.Type.ID, + "type argument name expected" + ) else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type argument") argFirst = false argSegs += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos)) val p = cc.savePos() val tt = cc.next() - if (tt.type == Token.Type.DOT) continue else { cc.restorePos(p); break } + if (tt.type == Token.Type.DOT) continue else { + cc.restorePos(p); break + } } val argNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true) val argEnd = cc.currentPos() @@ -825,7 +851,9 @@ class Compiler( val sep = cc.next() when (sep.type) { - Token.Type.COMMA -> { /* continue */ } + Token.Type.COMMA -> { /* continue */ + } + Token.Type.GT -> break else -> sep.raiseSyntax("expected ',' or '>' in generic arguments") } @@ -934,11 +962,14 @@ class Compiler( ): ObjRef { var detectedBlockArgument = blockArgument val args = if (blockArgument) { - val blockArg = ParsedArgument( - parseExpression() - ?: throw ScriptError(cc.currentPos(), "lambda body expected"), cc.currentPos() - ) - listOf(blockArg) + // Leading '{' has already been consumed by the caller token branch. + // Parse only the lambda expression as the last argument and DO NOT + // allow any subsequent selectors (like ".last()") to be absorbed + // into the lambda body. This ensures expected order: + // foo { ... }.bar() == (foo { ... }).bar() + val callableAccessor = parseLambdaExpression() + val argStmt = statement { callableAccessor.get(this).value } + listOf(ParsedArgument(argStmt, cc.currentPos())) } else { val r = parseArgs() detectedBlockArgument = r.second @@ -1058,6 +1089,7 @@ class Compiler( pendingDeclDoc = consumePendingDoc() parseVarDeclaration(false, Visibility.Public) } + "var" -> { pendingDeclStart = id.pos pendingDeclDoc = consumePendingDoc() @@ -1069,6 +1101,7 @@ class Compiler( pendingDeclDoc = consumePendingDoc() parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false) } + "fn" -> { pendingDeclStart = id.pos pendingDeclDoc = consumePendingDoc() @@ -1085,11 +1118,24 @@ class Compiler( when (k.value) { "val" -> parseVarDeclaration(false, Visibility.Private, isStatic = isStatic) "var" -> parseVarDeclaration(true, Visibility.Private, isStatic = isStatic) - "fun" -> parseFunctionDeclaration(visibility = Visibility.Private, isOpen = false, isExtern = false, isStatic = isStatic) - "fn" -> parseFunctionDeclaration(visibility = Visibility.Private, isOpen = false, isExtern = false, isStatic = isStatic) + "fun" -> parseFunctionDeclaration( + visibility = Visibility.Private, + isOpen = false, + isExtern = false, + isStatic = isStatic + ) + + "fn" -> parseFunctionDeclaration( + visibility = Visibility.Private, + isOpen = false, + isExtern = false, + isStatic = isStatic + ) + else -> k.raiseSyntax("unsupported private declaration kind: ${k.value}") } } + "protected" -> { var k = cc.requireToken(Token.Type.ID, "declaration expected after 'protected'") var isStatic = false @@ -1100,11 +1146,24 @@ class Compiler( when (k.value) { "val" -> parseVarDeclaration(false, Visibility.Protected, isStatic = isStatic) "var" -> parseVarDeclaration(true, Visibility.Protected, isStatic = isStatic) - "fun" -> parseFunctionDeclaration(visibility = Visibility.Protected, isOpen = false, isExtern = false, isStatic = isStatic) - "fn" -> parseFunctionDeclaration(visibility = Visibility.Protected, isOpen = false, isExtern = false, isStatic = isStatic) + "fun" -> parseFunctionDeclaration( + visibility = Visibility.Protected, + isOpen = false, + isExtern = false, + isStatic = isStatic + ) + + "fn" -> parseFunctionDeclaration( + visibility = Visibility.Protected, + isOpen = false, + isExtern = false, + isStatic = isStatic + ) + else -> k.raiseSyntax("unsupported protected declaration kind: ${k.value}") } } + "while" -> parseWhileStatement() "do" -> parseDoWhileStatement() "for" -> parseForStatement() @@ -1116,11 +1175,13 @@ class Compiler( pendingDeclDoc = consumePendingDoc() parseClassDeclaration() } + "enum" -> { pendingDeclStart = id.pos pendingDeclDoc = consumePendingDoc() parseEnumDeclaration() } + "try" -> parseTryStatement() "throw" -> parseThrowStatement(id.pos) "when" -> parseWhenStatement() @@ -1130,9 +1191,10 @@ class Compiler( val isExtern = cc.skipId("extern") when { cc.matchQualifiers("fun", "private") -> { - pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc() parseFunctionDeclaration(Visibility.Private, isExtern) } + cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration( Visibility.Private, isExtern, @@ -1149,27 +1211,78 @@ class Compiler( cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) - cc.matchQualifiers("fun") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern) } - cc.matchQualifiers("fn") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern) } + cc.matchQualifiers("fun") -> { + pendingDeclStart = id.pos; pendingDeclDoc = + consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern) + } - cc.matchQualifiers("val", "private", "static") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( - false, - Visibility.Private, - isStatic = true - ) } + cc.matchQualifiers("fn") -> { + pendingDeclStart = id.pos; pendingDeclDoc = + consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern) + } - cc.matchQualifiers("val", "static") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(false, Visibility.Public, isStatic = true) } - cc.matchQualifiers("val", "private") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(false, Visibility.Private) } - cc.matchQualifiers("var", "static") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(true, Visibility.Public, isStatic = true) } - cc.matchQualifiers("var", "static", "private") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( - true, - Visibility.Private, - isStatic = true - ) } + cc.matchQualifiers("val", "private", "static") -> { + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( + false, + Visibility.Private, + isStatic = true + ) + } + + cc.matchQualifiers("val", "static") -> { + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( + false, + Visibility.Public, + isStatic = true + ) + } + + cc.matchQualifiers("val", "private") -> { + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( + false, + Visibility.Private + ) + } + + cc.matchQualifiers("var", "static") -> { + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( + true, + Visibility.Public, + isStatic = true + ) + } + + cc.matchQualifiers("var", "static", "private") -> { + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( + true, + Visibility.Private, + isStatic = true + ) + } + + cc.matchQualifiers("var", "private") -> { + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( + true, + Visibility.Private + ) + } + + cc.matchQualifiers("val", "open") -> { + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( + false, + Visibility.Private, + true + ) + } + + cc.matchQualifiers("var", "open") -> { + pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration( + true, + Visibility.Private, + true + ) + } - cc.matchQualifiers("var", "private") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(true, Visibility.Private) } - cc.matchQualifiers("val", "open") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(false, Visibility.Private, true) } - cc.matchQualifiers("var", "open") -> { pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(true, Visibility.Private, true) } else -> { cc.next() null @@ -1306,9 +1419,10 @@ class Compiler( errorObject.extraData, errorObject.useStackTrace ) + else -> throwScope.raiseError("this is not an exception object: $errorObject") } - throwScope.raiseError(errorObject as ObjException) + throwScope.raiseError(errorObject) } } @@ -1473,6 +1587,7 @@ class Compiler( // Optional base list: ":" Base ("," Base)* where Base := ID ( "(" args? ")" )? data class BaseSpec(val name: String, val args: List?) + val baseSpecs = mutableListOf() if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) { do { @@ -1516,13 +1631,13 @@ class Compiler( val declRange = MiniRange(pendingDeclStart ?: nameToken.pos, cc.currentPos()) val bases = baseSpecs.map { it.name } // Collect constructor fields declared as val/var in primary constructor - val ctorFields = mutableListOf() + val ctorFields = mutableListOf() constructorArgsDeclaration?.let { ad -> for (p in ad.params) { val at = p.accessType if (at != null) { val mutable = at == AccessType.Var - ctorFields += net.sergeych.lyng.miniast.MiniCtorField( + ctorFields += MiniCtorField( name = p.name, mutable = mutable, type = p.miniType, @@ -1571,7 +1686,8 @@ class Compiler( // accessors, constructor registration, etc. // Resolve parent classes by name at execution time val parentClasses = baseSpecs.map { baseSpec -> - val rec = this[baseSpec.name] ?: throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}") + val rec = + this[baseSpec.name] ?: throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}") (rec.value as? ObjClass) ?: throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class") } @@ -2082,7 +2198,7 @@ class Compiler( val paramNames: Set = argsDeclaration.params.map { it.name }.toSet() // Parse function body while tracking declared locals to compute precise capacity hints - val fnLocalDeclStart = currentLocalDeclCount + currentLocalDeclCount localDeclCountStack.add(0) val fnStatements = if (isExtern) statement { raiseError("extern function not provided: $name") } @@ -2113,7 +2229,7 @@ class Compiler( } fnStatements.execute(context) } - val enclosingCtx = parentContext + parentContext val fnCreateStatement = statement(start) { context -> // we added fn in the context. now we must save closure // for the function, unless we're in the class scope: @@ -2363,7 +2479,7 @@ class Compiler( ) { // fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND - companion object {} + companion object } @@ -2377,15 +2493,24 @@ class Compiler( * Compile [source] while streaming a Mini-AST into the provided [sink]. * When [sink] is null, behaves like [compile]. */ - suspend fun compileWithMini(source: Source, importManager: ImportProvider, sink: net.sergeych.lyng.miniast.MiniAstSink?): Script { - return Compiler(CompilerContext(parseLyng(source)), importManager, Settings(miniAstSink = sink)).parseScript() + suspend fun compileWithMini( + source: Source, + importManager: ImportProvider, + sink: MiniAstSink? + ): Script { + return Compiler( + CompilerContext(parseLyng(source)), + importManager, + Settings(miniAstSink = sink) + ).parseScript() } /** Convenience overload to compile raw [code] with a Mini-AST [sink]. */ - suspend fun compileWithMini(code: String, sink: net.sergeych.lyng.miniast.MiniAstSink?): Script = + suspend fun compileWithMini(code: String, sink: MiniAstSink?): Script = compileWithMini(Source("", code), Script.defaultImportManager, sink) private var lastPriority = 0 + // Helpers for conservative constant folding (literal-only). Only pure, side-effect-free ops. private fun constOf(r: ObjRef): Obj? = (r as? ConstRef)?.constValue @@ -2404,30 +2529,35 @@ class Compiler( a is ObjChar && b is ObjChar -> if (a.value == b.value) ObjTrue else ObjFalse else -> null } + BinOp.NEQ -> when { a is ObjInt && b is ObjInt -> if (a.value != b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value != b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value != b.value) ObjTrue else ObjFalse else -> null } + BinOp.LT -> when { a is ObjInt && b is ObjInt -> if (a.value < b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value < b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value < b.value) ObjTrue else ObjFalse else -> null } + BinOp.LTE -> when { a is ObjInt && b is ObjInt -> if (a.value <= b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value <= b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value <= b.value) ObjTrue else ObjFalse else -> null } + BinOp.GT -> when { a is ObjInt && b is ObjInt -> if (a.value > b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value > b.value) ObjTrue else ObjFalse a is ObjChar && b is ObjChar -> if (a.value > b.value) ObjTrue else ObjFalse else -> null } + BinOp.GTE -> when { a is ObjInt && b is ObjInt -> if (a.value >= b.value) ObjTrue else ObjFalse a is ObjString && b is ObjString -> if (a.value >= b.value) ObjTrue else ObjFalse @@ -2441,6 +2571,7 @@ class Compiler( a is ObjString && b is ObjString -> ObjString(a.value + b.value) else -> null } + BinOp.MINUS -> if (a is ObjInt && b is ObjInt) ObjInt(a.value - b.value) else null BinOp.STAR -> if (a is ObjInt && b is ObjInt) ObjInt(a.value * b.value) else null BinOp.SLASH -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt(a.value / b.value) else null @@ -2468,6 +2599,7 @@ class Compiler( is ObjReal -> ObjReal(-a.value) else -> null } + UnaryOp.BITNOT -> if (a is ObjInt) ObjInt(a.value.inv()) else null } } @@ -2638,5 +2770,5 @@ class Compiler( } } -suspend fun eval(code: String) = Compiler.compile(code).execute() +suspend fun eval(code: String) = compile(code).execute() diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 1dc630c..624123c 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3544,6 +3544,40 @@ class ScriptTest { """.trimIndent()) } + @Test + fun testCallAndResultOrder() = runTest { + eval(""" + import lyng.stdlib + + fun test(a="a", b="b", c="c") { [a, b, c] } + + // the parentheses here are in fact unnecessary: + val ok1 = (test { void }).last() + assert( ok1 is Callable) + + // it should work without them, as the call test() {} must be executed + // first, then the result should be used to call methods on it: + + // the parentheses here are in fact unnecessary: + val ok2 = test { void }.last() + assert( ok2 is Callable) + """.trimIndent()) + + } + +// @Test +// fun namedArgsProposal() = runTest { +// eval(""" +// import lyng.stdlib +// +// fun test(a="a", b="b", c="c") { [a, b, c] } +// +// val l = (test{ void }).last() +// println(l) +// +// """.trimIndent()) +// } + // @Ignore // @Test