Skip to content

Add user-defined function support#86

Merged
jonathanpeppers merged 1 commit intomainfrom
functions
Feb 14, 2026
Merged

Add user-defined function support#86
jonathanpeppers merged 1 commit intomainfrom
functions

Conversation

@jonathanpeppers
Copy link
Owner

Summary

Adds support for user-defined functions in the .NET-to-6502 transpiler. C# static local functions now compile to JSR/RTS subroutine calls in 6502 assembly.

Changes

Transpiler pipeline

  • Transpiler.cs: Reads all static methods from the DLL (not just Main). User methods emitted as Program6502 blocks ending with RTS. Compiler-generated local function names cleaned to original names.
  • ReflectionCache.cs: New user method registry so GetNumberOfArguments() and HasReturnValue() work for user-defined methods alongside NESLib methods.
  • IL2NESWriter.cs: Accepts ReflectionCache via constructor (field stays readonly). Adds UserMethodNames set, merge helpers for sub-writer data, handles ILOpCode.Ret.

Sample

  • rletitle: Extracts fade_in() from inline code into a real static local function, matching rletitle.c. Transpiler emits JSR fade_in / RTS.

Testing

All 166 tests pass.

Extract fade_in() into a real static local function in the rletitle sample,
demonstrating user-defined function support in the .NET-to-6502 transpiler.

Transpiler changes:
- Transpiler.cs: Read all static methods (not just Main), store user method
  IL and metadata. Extract clean names from compiler-generated local function
  names (e.g. <<Main>$>g__fade_in|0_0 -> fade_in). Emit each user method as
  a separate Program6502 block with RTS.
- ReflectionCache.cs: Add user method registry so GetNumberOfArguments() and
  HasReturnValue() work for user-defined methods alongside NESLib methods.
- IL2NESWriter.cs: Accept ReflectionCache via constructor (remains readonly).
  Add UserMethodNames set and merge helpers for string/byte array data from
  user method sub-writers. Handle ILOpCode.Ret as no-op.

The rletitle sample now uses a real fade_in() function that compiles to
JSR fade_in / ... / RTS in 6502, matching the structure of rletitle.c.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 14, 2026 22:32
@jonathanpeppers jonathanpeppers merged commit 23267d3 into main Feb 14, 2026
5 checks passed
@jonathanpeppers jonathanpeppers deleted the functions branch February 14, 2026 22:33
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds initial support for user-defined (static/local) functions in the .NET→6502 transpiler by extracting non-Main static methods from the input DLL and emitting them as separate 6502 subroutine blocks.

Changes:

  • Extend Transpiler to collect static methods beyond Main, store their IL + signature metadata, and emit additional Program6502 blocks.
  • Extend ReflectionCache to understand user methods so call-site stack bookkeeping can use arg/return metadata.
  • Update IL2NESWriter to accept an injected ReflectionCache and ignore ILOpCode.Ret (with RTS appended in Transpiler).

Reviewed changes

Copilot reviewed 4 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/dotnes.tasks/Utilities/Transpiler.cs Collects and emits user-method blocks; merges per-method writer data into main writer.
src/dotnes.tasks/Utilities/ReflectionCache.cs Adds user-method registry for arg-count / return-value queries.
src/dotnes.tasks/Utilities/IL2NESWriter.cs Adds injected ReflectionCache, Ret handling stub, and merge helpers for tables.
samples/rletitle/Program.cs Refactors fade_in() into a local static function to exercise subroutine emission.
src/dotnes.tests/Data/rletitle.debug.dll Updates test input DLL for the sample refactor.
src/dotnes.tests/Data/rletitle.release.dll Updates test input DLL for the sample refactor.
src/dotnes.tests/TranspilerTests.Write.rletitle.verified.bin Updates ROM snapshot for the new output layout.

Comment on lines +156 to +159
/// <summary>
/// Set of user-defined method names (for detecting user method calls).
/// </summary>
public HashSet<string> UserMethodNames { get; init; } = new(StringComparer.Ordinal);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserMethodNames is introduced and populated from Transpiler, but it’s not referenced anywhere in IL2NESWriter (no call-site logic checks it). This makes the extra HashSet allocations dead code. Either wire it into call/label handling, or remove the property/initialization to avoid confusion and overhead.

Copilot uses AI. Check for mistakes.
Comment on lines 216 to 219
case ILOpCode.Nop:
case ILOpCode.Ret:
// Ret is handled at block level (RTS appended to user method blocks)
break;
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ILOpCode.Ret is currently treated as a no-op and the method-level RTS is only appended at the end of the block. Any user method with an early return; (or multiple return sites) will keep executing subsequent IL and only return at the very end, producing incorrect behavior. Consider translating Ret into an RTS at the point it appears (and ensuring subsequent IL in that block is not emitted), rather than relying on a single appended RTS.

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +166
/// </summary>
public void MergeStringTableEntry(string label, byte[] data)
{
if (!_stringTable.Any(s => s.Label == label))
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MergeStringTableEntry merges by label name only (e.g., string_0), but each IL2NESWriter instance generates labels starting from 0. When you transpile user methods with a separate writer, its string_0 / string_1 labels can collide with main’s labels and either be incorrectly deduped or refer to the wrong data. To avoid wrong label resolution, share a single label generator across writers, or remap method-writer labels to unique/global labels when merging.

Suggested change
/// </summary>
public void MergeStringTableEntry(string label, byte[] data)
{
if (!_stringTable.Any(s => s.Label == label))
/// Entries are only deduplicated when both the label and the data match.
/// </summary>
public void MergeStringTableEntry(string label, byte[] data)
{
if (!_stringTable.Any(s => s.Label == label && s.Data.SequenceEqual(data)))

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +176
public void MergeByteArray(ImmutableArray<byte> data)
{
_byteArrays.Add(data);
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MergeByteArray appends raw arrays, but each IL2NESWriter generates bytearray_{n} labels from its own _byteArrayLabelIndex starting at 0. If a user method loads a byte array, its code may reference bytearray_0 while the merged data ends up at a different index/label in the combined writer, producing incorrect addresses or duplicate labels. Consider compiling all blocks with a single writer (shared label/index allocator) or remapping bytearray labels during merge.

Copilot uses AI. Check for mistakes.
Comment on lines +219 to +223
using var methodWriter = new IL2NESWriter(new MemoryStream(), logger: _logger, reflectionCache: reflectionCache)
{
Instructions = methodIL,
UsedMethods = UsedMethods,
UserMethodNames = new HashSet<string>(UserMethods.Keys, StringComparer.Ordinal),
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User methods are transpiled with a separate IL2NESWriter instance, but label generation for string/byte-array tables is per-writer (e.g., string_0, bytearray_0). When blocks from different writers are combined, these labels can collide or point at different data than intended. To avoid incorrect label resolution, use a shared/global label allocator across writers (or remap table labels in the method blocks before adding them to the program).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant