Skip to content

Detect closure structs from local functions and throw helpful TranspileException#231

Merged
jonathanpeppers merged 4 commits intomainfrom
copilot/fix-closure-structs-transpiler
Mar 16, 2026
Merged

Detect closure structs from local functions and throw helpful TranspileException#231
jonathanpeppers merged 4 commits intomainfrom
copilot/fix-closure-structs-transpiler

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 15, 2026

When non-static local functions capture outer byte[] variables, the C# compiler generates closure structs (display classes). The transpiler skipped these in DetectStructLayouts() as compiler-generated types, then failed later with a confusing InvalidOperationException: Cannot resolve struct type for local 0 with field 'palette'.

Changes

  • Transpiler.cs — Detect closure structs early in DetectStructLayouts() by checking for DisplayClass in type names. Throws TranspileException naming the captured variables and suggesting workarounds.
  • LocalVariableManager.csResolveStructType() fallback now throws TranspileException mentioning closures as a possible cause instead of a generic InvalidOperationException.
  • IL2NESWriter.Helpers.csNewobj error message updated to mention closures (covers class-based display classes).
  • TestClosureCapturingByteArrayThrows validates the error path end-to-end.

Example error

Closures are not supported. The compiler generated a closure struct
'<>c__DisplayClass0_0' capturing variable(s): palette.
This happens when local functions reference outer variables (like byte[] arrays).
Workaround: pass captured variables as parameters to the function instead,
or inline the local function code into the main body.

Triggered by:

byte[] palette = [0x0F, 0x10, 0x20, 0x30];
apply_palette();
while (true) ;

void apply_palette()
{
    pal_bg(palette); // captures 'palette' → closure struct
}

Fix: make it static and pass as parameter:

static void apply_palette(byte[] pal)
{
    pal_bg(pal);
}
Original prompt

This section details on the original issue you should resolve

<issue_title>Transpiler: closure structs from local functions capturing byte[] arrays</issue_title>
<issue_description>Follow-up from PR #211 (crypto.c sample port).

When local functions in top-level statements reference outer byte[] variables, the C# compiler generates closure structs. The transpiler fails with:

System.InvalidOperationException: Cannot resolve struct type for local 0 with field 'palette'

Root cause

In C# top-level statements, all defined functions are local functions of the implicit Main method. When any local function references a byte[] array from the outer scope, the compiler wraps captured variables in a display class. The transpiler encounters stfld/ldfld on this compiler-generated struct and fails.

Workaround

  1. Inline all capturing functions into the main body (eliminates closures but loses abstraction)
  2. Convert to partial class methods with byte[] parameters (works for functions with few captures)
  3. Use const values instead of byte[] where possible (consts are compile-time inlined, no closure)

Long-term fix

Either teach the transpiler to handle compiler-generated closure types, or provide guidance to users to structure code to avoid closures (e.g., use partial class Program methods with parameters).
</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits March 15, 2026 13:54
…leException

When local functions capture outer byte[] variables, the C# compiler generates
closure structs (display classes). The transpiler now detects these early in
DetectStructLayouts() and throws a TranspileException with clear guidance:
- Explains what closures are and why they happen
- Suggests passing captured variables as parameters instead
- Suggests inlining the local function code

Also improves the fallback error in ResolveStructType() and the Newobj message
to mention closures as a possible cause.

Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
…spileException>

Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix transpiler issue with closure structs from local functions Detect closure structs from local functions and throw helpful TranspileException Mar 15, 2026
Copilot AI requested a review from jonathanpeppers March 15, 2026 14:00
Copy link
Copy Markdown
Owner

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

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

🤖 Looks good! Clean, focused change with solid coverage.

What I reviewed:

  • Closure detection in DetectStructLayouts() correctly catches struct-based display classes before they'd be silently skipped. The DisplayClass name check matches Roslyn's naming convention.
  • Class-based closures would still hit the updated Newobj error message path — good defense in depth.
  • ResolveStructType() fallback changed from InvalidOperationException to TranspileException — consistent error type and provides closure-specific guidance.
  • Test ClosureCapturingByteArrayThrows exercises the exact scenario from issue #226.
  • All 499 tests pass locally (64 analyzer + 435 transpiler).
  • CI is green.

Ready for human review.

@jonathanpeppers jonathanpeppers marked this pull request as ready for review March 15, 2026 14:02
Copilot AI review requested due to automatic review settings March 15, 2026 14:02
Copy link
Copy Markdown
Contributor

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

Improves transpiler diagnostics for C# local-function closures (compiler-generated display-class structs) by failing fast with actionable TranspileException messages, instead of later/unclear failures.

Changes:

  • Detect DisplayClass value types during DetectStructLayouts() and throw a targeted TranspileException listing captured fields + workaround guidance.
  • Update LocalVariableManager.ResolveStructType() to throw TranspileException with closure-related guidance instead of InvalidOperationException.
  • Expand the unsupported newobj guidance text to mention closure generation; add an end-to-end Roslyn test for the closure path.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/dotnes.tasks/Utilities/Transpiler.cs Adds early detection of compiler-generated closure structs (DisplayClass) and throws a more actionable error.
src/dotnes.tasks/Utilities/LocalVariableManager.cs Replaces a generic InvalidOperationException with a closure-aware TranspileException.
src/dotnes.tasks/Utilities/IL2NESWriter.Helpers.cs Improves newobj unsupported-opcode message to mention closures as a common cause.
src/dotnes.tests/RoslynTests.cs Adds an end-to-end test validating closure capture produces a TranspileException.
src/dotnes.tests/LocalVariableManagerTests.cs Updates test to expect TranspileException from ResolveStructType() failure path.

Comment thread src/dotnes.tests/RoslynTests.cs
Assert that the error message includes the captured variable name ('palette')
and workaround guidance, not just the word 'closure'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers merged commit 418f6ec into main Mar 16, 2026
1 check passed
@jonathanpeppers jonathanpeppers deleted the copilot/fix-closure-structs-transpiler branch March 16, 2026 00:50
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.

Transpiler: closure structs from local functions capturing byte[] arrays

3 participants