From 157b716eb7a78a5a647ecac3ca9c791225166a7f Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 22 Dec 2025 18:11:38 +0100 Subject: [PATCH] Implement automatic substitution for named parameters (auto-named arguments). Supported name: shorthand for name: name in function calls and class constructors. Updated documentation and tests. Built and deployed IDEA plugin and site. --- README.md | 10 +- docs/OOP.md | 6 + docs/declaring_arguments.md | 44 +++-- .../kotlin/net/sergeych/lyng/Compiler.kt | 12 ++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 187 ++++++++++++++---- 5 files changed, 197 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index d77ce24..3b69082 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,15 @@ Please visit the project homepage: [https://lynglang.com](https://lynglang.com) - simple, compact, intuitive and elegant modern code: -``` -class Point(x,y) { +```lyng +class Point(x, y) { fun dist() { sqrt(x*x + y*y) } } -Point(3,4).dist() //< 5 + +// Auto-named arguments shorthand (x: is x: x): +val x = 3 +val y = 4 +Point(x:, y:).dist() //< 5 fun swapEnds(first, args..., last, f) { f( last, ...args, first) diff --git a/docs/OOP.md b/docs/OOP.md index 979f7dd..a73a81d 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -85,6 +85,12 @@ statements discussed later, there could be default values, ellipsis, etc. // Named arguments in constructor calls use colon syntax: val p2 = Point(y: 10, x: 5) assert( p2.x == 5 && p2.y == 10 ) + + // Auto-substitution shorthand for named arguments: + val x = 1 + val y = 2 + val p3 = Point(x:, y:) + assert( p3.x == 1 && p3.y == 2 ) >>> void Note that unlike **Kotlin**, which uses `=` for named arguments, Lyng uses `:` to avoid ambiguity with assignment expressions. diff --git a/docs/declaring_arguments.md b/docs/declaring_arguments.md index 18d52c9..81a15da 100644 --- a/docs/declaring_arguments.md +++ b/docs/declaring_arguments.md @@ -107,16 +107,29 @@ There could be any number of splats at any positions. You can splat any other [I ## Named arguments in calls -Lyng supports named arguments at call sites using colon syntax `name: value`: +Lyng supports named arguments at call sites using colon syntax `name: value`. + +### Shorthand for Named Arguments + +If you want to pass a variable as a named argument and the variable has the same name as the parameter, you can omit the value and use the shorthand `name:`. This is highly readable and matches the shorthand for map literals. ```lyng - fun test(a="foo", b="bar", c="bazz") { [a, b, c] } - - assertEquals(["foo", "b", "bazz"], test(b: "b")) - assertEquals(["a", "bar", "c"], test("a", c: "c")) + fun test(a, b, c) { [a, b, c] } + + val a = 1 + val b = 2 + val c = 3 + + // Explicit: + assertEquals([1, 2, 3], test(a: a, b: b, c: c)) + + // Shorthand (preferred): + assertEquals([1, 2, 3], test(a:, b:, c:)) ``` -Rules: +This shorthand is elegant, reduces boilerplate, and is consistent with Lyng's map literal syntax. It works for both function calls and class constructors. + +Rules for named arguments: - Named arguments must follow positional arguments. After the first named argument, no positional arguments may appear inside the parentheses. - The only exception is the syntactic trailing block after the call: `f(args) { ... }`. This block is outside the parentheses and is handled specially (see below). @@ -127,21 +140,20 @@ Why `:` and not `=` at call sites? In Lyng, `=` is an expression (assignment), s ## Named splats (map splats) -Splat (`...`) of a Map provides named arguments to the call. Only string keys are allowed: +Splat (`...`) of a Map provides named arguments to the call. Only string keys are allowed. You can use the same auto-substitution shorthand inside map literals used for splats: ```lyng fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] } - val r = test("A?", ...Map("d" => "D!", "b" => "B!")) - assertEquals(["A?","B!","c","D!"], r) -``` - -The same with a map literal is often more concise. Define the literal, then splat the variable: - - fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] } - val patch = { d: "D!", b: "B!" } + + val b = "B!" + val d = "D!" + + // Auto-substitution in map literal: + val patch = { d:, b: } + val r = test("A?", ...patch) assertEquals(["A?","B!","c","D!"], r) - >>> void +``` Constraints: diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index e5f80f1..20b617c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1036,6 +1036,12 @@ class Compiler( if (t2.type == Token.Type.COLON) { // name: expr val name = t1.value + // Check for shorthand: name: (comma or rparen) + val next = cc.peekNextNonWhitespace() + if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) { + val localVar = LocalVarRef(name, t1.pos) + return ParsedArgument(statement(t1.pos) { localVar.get(it).value }, t1.pos, isSplat = false, name = name) + } val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'") return ParsedArgument(rhs, t1.pos, isSplat = false, name = name) } @@ -1102,6 +1108,12 @@ class Compiler( val t2 = cc.next() if (t2.type == Token.Type.COLON) { val name = t1.value + // Check for shorthand: name: (comma or rparen) + val next = cc.peekNextNonWhitespace() + if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) { + val localVar = LocalVarRef(name, t1.pos) + return ParsedArgument(statement(t1.pos) { localVar.get(it).value }, t1.pos, isSplat = false, name = name) + } val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'") return ParsedArgument(rhs, t1.pos, isSplat = false, name = name) } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index f919cca..dae01e5 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3022,7 +3022,8 @@ class ScriptTest { @Test fun testMapWithNonStringKeys() = runTest { - eval(""" + eval( + """ val map = Map( 1 => "one", 2 => "two" ) assertEquals( "one", map[1] ) assertEquals( "two", map[2] ) @@ -3046,7 +3047,8 @@ class ScriptTest { val map4 = map3 + (3 => "c") assertEquals("c", map4[3]) assertEquals("a", map4[1]) - """.trimIndent()) + """.trimIndent() + ) } @Test @@ -3948,13 +3950,15 @@ class ScriptTest { @Test fun testJsonTime() = runTest { val now = Clock.System.now() - val x = eval(""" + val x = eval( + """ import lyng.time Instant.now().truncateToSecond() - """.trimIndent()).decodeSerializable() + """.trimIndent() + ).decodeSerializable() println(x) assertIs(x) - assertTrue( (now - x).absoluteValue < 2.seconds) + assertTrue((now - x).absoluteValue < 2.seconds) } @Test @@ -4067,15 +4071,17 @@ class ScriptTest { @Serializable data class TestJson2( val value: Int, - val inner: Map + val inner: Map ) @Test fun deserializeMapWithJsonTest() = runTest { - val x = eval(""" + val x = eval( + """ import lyng.serialization { value: 1, inner: { "foo": 1, "bar": 2 }} - """.trimIndent()).decodeSerializable() + """.trimIndent() + ).decodeSerializable() assertEquals(TestJson2(1, mapOf("foo" to 1, "bar" to 2)), x) } @@ -4084,42 +4090,56 @@ class ScriptTest { val value: Int, val inner: JsonObject ) + @Test fun deserializeAnyMapWithJsonTest() = runTest { - val x = eval(""" + val x = eval( + """ import lyng.serialization { value: 12, inner: { "foo": 1, "bar": "two" }} - """.trimIndent()).decodeSerializable() - assertEquals(TestJson3(12, JsonObject(mapOf("foo" to JsonPrimitive(1), "bar" to Json.encodeToJsonElement("two")))), x) + """.trimIndent() + ).decodeSerializable() + assertEquals( + TestJson3( + 12, + JsonObject(mapOf("foo" to JsonPrimitive(1), "bar" to Json.encodeToJsonElement("two"))) + ), x + ) } @Serializable enum class TestEnum { One, Two } + @Serializable data class TestJson4(val value: TestEnum) @Test fun deserializeEnumJsonTest() = runTest { - val x = eval(""" + val x = eval( + """ import lyng.serialization enum TestEnum { One, Two } { value: TestEnum.One } - """.trimIndent()).decodeSerializable() - assertEquals( TestJson4(TestEnum.One), x) + """.trimIndent() + ).decodeSerializable() + assertEquals(TestJson4(TestEnum.One), x) } @Test fun testStringLast() = runTest { - eval(""" + eval( + """ assertEquals('t', "assert".last()) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testLogicalNot() = runTest { - eval(""" + eval( + """ val vf = false fun f() { false } assert( !false ) @@ -4156,7 +4176,8 @@ class ScriptTest { @Test fun testHangOnPrintlnInMethods() = runTest { - eval(""" + eval( + """ class T(someList) { fun f() { val x = [...someList] @@ -4164,12 +4185,14 @@ class ScriptTest { } } T([1,2]).f() - """) + """ + ) } @Test fun testHangOnNonexistingMethod() = runTest { - eval(""" + eval( + """ class T(someList) { fun f() { nonExistingMethod() @@ -4185,12 +4208,14 @@ class ScriptTest { println(t::class) // ok } - """) + """ + ) } @Test fun testUsingClassConstructorVars() = runTest { - val r = eval(""" + val r = eval( + """ import lyng.time class Request { @@ -4214,7 +4239,8 @@ class ScriptTest { } test() - """.trimIndent()).toJson() + """.trimIndent() + ).toJson() println(r) } @@ -4222,7 +4248,8 @@ class ScriptTest { fun testScopeShortCircuit() = runTest() { val baseScope = Script.newScope() - baseScope.eval(""" + baseScope.eval( + """ val exports = Map() fun Export(name,f) { exports[name] = f @@ -4233,7 +4260,8 @@ class ScriptTest { val exports: MutableMap = (baseScope.eval("exports") as ObjMap).map - baseScope.eval(""" + baseScope.eval( + """ class A(val a) { fun methodA() { a + 1 @@ -4250,29 +4278,41 @@ class ScriptTest { fun exportedFunction(x) { someFunction(x) } - """.trimIndent()) + """.trimIndent() + ) // Calling from the script is ok: val instanceScope = baseScope.createChildScope() - instanceScope.eval(""" + instanceScope.eval( + """ val a1 = a0 + 1 - """.trimIndent()) - assertEquals( ObjInt(2), instanceScope.eval(""" + """.trimIndent() + ) + assertEquals( + ObjInt(2), instanceScope.eval( + """ exportedFunction(1) - """)) - assertEquals( ObjInt(103), instanceScope.eval(""" + """ + ) + ) + assertEquals( + ObjInt(103), instanceScope.eval( + """ exportedFunction(a1 + 1) - """)) + """ + ) + ) val dummyThis = Obj() // but we should be able to call it directly val otherScope = baseScope.createChildScope() - val r = (exports["exportedFunction".toObj()] as Statement).invoke(otherScope, dummyThis,ObjInt(50)) + val r = (exports["exportedFunction".toObj()] as Statement).invoke(otherScope, dummyThis, ObjInt(50)) println(r) assertEquals(51, r.toInt()) } @Test fun testFirstInEnum() = runTest { - eval(""" + eval( + """ enum E { one, two, three } @@ -4282,12 +4322,14 @@ class ScriptTest { it.name in ["aaa", "two"] } ) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testAutoSplatArgs() = runTest { - eval(""" + eval( + """ fun tf(x, y, z) { "x=%s, y=%s, z=%s"(x,y,z) } @@ -4295,12 +4337,14 @@ class ScriptTest { val a = { x: 3, y: 4, z: 5 } assertEquals(tf(...a), "x=3, y=4, z=5") assertEquals(tf(...{ x: 3, y: 4, z: 50 }), "x=3, y=4, z=50") - """.trimIndent()) + """.trimIndent() + ) } @Test fun testCached() = runTest { - eval(""" + eval( + """ var counter = 0 val f = cached { ++counter } @@ -4308,12 +4352,14 @@ class ScriptTest { assertEquals(1, counter) assertEquals(1,f()) assertEquals(1, counter) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testCustomToStringBug() = runTest { - eval(""" + eval( + """ class A(x,y) class B(x,y) { fun toString() { @@ -4329,14 +4375,16 @@ class ScriptTest { // and this must be exactly same: assertEquals(":B(1,2)", ":" + B(1,2)) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testDestructuringAssignment() = runTest { - eval(""" + eval( + """ val abc = [1, 2, 3] // plain: val [a, b, c] = abc @@ -4373,7 +4421,8 @@ class ScriptTest { assertEquals( 10, x ) assertEquals( 5, y ) - """.trimIndent()) + """.trimIndent() + ) } @Test @@ -4411,6 +4460,58 @@ class ScriptTest { println(x) assertEquals(5, x.pos.line) assertContains(x.message!!, "throw \"success\"") + } + @Test + fun testClassAndFunAutoNamedArgs() = runTest { + // Shorthand for named arguments: name: is equivalent to name: name. + // This is consistent with map literal shorthand in Lyng. + eval( + """ + fun test(a, b, c) { + "%s-%s-%s"(a,b,c) + } + + val a = 1 + val b = 2 + val c = 3 + + // Basic usage: + assertEquals( "1-2-3", test(a:, b:, c:) ) + assertEquals( "1-2-3", test(c:, b:, a:) ) + + // Class constructors also support it: + class Point(x, y) { + val r = "x:%s, y:%s"(x, y) + } + val x = 10 + val y = 20 + assertEquals( "x:10, y:20", Point(x:, y:).r ) + assertEquals( "x:10, y:20", Point(y:, x:).r ) + + // Mixed with positional arguments: + assertEquals( "0-2-3", test(0, b:, c:) ) + + // Mixed with regular named arguments: + assertEquals( "1-99-3", test(a:, b: 99, c:) ) + + // Integration with splats (spread arguments): + val args = { b:, c: } // map literal shorthand + assertEquals( "1-2-3", test(a:, ...args) ) + + // Default values: + fun sum(a, b=10, c=100) { a + b + c } + assertEquals( 111, sum(a:) ) + assertEquals( 103, sum(a:, b:) ) + + // Complex scenario with multiple splats and shorthands: + val p1 = 1 + val p2 = 2 + val more = { c: 3, d: 4 } + fun quad(a, b, c, d) { a + b + c + d } + assertEquals( 10, quad(a: p1, b: p2, ...more) ) + + """.trimIndent() + ) } }