Transpiler: support structs with fixed-size array fields (Option A and B)#544
Conversation
Agent-Logs-Url: https://github.com/jonathanpeppers/dotnes/sessions/6da1081e-c84d-4e00-b827-95f5b6947d92 Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR extends dotnes’s IL→6502 pipeline to support struct fields that represent fixed-size byte buffers, enabling C# ports of C-style structs with inline byte arrays via either fixed byte buf[N] or [InlineArray(N)].
Changes:
- Adds struct-layout analysis for fixed buffers / inline arrays and exposes buffer-field byte sizes to the IL writer.
- Implements
ldfldalowering that pattern-matches buffer element reads/writes throughstind.i1/ldind.u1, emitting a singleLDA/STAusingAbsoluteorAbsoluteX. - Adds a skip mechanism (
SkipUntilIndex) to let a handler consume multi-instruction IL patterns, plus new Roslyn tests and updated docs.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/dotnes.tests/StructsTests.cs | Adds Roslyn tests for fixed-buffer and inline-array constant/runtime indexing. |
| src/dotnes.tasks/Utilities/Transpiler.StructAnalysis.cs | Detects buffer-field byte sizes from metadata (FixedBuffer, InlineArray, layout size) and incorporates them into struct sizing. |
| src/dotnes.tasks/Utilities/Transpiler.ILParsing.cs | Skips synthesized helper methods (e.g., inline-array helpers) and improves parsing of some MethodSpecification cases. |
| src/dotnes.tasks/Utilities/Transpiler.cs | Plumbs BufferFieldSizes into writers and adds SkipUntilIndex handling to the main/user-method IL dispatch loops. |
| src/dotnes.tasks/Utilities/IL2NESWriter.StructFields.cs | Implements HandleLdflda buffer access lowering + helpers to extract index/value and emit Absolute/AbsoluteX. |
| src/dotnes.tasks/Utilities/IL2NESWriter.ILDispatch.cs | Dispatches Ldflda and treats Initobj as a no-op for struct locals. |
| src/dotnes.tasks/Utilities/IL2NESWriter.cs | Adds BufferFieldSizes and SkipUntilIndex to support multi-instruction lowering. |
| docs/8bitworkshop-samples.md | Updates docs to reflect the new struct buffer-field capabilities and current limitations. |
Comments suppressed due to low confidence (1)
src/dotnes.tasks/Utilities/Transpiler.cs:411
- There are two statements on one line here (
var labelName = ...; if (...)), which hurts readability and makes future diffs noisier. Please split this into separate lines.
var labelName = $"{methodName}_instruction_{instruction.Offset:X2}"; if (methodWriter.CurrentBlock != null)
methodWriter.CurrentBlock.SetNextLabel(labelName);
- Key BufferFieldSizes by (structType, fieldName) tuple so same-named buffer fields in distinct structs no longer collide. - Validate FixedBuffer element type is System.Byte; reject other element types explicitly instead of silently producing wrong layouts. - Reject arithmetic in buffer index expressions (e.g. buf[i + 1]) by counting Add opcodes during the buffer-access scan. - Throw when the index expression cannot be recognised instead of falling back to index 0. - Remove dead storeValueRuntime flag/out param; storeValueConst already handles the only supported case. - EmitLoadIndexIntoX: reject ldarg-sourced runtime indices outright instead of silently aliasing them to a same-numbered local. - Tests: clarify wording (absolute RAM addresses, not zero-page). - Transpiler.cs: split two statements that were stuck on one line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ dotnes PR Reviewer completed successfully! |
There was a problem hiding this comment.
Review Summary
Verdict:
CI: ✅ All checks passing (build, smoke-test macOS, smoke-test Windows).
What this PR does
Adds transpiler support for struct buffer fields via C# fixed byte buf[N] (Option A) and [InlineArray(N)] (Option B). The implementation pattern-matches multi-instruction IL sequences (ldflda through stind.i1/ldind.u1) and emits direct LDA/STA at computed absolute addresses. Good defensive error handling with explicit TranspileException for unsupported patterns.
Issues
| Sev | Category | Summary |
|---|---|---|
| ❌ | Transpiler correctness | Buffer field stores only work with constant values — cur.layout[0] = someVariable will fail at transpile time |
| Error handling | Bare catch {} in GetInlineArrayLength swallows all exceptions silently |
|
| 💡 | Testing | Single-byte hex assertions ("8D", "AE") are too broad to catch offset bugs |
| 💡 | Code organization | Skip-loop logic duplicated between main and user-method dispatch |
Positives
- Thorough defensive coding — explicit
TranspileExceptionfor every unsupported pattern rather than silent miscompilation - Clean separation into
StructAnalysis(metadata) andStructFields(emission) - Good test coverage of both Option A and B × constant/runtime index combinations
- Well-documented IL pattern shapes in comments
- Parser fixes for
<PrivateImplementationDetails>andMethodSpecificationresolution are correct and targeted
Generated by dotnes PR Reviewer for issue #544 · ● 17.2M
- Rewrite HandleLdflda in IL2NESWriter.StructFields.cs to properly
classify the various IL patterns (fixed buffer with/without add,
inline array first-element ref, inline array element ref, Span path
for runtime indexes). This also fixes a pre-existing bug where
`cur.layout[0] = const` wrote the value to the wrong address
because the lone ldc.i4 was being interpreted as both index and
value (Roslyn elides the add when the index is 0 on a fixed buffer).
- Support runtime store values: `cur.layout[idx] = local` /
`ldsfld` now lower through a value scan + EmitLoadValueIntoA helper.
- Narrow bare `catch { }` in Transpiler.StructAnalysis.cs to
`catch (BadImageFormatException)` so genuine bugs aren't swallowed.
- Extract IL2NESWriter.ConsumeSkip helper and use it from both
dispatch loops so the skip-marker logic can't drift apart.
- Strengthen StructsTests assertions with multi-byte hex patterns
that pin down both the value and the absolute address.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds transpiler support for value-type
structfields that hold a fixed-size byte array, via either C#fixed byte buf[N](Option A,unsafe) or[InlineArray(N)](Option B). Enables porting C code like From Below'sstruct cluster { ... unsigned char layout[4]; ... }without resorting to structure-of-arrays.Changes
Transpiler.StructAnalysis.cs): newTryGetBufferFieldSizereads the[FixedBuffer]attribute, the field's value-type[InlineArray(N)]attribute, or the explicitClassLayoutof Roslyn's<name>e__FixedBufferhelper. Buffer fields contribute their full byte count to the struct layout so following fields land at the correct offset.Ldfldalowering (IL2NESWriter.StructFields.cs): pattern-matches the full buffer-access sub-sequence (fixed buffer: outer + innerldflda; inline array:InlineArrayElementRef/InlineArrayFirstElementRef; inline array runtime index:InlineArrayAsSpan+Span<>.get_Item) through the matchingstind.i1/ldind.u1and emits oneLDA/STAatbase + fieldOffset + index—Absolutefor constant indices,AbsoluteX(withLDX) for runtime indices.Transpiler.cs,IL2NESWriter.cs): newSkipUntilIndexlets a single handler consume a multi-instruction IL pattern; both main and user-method dispatch loops honor it.Initobjdispatched as a no-op soT cur = default;lowers correctly (consistent with existing semantics that treat struct locals as uninitialized zero-page bytes the user code fills in).Transpiler.ILParsing.cs): skip synthesized declaring types like<PrivateImplementationDetails>so Roslyn's emittedInlineArrayElementRef/InlineArrayAsSpanhelpers aren't treated as user methods; resolveMethodSpecificationwhose.Methodis aMethodDefinition(those helpers).RoslynTestsinStructsTestscovering both options × {constant, runtime} indices.docs/8bitworkshop-samples.md): drop "no struct support" notes; mention the new buffer-field capabilities.Example
Both lower identically to direct zero-page
LDA/STA(Absolute for[0], AbsoluteX for[i]):Deferred (stretch items called out in the issue)
refto a user method without copying (call-convention work).samples/.const2D inline-array initializer inside a struct.