Skip to content

Various fixes for generics support#3338

Merged
josesimoes merged 24 commits into
nanoframework:developfrom
josesimoes:fixes-for-generics
Apr 23, 2026
Merged

Various fixes for generics support#3338
josesimoes merged 24 commits into
nanoframework:developfrom
josesimoes:fixes-for-generics

Conversation

@josesimoes
Copy link
Copy Markdown
Member

@josesimoes josesimoes commented Apr 22, 2026

Description

  • Fix dumping opcodes: now properly resolves types in calls.
  • Fix initializing field from local vars when variable is MVAR: setting to OBJECT which will provide a null object ref.
  • Fix output and processing of VAR type when resolving a token.
  • InlineFrame now stores generic TypeSpec: required to prevent generic type from pointing to a caller's stack-local parameter.
  • Implement CEE_CONSTRAINED opcode.
  • Fix BuildTypeName for MVAR processing.
  • Fix BuildMethodName to deal with MVAR resolution.
  • Fix resolving token for TypeDef Instance for VAR and SZArrayHelper.
  • Fix PushInline for call and callvirt.
  • Fix newobj to properly resolve MVAR constructors.
  • Fix resolving token for TypeDef instance when dealing with MVAR.

Motivation and Context

How Has This Been Tested?

Screenshots

Types of changes

  • Improvement (non-breaking change that improves a feature, code or algorithm)
  • Bug fix (non-breaking change which fixes an issue with code or algorithm)
  • New feature (non-breaking change which adds functionality to code)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Config and build (change in the configuration and build system, has no impact on code or features)
  • Dev Containers (changes related with Dev Containers, has no impact on code or features)
  • Dependencies/declarations (update dependencies or assembly declarations and changes associated, has no impact on code or features)
  • Documentation (changes or updates in the documentation, has no impact on code or features)

Checklist

  • My code follows the code style of this project (only if there are changes in source code).
  • My changes require an update to the documentation (there are changes that require the docs website to be updated).
  • I have updated the documentation accordingly (the changes require an update on the docs in this repo).
  • I have read the CONTRIBUTING document.
  • I have tested everything locally and all new and existing tests passed (only if there are changes in source code).

nfbot and others added 2 commits April 22, 2026 13:59
Automated fixes for code style.
…88c4f-a34f-4599-a78a-537c7de3bc40

Code style fixes for nanoframework/nf-interpreter PR#3338
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Normalize and persist generic-type storage across stack and inline frames; implement constrained. prefix handling for CALLVIRT with BYREF this; change MVAR handling to prefer MethodSpec resolution and treat unresolved MVARs as object during local initialization; adjust generic-context propagation and diagnostic resolution/printing.

Changes

Cohort / File(s) Summary
Stack / Inline Frame storage & normalization
src/CLR/Core/CLR_RT_StackFrame.cpp, src/CLR/Include/nanoCLR_Runtime.h
Added m_genericTypeSpecStorage to inline frames and m_typeSpecStorage + Normalize helper on method instances. Push/PushInline/Restore/Save now copy/restore generic-type storage and normalize m_call with pointer fix-up when genericType previously referenced caller-local storage.
Interpreter: constrained dispatch & generic-context propagation
src/CLR/Core/Interpreter.cpp
Track CEE_CONSTRAINED token and apply it for CEE_CALLVIRT when this is a BYREF (dereference to object for reference types). Revise selection/propagation rules for effective caller generic and methodSpec for both inline and non-inline calls; inherit caller methodSpec for certain open-generic constructors.
Type resolution and naming (MVAR/VAR handling)
src/CLR/Core/TypeSystem.cpp
Refined DATATYPE_VAR/MVAR resolution: avoid resolving mapped VARs, prefer MethodSpec-based MVAR resolution (including chained resolution), use caller presence guards, store method-instance genericType in stable storage, and adjust type/method name formatting to prefer resolved MethodSpec context.
Execution engine: local MVAR initialization
src/CLR/Core/Execution.cpp
Changed InitializeReference to convert unresolved DATATYPE_MVAR to DATATYPE_OBJECT (treat as null object locally) instead of prior assertion-and-retry control flow.
Diagnostics: resolution and display for generics
src/CLR/Diagnostics/Info.cpp
Avoid assert on nested VAR when dumping TypeSpec; attempt resolution/display using caller frame methodSpec for open generics, retry resolution with call frame genericType and propagate methodSpec into resolved instance for accurate printing.
Other minor adjustments
(implicit in summaries)
Small pointer/normalization fixes across restore/save push paths; no exported/public API changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant Interpreter
  participant StackFrame as "Stack/InlineFrame"
  participant TypeSystem
  participant VM as "Dispatch/Runtime"

  Caller->>Interpreter: emit CALLVIRT with CEE_CONSTRAINED token
  Interpreter->>StackFrame: read `this` (BYREF) and constrained token
  alt constrained + BYREF and target is ref type
    Interpreter->>StackFrame: dereference BYREF -> object ref into `this` slot
  end
  Interpreter->>TypeSystem: resolve callee generic context (filter effectiveCallerGeneric / methodSpec)
  TypeSystem-->>Interpreter: resolved callee Instance (genericType/methodSpec normalized)
  Interpreter->>StackFrame: Push/PushInline (copy/normalize generic-type storage)
  Interpreter->>VM: perform virtual dispatch with normalized context
  VM-->>Caller: return
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Various fixes for generics support' is concise, descriptive, and clearly summarizes the main changes across multiple files related to generic type handling and opcode processing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description directly addresses the changeset, listing specific fixes and improvements made across multiple files related to generics support and type resolution.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@josesimoes
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nanoframework nanoframework deleted a comment from nfbot Apr 22, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/CLR/Core/CLR_RT_StackFrame.cpp (1)

