Fix while bytecode scoping and arithmetic fallback

This commit is contained in:
Sergey Chernov 2026-01-30 11:11:43 +03:00
parent 9bc59f4787
commit df48a06311
3 changed files with 191 additions and 45 deletions

View File

@ -173,6 +173,13 @@ class BytecodeCompiler(
if (!allowLocalSlots) return null if (!allowLocalSlots) return null
if (ref.isDelegated) return null if (ref.isDelegated) return null
if (ref.name.isEmpty()) return null if (ref.name.isEmpty()) return null
if (ref.captureOwnerScopeId == null) {
val byName = scopeSlotIndexByName[ref.name]
if (byName != null) {
val resolved = slotTypes[byName] ?: SlotType.UNKNOWN
return CompiledValue(byName, resolved)
}
}
val mapped = resolveSlot(ref) ?: return compileNameLookup(ref.name) val mapped = resolveSlot(ref) ?: return compileNameLookup(ref.name)
var resolved = slotTypes[mapped] ?: SlotType.UNKNOWN var resolved = slotTypes[mapped] ?: SlotType.UNKNOWN
if (resolved == SlotType.UNKNOWN && intLoopVarNames.contains(ref.name)) { if (resolved == SlotType.UNKNOWN && intLoopVarNames.contains(ref.name)) {
@ -191,6 +198,10 @@ class BytecodeCompiler(
is LocalVarRef -> { is LocalVarRef -> {
if (allowLocalSlots) { if (allowLocalSlots) {
if (!forceScopeSlots) { if (!forceScopeSlots) {
scopeSlotIndexByName[ref.name]?.let { slot ->
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
return CompiledValue(slot, resolved)
}
loopSlotOverrides[ref.name]?.let { slot -> loopSlotOverrides[ref.name]?.let { slot ->
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
return CompiledValue(slot, resolved) return CompiledValue(slot, resolved)
@ -415,7 +426,42 @@ class BytecodeCompiler(
b = CompiledValue(b.slot, SlotType.INT) b = CompiledValue(b.slot, SlotType.INT)
} }
val typesMismatch = a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN val typesMismatch = a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN
if (typesMismatch && op !in setOf(BinOp.EQ, BinOp.NEQ, BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE)) { val allowMixedNumeric = op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH)
if (typesMismatch && op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT)) {
val leftObj = ensureObjSlot(a)
val rightObj = ensureObjSlot(b)
val out = allocSlot()
val objOpcode = when (op) {
BinOp.PLUS -> Opcode.ADD_OBJ
BinOp.MINUS -> Opcode.SUB_OBJ
BinOp.STAR -> Opcode.MUL_OBJ
BinOp.SLASH -> Opcode.DIV_OBJ
BinOp.PERCENT -> Opcode.MOD_OBJ
else -> null
} ?: return null
builder.emit(objOpcode, leftObj.slot, rightObj.slot, out)
return CompiledValue(out, SlotType.OBJ)
}
if ((a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) &&
op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT)
) {
val leftObj = ensureObjSlot(a)
val rightObj = ensureObjSlot(b)
val out = allocSlot()
val objOpcode = when (op) {
BinOp.PLUS -> Opcode.ADD_OBJ
BinOp.MINUS -> Opcode.SUB_OBJ
BinOp.STAR -> Opcode.MUL_OBJ
BinOp.SLASH -> Opcode.DIV_OBJ
BinOp.PERCENT -> Opcode.MOD_OBJ
else -> null
} ?: return null
builder.emit(objOpcode, leftObj.slot, rightObj.slot, out)
return CompiledValue(out, SlotType.OBJ)
}
if (typesMismatch && !allowMixedNumeric &&
op !in setOf(BinOp.EQ, BinOp.NEQ, BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE)
) {
return null return null
} }
val out = allocSlot() val out = allocSlot()
@ -876,13 +922,32 @@ class BytecodeCompiler(
if (!allowLocalSlots) return null if (!allowLocalSlots) return null
if (!localTarget.isMutable || localTarget.isDelegated) return null if (!localTarget.isMutable || localTarget.isDelegated) return null
val value = compileRef(assignValue(ref)) ?: return null val value = compileRef(assignValue(ref)) ?: return null
val slot = resolveSlot(localTarget) ?: return null val resolvedSlot = resolveSlot(localTarget) ?: return null
val slot = if (resolvedSlot < scopeSlotCount && localTarget.captureOwnerScopeId == null) {
localSlotIndexByName[localTarget.name]?.let { scopeSlotCount + it } ?: resolvedSlot
} else {
resolvedSlot
}
if (slot < scopeSlotCount && value.type != SlotType.UNKNOWN) { if (slot < scopeSlotCount && value.type != SlotType.UNKNOWN) {
val addrSlot = ensureScopeAddr(slot) val addrSlot = ensureScopeAddr(slot)
emitStoreToAddr(value.slot, addrSlot, value.type) emitStoreToAddr(value.slot, addrSlot, value.type)
if (localTarget.captureOwnerScopeId == null) {
localSlotIndexByName[localTarget.name]?.let { mirror ->
val mirrorSlot = scopeSlotCount + mirror
emitMove(value, mirrorSlot)
updateSlotType(mirrorSlot, value.type)
}
}
} else if (slot < scopeSlotCount) { } else if (slot < scopeSlotCount) {
val addrSlot = ensureScopeAddr(slot) val addrSlot = ensureScopeAddr(slot)
emitStoreToAddr(value.slot, addrSlot, SlotType.OBJ) emitStoreToAddr(value.slot, addrSlot, SlotType.OBJ)
if (localTarget.captureOwnerScopeId == null) {
localSlotIndexByName[localTarget.name]?.let { mirror ->
val mirrorSlot = scopeSlotCount + mirror
emitMove(value, mirrorSlot)
updateSlotType(mirrorSlot, value.type)
}
}
} else { } else {
when (value.type) { when (value.type) {
SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, slot) SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, slot)
@ -2119,13 +2184,48 @@ class BytecodeCompiler(
private fun compileLoopBody(stmt: Statement, needResult: Boolean): CompiledValue? { private fun compileLoopBody(stmt: Statement, needResult: Boolean): CompiledValue? {
val target = if (stmt is BytecodeStatement) stmt.original else stmt val target = if (stmt is BytecodeStatement) stmt.original else stmt
return if (target is BlockStatement) emitInlineBlock(target, needResult) if (target is BlockStatement) {
else compileStatementValueOrFallback(target, needResult) val useInline = target.slotPlan.isEmpty() && target.captureSlots.isEmpty()
return if (useInline) emitInlineBlock(target, needResult) else emitBlock(target, needResult)
}
return compileStatementValueOrFallback(target, needResult)
} }
private fun emitVarDecl(stmt: VarDeclStatement): CompiledValue? { private fun emitVarDecl(stmt: VarDeclStatement): CompiledValue? {
val scopeId = stmt.scopeId ?: 0
val scopeSlot = stmt.slotIndex?.let { slotIndex ->
val key = ScopeSlotKey(scopeId, slotIndex)
scopeSlotMap[key]
} ?: run {
if (scopeId == 0) {
scopeSlotIndexByName[stmt.name]
} else {
null
}
}
if (scopeId == 0 && scopeSlot != null) {
val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run {
builder.emit(Opcode.CONST_NULL, scopeSlot)
updateSlotType(scopeSlot, SlotType.OBJ)
CompiledValue(scopeSlot, SlotType.OBJ)
}
if (value.slot != scopeSlot) {
emitMove(value, scopeSlot)
}
updateSlotType(scopeSlot, value.type)
val declId = builder.addConst(
BytecodeConst.LocalDecl(
stmt.name,
stmt.isMutable,
stmt.visibility,
stmt.isTransient
)
)
builder.emit(Opcode.DECL_LOCAL, declId, scopeSlot)
return CompiledValue(scopeSlot, value.type)
}
val localSlot = if (allowLocalSlots && stmt.slotIndex != null) { val localSlot = if (allowLocalSlots && stmt.slotIndex != null) {
val key = ScopeSlotKey(stmt.scopeId ?: 0, stmt.slotIndex) val key = ScopeSlotKey(scopeId, stmt.slotIndex)
val localIndex = localSlotIndexByKey[key] val localIndex = localSlotIndexByKey[key]
localIndex?.let { scopeSlotCount + it } localIndex?.let { scopeSlotCount + it }
} else { } else {
@ -2152,6 +2252,27 @@ class BytecodeCompiler(
builder.emit(Opcode.DECL_LOCAL, declId, localSlot) builder.emit(Opcode.DECL_LOCAL, declId, localSlot)
return CompiledValue(localSlot, value.type) return CompiledValue(localSlot, value.type)
} }
if (scopeSlot != null) {
val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run {
builder.emit(Opcode.CONST_NULL, scopeSlot)
updateSlotType(scopeSlot, SlotType.OBJ)
CompiledValue(scopeSlot, SlotType.OBJ)
}
if (value.slot != scopeSlot) {
emitMove(value, scopeSlot)
}
updateSlotType(scopeSlot, value.type)
val declId = builder.addConst(
BytecodeConst.LocalDecl(
stmt.name,
stmt.isMutable,
stmt.visibility,
stmt.isTransient
)
)
builder.emit(Opcode.DECL_LOCAL, declId, scopeSlot)
return CompiledValue(scopeSlot, value.type)
}
val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run { val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run {
val slot = allocSlot() val slot = allocSlot()
builder.emit(Opcode.CONST_NULL, slot) builder.emit(Opcode.CONST_NULL, slot)
@ -2839,6 +2960,12 @@ class BytecodeCompiler(
private fun refPos(ref: BinaryOpRef): Pos = Pos.builtIn private fun refPos(ref: BinaryOpRef): Pos = Pos.builtIn
private fun resolveSlot(ref: LocalSlotRef): Int? { private fun resolveSlot(ref: LocalSlotRef): Int? {
val scopeId = refScopeId(ref)
if (scopeId == 0) {
val key = ScopeSlotKey(scopeId, refSlot(ref))
scopeSlotMap[key]?.let { return it }
scopeSlotIndexByName[ref.name]?.let { return it }
}
if (ref.captureOwnerScopeId != null) { if (ref.captureOwnerScopeId != null) {
val scopeKey = ScopeSlotKey(refScopeId(ref), refSlot(ref)) val scopeKey = ScopeSlotKey(refScopeId(ref), refSlot(ref))
return scopeSlotMap[scopeKey] return scopeSlotMap[scopeKey]

View File

@ -298,7 +298,7 @@ class CmdStoreBoolAddr(internal val src: Int, internal val addrSlot: Int) : Cmd(
class CmdIntToReal(internal val src: Int, internal val dst: Int) : Cmd() { class CmdIntToReal(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
frame.setReal(dst, frame.getInt(src).toDouble()) frame.setReal(dst, frame.getReal(src))
return return
} }
} }
@ -319,7 +319,7 @@ class CmdBoolToInt(internal val src: Int, internal val dst: Int) : Cmd() {
class CmdIntToBool(internal val src: Int, internal val dst: Int) : Cmd() { class CmdIntToBool(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
frame.setBool(dst, frame.getInt(src) != 0L) frame.setBool(dst, frame.getBool(src))
return return
} }
} }
@ -1039,7 +1039,7 @@ class CmdPushScope(internal val planId: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val planConst = frame.fn.constants[planId] as? BytecodeConst.SlotPlan val planConst = frame.fn.constants[planId] as? BytecodeConst.SlotPlan
?: error("PUSH_SCOPE expects SlotPlan at $planId") ?: error("PUSH_SCOPE expects SlotPlan at $planId")
frame.pushScope(planConst.plan) frame.pushScope(planConst.plan, planConst.captures)
return return
} }
} }
@ -1504,6 +1504,7 @@ class CmdFrame(
internal val scopeVirtualStack = ArrayDeque<Boolean>() internal val scopeVirtualStack = ArrayDeque<Boolean>()
internal val slotPlanStack = ArrayDeque<Map<String, Int?>>() internal val slotPlanStack = ArrayDeque<Map<String, Int?>>()
internal val slotPlanScopeStack = ArrayDeque<Boolean>() internal val slotPlanScopeStack = ArrayDeque<Boolean>()
private val captureStack = ArrayDeque<List<String>>()
private var scopeDepth = 0 private var scopeDepth = 0
private var virtualDepth = 0 private var virtualDepth = 0
private val iterStack = ArrayDeque<Obj>() private val iterStack = ArrayDeque<Obj>()
@ -1519,7 +1520,11 @@ class CmdFrame(
} }
} }
fun pushScope(plan: Map<String, Int>) { fun pushScope(plan: Map<String, Int>, captures: List<String>) {
val parentScope = scope
if (captures.isNotEmpty() && fn.localSlotNames.isNotEmpty()) {
syncFrameToScope()
}
if (scope.skipScopeCreation) { if (scope.skipScopeCreation) {
val snapshot = scope.applySlotPlanWithSnapshot(plan) val snapshot = scope.applySlotPlanWithSnapshot(plan)
slotPlanStack.addLast(snapshot) slotPlanStack.addLast(snapshot)
@ -1534,6 +1539,14 @@ class CmdFrame(
scope.applySlotPlan(plan) scope.applySlotPlan(plan)
} }
} }
if (captures.isNotEmpty()) {
for (name in captures) {
val rec = parentScope.resolveCaptureRecord(name)
?: parentScope.raiseSymbolNotFound("symbol ${name} not found")
scope.updateSlotFor(name, rec)
}
}
captureStack.addLast(captures)
scopeDepth += 1 scopeDepth += 1
} }
@ -1548,7 +1561,11 @@ class CmdFrame(
} }
scope = scopeStack.removeLastOrNull() scope = scopeStack.removeLastOrNull()
?: error("Scope stack underflow in POP_SCOPE") ?: error("Scope stack underflow in POP_SCOPE")
val captures = captureStack.removeLastOrNull() ?: emptyList()
scopeDepth -= 1 scopeDepth -= 1
if (captures.isNotEmpty() && fn.localSlotNames.isNotEmpty()) {
syncScopeToFrame()
}
} }
fun pushIterator(iter: Obj) { fun pushIterator(iter: Obj) {
@ -1613,7 +1630,7 @@ class CmdFrame(
fun setObj(slot: Int, value: Obj) { fun setObj(slot: Int, value: Obj) {
if (slot < fn.scopeSlotCount) { if (slot < fn.scopeSlotCount) {
val target = resolveScope(scope, fn.scopeSlotDepths[slot]) val target = scope
val index = ensureScopeSlot(target, slot) val index = ensureScopeSlot(target, slot)
target.setSlotValue(index, value) target.setSlotValue(index, value)
} else { } else {
@ -1625,7 +1642,14 @@ class CmdFrame(
return if (slot < fn.scopeSlotCount) { return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot).toLong() getScopeSlotValue(slot).toLong()
} else { } else {
frame.getInt(slot - fn.scopeSlotCount) val local = slot - fn.scopeSlotCount
when (frame.getSlotTypeCode(local)) {
SlotType.INT.code -> frame.getInt(local)
SlotType.REAL.code -> frame.getReal(local).toLong()
SlotType.BOOL.code -> if (frame.getBool(local)) 1L else 0L
SlotType.OBJ.code -> frame.getObj(local).toLong()
else -> 0L
}
} }
} }
@ -1633,7 +1657,7 @@ class CmdFrame(
fun setInt(slot: Int, value: Long) { fun setInt(slot: Int, value: Long) {
if (slot < fn.scopeSlotCount) { if (slot < fn.scopeSlotCount) {
val target = resolveScope(scope, fn.scopeSlotDepths[slot]) val target = scope
val index = ensureScopeSlot(target, slot) val index = ensureScopeSlot(target, slot)
target.setSlotValue(index, ObjInt.of(value)) target.setSlotValue(index, ObjInt.of(value))
} else { } else {
@ -1649,13 +1673,20 @@ class CmdFrame(
return if (slot < fn.scopeSlotCount) { return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot).toDouble() getScopeSlotValue(slot).toDouble()
} else { } else {
frame.getReal(slot - fn.scopeSlotCount) val local = slot - fn.scopeSlotCount
when (frame.getSlotTypeCode(local)) {
SlotType.REAL.code -> frame.getReal(local)
SlotType.INT.code -> frame.getInt(local).toDouble()
SlotType.BOOL.code -> if (frame.getBool(local)) 1.0 else 0.0
SlotType.OBJ.code -> frame.getObj(local).toDouble()
else -> 0.0
}
} }
} }
fun setReal(slot: Int, value: Double) { fun setReal(slot: Int, value: Double) {
if (slot < fn.scopeSlotCount) { if (slot < fn.scopeSlotCount) {
val target = resolveScope(scope, fn.scopeSlotDepths[slot]) val target = scope
val index = ensureScopeSlot(target, slot) val index = ensureScopeSlot(target, slot)
target.setSlotValue(index, ObjReal.of(value)) target.setSlotValue(index, ObjReal.of(value))
} else { } else {
@ -1667,7 +1698,14 @@ class CmdFrame(
return if (slot < fn.scopeSlotCount) { return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot).toBool() getScopeSlotValue(slot).toBool()
} else { } else {
frame.getBool(slot - fn.scopeSlotCount) val local = slot - fn.scopeSlotCount
when (frame.getSlotTypeCode(local)) {
SlotType.BOOL.code -> frame.getBool(local)
SlotType.INT.code -> frame.getInt(local) != 0L
SlotType.REAL.code -> frame.getReal(local) != 0.0
SlotType.OBJ.code -> frame.getObj(local).toBool()
else -> false
}
} }
} }
@ -1675,7 +1713,7 @@ class CmdFrame(
fun setBool(slot: Int, value: Boolean) { fun setBool(slot: Int, value: Boolean) {
if (slot < fn.scopeSlotCount) { if (slot < fn.scopeSlotCount) {
val target = resolveScope(scope, fn.scopeSlotDepths[slot]) val target = scope
val index = ensureScopeSlot(target, slot) val index = ensureScopeSlot(target, slot)
target.setSlotValue(index, if (value) ObjTrue else ObjFalse) target.setSlotValue(index, if (value) ObjTrue else ObjFalse)
} else { } else {
@ -1688,7 +1726,7 @@ class CmdFrame(
} }
fun resolveScopeSlotAddr(scopeSlot: Int, addrSlot: Int) { fun resolveScopeSlotAddr(scopeSlot: Int, addrSlot: Int) {
val target = resolveScope(scope, fn.scopeSlotDepths[scopeSlot]) val target = scope
val index = ensureScopeSlot(target, scopeSlot) val index = ensureScopeSlot(target, scopeSlot)
addrScopes[addrSlot] = target addrScopes[addrSlot] = target
addrIndices[addrSlot] = index addrIndices[addrSlot] = index
@ -1877,10 +1915,7 @@ class CmdFrame(
} }
private fun resolveLocalScope(localIndex: Int): Scope? { private fun resolveLocalScope(localIndex: Int): Scope? {
val depth = fn.localSlotDepths.getOrNull(localIndex) ?: return scope return scope
val relativeDepth = scopeDepth - depth
if (relativeDepth < 0) return null
return if (relativeDepth == 0) scope else resolveScope(scope, relativeDepth)
} }
private fun localSlotToObj(localIndex: Int): Obj { private fun localSlotToObj(localIndex: Int): Obj {
@ -1894,7 +1929,7 @@ class CmdFrame(
} }
private fun getScopeSlotValue(slot: Int): Obj { private fun getScopeSlotValue(slot: Int): Obj {
val target = resolveScope(scope, fn.scopeSlotDepths[slot]) val target = scope
val index = ensureScopeSlot(target, slot) val index = ensureScopeSlot(target, slot)
val record = target.getSlotRecord(index) val record = target.getSlotRecord(index)
if (record.value !== ObjUnset) return record.value if (record.value !== ObjUnset) return record.value
@ -1933,8 +1968,10 @@ class CmdFrame(
if (existing != null) return existing if (existing != null) return existing
} }
val index = fn.scopeSlotIndices[slot] val index = fn.scopeSlotIndices[slot]
if (name == null) {
if (index < target.slotCount) return index if (index < target.slotCount) return index
if (name == null) return index return index
}
target.applySlotPlan(mapOf(name to index)) target.applySlotPlan(mapOf(name to index))
val existing = target.getLocalRecordDirect(name) val existing = target.getLocalRecordDirect(name)
if (existing != null) { if (existing != null) {
@ -1948,18 +1985,5 @@ class CmdFrame(
return index return index
} }
private fun resolveScope(start: Scope, depth: Int): Scope { // Scope depth resolution is no longer used; all scope slots are resolved against the current frame.
if (depth == 0) return start
var effectiveDepth = depth
if (virtualDepth > 0) {
if (effectiveDepth <= virtualDepth) return start
effectiveDepth -= virtualDepth
}
val next = when (start) {
is net.sergeych.lyng.ClosureScope -> start.closureScope
else -> start.parent
}
return next?.let { resolveScope(it, effectiveDepth - 1) }
?: error("Scope depth $depth is out of range")
}
} }

View File

@ -652,7 +652,6 @@ class ScriptTest {
} }
@Ignore("incremental enable")
@Test @Test
fun whileAssignTest() = runTest { fun whileAssignTest() = runTest {
eval( eval(
@ -665,7 +664,6 @@ class ScriptTest {
) )
} }
@Ignore("incremental enable")
@Test @Test
fun whileTest() = runTest { fun whileTest() = runTest {
assertEquals( assertEquals(
@ -720,7 +718,6 @@ class ScriptTest {
) )
} }
@Ignore("incremental enable")
@Test @Test
fun testAssignArgumentsNoEllipsis() = runTest { fun testAssignArgumentsNoEllipsis() = runTest {
// equal args, no ellipsis, no defaults, ok // equal args, no ellipsis, no defaults, ok
@ -762,7 +759,7 @@ class ScriptTest {
assertEquals(ObjInt(5), c["c"]?.value) assertEquals(ObjInt(5), c["c"]?.value)
} }
@Ignore("incremental enable") @Ignore("Scope.eval should seed compile-time symbols from current scope")
@Test @Test
fun testAssignArgumentsEndEllipsis() = runTest { fun testAssignArgumentsEndEllipsis() = runTest {
// equal args, // equal args,
@ -864,7 +861,6 @@ class ScriptTest {
} }
} }
@Ignore("incremental enable")
@Test @Test
fun testWhileBlockIsolation1() = runTest { fun testWhileBlockIsolation1() = runTest {
eval( eval(
@ -881,7 +877,6 @@ class ScriptTest {
) )
} }
@Ignore("incremental enable")
@Test @Test
fun testWhileBlockIsolation2() = runTest { fun testWhileBlockIsolation2() = runTest {
assertFails { assertFails {
@ -924,7 +919,7 @@ class ScriptTest {
) )
} }
@Ignore("incremental enable") @Ignore("bytecode fallback in labeled break")
@Test @Test
fun whileNonLocalBreakTest() = runTest { fun whileNonLocalBreakTest() = runTest {
assertEquals( assertEquals(