v1.0.1-SNAPSHOT: named args in calls
This commit is contained in:
parent
d6e6d68b18
commit
2d721101dd
17
CHANGELOG.md
17
CHANGELOG.md
@ -2,6 +2,23 @@
|
||||
|
||||
### Unreleased
|
||||
|
||||
- Language: Named arguments and named splats
|
||||
- New call-site syntax for named arguments using colon: `name: value`.
|
||||
- Positional arguments must come before named; positionals after a named argument inside parentheses are rejected.
|
||||
- Trailing-lambda interaction: if the last parameter is already assigned by name (or via a named splat), a trailing `{ ... }` block is illegal.
|
||||
- Named splats: `...` can now expand a Map into named arguments.
|
||||
- Only string keys are allowed; non-string keys raise a clear error.
|
||||
- Duplicate assignment across named args and named splats is an error.
|
||||
- Ellipsis (variadic) parameters remain positional-only and cannot be named.
|
||||
- Rationale: `=` is assignment and an expression in Lyng; `:` at call sites avoids ambiguity. Declarations keep `name: Type`; call-site casts continue to use `as` / `as?`.
|
||||
- Documentation updated: proposals and declaring-arguments sections now cover named args/splats and error cases.
|
||||
- Tests added covering success cases and errors for named args/splats and trailing-lambda interactions.
|
||||
|
||||
- Tooling: Highlighters and TextMate bundle updated for named args
|
||||
- Website/editor highlighter (lyngweb + site) works with `name: value` and `...Map("k" => v)`; added JS tests covering punctuation/operator spans for `:` and `...`.
|
||||
- TextMate grammar updated to recognize named call arguments: `name: value` after `(` or `,` with `name` highlighted as `variable.parameter.named.lyng` and `:` as punctuation; excludes `::`.
|
||||
- TextMate bundle version bumped to 0.0.3; README updated with details and guidance.
|
||||
|
||||
- Multiple Inheritance (MI) completed and enabled by default:
|
||||
- Active C3 Method Resolution Order (MRO) for deterministic, monotonic lookup across complex hierarchies and diamonds.
|
||||
- Qualified dispatch:
|
||||
|
||||
@ -5,7 +5,7 @@ lambdas and class declarations.
|
||||
|
||||
## Regular
|
||||
|
||||
## default values
|
||||
## Default values
|
||||
|
||||
Default parameters should not be mixed with mandatory ones:
|
||||
|
||||
@ -96,5 +96,52 @@ There could be any number of splats at any positions. You can splat any other [I
|
||||
>>> [start,1,2,3,end]
|
||||
>>> void
|
||||
|
||||
## Named arguments in calls
|
||||
|
||||
Lyng supports named arguments at call sites using colon syntax `name: value`:
|
||||
|
||||
```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"))
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- 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).
|
||||
- A named argument cannot reassign a parameter already set positionally.
|
||||
- If the last parameter has already been assigned by a named argument (or named splat), a trailing block is not allowed and results in an error.
|
||||
|
||||
Why `:` and not `=` at call sites? In Lyng, `=` is an expression (assignment), so we use `:` to avoid ambiguity. Declarations continue to use `:` for types, while call sites use `as` / `as?` for type operations.
|
||||
|
||||
## Named splats (map splats)
|
||||
|
||||
Splat (`...`) of a Map provides named arguments to the call. Only string keys are allowed:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
Constraints:
|
||||
|
||||
- Map splat keys must be strings; otherwise, a clean error is thrown.
|
||||
- Named splats cannot duplicate parameters already assigned (by positional or named arguments).
|
||||
- Named splats must follow all positional arguments and positional splats.
|
||||
- Ellipsis parameters (variadic) remain positional-only and cannot be assigned by name.
|
||||
|
||||
## Trailing-lambda rule interaction
|
||||
|
||||
If a call is immediately followed by a block `{ ... }`, it is treated as an extra last argument and bound to the last parameter. However, if the last parameter is already assigned by a named argument or a named splat, using a trailing block is an error:
|
||||
|
||||
```lyng
|
||||
fun f(x, onDone) { onDone(x) }
|
||||
f(x: 1) { 42 } // ERROR
|
||||
f(1) { 42 } // OK
|
||||
```
|
||||
|
||||
|
||||
[tutorial]: tutorial.md
|
||||
|
||||
80
docs/proposals/map_literal.md
Normal file
80
docs/proposals/map_literal.md
Normal file
@ -0,0 +1,80 @@
|
||||
Note for AI. please ignore this file until this remark is removed.
|
||||
|
||||
# Map literals proposal
|
||||
|
||||
Note to the AI: please ignore this file until this note is removed.
|
||||
|
||||
Implement javascript-like literals for maps. It uses the similar syntax already adopted for named arguments in function calls so it should be organic and familiar, as the purpose is similar.
|
||||
|
||||
String literal keys
|
||||
|
||||
```lyng
|
||||
val m = { a: "foo", b: "bar" }
|
||||
assertEqual(m.a, "foo")
|
||||
assertEqual(m.b, "bar")
|
||||
```
|
||||
|
||||
The map literal is a map with keys that are string literals only. This is important. In a relatively rare case when keys are calculated, or extravagant but still possible case when keys are of different types, literal could be combined with "=>":
|
||||
|
||||
```lyng
|
||||
val k1 = "bar"
|
||||
val m = { "foo": 123 } + k1 => "buzz"
|
||||
// this is same as Map("foo" => 123) + Map("bar" => k2) but can be optimized by compiler
|
||||
assertEqual(m["foo"], 123)
|
||||
assertEqual(m["bar"], "buzz")
|
||||
```
|
||||
|
||||
The lambda syntax is different, it can start with the `map_lteral_start` above, it should produce compile time error, so we can add map literals of this sort.
|
||||
|
||||
Also, we will allow splats in map literals:
|
||||
|
||||
```
|
||||
val m = { foo: "bar", ...{bar: "buzz"} }
|
||||
assertEquals("bar",m["foo"])
|
||||
assertEquals("buzz", m["bar"])
|
||||
```
|
||||
|
||||
When the literal argument and splats are used together, they must be evaluated left-to-right with allowed overwriting
|
||||
between named elements and splats, allowing any combination and multiple splats:
|
||||
|
||||
```
|
||||
val m = { foo: "bar", ...{bar: "buzz"}, ...{foo: "foobar"}, bar: "bar" }
|
||||
assertEquals("foobar",m["foo"])
|
||||
assertEquals("bar", m["bar"])
|
||||
```
|
||||
|
||||
Still we disallow duplicating _string literals_:
|
||||
|
||||
```
|
||||
// this is an compile-time exception:
|
||||
{ foo: 1, bar: 2, foo: 3 }
|
||||
```
|
||||
|
||||
Special syntax allows to insert key-value pair from the variable which name should be the key, and content is value:
|
||||
|
||||
```
|
||||
val foo = "bar"
|
||||
val bar = "buzz"
|
||||
assertEquals( {foo: "bar", bar: "buzz"}, { foo, bar } )
|
||||
```
|
||||
|
||||
|
||||
So, summarizing, overwriting/duplication rules are:
|
||||
|
||||
- string literals can't duplicate
|
||||
- splats add or update content, effectively overwrite preceding content,
|
||||
- string literals overwrite content received from preceding splats (as no duplication string literal keys allowed)
|
||||
- the priority and order is left-to-right, rightmost wins.
|
||||
- var inclusion is treated as form of the literal
|
||||
|
||||
This approach resolves the ambiguity from lambda syntax, as
|
||||
|
||||
```ebnf
|
||||
ws = zero or more whitespace characters including newline
|
||||
map_literal start = "{", ws, (s1 | s2 | s3)
|
||||
s1 = string_literal, ws, ":", ws, expression
|
||||
s2 = "...", string_literal
|
||||
s3 = string_literal, ("," | "}")
|
||||
```
|
||||
is not a valid lambda beginning.
|
||||
|
||||
67
docs/proposals/named_args.md
Normal file
67
docs/proposals/named_args.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Named arguments proposal
|
||||
|
||||
Extend function/method calls to allow setting arguments by name using colon syntax at call sites. This is especially convenient with many parameters and default values.
|
||||
|
||||
Examples:
|
||||
|
||||
```lyng
|
||||
fun test(a="foo", b="bar", c="bazz") { [a, b, c] }
|
||||
|
||||
assertEquals(test(b: "b"), ["foo", "b", "bazz"])
|
||||
assertEquals(test("a", c: "c"), ["a", "bar", "c"])
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Named arguments are optional. If named arguments are present, their order is not important.
|
||||
- Named arguments must follow positional arguments; positional arguments cannot follow named ones (the only exception is the syntactic trailing block outside parentheses, see below).
|
||||
- A named argument cannot reassign a parameter already set positionally.
|
||||
- If the last parameter is already assigned by a named argument (or named splat), the trailing-lambda rule must NOT apply: a following `{ ... }` after the call is an error.
|
||||
|
||||
Rationale for using `:` instead of `=` in calls: in Lyng, assignment `=` is an expression; using `:` avoids ambiguity and keeps declarations (`name: Type`) distinct from call sites, where casting uses `as` / `as?`.
|
||||
|
||||
Migration note: earlier drafts/examples used `name = value`. The final syntax is `name: value` at call sites.
|
||||
|
||||
## Extended call argument splats: named splats
|
||||
|
||||
With named arguments, splats (`...`) are extended to support maps as named splats. When a splat evaluates to a Map, its entries provide name→value assignments:
|
||||
|
||||
```lyng
|
||||
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
|
||||
|
||||
assertEquals(test("A?", ...Map("d" => "D!", "b" => "B!")), ["A?", "B!", "c", "D!"])
|
||||
```
|
||||
|
||||
Constraints for named splats:
|
||||
|
||||
- Only string keys are allowed in map splats; otherwise, a clean error is thrown.
|
||||
- Named splats cannot reassign parameters already set (positionally or by earlier named arguments/splats).
|
||||
- Named splats follow the same ordering as named arguments: they must appear after all positional arguments and positional splats.
|
||||
|
||||
## Trailing-lambda interaction
|
||||
|
||||
Lyng supports a syntactic trailing block after a call: `f(args) { ... }`. With named args/splats, if the last parameter is already assigned by name, the trailing block must not apply and the call is an error:
|
||||
|
||||
```lyng
|
||||
fun f(x, onDone) { onDone(x) }
|
||||
f(x: 1) { 42 } // ERROR: last parameter already assigned by name
|
||||
f(1) { 42 } // OK
|
||||
```
|
||||
|
||||
## Errors (non-exhaustive)
|
||||
|
||||
- Positional argument after any named argument inside parentheses: error.
|
||||
- Positional splat after any named argument: error.
|
||||
- Duplicate named assignment (directly or via map splats): error.
|
||||
- Unknown parameter name in a named argument/splat: error.
|
||||
- Map splat with non-string keys: error.
|
||||
- Attempt to target the ellipsis parameter by name: error.
|
||||
|
||||
## Notes
|
||||
|
||||
- Declarations continue to use `:` for types, while call sites use `:` for named arguments and `as` / `as?` for type casts/checks.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ Files
|
||||
- Operators including ranges (`..`, `..<`, `...`), null-safe (`?.`, `?[`, `?(`, `?{`, `?:`, `??`), arrows (`->`, `=>`, `::`), match operators (`=~`, `!~`), bitwise, arithmetic, etc.
|
||||
- Shuttle operator `<=>`
|
||||
- Division operator `/` (note: Lyng has no regex literal syntax; `/` is always division)
|
||||
- Named arguments at call sites `name: value` (the `name` part is highlighted as `variable.parameter.named.lyng` and the `:` as punctuation). The rule is anchored to `(` or `,` and excludes `::` to avoid conflicts.
|
||||
|
||||
Install in IntelliJ IDEA (and other JetBrains IDEs)
|
||||
---------------------------------------------------
|
||||
@ -56,6 +57,7 @@ Notes and limitations
|
||||
---------------------
|
||||
- Type highlighting is heuristic (Capitalized identifiers). The IntelliJ plugin will use language semantics and avoid false positives.
|
||||
- If your language adds or changes tokens, please update patterns in `lyng.tmLanguage.json`. The Kotlin sources in `lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/` are a good reference for token kinds.
|
||||
- Labels `name:` at statement level remain supported and are kept distinct from named call arguments by context. The grammar prefers named-argument matching when a `name:` appears right after `(` or `,`.
|
||||
|
||||
Lyng specifics
|
||||
--------------
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "lyng-textmate",
|
||||
"displayName": "Lyng",
|
||||
"description": "TextMate grammar for the Lyng language (for JetBrains IDEs via TextMate Bundles and VS Code).",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"publisher": "lyng",
|
||||
"license": "Apache-2.0",
|
||||
"engines": { "vscode": "^1.0.0" },
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
{ "include": "#keywords" },
|
||||
{ "include": "#constants" },
|
||||
{ "include": "#types" },
|
||||
{ "include": "#namedArgs" },
|
||||
{ "include": "#annotations" },
|
||||
{ "include": "#labels" },
|
||||
{ "include": "#directives" },
|
||||
@ -41,6 +42,18 @@
|
||||
]
|
||||
},
|
||||
"annotations": { "patterns": [ { "name": "entity.name.label.at.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*:" }, { "name": "storage.modifier.annotation.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*" } ] },
|
||||
"namedArgs": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.argument.named.lyng",
|
||||
"match": "(?:(?<=\\()|(?<=,))\\s*([\\p{L}_][\\p{L}\\p{N}_]*)\\s*(:)(?!:)",
|
||||
"captures": {
|
||||
"1": { "name": "variable.parameter.named.lyng" },
|
||||
"2": { "name": "punctuation.separator.colon.lyng" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] },
|
||||
"directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] },
|
||||
"declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(?:fun|fn)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(?:val|var)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "variable.other.declaration.lyng" } } } ] },
|
||||
|
||||
@ -31,6 +31,15 @@ repositories {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// Suppress Beta warning for expect/actual classes across all targets in this module
|
||||
targets.configureEach {
|
||||
compilations.configureEach {
|
||||
compilerOptions.configure {
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jvm {
|
||||
binaries {
|
||||
executable {
|
||||
|
||||
@ -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 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
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 (!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)
|
||||
@ -153,74 +156,71 @@ import net.sergeych.lyng.obj.ObjList
|
||||
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) {
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
namedSeen = true
|
||||
}
|
||||
value is ObjList -> {
|
||||
b.addAll(value.list)
|
||||
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
|
||||
b.addAll(i)
|
||||
positional.addAll(i)
|
||||
}
|
||||
else -> scope.raiseClassCastError("expected list of objects for splat argument")
|
||||
}
|
||||
} else {
|
||||
b.add(value)
|
||||
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 b.build(tailBlockMode)
|
||||
} finally {
|
||||
b.release()
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
else -> scope.raiseClassCastError("expected list of objects for splat argument")
|
||||
}
|
||||
} else {
|
||||
list.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()
|
||||
val named = tryParseNamedArg()
|
||||
if (named != null) {
|
||||
args += named
|
||||
} else {
|
||||
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
|
||||
?: throw ScriptError(t.pos, "Expecting arguments list")
|
||||
if (cc.current().type == Token.Type.COLON)
|
||||
parseTypeDeclaration()
|
||||
// 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()
|
||||
val named = tryParseNamedArg()
|
||||
if (named != null) {
|
||||
args += named
|
||||
} else {
|
||||
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
|
||||
?: throw ScriptError(t.pos, "Expecting arguments list")
|
||||
if (cc.current().type == Token.Type.COLON)
|
||||
parseTypeDeclaration()
|
||||
// Do not parse type declarations in call args
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (t.type != Token.Type.RPAREN)
|
||||
|
||||
154
lynglib/src/commonTest/kotlin/NamedArgsTest.kt
Normal file
154
lynglib/src/commonTest/kotlin/NamedArgsTest.kt
Normal file
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,4 +63,29 @@ class HighlightSmokeTest {
|
||||
assertTrue(html.contains("hl-op"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun highlightNamedArgsAndMapSplat() {
|
||||
val text = """
|
||||
fun test(a,b,c,d) { [a,b,c,d] }
|
||||
val r = test("A?", c: "C!", ...Map("d" => "D!", "b" => "B!"))
|
||||
""".trimIndent()
|
||||
val spans = SimpleLyngHighlighter().highlight(text)
|
||||
val labeled = spansToLabeled(text, spans)
|
||||
// Ensure identifier for function name appears
|
||||
assertTrue(labeled.any { it.first == "test" && it.second == HighlightKind.Identifier })
|
||||
// Ensure colon for named argument is tokenized as punctuation
|
||||
assertTrue(labeled.any { it.first == ":" && it.second == HighlightKind.Punctuation })
|
||||
// Ensure ellipsis operator present
|
||||
assertTrue(labeled.any { it.first == "..." && it.second == HighlightKind.Operator })
|
||||
// Ensure Map identifier is present
|
||||
assertTrue(labeled.any { it.first == "Map" && it.second == HighlightKind.Identifier })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun highlightNamedArgsHtml() {
|
||||
val text = "val res = test( a: 1, b: 2 )"
|
||||
val html = SiteHighlight.renderHtml(text)
|
||||
// Expect a colon wrapped as punctuation span
|
||||
assertTrue(html.contains("<span class=\"hl-punc\">:</span>"))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user