326-339: ⚠️ Potential issue | 🟠 Major

Make inline-frame generic context save/restore self-contained.

PushInline saves m_genericTypeSpecStorage, but the saved m_call.genericType can still point at the live stack frame’s storage while the inlined callee may overwrite it. SaveStack/RestoreStack also omit the new field entirely. Preserve the storage and normalize the pointer in every CLR_RT_InlineFrame save/restore path.

🛠️ Proposed fix
     m_inlineFrame->m_frame.m_genericTypeSpecStorage = m_genericTypeSpecStorage;
+    if (m_inlineFrame->m_frame.m_call.genericType == &m_genericTypeSpecStorage)
+    {
+        m_inlineFrame->m_frame.m_call.genericType = &m_inlineFrame->m_frame.m_genericTypeSpecStorage;
+    }
@@
     m_genericTypeSpecStorage = m_inlineFrame->m_frame.m_genericTypeSpecStorage;
     // If the restored m_call.genericType pointed into m_genericTypeSpecStorage, fix the pointer.
     if (m_call.genericType == &m_inlineFrame->m_frame.m_genericTypeSpecStorage)
     {
         m_call.genericType = &m_genericTypeSpecStorage;
     }
@@
     m_localAllocCount = frame.m_localAllocCount;
     for (CLR_INT32 i = 0; i < c_Max_Localloc_Count; i++)
     {
         m_localAllocs[i] = frame.m_localAllocs[i];
     }
+    m_genericTypeSpecStorage = frame.m_genericTypeSpecStorage;
+    if (m_call.genericType == &frame.m_genericTypeSpecStorage)
+    {
+        m_call.genericType = &m_genericTypeSpecStorage;
+    }
 }
@@
     frame.m_localAllocCount = m_localAllocCount;
     for (CLR_INT32 i = 0; i < c_Max_Localloc_Count; i++)
     {
         frame.m_localAllocs[i] = m_localAllocs[i];
     }
+    frame.m_genericTypeSpecStorage = m_genericTypeSpecStorage;
+    if (frame.m_call.genericType == &m_genericTypeSpecStorage)
+    {
+        frame.m_call.genericType = &frame.m_genericTypeSpecStorage;
+    }
 }

