fix #95 destructuring assignment and splats
This commit is contained in:
parent
3acd56f55a
commit
0add0ab54c
28
docs/List.md
28
docs/List.md
@ -92,6 +92,34 @@ Open end ranges remove head and tail elements:
|
||||
assert( [1, 2, 3] !== [1, 2, 3])
|
||||
>>> void
|
||||
|
||||
## Destructuring
|
||||
|
||||
Lists can be used as L-values for destructuring assignments. This allows you to unpack list elements into multiple variables.
|
||||
|
||||
### Basic Destructuring
|
||||
```lyng
|
||||
val [a, b, c] = [1, 2, 3]
|
||||
```
|
||||
|
||||
### With Splats (Variadic)
|
||||
A single ellipsis `...` can be used to capture remaining elements into a list. It can be placed at the beginning, middle, or end of the pattern.
|
||||
```lyng
|
||||
val [head, rest...] = [1, 2, 3] // head=1, rest=[2, 3]
|
||||
val [first, middle..., last] = [1, 2, 3, 4, 5] // first=1, middle=[2, 3, 4], last=5
|
||||
```
|
||||
|
||||
### Nested Patterns
|
||||
Destructuring patterns can be nested to unpack multi-dimensional lists.
|
||||
```lyng
|
||||
val [a, [b, c...], d] = [1, [2, 3, 4], 5]
|
||||
```
|
||||
|
||||
### Reassignment
|
||||
Destructuring can also be used to reassign existing variables:
|
||||
```lyng
|
||||
[x, y] = [y, x] // Swap values
|
||||
```
|
||||
|
||||
## In-place sort
|
||||
|
||||
List could be sorted in place, just like [Collection] provide sorted copies, in a very like way:
|
||||
|
||||
@ -75,6 +75,13 @@ destructuring arrays when calling functions and lambdas:
|
||||
getFirstAndLast( ...(1..10) ) // see "splats" section below
|
||||
>>> [1,10]
|
||||
|
||||
Note that array destructuring can also be used in assignments:
|
||||
|
||||
val [first, middle..., last] = [1, 2, 3, 4, 5]
|
||||
[x, y] = [y, x] // Swap
|
||||
|
||||
See [tutorial] and [List] documentation for more details on destructuring assignments.
|
||||
|
||||
# Splats
|
||||
|
||||
Ellipsis allows to convert argument lists to lists. The inversa algorithm that converts [List],
|
||||
@ -155,3 +162,4 @@ If a call is immediately followed by a block `{ ... }`, it is treated as an extr
|
||||
|
||||
|
||||
[tutorial]: tutorial.md
|
||||
[List]: List.md
|
||||
|
||||
@ -107,6 +107,41 @@ Assignemnt is an expression that changes its lvalue and return assigned value:
|
||||
>>> 11
|
||||
>>> 6
|
||||
|
||||
### Destructuring assignments
|
||||
|
||||
Lyng supports destructuring assignments for lists. This allows you to unpack list elements into multiple variables at once:
|
||||
|
||||
val [a, b, c] = [1, 2, 3]
|
||||
assertEquals(1, a)
|
||||
assertEquals(2, b)
|
||||
assertEquals(3, c)
|
||||
|
||||
It also supports *splats* (ellipsis) to capture multiple elements into a list:
|
||||
|
||||
val [head, rest...] = [1, 2, 3]
|
||||
assertEquals(1, head)
|
||||
assertEquals([2, 3], rest)
|
||||
|
||||
val [first, middle..., last] = [1, 2, 3, 4, 5]
|
||||
assertEquals(1, first)
|
||||
assertEquals([2, 3, 4], middle)
|
||||
assertEquals(5, last)
|
||||
|
||||
Destructuring can be nested:
|
||||
|
||||
val [x, [y, z...]] = [1, [2, 3, 4]]
|
||||
assertEquals(1, x)
|
||||
assertEquals(2, y)
|
||||
assertEquals([3, 4], z)
|
||||
|
||||
And it can be used for reassigning existing variables, for example, to swap values:
|
||||
|
||||
var x = 5
|
||||
var y = 10
|
||||
[x, y] = [y, x]
|
||||
assertEquals(10, x)
|
||||
assertEquals(5, y)
|
||||
|
||||
As the assignment itself is an expression, you can use it in strange ways. Just remember
|
||||
to use parentheses as assignment operation insofar is left-associated and will not
|
||||
allow chained assignments (we might fix it later). Use parentheses insofar:
|
||||
|
||||
@ -674,12 +674,19 @@ class Compiler(
|
||||
Token.Type.RBRACKET -> return entries
|
||||
Token.Type.ELLIPSIS -> {
|
||||
parseExpressionLevel()?.let { entries += ListEntry.Spread(it) }
|
||||
?: throw ScriptError(t.pos, "spread element must have an expression")
|
||||
}
|
||||
|
||||
else -> {
|
||||
cc.previous()
|
||||
parseExpressionLevel()?.let { entries += ListEntry.Element(it) }
|
||||
?: throw ScriptError(t.pos, "invalid list literal: expecting expression")
|
||||
parseExpressionLevel()?.let { expr ->
|
||||
if (cc.current().type == Token.Type.ELLIPSIS) {
|
||||
cc.next()
|
||||
entries += ListEntry.Spread(expr)
|
||||
} else {
|
||||
entries += ListEntry.Element(expr)
|
||||
}
|
||||
} ?: throw ScriptError(t.pos, "invalid list literal: expecting expression")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2551,11 +2558,51 @@ class Compiler(
|
||||
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
|
||||
isStatic: Boolean = false
|
||||
): Statement {
|
||||
val nameToken = cc.next()
|
||||
val start = nameToken.pos
|
||||
if (nameToken.type != Token.Type.ID)
|
||||
throw ScriptError(nameToken.pos, "Expected identifier here")
|
||||
val name = nameToken.value
|
||||
val nextToken = cc.next()
|
||||
val start = nextToken.pos
|
||||
|
||||
if (nextToken.type == Token.Type.LBRACKET) {
|
||||
// Destructuring
|
||||
if (isStatic) throw ScriptError(start, "static destructuring is not supported")
|
||||
|
||||
val entries = parseArrayLiteral()
|
||||
val pattern = ListLiteralRef(entries)
|
||||
|
||||
// Register all names in the pattern
|
||||
pattern.forEachVariable { name -> declareLocalName(name) }
|
||||
|
||||
val eqToken = cc.next()
|
||||
if (eqToken.type != Token.Type.ASSIGN)
|
||||
throw ScriptError(eqToken.pos, "destructuring declaration must be initialized")
|
||||
|
||||
val initialExpression = parseStatement(true)
|
||||
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
|
||||
|
||||
val names = mutableListOf<String>()
|
||||
pattern.forEachVariable { names.add(it) }
|
||||
|
||||
return statement(start) { context ->
|
||||
val value = initialExpression.execute(context)
|
||||
for (name in names) {
|
||||
context.addItem(name, true, ObjVoid, visibility)
|
||||
}
|
||||
pattern.setAt(start, context, value)
|
||||
if (!isMutable) {
|
||||
for (name in names) {
|
||||
val rec = context.objects[name]!!
|
||||
val immutableRec = rec.copy(isMutable = false)
|
||||
context.objects[name] = immutableRec
|
||||
context.localBindings[name] = immutableRec
|
||||
context.updateSlotFor(name, immutableRec)
|
||||
}
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
if (nextToken.type != Token.Type.ID)
|
||||
throw ScriptError(nextToken.pos, "Expected identifier or [ here")
|
||||
val name = nextToken.value
|
||||
|
||||
// Optional explicit type annotation
|
||||
val varTypeMini: MiniTypeRef? = if (cc.current().type == Token.Type.COLON) {
|
||||
@ -2597,7 +2644,7 @@ class Compiler(
|
||||
type = varTypeMini,
|
||||
initRange = initR,
|
||||
doc = pendingDeclDoc,
|
||||
nameStart = nameToken.pos
|
||||
nameStart = start
|
||||
)
|
||||
miniSink?.onValDecl(node)
|
||||
pendingDeclDoc = null
|
||||
@ -2621,14 +2668,14 @@ class Compiler(
|
||||
// Determine declaring class (if inside class body) at compile time, capture it in the closure
|
||||
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name
|
||||
|
||||
return statement(nameToken.pos) { context ->
|
||||
return statement(start) { context ->
|
||||
// In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions
|
||||
// Do NOT infer declaring class from runtime thisObj here; only the compile-time captured
|
||||
// ClassBody qualifies for class-field storage. Otherwise, this is a plain local.
|
||||
val declaringClassName = declaringClassNameCaptured
|
||||
if (declaringClassName == null) {
|
||||
if (context.containsLocal(name))
|
||||
throw ScriptError(nameToken.pos, "Variable $name is already defined")
|
||||
throw ScriptError(start, "Variable $name is already defined")
|
||||
}
|
||||
|
||||
// Register the local name so subsequent identifiers can be emitted as fast locals
|
||||
@ -2661,7 +2708,7 @@ class Compiler(
|
||||
if (isClassScope) {
|
||||
val cls = context.thisObj as ObjClass
|
||||
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
|
||||
val initStmt = statement(nameToken.pos) { scp ->
|
||||
val initStmt = statement(start) { scp ->
|
||||
val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: ObjNull
|
||||
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
|
||||
scp.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||
|
||||
@ -297,6 +297,10 @@ open class Scope(
|
||||
return idx
|
||||
}
|
||||
|
||||
fun updateSlotFor(name: String, record: ObjRecord) {
|
||||
nameToSlot[name]?.let { slots[it] = record }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this scope instance so it can be safely reused as a fresh child frame.
|
||||
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||
|
||||
@ -27,6 +27,7 @@ import net.sergeych.lyng.*
|
||||
*/
|
||||
sealed interface ObjRef {
|
||||
suspend fun get(scope: Scope): ObjRecord
|
||||
|
||||
/**
|
||||
* Fast path for evaluating an expression to a raw Obj value without wrapping it into ObjRecord.
|
||||
* Default implementation calls [get] and returns its value. Nodes can override to avoid record traffic.
|
||||
@ -35,6 +36,12 @@ sealed interface ObjRef {
|
||||
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
throw ScriptError(pos, "can't assign value")
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls [block] for each variable name that this reference targets for writing.
|
||||
* Used for declaring local variables in destructuring.
|
||||
*/
|
||||
fun forEachVariable(block: (String) -> Unit) {}
|
||||
}
|
||||
|
||||
/** Runtime-computed read-only reference backed by a lambda. */
|
||||
@ -1215,6 +1222,9 @@ class MethodCallRef(
|
||||
* Reference to a local/visible variable by name (Phase A: scope lookup).
|
||||
*/
|
||||
class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
override fun forEachVariable(block: (String) -> Unit) {
|
||||
block(name)
|
||||
}
|
||||
// Per-frame slot cache to avoid repeated name lookups
|
||||
private var cachedFrameId: Long = 0L
|
||||
private var cachedSlot: Int = -1
|
||||
@ -1453,6 +1463,9 @@ class FastLocalVarRef(
|
||||
private val name: String,
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
override fun forEachVariable(block: (String) -> Unit) {
|
||||
block(name)
|
||||
}
|
||||
// Cache the exact scope frame that owns the slot, not just the current frame
|
||||
private var cachedOwnerScope: Scope? = null
|
||||
private var cachedOwnerFrameId: Long = 0L
|
||||
@ -1610,6 +1623,15 @@ class FastLocalVarRef(
|
||||
}
|
||||
|
||||
class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
||||
override fun forEachVariable(block: (String) -> Unit) {
|
||||
for (e in entries) {
|
||||
when (e) {
|
||||
is ListEntry.Element -> e.ref.forEachVariable(block)
|
||||
is ListEntry.Spread -> e.ref.forEachVariable(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
// Heuristic capacity hint: count element entries; spreads handled opportunistically
|
||||
val elemCount = entries.count { it is ListEntry.Element }
|
||||
@ -1633,7 +1655,54 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
||||
}
|
||||
}
|
||||
}
|
||||
return ObjList(list).asReadonly
|
||||
return ObjList(list).asMutable
|
||||
}
|
||||
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
val sourceList = (newValue as? ObjList)?.list
|
||||
?: throw ScriptError(pos, "destructuring assignment requires a list on the right side")
|
||||
|
||||
val ellipsisIdx = entries.indexOfFirst { it is ListEntry.Spread }
|
||||
if (entries.count { it is ListEntry.Spread } > 1) {
|
||||
throw ScriptError(pos, "destructuring pattern can have only one splat")
|
||||
}
|
||||
|
||||
if (ellipsisIdx < 0) {
|
||||
if (sourceList.size < entries.size)
|
||||
throw ScriptError(pos, "too few elements for destructuring")
|
||||
for (i in entries.indices) {
|
||||
val entry = entries[i]
|
||||
if (entry is ListEntry.Element) {
|
||||
entry.ref.setAt(pos, scope, sourceList[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val headCount = ellipsisIdx
|
||||
val tailCount = entries.size - ellipsisIdx - 1
|
||||
if (sourceList.size < headCount + tailCount)
|
||||
throw ScriptError(pos, "too few elements for destructuring")
|
||||
|
||||
// head
|
||||
for (i in 0 until headCount) {
|
||||
val entry = entries[i]
|
||||
if (entry is ListEntry.Element) {
|
||||
entry.ref.setAt(pos, scope, sourceList[i])
|
||||
}
|
||||
}
|
||||
|
||||
// tail
|
||||
for (i in 0 until tailCount) {
|
||||
val entry = entries[entries.size - 1 - i]
|
||||
if (entry is ListEntry.Element) {
|
||||
entry.ref.setAt(pos, scope, sourceList[sourceList.size - 1 - i])
|
||||
}
|
||||
}
|
||||
|
||||
// ellipsis
|
||||
val spreadEntry = entries[ellipsisIdx] as ListEntry.Spread
|
||||
val spreadList = sourceList.subList(headCount, sourceList.size - tailCount)
|
||||
spreadEntry.ref.setAt(pos, scope, ObjList(spreadList.toMutableList()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4334,12 +4334,45 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
|
||||
// @Test
|
||||
// fun testSplatAssignemnt() = runTest {
|
||||
// eval("""
|
||||
// val abc = [1, 2, 3]
|
||||
// val [a, b, c] = ...abc
|
||||
// println(a, b, c)
|
||||
// """.trimIndent())
|
||||
// }
|
||||
@Test
|
||||
fun testDestructuringAssignment() = runTest {
|
||||
eval("""
|
||||
val abc = [1, 2, 3]
|
||||
// plain:
|
||||
val [a, b, c] = abc
|
||||
assertEquals( 1, a )
|
||||
assertEquals( 2, b )
|
||||
assertEquals( 3, c )
|
||||
// with splats, receiving into list:
|
||||
val [ab..., c1] = abc
|
||||
assertEquals( [1, 2], ab )
|
||||
assertEquals( 3, c1 )
|
||||
// also in the end
|
||||
val [a1, rest...] = abc
|
||||
assertEquals( 1, a1 )
|
||||
assertEquals( [2, 3], rest )
|
||||
// and in the middle
|
||||
val [a_mid, middle..., e0, e1] = [ 1, 2, 3, 4, 5, 6]
|
||||
assertEquals( [2, 3, 4], middle )
|
||||
assertEquals( 5, e0 )
|
||||
assertEquals( 6, e1 )
|
||||
assertEquals( 1, a_mid )
|
||||
|
||||
// nested destructuring:
|
||||
val [n1, [n2, n3...], n4] = [1, [2, 3, 4], 5]
|
||||
assertEquals(1, n1)
|
||||
assertEquals(2, n2)
|
||||
assertEquals([3, 4], n3)
|
||||
assertEquals(5, n4)
|
||||
|
||||
// also it could be used to reassign vars:
|
||||
var x = 5
|
||||
var y = 10
|
||||
|
||||
[x, y] = [y, x]
|
||||
assertEquals( 10, x )
|
||||
assertEquals( 5, y )
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user