-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Open
Description
When a lazily-loaded struct is modified and then self-assigned, the compiler loses track of the preserved tail slice, causing unloaded fields to serialize as null.
struct S { b: int32; c: int32?; }
fun onInternalMessage() {
val initial = S{ b: 2, c: 3 }.toCell();
var st = lazy S.fromCell(initial);
st.b += 1;
val good = st.toCell();
val bad = (st = st).toCell();
val shouldBe = S{ b: 3, c: 3 }.toCell();
val becomesBad = S{ b: 3, c: null }.toCell();
return (good.hash() == shouldBe.hash()) && (bad.hash() == becomesBad.hash());
}
Returns -1 (true) — confirming good has c=3 but bad has c=null.
When st.b is modified, the compiler preserves the unparsed tail containing c. On st.toCell() it appends this tail directly with STSLICE:
// val good = st.toCell()
DUP
NEWC
32 STI
s1 s2 XCHG
STSLICE // appends preserved tail containing c=3
ENDC
But st = st breaks tail tracking. The compiler switches to field-by-field serialization, reading st.c from the stack where it's still null:
// val bad = (st = st).toCell()
SWAP
NEWC
32 STI
s2 PUSH // st.c from stack — never loaded, still null
ISNULL
IF:<{
1 2 BLKDROP2
b{0} STSLICECONST // serializes c as absent
}>ELSE<{
...
}>
ENDC
Self-assignment should be a semantic no-op but silently corrupts unloaded fields.
Metadata
Metadata
Assignees
Labels
No labels