Skip to content

Commit 4c63e7d

Browse files
CopilotjonathanpeppersCopilot
authored
Fix incorrect codegen when two byte locals are reassigned from runtime intermediates (#539)
* Initial plan * Fix incorrect codegen for dual-assignment from runtime intermediates Agent-Logs-Url: https://github.com/jonathanpeppers/dotnes/sessions/41175c46-2f90-4f80-9967-605c064e987b Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> * Address review: tighten test assertions, drop misleading test - Anchor TEMP-reload assertions to the specific STA/LDA/STA sequence around the two byte local stores so we don't false-match on STA/LDA \ emitted by built-in subroutines. - Drop TwoLocals_UshortIntermediates_DualAssignBack_ReversedOrder: in the reversed source order Roslyn doesn't elide the intermediate stloc, so the recovery emit never fires. The original assertion was passing only because of A5 17 inside NTADR_A built-in code, making it a false-positive regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a01715b commit 4c63e7d

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

src/dotnes.tasks/Utilities/IL2NESWriter.LocalVariables.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,40 @@ void WriteStloc(Local local, bool isNewAllocation = true)
7676
_firstAndAfterPadPoll = false;
7777
_ushortInAX = false;
7878

79+
// Detect the "two pending IL stack values" pattern produced when
80+
// Roslyn keeps the second computed value on the IL stack across a
81+
// stloc, e.g.:
82+
// x1 = (byte)nx;
83+
// y1 = (byte)ny; // ny was computed earlier, never stored
84+
// becomes IL:
85+
// ldloc nx; conv.u1; stloc x1; conv.u1; stloc y1
86+
// with ny already on the evaluation stack BELOW nx. WriteLdloc saves
87+
// the runtime ny to TEMP ($17) before loading nx into A, but the
88+
// subsequent stloc would otherwise reuse A (still nx) for both stores.
89+
// When we see the buffer pattern [STA $TEMP, LDA absolute, STA absolute],
90+
// this stloc must consume the saved TEMP value instead of A.
91+
if (!local.IsWord && local.Value <= byte.MaxValue
92+
&& local.LabelName is null && CurrentBlock is { } block
93+
&& block.Count >= 3)
94+
{
95+
var last = block[block.Count - 1];
96+
var prev = block[block.Count - 2];
97+
var prev2 = block[block.Count - 3];
98+
if (last.Opcode == Opcode.STA && last.Mode == AddressMode.Absolute
99+
&& prev.Opcode == Opcode.LDA && prev.Mode == AddressMode.Absolute
100+
&& prev2.Opcode == Opcode.STA && prev2.Mode == AddressMode.ZeroPage
101+
&& prev2.Operand is ImmediateOperand tempOp && tempOp.Value == (byte)NESConstants.TEMP)
102+
{
103+
if (isNewAllocation) LocalCount += 1;
104+
Emit(Opcode.LDA, AddressMode.ZeroPage, (byte)NESConstants.TEMP);
105+
Emit(Opcode.STA, AddressMode.Absolute, (ushort)local.Address);
106+
_runtimeValueInA = false;
107+
_savedRuntimeToTemp = false;
108+
_immediateInA = null;
109+
return;
110+
}
111+
}
112+
79113
if (_ntadrRuntimeResult)
80114
{
81115
// NTADR result is in TEMP ($17 = hi) and TEMP2 ($19 = lo)

src/dotnes.tests/LocalsTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,4 +529,86 @@ public void TwoLocals_AddModulo_AssignBack()
529529
string between = hex.Substring(tempIdx, temp2Idx - tempIdx);
530530
Assert.Contains("20", between);
531531
}
532+
533+
[Fact]
534+
public void TwoLocals_UshortIntermediates_DualAssignBack()
535+
{
536+
// Regression test from issue #516:
537+
// ushort intermediates nx/ny, both byte locals reassigned in sequence.
538+
// Roslyn optimizes away `stloc ny` and keeps ny on the IL evaluation
539+
// stack across the next `stloc x1`. The transpiler must recover ny
540+
// from TEMP for the subsequent `stloc y1`, otherwise y1 gets the
541+
// value of nx (producing position (2,2) instead of (2,3)).
542+
var bytes = GetProgramBytes("""
543+
byte y1 = 1;
544+
byte x1 = 1;
545+
ushort nx;
546+
ushort ny;
547+
nx = (byte)((x1 + y1) % 32);
548+
ny = (byte)((x1 + y1 * 2) % 16);
549+
x1 = (byte)nx;
550+
y1 = (byte)ny;
551+
vrambuf_put(NTADR_A(x1, y1), "B");
552+
while (true) ;
553+
""");
554+
var hex = Convert.ToHexString(bytes);
555+
556+
// ny computation must save its runtime result to TEMP ($17): 8517
557+
Assert.Contains("8517", hex);
558+
559+
// After storing x1=nx (STA $0326 = 8D2603), the next stloc must
560+
// LDA TEMP (A517) to recover ny before STA y1 (8D2503).
561+
// This is the exact sequence that the fix introduces.
562+
Assert.Contains("8D2603A5178D2503", hex);
563+
}
564+
565+
[Fact]
566+
public void TwoByteLocals_RuntimeIntermediates_DualAssignBack()
567+
{
568+
// Same pattern but using byte intermediates instead of ushort. The
569+
// fix should generalize: when Roslyn keeps a runtime value on the
570+
// IL stack across an intervening stloc, the next stloc must recover
571+
// it from TEMP.
572+
var bytes = GetProgramBytes("""
573+
byte y1 = 1;
574+
byte x1 = 1;
575+
byte nx;
576+
byte ny;
577+
nx = (byte)((x1 + y1) % 32);
578+
ny = (byte)((x1 + y1 * 2) % 16);
579+
x1 = nx;
580+
y1 = ny;
581+
vrambuf_put(NTADR_A(x1, y1), "B");
582+
while (true) ;
583+
""");
584+
var hex = Convert.ToHexString(bytes);
585+
// Anchored recovery sequence: STA $0326 (x1=nx) → LDA $17 (recover ny)
586+
// → STA $0325 (y1=ny). Matching the full sequence avoids false
587+
// positives from STA/LDA $17 inside built-in subroutines.
588+
Assert.Contains("8D2603A5178D2503", hex);
589+
}
590+
591+
[Fact]
592+
public void TwoLocals_UshortIntermediates_NoNTADR()
593+
{
594+
// Same pattern without NTADR_A — verify the fix is independent of
595+
// the call site. After the dual assignment, x1+y1 should be 5
596+
// (nx=2, ny=3 → x1=2, y1=3 → 2+3=5), so pal_col(2, 5) is emitted.
597+
var bytes = GetProgramBytes("""
598+
byte y1 = 1;
599+
byte x1 = 1;
600+
ushort nx;
601+
ushort ny;
602+
nx = (byte)((x1 + y1) % 32);
603+
ny = (byte)((x1 + y1 * 2) % 16);
604+
x1 = (byte)nx;
605+
y1 = (byte)ny;
606+
pal_col(x1, y1);
607+
while (true) ;
608+
""");
609+
var hex = Convert.ToHexString(bytes);
610+
// Anchored recovery sequence around the two byte local stores,
611+
// independent of the call site (pal_col instead of NTADR_A).
612+
Assert.Contains("8D2603A5178D2503", hex);
613+
}
532614
}

0 commit comments

Comments
 (0)