fix #95 destructuring assignment and splats
This commit is contained in:
1 parent
3acd56f55a
commit
0add0ab54c
7 files changed
+244
-20
No files matched your search
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user