Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/dotnes.tasks/Utilities/IL2NESWriter.ILDispatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ public void Write(ILInstruction instruction)
case ILOpCode.Ldarg_3:
{
int argIndex = instruction.OpCode - ILOpCode.Ldarg_0;
if (IsClosureMethod)
{
if (argIndex == ClosureArgIndex)
{
// This arg is the closure struct ref (always last param).
// Set flag so the next ldfld/stfld accesses closure fields.
_pendingClosureAccess = true;
break;
}
// Real params keep their original indices — no shifting needed
}
WriteLdarg(argIndex);
}
break;
Expand Down Expand Up @@ -694,7 +705,19 @@ public void Write(ILInstruction instruction, int operand)
case ILOpCode.Nop:
break;
case ILOpCode.Ldarg_s:
WriteLdarg(operand);
{
int argIndex = operand;
if (IsClosureMethod)
{
if (argIndex == ClosureArgIndex)
{
_pendingClosureAccess = true;
break;
}
// Real params keep their original indices — no shifting needed
}
WriteLdarg(argIndex);
}
break;
case ILOpCode.Ldc_i4:
case ILOpCode.Ldc_i4_s:
Expand Down Expand Up @@ -996,6 +1019,29 @@ public void Write(ILInstruction instruction, int operand)
break;
case ILOpCode.Ldloca_s:
// Load address of local variable — used for struct field access
if (ClosureStructLocalIndex >= 0 && operand == ClosureStructLocalIndex
&& Instructions is not null && ClosureFieldTypes != null)
{
// Determine if this ldloca.s is for closure field init (stfld) or
// a method call. Scan forward: if we hit stfld/ldfld for a closure
// field first, it's initialization. If we hit call first, it's a
// method invocation — skip (closure ref is implicit).
bool isInit = false;
for (int k = Index + 1; k < Math.Min(Index + 12, Instructions.Length); k++)
{
if (Instructions[k].OpCode is ILOpCode.Stfld or ILOpCode.Ldfld
&& Instructions[k].String is string fn
&& ClosureFieldTypes.ContainsKey(fn))
{
isInit = true;
break;
}
if (Instructions[k].OpCode == ILOpCode.Call)
break;
}
if (!isInit)
break; // Skip — closure ref before a call
}
_pendingStructLocal = operand;
break;
case ILOpCode.Ldelema:
Expand Down
107 changes: 107 additions & 0 deletions src/dotnes.tasks/Utilities/IL2NESWriter.StructFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,72 @@ void HandleLdsfld(string fieldName)
/// </summary>
void HandleStfld(string fieldName)
{
// Handle closure struct field store
if (_pendingStructLocal != null
&& ClosureFieldTypes != null
&& ClosureFieldTypes.ContainsKey(fieldName))
{
_pendingStructLocal = null;
int fieldSize = ClosureFieldTypes[fieldName];

if (fieldSize == -1) // byte[] field
{
// Associate the byte array label from the preceding ldtoken
if (_lastByteArrayLabel != null)
{
ClosureFieldLabels[fieldName] = _lastByteArrayLabel;
_lastByteArrayLabel = null;
}
if (Stack.Count > 0) Stack.Pop();
_runtimeValueInA = false;
_ushortInAX = false;
_immediateInA = null;
return;
}
else // scalar field (byte, ushort, short, int captured in closure)
{
ushort addr = ClosureFieldAddresses[fieldName];
int value = Stack.Count > 0 ? Stack.Pop() : 0;

if (fieldSize == 1)
{
if (_runtimeValueInA)
{
Emit(Opcode.STA, AddressMode.Absolute, addr);
_runtimeValueInA = false;
}
else
{
RemoveLastInstructions(1);
Emit(Opcode.LDA, AddressMode.Immediate, (byte)(value & 0xFF));
Emit(Opcode.STA, AddressMode.Absolute, addr);
}
}
else
{
// Multi-byte scalar (16-bit word: low/high bytes)
if (_runtimeValueInA && _ushortInAX)
{
Emit(Opcode.STA, AddressMode.Absolute, addr);
Emit(Opcode.STX, AddressMode.Absolute, (ushort)(addr + 1));
}
else
{
byte low = (byte)(value & 0xFF);
byte high = (byte)((value >> 8) & 0xFF);
Emit(Opcode.LDA, AddressMode.Immediate, low);
Emit(Opcode.STA, AddressMode.Absolute, addr);
Emit(Opcode.LDA, AddressMode.Immediate, high);
Emit(Opcode.STA, AddressMode.Absolute, (ushort)(addr + 1));
}
_runtimeValueInA = false;
_ushortInAX = false;
}
_immediateInA = null;
return;
}
}

// Check for struct array element access (from ldelema)
if (_pendingStructElementType != null)
{
Expand Down Expand Up @@ -212,6 +278,47 @@ void HandleStfld(string fieldName)
/// </summary>
void HandleLdfld(string fieldName)
{
// Handle closure struct field load (from ldarg.0 in closure methods
// or ldloca.s in main)
if ((_pendingClosureAccess || _pendingStructLocal != null)
&& ClosureFieldTypes != null
&& ClosureFieldTypes.ContainsKey(fieldName))
{
_pendingClosureAccess = false;
_pendingStructLocal = null;
int fieldSize = ClosureFieldTypes[fieldName];

if (fieldSize == -1) // byte[] field
{
if (ClosureFieldLabels.TryGetValue(fieldName, out var label))
{
EmitWithLabel(Opcode.LDA, AddressMode.Immediate_LowByte, label);
EmitWithLabel(Opcode.LDX, AddressMode.Immediate_HighByte, label);
_ushortInAX = true;
}
Comment thread
jonathanpeppers marked this conversation as resolved.
else
{
throw new TranspileException(
$"Closure byte[] field '{fieldName}' has no ROM data label. " +
"Ensure the array is initialized before it is used in a closure method.");
}
}
else // scalar field
{
ushort addr = ClosureFieldAddresses[fieldName];
Emit(Opcode.LDA, AddressMode.Absolute, addr);
if (fieldSize > 1)
{
Emit(Opcode.LDX, AddressMode.Absolute, (ushort)(addr + 1));
_ushortInAX = true;
}
}
_runtimeValueInA = true;
_immediateInA = null;
Stack.Push(0);
Comment thread
jonathanpeppers marked this conversation as resolved.
return;
}

// Check for struct array element access (from ldelema)
if (_pendingStructElementType != null)
{
Expand Down
41 changes: 41 additions & 0 deletions src/dotnes.tasks/Utilities/IL2NESWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,47 @@ public IL2NESWriter(Stream stream, bool leaveOpen = false, ILogger? logger = nul
/// </summary>
public HashSet<string> ExternMethodNames { get; init; } = new(StringComparer.Ordinal);

/// <summary>
/// Closure field types: field name → size (-1 for byte[], positive for scalars).
/// When non-null, closure struct support is active.
/// </summary>
public Dictionary<string, int>? ClosureFieldTypes { get; init; }

/// <summary>
/// Closure byte[] field labels: field name → byte array label.
/// Shared between main and user method writers. Populated during main transpilation.
/// </summary>
public Dictionary<string, string> ClosureFieldLabels { get; init; } = new(StringComparer.Ordinal);

/// <summary>
/// Closure scalar field addresses: field name → zero-page address.
/// Pre-allocated and shared between all writers.
/// </summary>
public Dictionary<string, ushort> ClosureFieldAddresses { get; init; } = new(StringComparer.Ordinal);

/// <summary>
/// The IL argument index of the closure struct ref in a closure method (-1 if not a closure method).
/// Roslyn places the closure ref as the LAST parameter, so for a method with N real params,
/// this is N. For 0-param closures it's 0.
/// </summary>
public int ClosureArgIndex { get; init; } = -1;

/// <summary>
/// True when this writer is transpiling a closure-capturing user method.
/// </summary>
public bool IsClosureMethod => ClosureArgIndex >= 0;

/// <summary>
/// The local variable index in main that holds the closure struct instance (-1 if no closure).
/// </summary>
public int ClosureStructLocalIndex { get; init; } = -1;

/// <summary>
/// Set when ldarg.0 is encountered in a closure method, indicating the next ldfld/stfld
/// should access a closure field rather than a struct field.
/// </summary>
bool _pendingClosureAccess;

/// <summary>
/// Generates a branch target label name scoped to the current method.
/// For main(), returns "instruction_XX". For user methods, returns "methodName_instruction_XX".
Expand Down
19 changes: 16 additions & 3 deletions src/dotnes.tasks/Utilities/Transpiler.LocalFrameAllocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ static HashSet<int> DetectWordLocals(ILInstruction[] instructions, ReflectionCac
static int EstimateMethodLocalBytes(
ILInstruction[] instructions,
HashSet<int> wordLocals,
Dictionary<string, List<(string Name, int Size)>>? structLayouts)
Dictionary<string, List<(string Name, int Size)>>? structLayouts,
int closureStructLocalIndex = -1,
Dictionary<string, int>? closureFieldTypes = null)
{
int totalBytes = 0;

Expand Down Expand Up @@ -123,11 +125,19 @@ static int EstimateMethodLocalBytes(
&& !newarrStlocTargets.Contains(ldlocaIdx)
&& !structLocalsCounted.Contains(ldlocaIdx))
{
// Skip closure struct local — its fields are allocated separately
if (ldlocaIdx == closureStructLocalIndex)
continue;

for (int j = i + 1; j < instructions.Length && j <= i + 3; j++)
{
if (instructions[j].OpCode is ILOpCode.Stfld or ILOpCode.Ldfld
&& instructions[j].String is string fieldName)
{
// Skip closure fields — they are pre-allocated
if (closureFieldTypes != null && closureFieldTypes.ContainsKey(fieldName))
break;

int structSize = FindStructSizeByField(fieldName, structLayouts);
if (structSize > 0)
{
Expand Down Expand Up @@ -189,14 +199,17 @@ static Dictionary<string, int> ComputeMethodFrameOffsets(
Dictionary<string, ILInstruction[]> userMethods,
ReflectionCache? reflectionCache,
int baseOffset,
Dictionary<string, List<(string Name, int Size)>>? structLayouts)
Dictionary<string, List<(string Name, int Size)>>? structLayouts,
int closureStructLocalIndex = -1,
Dictionary<string, int>? closureFieldTypes = null)
{
// Step 1: Estimate local byte counts for each method
var localByteCounts = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var kvp in userMethods)
{
var wordLocals = DetectWordLocals(kvp.Value, reflectionCache);
localByteCounts[kvp.Key] = EstimateMethodLocalBytes(kvp.Value, wordLocals, structLayouts);
localByteCounts[kvp.Key] = EstimateMethodLocalBytes(kvp.Value, wordLocals, structLayouts,
closureStructLocalIndex, closureFieldTypes);
}

// Step 2: Build call graph — which user methods does each method call?
Expand Down
Loading