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.

This commit is contained in:
Sergey Chernov 2025-12-22 18:11:38 +01:00
parent c9f96464f3
commit 157b716eb7
5 changed files with 197 additions and 62 deletions

View File

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

View File

@ -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.

View File

@ -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:

View File

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

View File

@ -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<Instant>()
""".trimIndent()
).decodeSerializable<Instant>()
println(x)
assertIs<Instant>(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<String,Int>
val inner: Map<String, Int>
)
@Test
fun deserializeMapWithJsonTest() = runTest {
val x = eval("""
val x = eval(
"""
import lyng.serialization
{ value: 1, inner: { "foo": 1, "bar": 2 }}
""".trimIndent()).decodeSerializable<TestJson2>()
""".trimIndent()
).decodeSerializable<TestJson2>()
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<TestJson3>()
assertEquals(TestJson3(12, JsonObject(mapOf("foo" to JsonPrimitive(1), "bar" to Json.encodeToJsonElement("two")))), x)
""".trimIndent()
).decodeSerializable<TestJson3>()
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<TestJson4>()
assertEquals( TestJson4(TestEnum.One), x)
""".trimIndent()
).decodeSerializable<TestJson4>()
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<Obj, Obj> = (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()
)
}
}