Conversation
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>
There was a problem hiding this comment.
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
Transpilerto collect static methods beyondMain, store their IL + signature metadata, and emit additionalProgram6502blocks. - Extend
ReflectionCacheto understand user methods so call-site stack bookkeeping can use arg/return metadata. - Update
IL2NESWriterto accept an injectedReflectionCacheand ignoreILOpCode.Ret(withRTSappended inTranspiler).
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. |
| /// <summary> | ||
| /// Set of user-defined method names (for detecting user method calls). | ||
| /// </summary> | ||
| public HashSet<string> UserMethodNames { get; init; } = new(StringComparer.Ordinal); |
There was a problem hiding this comment.
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.
| case ILOpCode.Nop: | ||
| case ILOpCode.Ret: | ||
| // Ret is handled at block level (RTS appended to user method blocks) | ||
| break; |
There was a problem hiding this comment.
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.
| /// </summary> | ||
| public void MergeStringTableEntry(string label, byte[] data) | ||
| { | ||
| if (!_stringTable.Any(s => s.Label == label)) |
There was a problem hiding this comment.
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.
| /// </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))) |
| public void MergeByteArray(ImmutableArray<byte> data) | ||
| { | ||
| _byteArrays.Add(data); | ||
| } |
There was a problem hiding this comment.
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.
| using var methodWriter = new IL2NESWriter(new MemoryStream(), logger: _logger, reflectionCache: reflectionCache) | ||
| { | ||
| Instructions = methodIL, | ||
| UsedMethods = UsedMethods, | ||
| UserMethodNames = new HashSet<string>(UserMethods.Keys, StringComparer.Ordinal), |
There was a problem hiding this comment.
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).
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
Sample
Testing
All 166 tests pass.