Also applies to: 426-480

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLR/Core/CLR_RT_StackFrame.cpp` around lines 326 - 339, The inline-frame
generic context isn't self-contained because PushInline saves
m_genericTypeSpecStorage but leaves m_call.genericType pointing to the live
frame's storage; update PushInline, SaveStack and RestoreStack so every
CLR_RT_InlineFrame saves a copy of m_genericTypeSpecStorage into the inline
frame (CLR_RT_InlineFrame::m_frame.m_genericTypeSpecStorage) and then
normalize/reset the saved m_call.genericType to point at that copied storage
(not the live m_genericTypeSpecStorage); likewise, when restoring, copy the
saved genericTypeSpec storage back (or restore pointers) so restored
m_call.genericType references the inline frame's storage consistently; apply
same fixes to the other save/restore paths referenced around lines 426-480 to
ensure all paths preserve and normalize the generic context pointer.
src/CLR/Core/TypeSystem.cpp (2)

8250-8288: ⚠️ Potential issue | 🔴 Critical

Use the effective generic context before dereferencing genericType.

This branch can be entered because mdInst.genericType is valid while the genericType parameter is null. If a MethodSpec argument is VAR, Line 8285 dereferences *genericType and can crash.

🐛 Proposed fix
                         CLR_RT_TypeSpec_Instance contextTs;
                         CLR_RT_SignatureParser::Element paramElement;
+                        const CLR_RT_TypeSpec_Index *contextGenericType =
+                            (mdInst.genericType != nullptr && NANOCLR_INDEX_IS_VALID(*mdInst.genericType))
+                                ? mdInst.genericType
+                                : genericType;
 
                         // try to resolve from method context
-                        if (!contextTs.InitializeFromIndex(*genericType))
+                        if (contextGenericType == nullptr || !NANOCLR_INDEX_IS_VALID(*contextGenericType) ||
+                            !contextTs.InitializeFromIndex(*contextGenericType))
                         {
                             NANOCLR_SET_AND_LEAVE(CLR_E_FAIL);
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLR/Core/TypeSystem.cpp` around lines 8250 - 8288, The code dereferences
the pointer genericType when resolving VAR generic arguments (in the block
handling msInst from mdInst.methodSpec), which can be null; change the
resolution to use the effective generic context: if genericType is null fall
back to mdInst.genericType (or the method-spec's genericType from msInst) before
calling contextTs.InitializeFromIndex, i.e., compute an effectiveGenericIndex =
genericType ? *genericType : mdInst.genericType (or msInst's appropriate field)
and pass that to contextTs.InitializeFromIndex; update references in the VAR
handling branch (CLR_RT_MethodSpec_Instance msInst, mdInst, contextTs,
parser.Advance) to use effectiveGenericIndex instead of dereferencing
genericType directly.

1315-1365: ⚠️ Potential issue | 🔴 Critical

Guard caller before the nested-VAR fallback.

With the new paramElement.DataType != DATATYPE_VAR path, an open contextTypeSpec can fall through to caller->arrayElementType while caller is null. Also avoid dereferencing a null caller->genericType.

🐛 Proposed fix
-                            else if (caller != nullptr && NANOCLR_INDEX_IS_VALID(*caller->genericType))
+                            else if (caller != nullptr && caller->genericType != nullptr &&
+                                     NANOCLR_INDEX_IS_VALID(*caller->genericType))
                             {
                                 effectiveContext = caller->genericType;
                             }
@@
-                            else if (NANOCLR_INDEX_IS_VALID(caller->arrayElementType) && genericPosition == 0)
+                            else if (caller != nullptr && NANOCLR_INDEX_IS_VALID(caller->arrayElementType) &&
+                                     genericPosition == 0)
                             {
                                 // Fallback to arrayElementType: covers SZArrayHelper scenarios and
                                 // the nested-VAR case where the context TypeSpec is still open.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLR/Core/TypeSystem.cpp` around lines 1315 - 1365, The code can
dereference caller when it's null during the nested-VAR fallback; ensure you
guard all uses of caller and caller->genericType. Specifically, update the
fallback condition that reads "else if
(NANOCLR_INDEX_IS_VALID(caller->arrayElementType) && genericPosition == 0)" to
require caller != nullptr (e.g. "else if (caller != nullptr &&
NANOCLR_INDEX_IS_VALID(caller->arrayElementType) && genericPosition == 0)"), and
audit any other places in this block that dereference caller or
*caller->genericType (such as the earlier genericType check and uses of
caller->arrayElementType.Type/Assembly) to only access them after confirming
caller is non-null; keep the existing resolve_generic_argument/genericPosition
logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CLR/Core/Interpreter.cpp`:
- Around line 2479-2504: The constrained prefix token (constrainedTypeToken) can
persist past the intended call site because it's only cleared inside the
DATATYPE_BYREF branch and only when pThis[0] is a BYREF; fix by gating the
constrained handling to the CEE_CALLVIRT path and ensure constrainedTypeToken is
cleared unconditionally after handling (or after deciding not to handle) so it
cannot leak to later calls. Specifically, in the code around
constrainedTypeToken/pThis/CLR_RT_TypeDef_Instance/ResolveToken/DATATYPE_BYREF/Dereference/Assign,
only attempt the dereference-and-assign logic when the current opcode is
CEE_CALLVIRT and, regardless of whether pThis[0].DataType() == DATATYPE_BYREF or
ResolveToken succeeds, set constrainedTypeToken = 0 before exiting that handling
block so the token is always consumed.

In `@src/CLR/Core/TypeSystem.cpp`:
- Around line 2966-2990: The fallback currently looks up generic parameters
using the token assembly (assm) and can pick the wrong GenericParam table across
assemblies and also treats MethodSpec mappings like "!!U -> !T" as unresolved;
change the fallback to resolve GenericParam using the caller's assembly/context
(use the caller's assembly reference from the stack frame instead of assm when
calling FindGenericParamAtMethodDef and indexing crossReferenceGenericParam)
and, before falling back to the declared constraint, detect MethodSpec-level
mappings (via CLR_RT_MethodSpec_Instance msInst / msInst.GetGenericArgument)
that map a method generic to a type generic and use that resolved type
(msElem.Class) when available; update uses of assm->FindGenericParamAtMethodDef,
assm->crossReferenceGenericParam, and the fallback InitializeFromTypeDef code to
operate on the caller's assembly/context.

In `@src/CLR/Include/nanoCLR_Runtime.h`:
- Around line 2280-2282: The issue is that CLR_RT_MethodDef_Instance becomes
self-referential when InitializeFromIndex sets genericType = &m_typeSpecStorage,
so plain by-value copies leave genericType pointing at the source's
m_typeSpecStorage and can dangle; fix this by adding a proper copy helper for
CLR_RT_MethodDef_Instance: implement a copy constructor and copy assignment (or
a Normalize/Fixup method) that when copying checks if src.genericType ==
&src.m_typeSpecStorage and, if so, copies src.m_typeSpecStorage into the
destination's m_typeSpecStorage and sets dst.genericType =
&dst.m_typeSpecStorage (otherwise copy genericType as-is); then replace the
ad-hoc pointer fixes in CLR_RT_StackFrame::Push, RestoreFromInlineStack,
RestoreStack (and remove manual fixes in Interpreter.cpp and Thread.cpp) by
invoking this copy helper or calling the normalization method immediately after
any by-value assignment of m_call to ensure the self-referential relationship is
preserved.

---

Outside diff comments:
In `@src/CLR/Core/CLR_RT_StackFrame.cpp`:
- Around line 326-339: The inline-frame generic context isn't self-contained
because PushInline saves m_genericTypeSpecStorage but leaves m_call.genericType
pointing to the live frame's storage; update PushInline, SaveStack and
RestoreStack so every CLR_RT_InlineFrame saves a copy of
m_genericTypeSpecStorage into the inline frame
(CLR_RT_InlineFrame::m_frame.m_genericTypeSpecStorage) and then normalize/reset
the saved m_call.genericType to point at that copied storage (not the live
m_genericTypeSpecStorage); likewise, when restoring, copy the saved
genericTypeSpec storage back (or restore pointers) so restored
m_call.genericType references the inline frame's storage consistently; apply
same fixes to the other save/restore paths referenced around lines 426-480 to
ensure all paths preserve and normalize the generic context pointer.

In `@src/CLR/Core/TypeSystem.cpp`:
- Around line 8250-8288: The code dereferences the pointer genericType when
resolving VAR generic arguments (in the block handling msInst from
mdInst.methodSpec), which can be null; change the resolution to use the
effective generic context: if genericType is null fall back to
mdInst.genericType (or the method-spec's genericType from msInst) before calling
contextTs.InitializeFromIndex, i.e., compute an effectiveGenericIndex =
genericType ? *genericType : mdInst.genericType (or msInst's appropriate field)
and pass that to contextTs.InitializeFromIndex; update references in the VAR
handling branch (CLR_RT_MethodSpec_Instance msInst, mdInst, contextTs,
parser.Advance) to use effectiveGenericIndex instead of dereferencing
genericType directly.
- Around line 1315-1365: The code can dereference caller when it's null during
the nested-VAR fallback; ensure you guard all uses of caller and
caller->genericType. Specifically, update the fallback condition that reads
"else if (NANOCLR_INDEX_IS_VALID(caller->arrayElementType) && genericPosition ==
0)" to require caller != nullptr (e.g. "else if (caller != nullptr &&
NANOCLR_INDEX_IS_VALID(caller->arrayElementType) && genericPosition == 0)"), and
audit any other places in this block that dereference caller or
*caller->genericType (such as the earlier genericType check and uses of
caller->arrayElementType.Type/Assembly) to only access them after confirming
caller is non-null; keep the existing resolve_generic_argument/genericPosition
logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: dafc7198-ba79-4b37-a5fd-67f1a4758846

📥 Commits

Reviewing files that changed from the base of the PR and between 96ce2da and 43aba2e.

📒 Files selected for processing (6)
  • src/CLR/Core/CLR_RT_StackFrame.cpp
  • src/CLR/Core/Execution.cpp
  • src/CLR/Core/Interpreter.cpp
  • src/CLR/Core/TypeSystem.cpp
  • src/CLR/Diagnostics/Info.cpp
  • src/CLR/Include/nanoCLR_Runtime.h

Comment thread src/CLR/Core/Interpreter.cpp
Comment thread src/CLR/Core/TypeSystem.cpp
Comment thread src/CLR/Include/nanoCLR_Runtime.h
- Fix PushInline to normalize saved m_call.genericType pointer into inline frame storage.
- Fix SaveStack to save m_genericTypeSpecStorage and normalize pointer.
- Fix RestoreStack to restore m_genericTypeSpecStorage and normalize pointer.
- Fix null dereference of genericType in VAR handling.
- Fix null dereference of caller in arrayElementType fallback.
@josesimoes
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/CLR/Core/Interpreter.cpp (1)

2530-2642: 🧹 Nitpick | 🔵 Trivial

Consider folding the duplicated "TypeSpec refers to callee owner" lambdas.

matchesCalleeOwner (PushInline path, 2530–2540) and refersToCalleeOwner (Push path, 2622–2642) implement the same predicate and each re-derives calleeOwner…TypeDefData by calling calleeInst.GetDeclaringType(). A single static helper (or a single lambda hoisted above both paths sharing one cached calleeOwnerTypeDefData) would remove the duplication and one extra GetDeclaringType call. No behavior change intended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLR/Core/Interpreter.cpp` around lines 2530 - 2642, The two duplicated
predicates matchesCalleeOwner and refersToCalleeOwner (used in the PushInline
and Push paths) both compute calleeOwnerTypeDefData via
calleeInst.GetDeclaringType() and repeat the same InitializeFromIndex logic;
replace them with a single helper predicate (or hoist one lambda) that takes a
CLR_RT_TypeSpec_Index* and uses a shared cached calleeOwnerTypeDefData computed
once (via CLR_RT_TypeDef_Instance + calleeInst.GetDeclaringType()) and reuse
that helper in both places so you remove the duplicate logic and the extra
GetDeclaringType call.
src/CLR/Core/TypeSystem.cpp (1)

1396-1405: ⚠️ Potential issue | 🟠 Major

Resolve !!U -> !T through the caller generic context when no explicit context is passed.

Line 1400 only accepts contextTypeSpec; valid calls that carry the closed type on caller->genericType still fail after MethodSpec maps an MVAR to a VAR. Mirror the effective-context selection used above for DATATYPE_VAR.

🐛 Proposed fix
                                     if (element.DataType == DATATYPE_VAR)
                                     {
                                         // need to defer to generic type parameter
+                                        const CLR_RT_TypeSpec_Index *effectiveContext = nullptr;
+
+                                        if (contextTypeSpec != nullptr && NANOCLR_INDEX_IS_VALID(*contextTypeSpec))
+                                        {
+                                            effectiveContext = contextTypeSpec;
+                                        }
+                                        else if (
+                                            caller->genericType != nullptr && NANOCLR_INDEX_IS_VALID(*caller->genericType))
+                                        {
+                                            effectiveContext = caller->genericType;
+                                        }
+
                                         CLR_RT_TypeSpec_Instance contextTs;
-                                        if (!contextTypeSpec || !contextTs.InitializeFromIndex(*contextTypeSpec))
+                                        if (effectiveContext == nullptr || !contextTs.InitializeFromIndex(*effectiveContext))
                                         {
                                             return false;
                                         }
 
                                         if (!contextTs.GetGenericParam(element.GenericParamPosition, element))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLR/Core/TypeSystem.cpp` around lines 1396 - 1405, When handling
element.DataType == DATATYPE_VAR, if contextTypeSpec is null we must fall back
to the effective caller generic context (the same selection used earlier) so
MVAR mapped to VAR can be resolved; instantiate a CLR_RT_TypeSpec_Instance from
caller->genericType when contextTypeSpec is absent and then call
InitializeFromIndex and GetGenericParam(element.GenericParamPosition, element)
on that instance instead of immediately returning false. Ensure you reference
contextTypeSpec, CLR_RT_TypeSpec_Instance contextTs,
contextTs.InitializeFromIndex, caller->genericType, and GetGenericParam in the
fix so the VAR is resolved through the caller generic context when present.
♻️ Duplicate comments (3)
src/CLR/Include/nanoCLR_Runtime.h (1)

2293-2306: ⚠️ Potential issue | 🔴 Critical

Make CLR_RT_MethodDef_Instance copies self-normalizing.

Normalize(src) only works after explicit assignments where src is still known. It cannot repair implicit copy construction or by-value parameters such as CLR_RT_StackFrame::MakeCall(CLR_RT_MethodDef_Instance md, ...); those copies can still carry a genericType pointer to another instance’s m_typeSpecStorage, and Push() can persist that dangling pointer into a stack frame. Prefer a copy constructor and copy-assignment operator that re-anchor automatically.

🛠️ Proposed fix
 struct CLR_RT_MethodDef_Instance : public CLR_RT_MethodDef_Index
 {
     CLR_RT_Assembly *assembly;
     const CLR_RECORD_METHODDEF *target;
@@
     //--//
 
+    CLR_RT_MethodDef_Instance() = default;
+
+    CLR_RT_MethodDef_Instance(const CLR_RT_MethodDef_Instance &src)
+    {
+        CopyFrom(src);
+    }
+
+    CLR_RT_MethodDef_Instance &operator=(const CLR_RT_MethodDef_Instance &src)
+    {
+        if (this != &src)
+        {
+            CopyFrom(src);
+        }
+
+        return *this;
+    }
+
+    inline void CopyFrom(const CLR_RT_MethodDef_Instance &src)
+    {
+        data = src.data;
+        assembly = src.assembly;
+        target = src.target;
+        genericType = src.genericType;
+        methodSpec = src.methodSpec;
+        m_typeSpecStorage = src.m_typeSpecStorage;
+        arrayElementType = src.arrayElementType;
+#if defined(NANOCLR_INSTANCE_NAMES)
+        name = src.name;
+#endif
+
+        Normalize(src);
+    }
+
     // After any plain by-value copy of a CLR_RT_MethodDef_Instance, call
     // Normalize(src) to re-anchor the self-referential genericType pointer.

Run this read-only check to find remaining by-value copy sites that should become safe via the copy constructor/operator:

#!/bin/bash
# Description: Find CLR_RT_MethodDef_Instance by-value parameters and explicit copy sites.
# Expectation: Either CLR_RT_MethodDef_Instance has self-normalizing copy construction/assignment,
# or each by-value signature/copy site is changed to avoid persisting stale genericType pointers.

rg -nP -C3 '\bCLR_RT_MethodDef_Instance\s+\w+\s*(,|\))|\bCLR_RT_MethodDef_Instance\s+\w+\s*=|\bm_call\s*=' --type=cpp --type=h
rg -nP -C4 'CLR_RT_MethodDef_Instance\s*\(\s*const\s+CLR_RT_MethodDef_Instance\s*&|operator=\s*\(\s*const\s+CLR_RT_MethodDef_Instance\s*&' src/CLR/Include/nanoCLR_Runtime.h
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLR/Include/nanoCLR_Runtime.h` around lines 2293 - 2306, Add
self-normalizing copy semantics to CLR_RT_MethodDef_Instance by implementing a
copy constructor and copy-assignment operator that perform a memberwise copy and
then re-anchor any self-referential genericType pointer: in both
CLR_RT_MethodDef_Instance(const CLR_RT_MethodDef_Instance &src) and
operator=(const CLR_RT_MethodDef_Instance &src) copy all fields, then if
src.genericType == &src.m_typeSpecStorage set this->genericType =
&this->m_typeSpecStorage (otherwise copy the pointer), or simply call
Normalize(src) after the copy; update the class declaration to declare these
functions so implicit by-value copies (e.g. CLR_RT_StackFrame::MakeCall or Push
sites) no longer carry dangling pointers to another instance’s
m_typeSpecStorage.
src/CLR/Core/Interpreter.cpp (1)

2459-2507: ⚠️ Potential issue | 🟡 Minor

constrainedTypeToken still leaks on the AppDomain-transition path.

The unconditional constrainedTypeToken = 0 at line 2506 lives inside the else branch of if (fAppDomainTransition). When fAppDomainTransition is true the code calls PushAppDomainTransition and falls through to goto Execute_Restart at line 2691 without touching constrainedTypeToken. Because Execute_Restart continues inside the same Execute_IL invocation when the frame is compatible (see line 4770–4781), the stale token can be consumed by a later CALL/CALLVIRT in the callee or in this frame after return, resolving the wrong constrained type.

Co-occurrence (BYREF this + transparent proxy) is unlikely but cheap to make watertight — clear the token unconditionally before branching on fAppDomainTransition, or duplicate the clear in the AppDomain arm.

💡 Proposed fix
+                    // Always consume the constrained. prefix token at the call site so it
+                    // cannot leak past this instruction (including the AppDomain path below).
+                    CLR_UINT32 pendingConstrainedToken = constrainedTypeToken;
+                    constrainedTypeToken = 0;
+
 `#if` defined(NANOCLR_APPDOMAINS)
                     if (fAppDomainTransition)
                     {
                         WRITEBACK(stack, evalPos, ip, fDirty);
                         ...
                     }
                     else
 `#endif` // NANOCLR_APPDOMAINS
                     {
                         ...
-                        if (constrainedTypeToken != 0)
+                        if (pendingConstrainedToken != 0)
                         {
                             if (op == CEE_CALLVIRT && pThis[0].DataType() == DATATYPE_BYREF)
                             {
                                 CLR_RT_TypeDef_Instance constrainedType{};
-                                if (constrainedType.ResolveToken(constrainedTypeToken, assm, &stack->m_call))
+                                if (constrainedType.ResolveToken(pendingConstrainedToken, assm, &stack->m_call))
                                 {
                                     ...
                                 }
                             }
-                            constrainedTypeToken = 0;
                         }

The prior review note about gating the deref to CEE_CALLVIRT and ensuring the token is cleared in the non-AppDomain branch has already been addressed by the current structure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLR/Core/Interpreter.cpp` around lines 2459 - 2507, Clear
constrainedTypeToken unconditionally before the AppDomain-transition branch to
avoid leaking a stale token across PushAppDomainTransition/Execute_Restart;
specifically, set constrainedTypeToken = 0 prior to checking
fAppDomainTransition (the block that calls
CLR_RT_StackFrame::PushAppDomainTransition) or alternatively duplicate the clear
immediately after the PushAppDomainTransition call, ensuring any subsequent
execution (including via Execute_Restart) cannot reuse the old
constrainedTypeToken during later CEE_CALLVIRT handling of pThis.
src/CLR/Core/TypeSystem.cpp (1)

3010-3015: ⚠️ Potential issue | 🟠 Major

Validate the GenericParam fallback before indexing the cross-reference table.

FindGenericParamAtMethodDef() can fail or produce an out-of-range parameter index, but Line 3014 indexes crossReferenceGenericParam unconditionally. Return CLR_E_WRONG_TYPE instead of reading the wrong row or running past the table.

🐛 Proposed fix
                 if (caller != nullptr)
                 {
                     CLR_RT_GenericParam_Index gp;
-                    caller->assembly->FindGenericParamAtMethodDef(*caller, elem.GenericParamPosition, gp);
+                    if (caller->assembly == nullptr ||
+                        !caller->assembly->FindGenericParamAtMethodDef(*caller, elem.GenericParamPosition, gp) ||
+                        gp.GenericParam() >= caller->assembly->tablesSize[TBL_GenericParam])
+                    {
+                        NANOCLR_SET_AND_LEAVE(CLR_E_WRONG_TYPE);
+                    }
+
                     auto &info = caller->assembly->crossReferenceGenericParam[gp.GenericParam()];
-                    this->InitializeFromTypeDef(info.classTypeDef);
+                    NANOCLR_CHECK_HRESULT(this->InitializeFromTypeDef(info.classTypeDef));
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLR/Core/TypeSystem.cpp` around lines 3010 - 3015,
FindGenericParamAtMethodDef can fail or produce an invalid index but the code
currently unconditionally indexes
caller->assembly->crossReferenceGenericParam[gp.GenericParam()]; update the
logic in the block that uses CLR_RT_GenericParam_Index gp so that after calling
caller->assembly->FindGenericParamAtMethodDef(*caller,
elem.GenericParamPosition, gp) you validate the result and that
gp.GenericParam() is within bounds of
caller->assembly->crossReferenceGenericParam; if FindGenericParamAtMethodDef
fails or the index is out of range, return CLR_E_WRONG_TYPE instead of indexing;
keep the subsequent call to InitializeFromTypeDef(info.classTypeDef) only when
validation succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CLR/Core/TypeSystem.cpp`:
- Around line 8003-8013: The resolution path for VAR inside MethodSpec args
currently ignores the provided contextTypeSpec and only checks
contextMethodDef->genericType; update the logic in the block handling
paramElement.DataType == DATATYPE_VAR to first attempt resolving against the
supplied contextTypeSpec (use its CLR_RT_TypeSpec_Instance and call
GetGenericParam with paramElement.GenericParamPosition into argElement) and only
if that fails fall back to initializing contextTs from
contextMethodDef->genericType and calling GetGenericParam there; ensure
paramTypeDef is set from argElement.Class when the contextTypeSpec resolution
succeeds and preserve existing behavior when it does not.

---

Outside diff comments:
In `@src/CLR/Core/Interpreter.cpp`:
- Around line 2530-2642: The two duplicated predicates matchesCalleeOwner and
refersToCalleeOwner (used in the PushInline and Push paths) both compute
calleeOwnerTypeDefData via calleeInst.GetDeclaringType() and repeat the same
InitializeFromIndex logic; replace them with a single helper predicate (or hoist
one lambda) that takes a CLR_RT_TypeSpec_Index* and uses a shared cached
calleeOwnerTypeDefData computed once (via CLR_RT_TypeDef_Instance +
calleeInst.GetDeclaringType()) and reuse that helper in both places so you
remove the duplicate logic and the extra GetDeclaringType call.

In `@src/CLR/Core/TypeSystem.cpp`:
- Around line 1396-1405: When handling element.DataType == DATATYPE_VAR, if
contextTypeSpec is null we must fall back to the effective caller generic
context (the same selection used earlier) so MVAR mapped to VAR can be resolved;
instantiate a CLR_RT_TypeSpec_Instance from caller->genericType when
contextTypeSpec is absent and then call InitializeFromIndex and
GetGenericParam(element.GenericParamPosition, element) on that instance instead
of immediately returning false. Ensure you reference contextTypeSpec,
CLR_RT_TypeSpec_Instance contextTs, contextTs.InitializeFromIndex,
caller->genericType, and GetGenericParam in the fix so the VAR is resolved
through the caller generic context when present.

---

Duplicate comments:
In `@src/CLR/Core/Interpreter.cpp`:
- Around line 2459-2507: Clear constrainedTypeToken unconditionally before the
AppDomain-transition branch to avoid leaking a stale token across
PushAppDomainTransition/Execute_Restart; specifically, set constrainedTypeToken
= 0 prior to checking fAppDomainTransition (the block that calls
CLR_RT_StackFrame::PushAppDomainTransition) or alternatively duplicate the clear
immediately after the PushAppDomainTransition call, ensuring any subsequent
execution (including via Execute_Restart) cannot reuse the old
constrainedTypeToken during later CEE_CALLVIRT handling of pThis.

In `@src/CLR/Core/TypeSystem.cpp`:
- Around line 3010-3015: FindGenericParamAtMethodDef can fail or produce an
invalid index but the code currently unconditionally indexes
caller->assembly->crossReferenceGenericParam[gp.GenericParam()]; update the
logic in the block that uses CLR_RT_GenericParam_Index gp so that after calling
caller->assembly->FindGenericParamAtMethodDef(*caller,
elem.GenericParamPosition, gp) you validate the result and that
gp.GenericParam() is within bounds of
caller->assembly->crossReferenceGenericParam; if FindGenericParamAtMethodDef
fails or the index is out of range, return CLR_E_WRONG_TYPE instead of indexing;
keep the subsequent call to InitializeFromTypeDef(info.classTypeDef) only when
validation succeeds.

In `@src/CLR/Include/nanoCLR_Runtime.h`:
- Around line 2293-2306: Add self-normalizing copy semantics to
CLR_RT_MethodDef_Instance by implementing a copy constructor and copy-assignment
operator that perform a memberwise copy and then re-anchor any self-referential
genericType pointer: in both CLR_RT_MethodDef_Instance(const
CLR_RT_MethodDef_Instance &src) and operator=(const CLR_RT_MethodDef_Instance
&src) copy all fields, then if src.genericType == &src.m_typeSpecStorage set
this->genericType = &this->m_typeSpecStorage (otherwise copy the pointer), or
simply call Normalize(src) after the copy; update the class declaration to
declare these functions so implicit by-value copies (e.g.
CLR_RT_StackFrame::MakeCall or Push sites) no longer carry dangling pointers to
another instance’s m_typeSpecStorage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 37219361-73f7-4271-b6e8-3b02eb50f8e7

📥 Commits

Reviewing files that changed from the base of the PR and between 43aba2e and 38497ee.

📒 Files selected for processing (5)
  • src/CLR/Core/CLR_RT_StackFrame.cpp
  • src/CLR/Core/Interpreter.cpp
  • src/CLR/Core/TypeSystem.cpp
  • src/CLR/Include/nanoCLR_Runtime.h
  • targets/netcore/src/CLR/Core/CLR_RT_StackFrame.cpp

Comment thread src/CLR/Core/TypeSystem.cpp
nfbot and others added 2 commits April 23, 2026 08:36
Automated fixes for code style.
…909ca-2718-4e9c-82cb-82fc3dc75d98

Code style fixes for nanoframework/nf-interpreter PR#3338
@josesimoes josesimoes added the Area: Common libs Everything related with common libraries label Apr 23, 2026
@josesimoes josesimoes merged commit dfc4c47 into nanoframework:develop Apr 23, 2026
23 checks passed
@josesimoes josesimoes deleted the fixes-for-generics branch April 23, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: Common libs Everything related with common libraries Type: bug Type: enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants