v1.0.1-SNAPSHOT: named args in calls

This commit is contained in:
sergeych committed 2025-11-27 11:24:50 +01:00
1 parent d6e6d68b18
commit 2d721101dd
14 files changed
+695 -170

No files matched your search

+10 -1
View File
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "1.0.0-SNAPSHOT"
version = "1.0.1-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
@@ -72,6 +72,15 @@ kotlin {
nodejs()
}
// Suppress Beta warning for expect/actual classes across all targets
targets.configureEach {
compilations.configureEach {
compilerOptions.configure {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
@@ -57,72 +57,137 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
recordType = ObjRecord.Type.Argument)
}
// will be used with last lambda arg fix
// Prepare positional args and parameter count, handle tail-block binding
val callArgs: List<Obj>
val paramsSize: Int
if( arguments.tailBlockMode ) {
if (arguments.tailBlockMode) {
// If last parameter is already assigned by a named argument, it's an error
val lastParam = params.last()
if (arguments.named.containsKey(lastParam.name))
scope.raiseIllegalArgument("trailing block cannot be used when the last parameter is already assigned by a named argument")
paramsSize = params.size - 1
assign(params.last(), arguments.list.last())
assign(lastParam, arguments.list.last())
callArgs = arguments.list.dropLast(1)
} else {
paramsSize = params.size
callArgs = arguments.list
}
suspend fun processHead(index: Int): Int {
// Compute which parameter indexes are inevitably covered by positional arguments
// based on the number of supplied positionals, defaults and ellipsis placement.
val coveredByPositional = BooleanArray(paramsSize)
run {
// Count required (non-default, non-ellipsis) params in head and in tail
var headRequired = 0
var tailRequired = 0
val ellipsisIdx = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
if (ellipsisIdx >= 0) {
for (i in 0 until ellipsisIdx) if (!params[i].isEllipsis && params[i].defaultValue == null) headRequired++
for (i in paramsSize - 1 downTo ellipsisIdx + 1) if (params[i].defaultValue == null) tailRequired++
} else {
for (i in 0 until paramsSize) if (params[i].defaultValue == null) headRequired++
}
val P = callArgs.size
if (ellipsisIdx < 0) {
// No ellipsis: all positionals go to head until exhausted
val k = minOf(P, paramsSize)
for (i in 0 until k) coveredByPositional[i] = true
} else {
// With ellipsis: head takes min(P, headRequired) first
val headTake = minOf(P, headRequired)
for (i in 0 until headTake) coveredByPositional[i] = true
val remaining = P - headTake
// tail takes min(remaining, tailRequired) from the end
val tailTake = minOf(remaining, tailRequired)
var j = paramsSize - 1
var taken = 0
while (j > ellipsisIdx && taken < tailTake) {
coveredByPositional[j] = true
j--
taken++
}
}
}
// Prepare arrays for named assignments
val assignedByName = BooleanArray(paramsSize)
val namedValues = arrayOfNulls<Obj>(paramsSize)
if (arguments.named.isNotEmpty()) {
for ((k, v) in arguments.named) {
val idx = params.subList(0, paramsSize).indexOfFirst { it.name == k }
if (idx < 0) scope.raiseIllegalArgument("unknown parameter '$k'")
if (params[idx].isEllipsis) scope.raiseIllegalArgument("ellipsis (variadic) parameter cannot be assigned by name: '$k'")
if (coveredByPositional[idx]) scope.raiseIllegalArgument("argument '$k' is already set by positional argument")
if (assignedByName[idx]) scope.raiseIllegalArgument("argument '$k' is already set")
assignedByName[idx] = true
namedValues[idx] = v
}
}
// Helper: assign head part, consuming from headPos; stop at ellipsis
suspend fun processHead(index: Int, headPos: Int): Pair<Int, Int> {
var i = index
while (i != paramsSize) {
var hp = headPos
while (i < paramsSize) {
val a = params[i]
if (a.isEllipsis) break
val value = when {
i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(scope)
else -> {
// println("callArgs: ${callArgs.joinToString()}")
// println("tailBlockMode: ${arguments.tailBlockMode}")
scope.raiseIllegalArgument("too few arguments for the call")
}
if (assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (hp < callArgs.size) callArgs[hp++]
else a.defaultValue?.execute(scope)
?: scope.raiseIllegalArgument("too few arguments for the call")
assign(a, value)
}
assign(a, value)
i++
}
return i
return i to hp
}
suspend fun processTail(index: Int): Int {
// Helper: assign tail part from the end, consuming from tailPos; stop before ellipsis index
// Do not consume elements below headPosBound to avoid overlap with head consumption
suspend fun processTail(startExclusive: Int, tailStart: Int, headPosBound: Int): Int {
var i = paramsSize - 1
var j = callArgs.size - 1
while (i > index) {
var tp = tailStart
while (i > startExclusive) {
val a = params[i]
if (a.isEllipsis) break
val value = when {
j >= index -> {
callArgs[j--]
}
a.defaultValue != null -> a.defaultValue.execute(scope)
else -> scope.raiseIllegalArgument("too few arguments for the call")
if (i < assignedByName.size && assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (tp >= headPosBound) callArgs[tp--]
else a.defaultValue?.execute(scope)
?: scope.raiseIllegalArgument("too few arguments for the call")
assign(a, value)
}
assign(a, value)
i--
}
return j
return tp
}
fun processEllipsis(index: Int, toFromIndex: Int) {
fun processEllipsis(index: Int, headPos: Int, tailPos: Int) {
val a = params[index]
val l = if (index > toFromIndex) ObjList()
else ObjList(callArgs.subList(index, toFromIndex + 1).toMutableList())
val from = headPos
val to = tailPos
val l = if (from > to) ObjList()
else ObjList(callArgs.subList(from, to + 1).toMutableList())
assign(a, l)
}
val leftIndex = processHead(0)
if (leftIndex < paramsSize) {
val end = processTail(leftIndex)
processEllipsis(leftIndex, end)
// Locate ellipsis index within considered parameters
val ellipsisIndex = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
if (ellipsisIndex >= 0) {
// Assign head first to know how many positionals are consumed from the start
val (afterHead, headConsumedTo) = processHead(0, 0)
// Then assign tail consuming from the end down to headConsumedTo boundary
val tailConsumedFrom = processTail(ellipsisIndex, callArgs.size - 1, headConsumedTo)
// Assign ellipsis list from remaining positionals between headConsumedTo..tailConsumedFrom
processEllipsis(ellipsisIndex, headConsumedTo, tailConsumedFrom)
} else {
if (leftIndex < callArgs.size)
// No ellipsis: assign head only; any leftover positionals → error
val (_, headConsumedTo) = processHead(0, 0)
if (headConsumedTo != callArgs.size)
scope.raiseIllegalArgument("too many arguments for the call")
}
}
@@ -17,24 +17,27 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjIterable
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.*
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
data class ParsedArgument(
val value: Statement,
val pos: Pos,
val isSplat: Boolean = false,
val name: String? = null,
)
suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
// Small-arity fast path (no splats) to reduce allocations
// Detect if we can use the fast path: no splats and no named args
if (PerfFlags.ARG_BUILDER) {
val limit = if (PerfFlags.ARG_SMALL_ARITY_12) 12 else 8
var hasSplat = false
val limit = if (PerfFlags.ARG_SMALL_ARITY_12) 12 else 8
var hasSplatOrNamed = false
var count = 0
for (pa in this) {
if (pa.isSplat) { hasSplat = true; break }
if (pa.isSplat || pa.name != null) { hasSplatOrNamed = true; break }
count++
if (count > limit) break
if (count > limit) break
}
if (!hasSplat && count == this.size) {
if (!hasSplatOrNamed && count == this.size) {
val quick = when (count) {
0 -> Arguments.EMPTY
1 -> Arguments(listOf(this.elementAt(0).value.execute(scope)), tailBlockMode)
@@ -94,133 +97,130 @@ import net.sergeych.lyng.obj.ObjList
val a7 = this.elementAt(7).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7), tailBlockMode)
}
9 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8), tailBlockMode)
} else null
10 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9), tailBlockMode)
} else null
11 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
val a10 = this.elementAt(10).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10), tailBlockMode)
} else null
12 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
val a10 = this.elementAt(10).value.execute(scope)
val a11 = this.elementAt(11).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11), tailBlockMode)
} else null
9 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8), tailBlockMode)
} else null
10 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9), tailBlockMode)
} else null
11 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
val a10 = this.elementAt(10).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10), tailBlockMode)
} else null
12 -> if (PerfFlags.ARG_SMALL_ARITY_12) {
val a0 = this.elementAt(0).value.execute(scope)
val a1 = this.elementAt(1).value.execute(scope)
val a2 = this.elementAt(2).value.execute(scope)
val a3 = this.elementAt(3).value.execute(scope)
val a4 = this.elementAt(4).value.execute(scope)
val a5 = this.elementAt(5).value.execute(scope)
val a6 = this.elementAt(6).value.execute(scope)
val a7 = this.elementAt(7).value.execute(scope)
val a8 = this.elementAt(8).value.execute(scope)
val a9 = this.elementAt(9).value.execute(scope)
val a10 = this.elementAt(10).value.execute(scope)
val a11 = this.elementAt(11).value.execute(scope)
Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11), tailBlockMode)
} else null
else -> null
}
if (quick != null) return quick
}
}
// Single-splat fast path: if there is exactly one splat argument that evaluates to ObjList,
// avoid builder and copies by returning its list directly.
if (PerfFlags.ARG_BUILDER) {
if (this.size == 1) {
val only = this.first()
if (only.isSplat) {
val v = only.value.execute(scope)
if (v is ObjList) {
return Arguments(v.list, tailBlockMode)
} else if (v.isInstanceOf(ObjIterable)) {
// Convert iterable to list once and return directly
val i = (v.invokeInstanceMethod(scope, "toList") as ObjList).list
return Arguments(i, tailBlockMode)
} else {
scope.raiseClassCastError("expected list of objects for splat argument")
}
}
}
}
// General path with builder or simple list fallback
if (PerfFlags.ARG_BUILDER) {
val b = ArgBuilderProvider.acquire()
try {
b.reset(this.size)
for (x in this) {
val value = x.value.execute(scope)
if (x.isSplat) {
when {
value is ObjList -> {
b.addAll(value.list)
}
value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
b.addAll(i)
}
else -> scope.raiseClassCastError("expected list of objects for splat argument")
}
} else {
b.add(value)
}
}
return b.build(tailBlockMode)
} finally {
b.release()
// General path: build positional list and named map, enforcing ordering rules
val positional: MutableList<Obj> = mutableListOf()
var named: MutableMap<String, Obj>? = null
var namedSeen = false
for ((idx, x) in this.withIndex()) {
if (x.name != null) {
// Named argument
if (named == null) named = linkedMapOf()
if (named!!.containsKey(x.name)) scope.raiseIllegalArgument("argument '${x.name}' is already set")
val v = x.value.execute(scope)
named!![x.name] = v
namedSeen = true
continue
}
} else {
val list: MutableList<Obj> = mutableListOf()
for (x in this) {
val value = x.value.execute(scope)
if (x.isSplat) {
when {
value is ObjList -> list.addAll(value.list)
value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
list.addAll(i)
val value = x.value.execute(scope)
if (x.isSplat) {
when {
// IMPORTANT: handle ObjMap BEFORE generic Iterable to ensure map splats
// are treated as named splats, not as positional iteration over entries
value is ObjMap -> {
if (named == null) named = linkedMapOf()
for ((k, v) in value.map) {
if (k !is ObjString) scope.raiseIllegalArgument("named splat expects a Map with string keys")
val key = k.value
if (named!!.containsKey(key)) scope.raiseIllegalArgument("argument '$key' is already set")
named!![key] = v
}
else -> scope.raiseClassCastError("expected list of objects for splat argument")
namedSeen = true
}
} else {
list.add(value)
value is ObjList -> {
if (namedSeen) {
// allow only if this is the very last positional which will be the trailing block; but
// splat can never be a trailing block, so it's always illegal here
scope.raiseIllegalArgument("positional splat cannot follow named arguments")
}
positional.addAll(value.list)
}
value.isInstanceOf(ObjIterable) -> {
if (namedSeen) scope.raiseIllegalArgument("positional splat cannot follow named arguments")
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
positional.addAll(i)
}
else -> scope.raiseClassCastError("expected list of objects for splat argument")
}
} else {
if (namedSeen) {
// Allow exactly one positional after named only when it is the very last argument overall
// and tailBlockMode is true (syntactic trailing block). Otherwise, forbid it.
val isLast = idx == this.size - 1
if (!(isLast && tailBlockMode))
scope.raiseIllegalArgument("positional argument cannot follow named arguments")
}
positional.add(value)
}
return Arguments(list, tailBlockMode)
}
val namedFinal = named ?: emptyMap()
return Arguments(positional, tailBlockMode, namedFinal)
}
data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) : List<Obj> by list {
data class Arguments(
val list: List<Obj>,
val tailBlockMode: Boolean = false,
val named: Map<String, Obj> = emptyMap(),
) : List<Obj> by list {
constructor(vararg values: Obj) : this(values.toList())
@@ -881,6 +881,21 @@ class Compiler(
private suspend fun parseArgs(): Pair<List<ParsedArgument>, Boolean> {
val args = mutableListOf<ParsedArgument>()
suspend fun tryParseNamedArg(): ParsedArgument? {
val save = cc.savePos()
val t1 = cc.next()
if (t1.type == Token.Type.ID) {
val t2 = cc.next()
if (t2.type == Token.Type.COLON) {
// name: expr
val name = t1.value
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
}
}
cc.restorePos(save)
return null
}
do {
val t = cc.next()
when (t.type) {
@@ -895,10 +910,14 @@ class Compiler(
else -> {
cc.previous()
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
if (cc.current().type == Token.Type.COLON)
parseTypeDeclaration()
val named = tryParseNamedArg()
if (named != null) {
args += named
} else {
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
// In call-site arguments, ':' is reserved for named args. Do not parse type declarations here.
}
// Here should be a valid termination:
}
}
@@ -929,6 +948,20 @@ class Compiler(
*/
private suspend fun parseArgsNoTailBlock(): List<ParsedArgument> {
val args = mutableListOf<ParsedArgument>()
suspend fun tryParseNamedArg(): ParsedArgument? {
val save = cc.savePos()
val t1 = cc.next()
if (t1.type == Token.Type.ID) {
val t2 = cc.next()
if (t2.type == Token.Type.COLON) {
val name = t1.value
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
}
}
cc.restorePos(save)
return null
}
do {
val t = cc.next()
when (t.type) {
@@ -943,10 +976,14 @@ class Compiler(
else -> {
cc.previous()
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
if (cc.current().type == Token.Type.COLON)
parseTypeDeclaration()
val named = tryParseNamedArg()
if (named != null) {
args += named
} else {
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
// Do not parse type declarations in call args
}
}
}
} while (t.type != Token.Type.RPAREN)
@@ -0,0 +1,154 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/*
* Named arguments and named splats test suite
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.eval
import kotlin.test.Test
import kotlin.test.assertFailsWith
class NamedArgsTest {
@Test
fun basicNamedArgsAndDefaults() = runTest {
eval(
"""
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") )
""".trimIndent()
)
}
@Test
fun positionalAfterNamedIsError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a, b) { [a,b] }
f(a: 1, 2)
""".trimIndent()
)
}
}
@Test
fun namedSplatsBasic() = runTest {
eval(
"""
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)
""".trimIndent()
)
}
@Test
fun namedSplatsNonStringKeysError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun test(a,b) {}
test(1, ...Map(1 => "x"))
""".trimIndent()
)
}
}
@Test
fun trailingBlockConflictWhenLastNamed() = runTest {
// Error: last parameter already assigned by a named argument; trailing block must be rejected
assertFailsWith<ExecutionError> {
eval(
"""
fun f(x, onDone) { onDone(x) }
// Name the last parameter inside parentheses, then try to pass a trailing block
f(1, onDone: { it }) { 42 }
""".trimIndent()
)
}
// Normal case still works when last parameter is not assigned by name
eval(
"""
fun f(x, onDone) { onDone(x) }
var res = 0
f(1) { it -> res = it }
assertEquals(1, res)
""".trimIndent()
)
}
@Test
fun duplicateNamedIsError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a,b,c) {}
f(a: 1, a: 2)
""".trimIndent()
)
}
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a,b,c) {}
f(a: 1, ...Map("a" => 2))
""".trimIndent()
)
}
}
@Test
fun unknownParameterIsError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a,b) {}
f(z: 1)
""".trimIndent()
)
}
}
@Test
fun ellipsisCannotBeNamed() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun g(args..., tail) {}
g(args: [1], tail: 2)
""".trimIndent()
)
}
}
@Test
fun positionalSplatAfterNamedIsError() = runTest {
assertFailsWith<ExecutionError> {
eval(
"""
fun f(a,b,c) {}
f(a: 1, ...[2,3])
""".trimIndent()
)
}
}
}