Skip to content

Conversation

@josesimoes
Copy link
Member

@josesimoes josesimoes commented Nov 25, 2025

Description

  • Implement localloc IL instruction.
    • Add handler for instruction.
    • Add storage pointers to stack frame to allow deallocation on method return.
  • Implement cpblk IL instruction.
  • Add new data type DATATYPE_PTR.
  • Add new helper APIs in heap block to set and get unmanaged pointers.
  • Numeric add and substract can now handle operations with unmanaged pointers.
  • ldsflda now properly handles static fields with RVA (now gets pointer to metadata storage).
  • Implement ctor from pointer for Span<T> and ReadOnlySpan<T>.
  • Add new APIs to heap block to deal with unmanaged pointers.
  • Fix call to BuildTypeName for TypeSpec in DumpToken (now passing context TypeSpec for correct type resolution).
  • Add new reflection type for storage pointer.
  • Add support for storage pointers in heap block array.
  • Adjust get element API to return pointer to storage, when appropriate.
  • Add API to create array from storage.
  • GetGenericArgument at MethodSpec instance now returns a signature element (required to deal with VAR params).
  • Resolving token in TypeDef instance now take optional parameter typeSpec for context (rework code to resolve nested VAR and MVAR situations).
  • BuildTypeName now is able to resolve VAR elements.
  • Creating HeapBlock array now accepts typeSpec parameter for type resolution.
  • AllocateGenericStaticFieldsOnDemand now correctly stores TypeSpec context.
  • Improve BuildMethodName to build type name for generic instances.
  • Signature parser now can consume modifiers in types (following Add processing of type modifiers metadata-processor#221).
  • Moved readonly. prefix to group of "do nothing" op codes.
  • Fix resolving token of TypeDef for a VAR in a TypeSpec.
  • Dump assembly token now takes TypeSpec parameter for context.
  • Update mscorlib declaration.

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).

Summary by CodeRabbit

  • New Features

    • Create Span/ReadOnlySpan directly from raw pointers.
    • Support for LOCALLOC and CPBLK IL opcodes (bounded local allocations and block copy).
    • Per-frame local-allocation tracking to manage LOCALLOC lifetimes.
    • Unmanaged-pointer values and basic pointer arithmetic support.
  • Improvements

    • Support for storage-backed arrays and improved generic/type resolution for nested generic contexts.
    • RVA-backed static field loading and refined debugging/type-name output.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add handler for instruction. Add storage pointers to stack frame to allow dealocation on moethod return.
@josesimoes josesimoes added the Area: Common libs Everything related with common libraries label Nov 25, 2025
@nfbot nfbot added Type: enhancement Type: dependencies Pull requests that update a dependency file(s) or version labels Nov 25, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 25, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds per-frame localloc tracking and implementations for CEE_LOCALLOC/CEE_CPBLK and RVA field loads; introduces DATATYPE_PTR, storage-backed arrays and pointer-aware HeapBlock operations; adds native Span/ReadOnlySpan constructors that bind storage to a void* (or memmove for CopyTo), updates native tables and an mscorlib metadata token.

Changes

Cohort / File(s) Summary
Span/ReadOnlySpan native declarations & impl
src/CLR/CorLib/corlib_native.h, src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp, src/CLR/CorLib/corlib_native_System_Span_1.cpp
Added native constructor declarations _ctor___VOID__VOIDptr__I4 for ReadOnlySpan1 and Span1; implemented constructors that validate generic element type (no refs), validate length and pointer, and create array instances bound to the provided void* via CreateInstanceWithStorage (no element-by-element copy). Updated Span CopyTo to use memmove for raw copies and adjusted native NativeSpanConstructor to bind storage pointer instead of copying.
Native method table & metadata token
src/CLR/CorLib/corlib_native.cpp
Inserted new Span/ReadOnlySpan entries into native method lookup tables for both reflective and non-reflective paths, adding placeholder null slots to preserve alignment; updated g_CLR_AssemblyNative_mscorlib token when NANOCLR_REFLECTION is TRUE.
HeapBlock storage-pointer support & pointer ops
src/CLR/Include/nanoCLR_Runtime__HeapBlock.h, src/CLR/Include/nanoCLR_Types.h, src/CLR/Core/CLR_RT_HeapBlock.cpp, src/CLR/Core/CLR_RT_HeapBlock_Array.cpp
Added DATATYPE_PTR and REFLECTION_STORAGE_PTR; added UnmanagedPointer()/SetUnmanagedPointer() accessors; extended CLR_RT_HeapBlock_Array with m_StoragePointer, CreateInstanceWithStorage, storage-aware GetFirstElement/GetFirstElementUInt16, IsStoragePointer() and guarded element clearing/copy paths; StoreToReference now supports copying DATATYPE_PTR; NumericAdd/NumericSub perform pointer arithmetic with I4 offsets.
Interpreter: opcodes & RVA fields
src/CLR/Core/Interpreter.cpp
Implemented CEE_LOCALLOC and CEE_CPBLK in main opcode loop (platform heap allocation, zero-init, tracking, and memmove-backed cpblk); added RVA-based field load for ldfld/ldsfld (load raw bytes from signature blob); added helper to reschedule generic cctors; integrated local-allocation bookkeeping into execution.
Stack/inline frame localloc tracking & lifecycle
src/CLR/Core/CLR_RT_StackFrame.cpp, src/CLR/Include/nanoCLR_Runtime.h
Added MAX_LOCALALLOC_COUNT = 4, m_localAllocCount and m_localAllocs[] to stack and inline frames; initialize, propagate to inline frames, save/restore across stack operations, and free/clear on Pop.
Type system generic-context resolution & APIs
src/CLR/Core/TypeSystem.cpp, src/CLR/Core/Execution.cpp, src/CLR/Include/nanoCLR_Runtime.h, src/CLR/Core/CLR_RT_HeapBlock_Array.cpp, src/CLR/Diagnostics/Info.cpp
Introduced context-aware VAR/MVAR resolution via optional contextTypeSpec and CLR_RT_SignatureParser::Element usage; changed GetGenericArgument signature to return an Element; propagated contextTypeSpec through ResolveToken, BuildTypeName, ComputeHashForClosedGenericType and related calls; updated CreateInstance overloads to accept contextTypeSpec.
Misc debug/info updates
src/CLR/Debugger/Debugger.cpp, src/CLR/Diagnostics/Info.cpp
Added assertion branch for storage-backed arrays in debugger array retrieval; updated BuildTypeName callsites to pass generic context for TypeSpec handling.
Stack/Execution small updates
src/CLR/Core/CLR_RT_StackFrame.cpp, src/CLR/Core/Execution.cpp
Ensured extracted arrays initialize m_StoragePointer to 0 on allocation; adjusted InitializeLocals to use the new Element-based generic-argument API.

Sequence Diagram(s)

sequenceDiagram
    participant IL as IL Interpreter
    participant Frame as Stack Frame
    participant Heap as Heap / Array
    participant GC as GC

    IL->>Frame: CEE_LOCALLOC(size)
    Frame->>Heap: allocate byte[] (size)
    Heap-->>Frame: arrayRef (m_StoragePointer == 0 or storage-backed)
    Frame->>GC: protect arrayRef (store in m_localAllocs)
    Frame->>Frame: m_localAllocCount++
    Frame-->>IL: push arrayRef
    Note right of Frame: PushInline/RestoreFromInline copy/restore\nm_localAllocCount & m_localAllocs
    Note right of Frame: On Pop: free/clear m_localAllocs and reset count
Loading
sequenceDiagram
    participant Caller as Managed Call
    participant SpanNative as Span native ctor
    participant TypeSys as Type System
    participant Heap as Heap / Array
    participant GC as GC

    Caller->>SpanNative: _ctor(void* ptr, int length)
    SpanNative->>TypeSys: resolve and validate generic T (no refs)
    TypeSys-->>SpanNative: resolved / error
    SpanNative->>Heap: CreateInstanceWithStorage(length, storageAddr, elementType)
    Heap-->>SpanNative: arrayRef (m_StoragePointer = storageAddr)
    SpanNative->>SpanNative: set span length & backing array reference
    SpanNative-->>Caller: initialized Span instance (or throw)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Areas needing extra attention:
    • Interpreter: correctness and safety of CEE_LOCALLOC/CEE_CPBLK, address-width handling, GC interaction and overlapping memmove semantics.
    • Heap/Array storage-backed path: correctness of CreateInstanceWithStorage, m_StoragePointer invariants, and guarded Clear/Copy behavior.
    • Span native ctors and CopyTo: generic resolution, element-size calculations, storage binding vs copy semantics, and exception paths.
    • Type system changes: VAR/MVAR resolution with contextTypeSpec propagation, signature parsing edge cases, and hash/name computation impacts.
    • Stack frame localloc lifecycle: propagation across inline frames, saves/restores, and Pop cleanup correctness.
    • Pointer arithmetic and DATATYPE_PTR: boundary/overflow checks and interactions with existing numeric operations.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ⚠️ Warning The title 'More work on supporting generics in type system and execution engine' is partially related to the changeset but does not reflect the primary change. Revise the title to emphasize the main feature: 'Implement localloc and cpblk IL instructions with unmanaged pointer support' or similar, to better represent the core objectives.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 25, 2025

✅ 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
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: 2

🧹 Nitpick comments (4)
src/CLR/CorLib/corlib_native_System_Span_1.cpp (1)

9-151: Span(void, int) ctor: logic is sound; consider minor robustness tweaks*

Overall the implementation is careful: it reconstructs T from the caller’s generic context, rejects reference/contains‑reference element types, validates length, enforces that the backing buffer is byte[], computes the element size via c_CLR_RT_DataTypeLookup, validates capacity, reshapes the array metadata, and finally sets the Span length. This aligns with the ReadOnlySpan variant and the intended localloc‑backed usage.

A few points to double‑check:

  • sourceArray = stack.Arg1().DereferenceArray(); assumes the VOID* argument is always a managed array reference. If any other caller ever passes a non‑array object (or null), this will AV; consider adding a null/array check and returning CLR_E_WRONG_TYPE or CLR_E_NULL_REFERENCE for robustness.
  • newNumElements = sourceArray->m_numOfElements / elementSize silently truncates if the byte count is not an exact multiple of elementSize. If the intent is to require exact divisibility (to avoid silently “losing” trailing bytes), you might want to check m_numOfElements % elementSize == 0 and treat mismatch as CLR_E_INVALID_PARAMETER.
  • ClearElements(0, length) after reshaping is good for safety, but note that it zeroes the region even if the caller had already written into the localloc buffer; this is acceptable if the design is that the Span ctor owns initialization, but it’s worth confirming that no caller expects preserved contents.

If all current call sites are only the new CoreLib APIs that hand in localloc-allocated byte[], the current behavior is acceptable; the above mainly protects against future misuse.

src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (1)

9-151: ReadOnlySpan(void, int) mirrors Span correctly*

The ReadOnlySpan pointer ctor is essentially identical to the Span version: it reconstructs T from the generic context, blocks reference/contains‑reference types, validates length, enforces byte[] backing, reshapes the array metadata, and sets span length. That symmetry is good and keeps the two behaviors aligned.

Same optional points as for Span:

  • Consider a defensive null/array check on sourceArray = stack.Arg1().DereferenceArray(); to avoid hard faults on misuse.
  • Decide whether m_numOfElements % elementSize != 0 should be rejected explicitly rather than truncating via /.
  • Confirm that clearing elements via ClearElements(0, length) matches expected semantics for any caller that might preinitialize the underlying buffer.

Given the current intended use (backing from localloc), the implementation is coherent; the suggestions are mainly to harden against future callers.

src/CLR/Core/CLR_RT_StackFrame.cpp (1)

334-359: Inline frame save/restore of localloc state is consistent

In PushInline, you:

  • Save m_localAllocCount and all m_localAllocs[] into the inline frame backup.
  • Then reset m_localAllocCount and clear m_localAllocs[] for the inlined callee.

This achieves the desired behavior: the caller’s locallocs are preserved and isolated from any additional locallocs in the inlined method. On PopInline, RestoreFromInlineStack restores the caller’s m_localAllocCount and array. This is logically sound and matches how other frame state is managed.

src/CLR/Include/nanoCLR_Runtime.h (1)

2532-2534: Localloc tracking fields are consistent; just confirm init/GC integration and fix a small comment nit

The shared MAX_LOCALALLOC_COUNT and mirrored m_localAllocCount/m_localAllocs[] in both CLR_RT_InlineFrame and CLR_RT_StackFrame look coherent and match the intended per-frame localloc tracking.

Two follow‑ups to double‑check in the implementation files:

  • Ensure m_localAllocCount is always reset and m_localAllocs[] are zeroed when stack frames are created or reused (including inline frames pulled from CLR_RT_EventCache::m_inlineBufferStart), so no stale GC roots are left behind.
  • Ensure the GC mark path that walks stack/frame roots also walks each non‑null entry in m_localAllocs[], otherwise these temporary arrays could be collected while still in use.

Minor: the comment at Line 2570 has a typo (“max mumber” → “max number”).

Also applies to: 2545-2547, 2570-2572, 2674-2676

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 577be8f and 9c548a9.

📒 Files selected for processing (7)
  • src/CLR/CorLib/corlib_native.cpp (5 hunks)
  • src/CLR/CorLib/corlib_native.h (2 hunks)
  • src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (1 hunks)
  • src/CLR/CorLib/corlib_native_System_Span_1.cpp (1 hunks)
  • src/CLR/Core/CLR_RT_StackFrame.cpp (7 hunks)
  • src/CLR/Core/Interpreter.cpp (1 hunks)
  • src/CLR/Include/nanoCLR_Runtime.h (4 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-01-09T13:32:43.711Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3062
File: src/System.Device.Spi/sys_dev_spi_native_System_Device_Spi_SpiDevice.cpp:106-188
Timestamp: 2025-01-09T13:32:43.711Z
Learning: In nanoFramework, CLR_RT_HeapBlock_Array::Pin() method returns void and cannot fail. It should be called without error handling.

Applied to files:

  • src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp
  • src/CLR/CorLib/corlib_native_System_Span_1.cpp
📚 Learning: 2024-10-12T19:00:39.000Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3023
File: targets/netcore/nanoFramework.nanoCLR/nanoCLR_native.cpp:191-225
Timestamp: 2024-10-12T19:00:39.000Z
Learning: When working with `nanoCLR_GetNativeAssemblyInformation`, fixed-size assembly names are required, so code that deals with variable-length names cannot be used.

Applied to files:

  • src/CLR/CorLib/corlib_native.h
  • src/CLR/CorLib/corlib_native.cpp
  • src/CLR/Include/nanoCLR_Runtime.h
📚 Learning: 2025-06-26T09:16:55.184Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3190
File: src/CLR/Core/TypeSystem.cpp:0-0
Timestamp: 2025-06-26T09:16:55.184Z
Learning: In nanoFramework's CLR attribute parsing (src/CLR/Core/TypeSystem.cpp), the sentinel value 0xFFFF in string tokens represents a null string. When encountered, this should result in a true null reference (using SetObjectReference(nullptr)) rather than an empty string instance, and the boxing operation should be skipped via early return.

Applied to files:

  • src/CLR/CorLib/corlib_native.cpp
📚 Learning: 2024-09-25T11:28:38.536Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3023
File: targets/netcore/nanoFramework.nanoCLR/nanoCLR_native.cpp:191-225
Timestamp: 2024-09-25T11:28:38.536Z
Learning: In `nanoCLR_GetNativeAssemblyInformation`, there is no need to return the number of bytes written, as the memory buffer is zeroed, making the string buffer terminated.

Applied to files:

  • src/CLR/CorLib/corlib_native.cpp
📚 Learning: 2025-01-22T03:38:57.394Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3074
File: src/CLR/Core/GarbageCollector_Info.cpp:107-167
Timestamp: 2025-01-22T03:38:57.394Z
Learning: In nanoFramework's memory management code, DataSize() validation is comprehensively handled through CLR_RT_HeapCluster::ValidateBlock() and other caller code. Additional size checks in ValidateCluster() are redundant as the validation is already performed at multiple levels.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime.h
🧬 Code graph analysis (3)
src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (1)
src/CLR/CorLib/corlib_native_System_Span_1.cpp (2)
  • _ctor___VOID__VOIDptr__I4 (11-151)
  • _ctor___VOID__VOIDptr__I4 (11-11)
src/CLR/CorLib/corlib_native_System_Span_1.cpp (1)
src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (2)
  • _ctor___VOID__VOIDptr__I4 (11-151)
  • _ctor___VOID__VOIDptr__I4 (11-11)
src/CLR/CorLib/corlib_native.cpp (2)
src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (2)
  • _ctor___VOID__VOIDptr__I4 (11-151)
  • _ctor___VOID__VOIDptr__I4 (11-11)
src/CLR/CorLib/corlib_native_System_Span_1.cpp (2)
  • _ctor___VOID__VOIDptr__I4 (11-151)
  • _ctor___VOID__VOIDptr__I4 (11-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: nf-interpreter (Build_Azure_RTOS_targets SL_STK3701A)
  • GitHub Check: nf-interpreter (Build_WIN32_nanoCLR)
  • GitHub Check: nf-interpreter (Build_ESP32_targets ESP32_P4_UART)
  • GitHub Check: nf-interpreter (Build_ESP32_targets ESP32_ETHERNET_KIT_1.2)
  • GitHub Check: nf-interpreter (Build_ESP32_targets ESP32_S3_ALL)
  • GitHub Check: nf-interpreter (Build_ESP32_targets ESP32_H2_THREAD)
  • GitHub Check: nf-interpreter (Build_ESP32_targets ESP32_C6_THREAD)
  • GitHub Check: nf-interpreter (Build_ESP32_targets ESP32_C3)
  • GitHub Check: nf-interpreter (Build_ESP32_targets ESP32_BLE_REV0)
  • GitHub Check: nf-interpreter (Build_STM32_targets ST_STM32F769I_DISCOVERY)
  • GitHub Check: nf-interpreter (Build_STM32_targets ST_STM32F429I_DISCOVERY)
  • GitHub Check: nf-interpreter (Build_ESP32_targets ESP32_PSRAM_REV0)
  • GitHub Check: nf-interpreter (Build_NXP_targets NXP_MIMXRT1060_EVK)
  • GitHub Check: nf-interpreter (Build_TI_SimpleLink_targets TI_CC1352R1_LAUNCHXL_915)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets MXCHIP_AZ3166)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets ORGPAL_PALX)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets ORGPAL_PALTHREE)
  • GitHub Check: nf-interpreter (Check_Code_Style)
  • GitHub Check: nf-interpreter (Nightly build) (Check_Build_Options)
  • GitHub Check: nf-interpreter (Check_Build_Options)
🔇 Additional comments (6)
src/CLR/CorLib/corlib_native.h (1)

781-790: Span/ReadOnlySpan pointer ctor declarations look consistent

The new native declarations for _ctor___VOID__VOIDptr__I4 on both Library_corlib_native_System_ReadOnlySpan_1 and Library_corlib_native_System_Span_1 match the corresponding implementations and are correctly guarded by #if (NANOCLR_REFLECTION == TRUE). No further changes needed here.

src/CLR/Core/Interpreter.cpp (1)

1005-1034: Generic .cctor reschedule helper is correct but edge cases should be confirmed

The new HandleGenericCctorReschedule helper cleanly centralizes the pattern of:

  • Computing a hash for a closed generic type using ComputeHashForClosedGenericType.
  • Treating 0xFFFFFFFF as an invalid hash and returning CLR_E_WRONG_TYPE.
  • Looking up or creating the CLR_RT_GenericCctorExecutionRecord.
  • If a .cctor is scheduled but not executed, rewinding ip by 3 bytes (opcode + compressed field token) and returning CLR_E_RESCHEDULE.

Two points to verify:

  • The “3 bytes” rewind (*pIp -= 3) assumes this is only used for opcodes with exactly one‑byte opcode + two‑byte compressed token. You’re using it with field opcodes like LDSFLD/LDSFLDA/STSFLD whose encoding here indeed uses a 2‑byte compressed token; please confirm we don’t call this helper from any opcode with a different operand layout in future.
  • FindOrCreateGenericCctorRecord(hash, nullptr) can return non‑null with flags that don’t have c_Scheduled set; you correctly treat that as “no reschedule” and return S_OK, which is fine. Just be aware this will not attempt to schedule .cctors, only observe already‑scheduled ones.

Conceptually this is a good extraction and keeps the LDSFLD/LDSFLDA/STSFLD paths simpler.

src/CLR/Core/CLR_RT_StackFrame.cpp (2)

124-130: Initialize localloc tracking in Push to a known state

Zeroing m_localAllocCount and clearing m_localAllocs[] on frame creation is correct and prevents stale pointers from previous uses of the same event‑heap block. No changes needed here.


425-473: RestoreStack/SaveStack correctly propagate localloc metadata across inline frames

SaveStack and RestoreStack now copy m_localAllocCount and each m_localAllocs[i] to and from the CLR_RT_InlineFrame. This keeps the localloc tracking coherent across stack saves/restores (e.g., inlining transitions, exception unwinds). The loops mirror each other and use c_Max_Localloc_Count, so they’ll stay in sync with the header definition.

src/CLR/CorLib/corlib_native.cpp (2)

699-702: ReadOnlySpan pointer ctor wiring looks correct; please confirm index alignment

The new Library_corlib_native_System_ReadOnlySpan_1::_ctor___VOID__VOIDptr__I4 entry, with surrounding nullptr padding and the existing NativeReadOnlySpanConstructor slot, matches the established layout pattern for generic natives in this table.

Given how fragile this array/index mapping is, please confirm that:

  • The managed ReadOnlySpan<T> pointer ctor is marked as native and its method index matches this new slot in the reflection-enabled mscorlib.
  • Any other new ReadOnlySpan<T> natives introduced in the managed assembly are either accounted for here or intentionally left as managed.

Also applies to: 708-709, 718-725


813-825: Span pointer ctor and updated reflection token look consistent; verify against regenerated mscorlib

The added Library_corlib_native_System_Span_1::_ctor___VOID__VOIDptr__I4 entry, along with the nearby CopyTo and NativeSpanConstructor entries and the extra nullptr padding before the threading methods, follows the existing grouping and spacing conventions for Span-related natives.

The updated native assembly token 0xCECAB752 under NANOCLR_REFLECTION == TRUE is appropriate given the mscorlib metadata change; the non‑reflection token and table remain unchanged.

Please verify that:

  • The managed Span<T> pointer ctor’s native index matches this new table position in the reflection-enabled mscorlib build.
  • The updated token matches the regenerated managed mscorlib (so loader and debugger see this as a single coherent version).
  • For NANOCLR_REFLECTION == FALSE, either the managed mscorlib did not change or its native table/token are regenerated in lockstep in the corresponding build configuration.

Also applies to: 843-846, 1616-1616

@josesimoes josesimoes changed the title Implement localloc Implement localloc and cpblk IL instructions Nov 27, 2025
@josesimoes
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

✅ 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.

Automated fixes for code style.
Copy link
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: 5

Caution

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

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

496-657: Fix DATATYPE_PTR handling in StoreToReference; pointer arithmetic looks OK

The new DATATYPE_PTR support in NumericAdd/NumericSub is fine in spirit (treat pointers as byte-wise addresses and only allow I4 offsets), but the StoreToReference branch for DATATYPE_PTR is unsafe and does not implement cpblk semantics correctly:

  • dt in StoreToReference is ref.DataType(), so the DATATYPE_PTR case runs when the reference is an unmanaged pointer.
  • obj = ref.Dereference(); returns the raw address stored in that pointer, which is an arbitrary unmanaged buffer, not a CLR_RT_HeapBlock*.
  • &NumericByRef() is the address of this heap block’s internal Numeric union, not the memory pointed to by the source pointer.
  • memcpy((void *)obj, (void *)&NumericByRef(), size); therefore copies bytes from the heapblock’s internal storage into the unmanaged buffer, not from the source buffer.
  • After that, the common obj->Assign(*this); path treats the unmanaged buffer as a CLR_RT_HeapBlock and writes headers/fields into it. That is undefined behavior and will corrupt whatever memory the pointer happens to reference.

For cpblk-style scenarios (dest ptr, src ptr, size), you likely want:

  • Both operands to be unmanaged pointers (e.g., src.StoreToReference(dest, size)), and
  • A raw block copy between the underlying addresses, without interpreting them as heap blocks or assigning heapblock headers.

A safer implementation for the DATATYPE_PTR branch would look like:

-    else if (dt == DATATYPE_PTR)
-    {
-        // unmanaged pointer, perform a direct memory copy
-        obj = ref.Dereference();
-        memcpy((void *)obj, (void *)&NumericByRef(), size);
-    }
+    else if (dt == DATATYPE_PTR)
+    {
+        // Unmanaged pointer: copy raw bytes from this->Dereference() to ref.Dereference().
+        void *dst = (void *)ref.Dereference();
+        void *src = (void *)this->Dereference();
+
+        // cpblk allows overlapping regions; memmove is safer than memcpy here.
+        memmove(dst, src, size);
+
+        // No heapblock assignment for raw pointer copies.
+        NANOCLR_SET_AND_LEAVE(S_OK);
+    }
@@
-    obj->Assign(*this);
+    obj->Assign(*this);

(Where this->Dereference() and ref.Dereference() for DATATYPE_PTR are assumed to return the stored raw addresses.)

This avoids treating arbitrary unmanaged buffers as CLR_RT_HeapBlock instances and aligns DATATYPE_PTR with the intended “raw address” semantics.

The NumericAdd/NumericSub DATATYPE_PTR cases themselves look reasonable (byte-wise pointer arithmetic with an I4 offset), so once StoreToReference is fixed, the unmanaged pointer story should be consistent.

Also applies to: 2005-2095, 2103-2199

♻️ Duplicate comments (1)
src/CLR/Core/Interpreter.cpp (1)

4224-4259: Validate negative sizes and address GC rooting concerns from previous review.

This implementation has two issues:

  1. New concern: Negative size check needed. Line 4226 reads the size as u4 (unsigned). If IL passes a negative int32 value, it will be reinterpreted as a large unsigned value, potentially causing a huge allocation attempt. Unlike the array creation path that uses CreateInstance (which checks for negative lengths), platform_malloc has no such guard.

  2. Previously flagged: GC rooting and OOM handling still missing. A past review comment (lines 4224-4293) identified that m_localAllocs pointers are not scanned or relocated by the GC, and that OOM handling should include compaction+retry. Those concerns remain unaddressed.

Immediate fix for negative size:

 OPDEF(CEE_LOCALLOC, "localloc", PopI, PushI, InlineNone, IPrimitive, 2, 0xFE, 0x0F, NEXT)
 {
-    CLR_UINT32 size = evalPos[0].NumericByRef().u4;
+    CLR_INT32 sizeRaw = evalPos[0].NumericByRef().s4;
+    
+    if (sizeRaw < 0)
+    {
+        NANOCLR_SET_AND_LEAVE(CLR_E_OUT_OF_RANGE);
+    }
+    
+    CLR_UINT32 size = (CLR_UINT32)sizeRaw;

     evalPos--;
     CHECKSTACK(stack, evalPos);

For the GC rooting and OOM handling issues, please see the detailed previous review comment that recommended:

  • Adding CheckMultipleBlocks(stack->m_localAllocs, stack->m_localAllocCount) in Thread_Mark (GC marking)
  • Calling Heap_Relocate on each stored pointer in the relocation path
  • Adding compaction+retry on CLR_E_OUT_OF_MEMORY similar to the array allocation flow (lines ~3410-3420)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c548a9 and 750b1ef.

📒 Files selected for processing (8)
  • src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (1 hunks)
  • src/CLR/CorLib/corlib_native_System_Span_1.cpp (1 hunks)
  • src/CLR/Core/CLR_RT_HeapBlock.cpp (3 hunks)
  • src/CLR/Core/CLR_RT_StackFrame.cpp (7 hunks)
  • src/CLR/Core/Interpreter.cpp (3 hunks)
  • src/CLR/Include/nanoCLR_Runtime.h (4 hunks)
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h (1 hunks)
  • src/CLR/Include/nanoCLR_Types.h (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-01-22T03:38:57.394Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3074
File: src/CLR/Core/GarbageCollector_Info.cpp:107-167
Timestamp: 2025-01-22T03:38:57.394Z
Learning: In nanoFramework's memory management code, DataSize() validation is comprehensively handled through CLR_RT_HeapCluster::ValidateBlock() and other caller code. Additional size checks in ValidateCluster() are redundant as the validation is already performed at multiple levels.

Applied to files:

  • src/CLR/Core/CLR_RT_HeapBlock.cpp
  • src/CLR/Include/nanoCLR_Runtime.h
📚 Learning: 2025-01-09T13:32:43.711Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3062
File: src/System.Device.Spi/sys_dev_spi_native_System_Device_Spi_SpiDevice.cpp:106-188
Timestamp: 2025-01-09T13:32:43.711Z
Learning: In nanoFramework, CLR_RT_HeapBlock_Array::Pin() method returns void and cannot fail. It should be called without error handling.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h
📚 Learning: 2024-10-12T19:00:39.000Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3023
File: targets/netcore/nanoFramework.nanoCLR/nanoCLR_native.cpp:191-225
Timestamp: 2024-10-12T19:00:39.000Z
Learning: When working with `nanoCLR_GetNativeAssemblyInformation`, fixed-size assembly names are required, so code that deals with variable-length names cannot be used.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime.h
📚 Learning: 2025-11-20T14:08:30.044Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3240
File: src/CLR/Core/TypeSystem.cpp:812-818
Timestamp: 2025-11-20T14:08:30.044Z
Learning: Repo: nanoframework/nf-interpreter
File: src/CLR/Core/TypeSystem.cpp
Context: Generics resolution in CLR_RT_TypeSpec_Instance::ResolveToken
Learning: When resolving a VAR (!T) TypeSpec using the caller’s closed generic, after switching to the caller context (Set(caller->genericType->Assembly(), closedTsRow) and assigning assembly accordingly), always fetch the TypeSpec with target = assembly->GetTypeSpec(closedTsRow) rather than assm->GetTypeSpec(closedTsRow) to avoid cross-assembly mismatches.

Applied to files:

  • src/CLR/Core/Interpreter.cpp
🧬 Code graph analysis (1)
src/CLR/CorLib/corlib_native_System_Span_1.cpp (1)
src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (2)
  • _ctor___VOID__VOIDptr__I4 (11-112)
  • _ctor___VOID__VOIDptr__I4 (11-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: nf-interpreter (Check_Code_Style)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets MXCHIP_AZ3166)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets ORGPAL_PALX)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets ORGPAL_PALTHREE)
  • GitHub Check: nf-interpreter (Nightly build) (Check_Build_Options)
  • GitHub Check: nf-interpreter (Check_Build_Options)
🔇 Additional comments (5)
src/CLR/Include/nanoCLR_Runtime.h (4)

2570-2571: LGTM - Good practice using a class-scoped constant.

The constant c_Max_Localloc_Count correctly references the global macro and follows the existing naming convention for constants in this structure.


2545-2546: I'll verify the concerns raised in the original review comment about the new inline frame local allocation tracking fields. Let me start by examining the codebase to understand the implementation details.
<function_calls>

#!/bin/bash

Find the full CLR_RT_InlineFrame structure context

rg -n "struct CLR_RT_InlineFrame|class CLR_RT_InlineFrame" -A 50 --type=cpp --type=h | head -100


</function_calls>

Now let me search for the PopInline implementation and localloc cleanup logic:
<function_calls>

#!/bin/bash

Search for PopInline and inline frame cleanup

rg -n "PopInline\s*(" -B 3 -A 15 --type=cpp --type=h | head -150


</function_calls>

Let me find the MAX_LOCALALLOC_COUNT definition and search for localloc-related cleanup:
<function_calls>

#!/bin/bash

Find MAX_LOCALALLOC_COUNT and localloc handling

rg -n "MAX_LOCALALLOC_COUNT|m_localAlloc" --type=cpp --type=h | head -100


</function_calls>

Let me also check the frame cleanup mechanism to understand how memory is deallocated:
<function_calls>

#!/bin/bash

Search for frame cleanup and platform_free calls related to allocations

rg -n "platform_free|CleanupAlllocations|m_localAlloc" -B 3 -A 3 --type=cpp | head -150


</function_calls>

Let me also look for how regular frames handle localloc allocations to understand the pattern:
<function_calls>

#!/bin/bash

Check if there's a regular frame localloc tracking similar to what's being added for inline frames

rg -n "m_localAllocCount|m_localAllocs" --type=cpp --type=h -B 5 -A 5


</function_calls>


2674-2676: I'm unable to access the repository to verify the specific concerns raised in the review comment. However, based on the review comment itself, I can provide a rewritten version that clarifies the key verification points that need manual confirmation:


Verify cleanup responsibility, document fields, and confirm struct size constraints.

These fields mirror the inline frame additions and share the same requirements for proper maintenance:

  1. Cleanup responsibility: Confirm that CLR_RT_StackFrame::Pop() properly deallocates pointers tracked in m_localAllocs before frame destruction. If cleanup is missing, localloc'd memory could leak.

  2. Documentation: Add inline comments explaining that m_localAllocCount and m_localAllocs track unmanaged localloc pointers and clarify the cleanup lifecycle.

  3. Struct size impact: These additions increase the per-frame overhead by approximately 17–33 bytes (1 byte count + 4–8 bytes × 4 pointers, architecture-dependent). For deeply nested call stacks, verify this trade-off is acceptable and document it.

  4. Struct layout validation: Confirm the placement before m_extension[1] does not violate alignment expectations and that the CT_ASSERT statements at lines 2876–2887 still validate field offsets correctly with these new members.


2532-2533: The review comment cannot be verified due to repository access limitations in the sandbox environment.

The original comment raises valid concerns about the MAX_LOCALALLOC_COUNT hard limit and requests verification of:

  • Overflow handling when the limit of 4 is exceeded
  • Documentation explaining why 4 was chosen
  • Bounds checking in the CEE_LOCALLOC implementation

However, these verifications require access to:

  • The CEE_LOCALLOC instruction handler implementation
  • Memory management and frame cleanup code
  • Any validation logic related to m_localAllocCount

Since the header file alone cannot confirm whether proper overflow handling exists in the implementation, and sandbox repository access is unavailable, the original review comment's verification request remains unresolved but valid.

The comment should be addressed by the developer reviewing/implementing this change by confirming overflow behavior and adding documentation if not already present.

src/CLR/Include/nanoCLR_Types.h (1)

521-524: I'll help you verify and rewrite this review comment. Let me start by examining the enum structure and the specific concerns raised.
<function_calls>

First, let's read the header file to see the full enum context

cat -n src/CLR/Include/nanoCLR_Types.h | sed -n '500,540p'


</function_calls>

<function_calls>

Check the full DataType enum to understand the structure

rg -n "enum.*DataType|DATATYPE_" src/CLR/Include/nanoCLR_Types.h | head -50


</function_calls>

<function_calls>

Let's see more context around DATATYPE_FIRST_INVALID and DATATYPE_PTR

rg -n -B5 -A5 "DATATYPE_FIRST_INVALID|DATATYPE_PTR" src/CLR/Include/nanoCLR_Types.h


</function_calls>

josesimoes and others added 7 commits November 27, 2025 01:31
…d0f50-b140-488f-b95f-db856b2f79b6

Code style fixes for nanoframework/nf-interpreter PR#3242
Automated fixes for code style.
…c26bb-406f-4265-bae1-c4f38e4cd866

Code style fixes for nanoframework/nf-interpreter PR#3242
@josesimoes
Copy link
Member Author

@coderabbitai review

- Noe correctly resolves cross refs from VAR and MVAR.
…pec for context

- Rework code to resolve nested VAR and MVAR situations.
- General improvement to use recently added helper funcions.
…lution

- Update callers and callees accordingly.
- Pre-Execution Validation (before each opcode executes):
  + Stack Underflow Check: Verifies that the stack has enough items for the opcode to pop
  + Stack Overflow Check: Ensures the opcode won't exceed maximum stack depth
- Post-Execution Validation (after each opcode completes):
  + Stack Depth Consistency Check: Verifies the stack changed by the expected amount
- Enhanced Error Reporting:
  + All validation errors include: Opcode name and hex value, expected vs. actual values, current stack state

Motivation:
- Catches underflow before it happens - validates stack has required items
- Detects implementation bugs - verifies opcodes modify stack correctly
- Pinpoints exact failure - shows which opcode caused the problem
- Works with existing infrastructure - uses NANOCLR_OPCODE_STACKCHANGES flag
- No performance impact when disabled - all checks are conditional
@josesimoes
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

✅ 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.

Automated fixes for code style.
@josesimoes josesimoes changed the title Implement localloc and cpblk IL instructions More work on generic support Dec 3, 2025
…8a279-7b18-46ee-a1c2-e92d9cb20f49

Code style fixes for nanoframework/nf-interpreter PR#3242
Copy link
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: 0

Caution

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

⚠️ Outside diff range comments (5)
src/CLR/Core/CLR_RT_HeapBlock.cpp (1)

644-652: Fix uninitialized obj in DATATYPE_PTR case of StoreToReference

In StoreToReference, the new dt == DATATYPE_PTR branch performs a memcpy but then falls through to obj->Assign(*this) with obj never initialized on that path. If this branch is taken, obj is indeterminate and dereferenced, which is undefined behavior.

You likely want to treat the unmanaged pointer case as “write raw bytes and return” without touching the heap block target. For example:

-    else if (dt == DATATYPE_PTR)
-    {
-        // unmanaged pointer, perform a direct memory copy
-        memcpy((void *)ref.UnmanagedPointer(), (void *)&NumericByRef(), size);
-    }
+    else if (dt == DATATYPE_PTR)
+    {
+        // unmanaged pointer, perform a direct memory copy
+        memcpy((void *)ref.UnmanagedPointer(), (void *)&NumericByRef(), size);
+
+        // Nothing to assign back to a HeapBlock in this case
+        NANOCLR_SET_AND_LEAVE(S_OK);
+    }

(Optionally also guard against non‑positive size if callers might ever pass 0 or −1 here.)

src/CLR/Core/TypeSystem.cpp (3)

1170-1401: Guard against null caller / generic context in TypeDef::ResolveToken TypeSpec VAR/MVAR path

In the TBL_TypeSpec case you now resolve VAR/MVAR via the caller method context and optionally contextTypeSpec. Two problems:

  1. In the DATATYPE_VAR branch you dereference caller and caller->genericType and later caller->arrayElementType without checking that caller and caller->genericType are non-null:

    CLR_RT_TypeSpec_Instance callerTypeSpec;
    if (!callerTypeSpec.InitializeFromIndex(*caller->genericType)) // potential null deref
    ...
    else if (NANOCLR_INDEX_IS_VALID(caller->arrayElementType) && genericPosition == 0) // same

    If ResolveToken is ever called with a TypeSpec token that encodes VAR but caller is null (or caller->genericType is null), this will crash. Other code paths (e.g., the TypeSpec::ResolveToken VAR handler from previous PRs) explicitly check caller == nullptr || caller->genericType == nullptr before dereferencing. Based on learnings, this should be consistent.

  2. In the DATATYPE_MVAR path you correctly guard on caller == nullptr || caller->genericType == nullptr and on contextTypeSpec for the deferred VAR resolution, but the goto resolve_generic_argument target is shared with the VAR branch. If the VAR branch is entered with a null caller, the goto will still jump into code that assumes caller is valid.

Recommend tightening this block as follows:

  • Early-return if caller == nullptr or caller->genericType == nullptr before any use in the DATATYPE_VAR path.
  • Guard access to caller->arrayElementType with a caller != nullptr check.
  • Keep the single resolve_generic_argument label, but ensure every path that reaches it has already validated caller / generic context.

Example sketch:

-                    if (elem.DataType == DATATYPE_VAR)
-                    {
+                    if (elem.DataType == DATATYPE_VAR)
+                    {
+                        if (caller == nullptr || caller->genericType == nullptr)
+                        {
+                            return false;
+                        }
                         CLR_RT_TypeSpec_Instance callerTypeSpec;
                         if (!callerTypeSpec.InitializeFromIndex(*caller->genericType))
                             return false;
                         ...
-                        else if (NANOCLR_INDEX_IS_VALID(caller->arrayElementType) && genericPosition == 0)
+                        else if (caller != nullptr &&
+                                 NANOCLR_INDEX_IS_VALID(caller->arrayElementType) &&
+                                 genericPosition == 0)
                         {
                             ...
                         }

This keeps the new behavior but avoids undefined behavior on missing context.


5495-5738: Possible null-dereference of contextTypeSpec when scheduling generic static constructors

In AllocateGenericStaticFieldsOnDemand, after allocating generic static fields and setting up the registry entry, you schedule the generic type’s static constructor:

if (SUCCEEDED(CLR_RT_HeapBlock_Delegate::CreateInstance(refDlg, cctorIndex, nullptr)))
{
    CLR_RT_HeapBlock_Delegate *dlg = refDlg.DereferenceDelegate();
    ...
    // Store the TypeSpec index so the .cctor can resolve type generic parameters
    dlg->m_genericTypeSpec = *contextTypeSpec;

contextTypeSpec is a const CLR_RT_TypeSpec_Index * parameter and is allowed to be null (and is passed straight through from GetStaticFieldByFieldDef). Here it’s dereferenced without a null check, which will crash if AllocateGenericStaticFieldsOnDemand is ever called with contextTypeSpec == nullptr.

Given that this helper is already explicitly passed a typeSpecIndex for the generic instance being allocated, and tsInstance is successfully initialized from it earlier, you can safely store that instead when no external context is needed:

-                    // Store the TypeSpec index so the .cctor can resolve type generic parameters
-                    dlg->m_genericTypeSpec = *contextTypeSpec;
+                    // Prefer the explicit typeSpecIndex; fall back to contextTypeSpec only if provided
+                    if (contextTypeSpec != nullptr && NANOCLR_INDEX_IS_VALID(*contextTypeSpec))
+                    {
+                        dlg->m_genericTypeSpec = *contextTypeSpec;
+                    }
+                    else
+                    {
+                        dlg->m_genericTypeSpec = typeSpecIndex;
+                    }

This keeps the desired behavior and removes a potential null dereference.


7844-7973: BuildMethodName(MethodDef overload): several safety issues around genericType and VAR formatting

In the useGeneric path of CLR_RT_TypeSystem::BuildMethodName(const CLR_RT_MethodDef_Instance &mdInst, ...) there are a few correctness/safety issues:

  1. Possible null deref of genericType when formatting method generic arguments

    In the loop over parser.ParamCount for MethodSpec generic arguments:

    if (elem.DataType == DATATYPE_VAR)
    {
        CLR_RT_TypeSpec_Instance contextTs;
        CLR_RT_SignatureParser::Element paramElement;
    
        // try to resolve from method context
        if (!contextTs.InitializeFromIndex(*genericType))
            NANOCLR_SET_AND_LEAVE(CLR_E_FAIL);
        ...
    }

    Here genericType is the function parameter, not mdInst.genericType. It may legitimately be nullptr (e.g., when the method itself carries its generic context in mdInst.genericType). Dereferencing *genericType without a null check will crash in that case.

    You should either:

    • Prefer mdInst.genericType when it’s valid, and only fall back to genericType if provided, and
    • Guard all *genericType uses with a null check.
  2. Using contextTs.GetGenericParam failure but still calling BuildTypeName on an uninitialized element

    In the same VAR branch:

    if (!contextTs.GetGenericParam(elem.GenericParamPosition, paramElement))
    {
        // Couldn't resolve
        CLR_SafeSprintf(szBuffer, iBuffer, "!%d", elem.GenericParamPosition);
    }
    
    NANOCLR_CHECK_HRESULT(BuildTypeName(paramElement.Class, szBuffer, iBuffer));

    If GetGenericParam fails, you print the !N encoding but then still call BuildTypeName(paramElement.Class, ...) on an uninitialized paramElement. This is undefined behavior and may also produce duplicate output.

    This should be:

    if (!contextTs.GetGenericParam(elem.GenericParamPosition, paramElement))
    {
        CLR_SafeSprintf(szBuffer, iBuffer, "!%d", elem.GenericParamPosition);
        continue; // don't use paramElement
    }
    
    NANOCLR_CHECK_HRESULT(BuildTypeName(paramElement.Class, szBuffer, iBuffer));
  3. DATATYPE_VAR in the earlier MVAR resolution path

    Earlier in the file (BuildTypeName’s MVAR handling and similar code), you already have patterns for deferring VAR resolution back into a type context. It would be good to mirror that here to keep behaviors consistent; right now the VAR handling in this loop is more brittle.

Addressing (1) and (2) is important to avoid crashes or malformed method names when generic context is missing or cannot be resolved.

src/CLR/Debugger/Debugger.cpp (1)

3058-3083: Storage-backed arrays in Debugging_Value_GetArray are only asserting, not handled

When array->m_fReference == false and array->m_StoragePointer != 0, the new branch just ASSERT(FALSE) and leaves blk/reference unset. In debug builds this will break as soon as a debugger inspects a Span/ReadOnlySpan element backed by storage; in release builds it quietly reports a null/empty value instead of the real element.

Either implement the storage-backed element read (e.g., load the value into tmp from m_StoragePointer + index) or at least fall back to a defined behavior (e.g., returning an error reply) instead of asserting and proceeding with an uninitialized blk.

🧹 Nitpick comments (9)
src/CLR/Include/nanoCLR_Types.h (1)

516-524: Tighten DATATYPE_PTR docs and fix comment typo

The new DATATYPE_PTR / REFLECTION_STORAGE_PTR entries look consistent with the pointer/storage work in this PR. Minor nits:

  • Line 522: comment reads // unamaged pointer; consider correcting to // unmanaged pointer.
  • Given the nearby “KEEP IN SYNC” notes, double‑check that metadata tooling (NanoCLRDataType mirrors in VS extension/MDP, reflection handling for REFLECTION_STORAGE_PTR) has been updated to understand these new enum values.

Also applies to: 526-538

src/CLR/Core/CLR_RT_HeapBlock.cpp (1)

1474-1476: Ensure ASSERT macro is defined consistently across targets

The new SZARRAY equality fast path adds:

// check that array is not stored in stack
ASSERT(objLeft->m_StoragePointer == 0);

Elsewhere in this file you use _ASSERTE / NANOCLR_DEBUG_STOP / NANOCLR_STOP. If ASSERT isn’t defined uniformly for all platforms/toolchains, this could become a compile issue. Consider either:

  • Reusing the existing _ASSERTE macro here, or
  • Confirming ASSERT is a project‑wide alias that’s available in all build configurations.
src/CLR/Core/Interpreter.cpp (2)

3135-3152: RVA field handling implementation is correct.

The implementation properly uses SetUnmanagedPointer() for RVA-backed fields, which is appropriate for exposing raw metadata storage. The dummyVar pattern to advance past the element count header works correctly.

Minor suggestion: Consider renaming dummyVar to elementCount (even if unused) for clarity, or use (void) cast pattern to explicitly indicate the intentional discard:

-                        CLR_UINT32 dummyVar;
-                        NANOCLR_READ_UNALIGNED_UINT16(dummyVar, ptrSrc);
+                        CLR_UINT32 elementCount;
+                        NANOCLR_READ_UNALIGNED_UINT16(elementCount, ptrSrc);
+                        (void)elementCount; // Skip element count to get to raw data

4334-4364: CEE_CPBLK implementation follows ECMA-335 specification.

The implementation correctly uses memmove() which handles overlapping memory regions safely. Based on learnings, per ECMA-335 specification, cpblk is an unverifiable instruction where the CIL compiler ensures correctness, so runtime validation is not required.

One minor observation: for the 32-bit case, the code uses .s4 (signed) for pointer extraction. While this works in practice (the bit pattern is the same), using .u4 would be more semantically correct for pointer values:

 #else
-                uintptr_t sourceAddress = evalPos[0].NumericByRef().s4;
+                uintptr_t sourceAddress = evalPos[0].NumericByRef().u4;
 #endif
                     evalPos--;
 
                     // get destination address
 #ifdef _WIN64
                     uintptr_t destinationAddress = evalPos[0].NumericByRef().s8;
 #else
-                uintptr_t destinationAddress = evalPos[0].NumericByRef().s4;
+                uintptr_t destinationAddress = evalPos[0].NumericByRef().u4;
 #endif
src/CLR/Core/CLR_RT_HeapBlock_Array.cpp (1)

391-392: Consider promoting ASSERT to a runtime check for safety.

The ASSERT(arraySrc->m_StoragePointer == 0) only fires in debug builds. In release builds, calling memmove on a storage-pointer-backed array could silently corrupt memory or cause undefined behavior. Consider either:

  1. Adding a runtime check that returns an error (e.g., CLR_E_INVALID_OPERATION)
  2. Using the IsStoragePointer() helper for consistency
             if (!arraySrc->m_fReference)
             {
-                // check that array is not stored in stack
-                ASSERT(arraySrc->m_StoragePointer == 0);
+                // storage-pointer-backed arrays cannot be copied via memmove
+                if (arraySrc->IsStoragePointer() || arrayDst->IsStoragePointer())
+                {
+                    NANOCLR_SET_AND_LEAVE(CLR_E_INVALID_OPERATION);
+                }

                 memmove(dataDst, dataSrc, length * sizeElem);
             }
src/CLR/Core/TypeSystem.cpp (3)

2501-2556: InitializeFromSignatureParser: VAR resolution via contextTypeSpec is good, but fallback may mask resolution failures

The new overload of CLR_RT_TypeDescriptor::InitializeFromSignatureParser uses contextTypeSpec to resolve a DATATYPE_VAR into a concrete type via contextTs.GetGenericParam, which aligns with the earlier generic-resolution learnings. Based on learnings, this is desirable.

One nuance: if GetGenericParam fails, you fall back to:

// Couldn't resolve, fall back to original behavior
NANOCLR_CHECK_HRESULT(InitializeFromType(res.Class));

For a VAR, res.Class is not meaningful, so this will typically fail with CLR_E_WRONG_TYPE anyway; the fallback may obscure that this was specifically a failed generic resolution. Consider, when res.DataType == DATATYPE_VAR, either:

  • Returning a clearer error (e.g., CLR_E_WRONG_TYPE with a comment), or
  • Leaving a brief comment that this path is expected to fail for VAR and is only here to preserve previous behavior.

No functional bug, but clarity would help future maintainers.


5403-5481: On-demand generic static field allocation: behavior is good, but context assumptions should be documented

CLR_RT_Assembly::GetStaticFieldByFieldDef now:

  • Prefers per-TypeSpec generic statics via GetGenericStaticField.
  • For open generics (!IsClosedGenericType()), calls AllocateGenericStaticFieldsOnDemand with both contextTypeSpec and contextMethod so that VAR/MVAR resolution during hashing and .cctor scheduling is context-aware.
  • Falls back to per-assembly static fields when no genericType is provided.

This is a solid design for supporting runtime-bound generic statics.

Given that AllocateGenericStaticFieldsOnDemand relies on contextTypeSpec/contextMethod being the same context that was used to bind the generic at IL execution time, it would be worth adding a short comment at the call site clarifying that callers must pass the current closed generic TypeSpec and method context when available; otherwise, hash collisions or missed reuse could occur for open generics that are resolved differently at runtime.

No functional bug here, just a suggestion to clarify invariants.


7511-7692: BuildTypeName(TypeSpec): minor robustness issue on VAR resolution failure

The new BuildTypeName(const CLR_RT_TypeSpec_Index &...) correctly:

  • Parses the TypeSpec and prints the generic type definition name.
  • Iterates generic arguments and resolves DATATYPE_VAR via contextTypeSpec, and DATATYPE_MVAR via contextMethodDef and its MethodSpec, falling back to !N / !!N syntax when unresolved.

One small robustness gap:

if (contextTs.GetGenericParam(element.GenericParamPosition, paramElement))
{
    // now print the *actual* type name
    BuildTypeName(paramElement.Class, szBuffer, iBuffer);
}

If BuildTypeName(paramElement.Class, ...) fails (e.g., malformed metadata), the HRESULT is ignored. In other places you wrap this with NANOCLR_CHECK_HRESULT. For consistency and easier debugging, it would be better to propagate the error:

-    if (contextTs.GetGenericParam(element.GenericParamPosition, paramElement))
-    {
-        // now print the *actual* type name
-        BuildTypeName(paramElement.Class, szBuffer, iBuffer);
-    }
+    if (contextTs.GetGenericParam(element.GenericParamPosition, paramElement))
+    {
+        // now print the *actual* type name
+        NANOCLR_CHECK_HRESULT(BuildTypeName(paramElement.Class, szBuffer, iBuffer));
+    }

This doesn’t change expected behavior in the normal case but makes failures clearer.

src/CLR/Core/Execution.cpp (1)

2032-2064: Generic parameter resolution (VAR/MVAR) via TypeSpec/MethodSpec is consistent and robust

The new ResolveGenericTypeParameter() helper plus the updated DATATYPE_VAR/DATATYPE_MVAR handling in InitializeLocals() correctly:

  • Use the enclosing closed genericType TypeSpec to resolve VAR (!T) locals.
  • Use CLR_RT_MethodSpec::GetGenericArgument() with a CLR_RT_SignatureParser::Element to resolve MVAR (!!T) when a MethodSpec is present.
  • Fall back to FindGenericParamAtMethodDef and cross-reference lookup for open generic methods.

This matches the existing TypeSpec/VAR learnings and centralizes the logic, reducing duplication and future bugs. Based on learnings, this is the intended pattern for consuming VAR/MVAR from signatures.

Also applies to: 2335-2395

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90e4a41 and 29c82dc.

📒 Files selected for processing (12)
  • src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (2 hunks)
  • src/CLR/CorLib/corlib_native_System_Span_1.cpp (3 hunks)
  • src/CLR/Core/CLR_RT_HeapBlock.cpp (4 hunks)
  • src/CLR/Core/CLR_RT_HeapBlock_Array.cpp (6 hunks)
  • src/CLR/Core/Execution.cpp (2 hunks)
  • src/CLR/Core/Interpreter.cpp (5 hunks)
  • src/CLR/Core/TypeSystem.cpp (18 hunks)
  • src/CLR/Debugger/Debugger.cpp (1 hunks)
  • src/CLR/Diagnostics/Info.cpp (3 hunks)
  • src/CLR/Include/nanoCLR_Runtime.h (6 hunks)
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h (6 hunks)
  • src/CLR/Include/nanoCLR_Types.h (2 hunks)
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3242
File: src/CLR/Core/Interpreter.cpp:4263-4293
Timestamp: 2025-11-27T02:57:09.957Z
Learning: In nanoFramework's nf-interpreter, the cpblk IL instruction implementation in src/CLR/Core/Interpreter.cpp does not require runtime validation of pointer addresses, size, or bounds. Per ECMA-335 specification, cpblk is an unverifiable instruction where the CIL compiler ensures correctness, not the runtime.
📚 Learning: 2025-01-22T03:38:57.394Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3074
File: src/CLR/Core/GarbageCollector_Info.cpp:107-167
Timestamp: 2025-01-22T03:38:57.394Z
Learning: In nanoFramework's memory management code, DataSize() validation is comprehensively handled through CLR_RT_HeapCluster::ValidateBlock() and other caller code. Additional size checks in ValidateCluster() are redundant as the validation is already performed at multiple levels.

Applied to files:

  • src/CLR/CorLib/corlib_native_System_Span_1.cpp
  • src/CLR/Core/Interpreter.cpp
  • src/CLR/Core/CLR_RT_HeapBlock.cpp
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h
📚 Learning: 2025-01-09T13:32:43.711Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3062
File: src/System.Device.Spi/sys_dev_spi_native_System_Device_Spi_SpiDevice.cpp:106-188
Timestamp: 2025-01-09T13:32:43.711Z
Learning: In nanoFramework, CLR_RT_HeapBlock_Array::Pin() method returns void and cannot fail. It should be called without error handling.

Applied to files:

  • src/CLR/CorLib/corlib_native_System_Span_1.cpp
  • src/CLR/Core/Interpreter.cpp
  • src/CLR/Core/CLR_RT_HeapBlock.cpp
  • src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h
📚 Learning: 2024-09-25T11:28:38.536Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3023
File: targets/netcore/nanoFramework.nanoCLR/nanoCLR_native.cpp:191-225
Timestamp: 2024-09-25T11:28:38.536Z
Learning: In `nanoCLR_GetNativeAssemblyInformation`, there is no need to return the number of bytes written, as the memory buffer is zeroed, making the string buffer terminated.

Applied to files:

  • src/CLR/CorLib/corlib_native_System_Span_1.cpp
  • src/CLR/Include/nanoCLR_Runtime.h
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h
📚 Learning: 2025-11-20T14:08:30.044Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3240
File: src/CLR/Core/TypeSystem.cpp:812-818
Timestamp: 2025-11-20T14:08:30.044Z
Learning: Repo: nanoframework/nf-interpreter
File: src/CLR/Core/TypeSystem.cpp
Context: Generics resolution in CLR_RT_TypeSpec_Instance::ResolveToken
Learning: When resolving a VAR (!T) TypeSpec using the caller’s closed generic, after switching to the caller context (Set(caller->genericType->Assembly(), closedTsRow) and assigning assembly accordingly), always fetch the TypeSpec with target = assembly->GetTypeSpec(closedTsRow) rather than assm->GetTypeSpec(closedTsRow) to avoid cross-assembly mismatches.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime.h
  • src/CLR/Core/Interpreter.cpp
  • src/CLR/Diagnostics/Info.cpp
  • src/CLR/Core/CLR_RT_HeapBlock_Array.cpp
  • src/CLR/Core/Execution.cpp
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h
  • src/CLR/Core/TypeSystem.cpp
📚 Learning: 2025-06-26T09:16:55.184Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3190
File: src/CLR/Core/TypeSystem.cpp:0-0
Timestamp: 2025-06-26T09:16:55.184Z
Learning: In nanoFramework's CLR attribute parsing (src/CLR/Core/TypeSystem.cpp), the sentinel value 0xFFFF in string tokens represents a null string. When encountered, this should result in a true null reference (using SetObjectReference(nullptr)) rather than an empty string instance, and the boxing operation should be skipped via early return.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime.h
  • src/CLR/Core/CLR_RT_HeapBlock_Array.cpp
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h
📚 Learning: 2024-10-12T19:00:39.000Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3023
File: targets/netcore/nanoFramework.nanoCLR/nanoCLR_native.cpp:191-225
Timestamp: 2024-10-12T19:00:39.000Z
Learning: When working with `nanoCLR_GetNativeAssemblyInformation`, fixed-size assembly names are required, so code that deals with variable-length names cannot be used.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime.h
  • src/CLR/Core/Interpreter.cpp
  • src/CLR/Core/TypeSystem.cpp
📚 Learning: 2025-05-14T16:27:02.573Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3172
File: src/CLR/Core/CLR_RT_HeapBlock.cpp:899-900
Timestamp: 2025-05-14T16:27:02.573Z
Learning: The CLR_RT_TypeDescriptor type in nanoFramework doesn't have a GetElementType() API for extracting array element types.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime.h
  • src/CLR/Core/Execution.cpp
  • src/CLR/Core/TypeSystem.cpp
📚 Learning: 2025-05-14T17:33:51.546Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3172
File: src/CLR/Core/TypeSystem.cpp:4556-4589
Timestamp: 2025-05-14T17:33:51.546Z
Learning: When parsing TypeSpec signatures in nanoFramework, the first Advance() call consumes the VAR/MVAR token, followed by additional Advance() calls to walk to the specific generic parameter position.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime.h
  • src/CLR/Core/Execution.cpp
  • src/CLR/Core/TypeSystem.cpp
📚 Learning: 2025-11-27T02:57:09.957Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3242
File: src/CLR/Core/Interpreter.cpp:4263-4293
Timestamp: 2025-11-27T02:57:09.957Z
Learning: In nanoFramework's nf-interpreter, the cpblk IL instruction implementation in src/CLR/Core/Interpreter.cpp does not require runtime validation of pointer addresses, size, or bounds. Per ECMA-335 specification, cpblk is an unverifiable instruction where the CIL compiler ensures correctness, not the runtime.

Applied to files:

  • src/CLR/Include/nanoCLR_Runtime.h
  • src/CLR/Core/Interpreter.cpp
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h
📚 Learning: 2025-01-22T03:38:57.394Z
Learnt from: josesimoes
Repo: nanoframework/nf-interpreter PR: 3074
File: src/CLR/Core/GarbageCollector_Info.cpp:107-167
Timestamp: 2025-01-22T03:38:57.394Z
Learning: In CLR_RT_GarbageCollector::ValidateCluster, DataSize() validation is already handled by ValidateBlock() and other caller code, making additional size checks redundant.

Applied to files:

  • src/CLR/Core/Interpreter.cpp
  • src/CLR/Include/nanoCLR_Runtime__HeapBlock.h
🧬 Code graph analysis (8)
src/CLR/CorLib/corlib_native_System_Span_1.cpp (2)
src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (2)
  • _ctor___VOID__VOIDptr__I4 (11-106)
  • _ctor___VOID__VOIDptr__I4 (11-11)
src/CLR/Core/CLR_RT_HeapBlock_Array.cpp (2)
  • CreateInstanceWithStorage (91-119)
  • CreateInstanceWithStorage (91-95)
src/CLR/Include/nanoCLR_Runtime.h (2)
src/CLR/Core/TypeSystem.cpp (12)
  • ResolveToken (760-846)
  • ResolveToken (760-763)
  • ResolveToken (1167-1427)
  • ResolveToken (1167-1171)
  • ResolveToken (1623-1718)
  • ResolveToken (1623-1626)
  • ResolveToken (1873-2109)
  • ResolveToken (1873-1876)
  • ResolveToken (2206-2230)
  • ResolveToken (2206-2206)
  • GetGenericArgument (2263-2286)
  • GetGenericArgument (2263-2265)
src/CLR/Include/nanoCLR_Types.h (2)
  • const (1112-1184)
  • const (1618-1675)
src/CLR/Core/Interpreter.cpp (1)
src/CLR/Core/CLR_RT_HeapBlock_Array.cpp (4)
  • CreateInstance (10-69)
  • CreateInstance (10-13)
  • CreateInstance (71-88)
  • CreateInstance (71-74)
src/CLR/Core/CLR_RT_HeapBlock.cpp (1)
src/CLR/Include/nanoCLR_Runtime__HeapBlock.h (1)
  • SetUnmanagedPointer (1108-1112)
src/CLR/Core/CLR_RT_HeapBlock_Array.cpp (1)
src/CLR/Include/nanoCLR_Runtime__HeapBlock.h (1)
  • IsStoragePointer (1918-1921)
src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (1)
src/CLR/Core/CLR_RT_HeapBlock_Array.cpp (2)
  • CreateInstanceWithStorage (91-119)
  • CreateInstanceWithStorage (91-95)
src/CLR/Include/nanoCLR_Runtime__HeapBlock.h (1)
src/CLR/Core/CLR_RT_HeapBlock_Array.cpp (2)
  • CreateInstanceWithStorage (91-119)
  • CreateInstanceWithStorage (91-95)
src/CLR/Core/TypeSystem.cpp (1)
src/CLR/Include/nanoCLR_Types.h (1)
  • CLR_TkFromStream (650-657)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: nf-interpreter (Check_Code_Style)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets MXCHIP_AZ3166)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets ORGPAL_PALX)
  • GitHub Check: nf-interpreter (Nightly build) (Build_STM32_targets ORGPAL_PALTHREE)
  • GitHub Check: nf-interpreter (Nightly build) (Check_Build_Options)
  • GitHub Check: nf-interpreter (Check_Build_Options)
🔇 Additional comments (30)
src/CLR/Core/CLR_RT_HeapBlock.cpp (1)

2084-2099: Pointer add/sub semantics for DATATYPE_PTR look correct and intentionally strict

The new DATATYPE_PTR branches in NumericAdd / NumericSub:

  • Treat the left operand as an unmanaged byte pointer (uint8_t* from UnmanagedPointer()), and
  • Only allow addition/subtraction of an I4 right operand, updating the stored pointer in place; all other right‑hand types fall through to CLR_E_WRONG_TYPE.

This is a clear and predictable model for unmanaged pointer arithmetic, and the explicit comment (“binary numeric add/sub (byte wise)”) documents the byte‑offset behavior well. No issues from a runtime‑semantics perspective; just be aware that adding support for other integral datatypes (e.g., U4) later will require extending the type check here.

Also applies to: 2185-2200

src/CLR/Include/nanoCLR_Runtime.h (2)

2219-2224: ResolveToken / GetGenericArgument signature changes line up with enhanced generic resolution

The updated declarations:

  • CLR_RT_TypeDef_Instance::ResolveToken(...) gaining an optional const CLR_RT_TypeSpec_Index* contextTypeSpec, and
  • CLR_RT_MethodSpec_Instance::GetGenericArgument(...) now returning a full CLR_RT_SignatureParser::Element &

are consistent with the generics work described (VAR/MVAR resolution with an explicit TypeSpec context and richer generic-argument inspection). The header signatures match the referenced implementations in TypeSystem.cpp and nanoCLR_Types.h; no issues from an API or ABI standpoint within the CLR.

Also applies to: 2356-2357


2536-2551: Guard localloc tracking against overflow of the per-frame arrays

The new localloc tracking implementation—using MAX_LOCALALLOC_COUNT and the m_localAllocCount / m_localAllocs fields in CLR_RT_InlineFrame and CLR_RT_StackFrame—establishes a fixed upper bound of 4 locallocs per frame. This design requires that every writer of m_localAllocCount enforces the bound before appending to m_localAllocs[...]. If any execution path executes more than 4 localloc instructions without bounds checking, it will write past the array and corrupt the frame.

Verify that all interpreter / execution engine code writing locallocs:

  • Checks m_localAllocCount < c_Max_Localloc_Count before storing into m_localAllocs[...], and
  • Fails gracefully (throws OOM or execution engine error) if the bound is exceeded.
src/CLR/Core/Interpreter.cpp (3)

3468-3474: Generic context propagation for array creation looks correct.

The additional parameters properly pass the calling context (&stack->m_call) and generic type storage (&stack->m_genericTypeSpecStorage) to enable correct generic type resolution during array instantiation.


4376-4376: CEE_READONLY prefix correctly added to no-op group.

The readonly. prefix (used before ldelema to indicate the reference won't be used for modification) is appropriately treated as a no-op in nanoFramework's execution model. Good placement with the other instruction prefixes.


4295-4330: CEE_LOCALLOC implementation uses native heap allocation and requires cleanup verification.

The implementation correctly:

  • Uses platform_malloc() for native heap allocation (avoiding GC complexity)
  • Zero-initializes per ECMA-335 specification
  • Bounds-checks against c_Max_Localloc_Count
  • Checks for allocation failures
  • Uses SetUnmanagedPointer() to push the result

Allocations stored in stack->m_localAllocs must be freed when the stack frame is popped to prevent memory leaks. Verify that deallocation occurs in CLR_RT_StackFrame::Pop() or the frame's destructor.

src/CLR/Diagnostics/Info.cpp (1)

581-587: Consistent generic context propagation to BuildTypeName.

All three call sites now correctly pass methodDefInstance.genericType and &methodDefInstance to enable proper resolution of VAR/MVAR parameters in nested type names. The changes are consistent and align with the broader PR objective of enhanced generic context handling across the type system.

Also applies to: 731-737, 747-753

src/CLR/Include/nanoCLR_Runtime__HeapBlock.h (4)

763-768: New UnmanagedPointer struct for DATATYPE_PTR support.

The new union member correctly stores unmanaged pointer values for the localloc instruction implementation. The uintptr_t type ensures proper size across 32/64-bit platforms.


1108-1117: Clean API for unmanaged pointer access.

SetUnmanagedPointer correctly tags the block as DATATYPE_PTR with size 1, and UnmanagedPointer() safely returns 0 when the data type doesn't match. This defensive approach prevents misuse.


1886-1893: Storage pointer branch logic in element accessors looks correct.

Both GetFirstElement() and GetFirstElementUInt16() correctly branch on REFLECTION_STORAGE_PTR to return either the external storage address or the inline storage. This enables transparent access regardless of backing store.

Also applies to: 1903-1910


1862-1862: Verify m_StoragePointer initialization in all array creation paths.

The new m_StoragePointer field requires verification that all array creation paths (particularly ExtractHeapBlocksForArray in the execution engine) zero-initialize this field, or that the HB_InitializeToZero flag is always used for array allocations.

src/CLR/Core/CLR_RT_HeapBlock_Array.cpp (4)

27-27: Correct handling of REFLECTION_STORAGE_PTR in CreateInstance.

The expanded kind check and conditional ClearElements skip are correct—storage pointer arrays have externally-managed memory that shouldn't be zeroed by the runtime.

Also applies to: 60-65


91-119: CreateInstanceWithStorage implementation is sound.

The approach of creating a zero-length array first (to avoid allocating unused backing storage), then setting m_StoragePointer and adjusting m_numOfElements, is efficient and correct. The relevant code snippet from the header confirms the pattern is used consistently.


126-127: Enhanced token resolution with generic context.

Passing contextTypeSpec to ResolveToken enables proper resolution of generic type parameters (VAR/MVAR) when creating arrays in generic contexts. This aligns with the broader PR changes for generic context propagation.

Also applies to: 138-138


172-176: Defensive guard in ClearElements for storage pointers.

Returning CLR_E_WRONG_TYPE when attempting to clear elements of a storage-pointer-backed array is the correct behavior—external storage shouldn't be modified by the runtime's array clearing logic.

src/CLR/Core/TypeSystem.cpp (9)

543-551: CMOD_REQD/CMOD_OPT handling in SignatureParser::Advance looks correct

Skipping CMOD_REQD/CMOD_OPT by consuming the TypeDefOrRef coded index with CLR_TkFromStream(Signature) and looping to the actual type is consistent with ECMA-335 encoding and reuses the existing token reader correctly. No issues here.


2263-2286: MethodSpec::GetGenericArgument change to use SignatureParser Element looks fine

Switching CLR_RT_MethodSpec_Instance::GetGenericArgument to return a CLR_RT_SignatureParser::Element &element and iterating with parser.Advance(element) up to argumentPosition is consistent with how generics are parsed elsewhere in this file. Bounds checking via argumentPosition >= parser.ParamCount is correct. No issues here.


2593-2653: InitializeFromSignatureToken: context-aware TypeSpec handling generally sound

In CLR_RT_TypeDescriptor::InitializeFromSignatureToken, the updated TBL_TypeSpec handling:

  • Correctly special-cases DATATYPE_VAR with caller->arrayElementType for SZArrayHelper and then falls back to resolving against caller->genericType via GetGenericParam.
  • Handles DATATYPE_MVAR by mapping through FindGenericParamAtMethodDef and the generic-param cross-reference.
  • For DATATYPE_GENERICINST, delegates to InitializeFromSignatureParser(parser, contextTypeSpec) so that VARs inside the instantiation can be resolved using the caller’s closed generic context.

Given how CLR_RT_SignatureParser::Advance works for TypeSpec signatures, the extra initial parser.Advance(elem) before delegating is consistent with existing patterns in this file. Overall this looks consistent with the generics behavior described in the retrieved learnings.


5235-5311: ResolveAllocateGenericTypeStaticFields: hash usage with new ComputeHash overload is OK

ResolveAllocateGenericTypeStaticFields still calls ComputeHashForClosedGenericType(genericTypeInstance) without a context. With the new signature that accepts optional contextTypeSpec/contextMethod, this implicitly uses the default arguments (nullptrs), which is appropriate here because metadata TypeSpecs for preallocated generic statics should already be fully closed without needing runtime context. No changes needed.


6507-6517: Relocating generic static fields in CLR_RT_Assembly::Relocate is correct

The added loop:

for (CLR_UINT32 i = 0; i < g_CLR_RT_TypeSystem.m_genericStaticFieldsCount; i++)
{
    CLR_RT_GarbageCollector::RelocateGenericStaticField(&g_CLR_RT_TypeSystem.m_genericStaticFields[i]);
}

ensures that all generic static field records are updated during GC relocation, matching how per-assembly staticFields are already handled. This is required for correctness once generic static storage is heap-allocated. Looks good.


6528-6585: TypeSystem_Initialize/Cleanup generic registries are consistent and leak-free

Initializing and tearing down:

  • m_genericStaticFields, m_genericStaticFieldsCount, m_genericStaticFieldsMaxCount
  • m_genericCctorRegistry, m_genericCctorRegistryCount, m_genericCctorRegistryMaxCount

and freeing m_genericStaticFields[i].m_fieldDefs in TypeSystem_Cleanup is consistent with how platform_malloc is used elsewhere in the generic static field code. This should avoid leaks when the type system is torn down. No issues.


8388-8498: FindOrCreateGenericStaticFields: good reuse, but double-alloc path is carefully handled

The new CLR_RT_TypeSystem::FindOrCreateGenericStaticFields:

  • Reuses an existing record when the hash matches.
  • Grows the registry with platform_malloc and copies old entries when needed.
  • Allocates static field storage via ExtractHeapBlocksForObjects and field-def mapping via platform_malloc.
  • Correctly initializes each field via InitializeReference.

The out-of-memory path for pFieldDefs resets the just-allocated heap blocks to nullptr so they can be reclaimed by GC later, which is reasonable given there’s no explicit free for ExtractHeapBlocksForObjects. Overall this is careful and consistent with the rest of the generic static field machinery.


8501-8621: ComputeHashForClosedGenericType: context-aware VAR/MVAR resolution is solid

The new ComputeHashForClosedGenericType overload:

  • Starts with the generic type definition token.
  • Walks each generic argument via CLR_RT_SignatureParser.
  • For DATATYPE_VAR, resolves via contextTypeSpec and GetGenericParam.
  • For DATATYPE_MVAR, resolves via contextMethod->methodSpec and MethodSpec::GetGenericArgument, and can defer back to VAR resolution when the method argument is itself a VAR.
  • Produces 0 on unresolvable VAR/MVAR, which the callers treat as “not supported”.

This ties in correctly with both the metadata preallocated path (which calls with null context) and the on-demand path from AllocateGenericStaticFieldsOnDemand (which passes both context parameters). The logic matches the VAR/MVAR workflows described in your previous learnings. No issues spotted.


8067-8090: OwnerIndex() on MethodRef refers to the declaring type, not a method definition

Per ECMA-335 II.22, the OwnerIndex (Class column) on a MemberRef/MethodRef is a MemberRefParent coded-index that encodes the declaring type (typically TypeRef, TypeDef, or TypeSpec), not a method definition row. The technical concern in the review is valid: using mr->OwnerIndex() directly as a MethodDef index would be incorrect and could cause out-of-range access or incorrect name resolution.

The suggestion to use crossReferenceMethodRef[mri.Method()].target.data for proper MethodRef-to-MethodDef mapping should be verified against existing patterns in ResolveMethodRef and confirmed to align with the codebase's metadata resolution strategy.

src/CLR/Core/Execution.cpp (1)

1716-1724: Explicitly zeroing m_StoragePointer on normal array allocation looks correct

Initializing pArray->m_StoragePointer = 0 ensures non–storage-backed arrays start in a well-defined state, which makes later checks against m_StoragePointer != 0 (e.g., debugger/storage-aware paths) reliable without relying on implicit zeroing.

src/CLR/CorLib/corlib_native_System_Span_1.cpp (3)

9-107: Pointer-based Span<T>(void* pointer, int length) ctor is consistent with ReadOnlySpan and validates T correctly

The new ctor mirrors the ReadOnlySpan version: it parses the generic instantiation, rejects reference/contains-references element types via RuntimeHelpers::CheckReferenceOrContainsReferences, validates length and DATATYPE_PTR, and then uses CreateInstanceWithStorage. This matches the intended unsafe span semantics.


124-143: CopyTo reimplementation with memmove and GC protection looks correct

The updated CopyTo:

  • Checks destination span is large enough.
  • Fast-paths the zero-length case.
  • Protects both source and destination arrays from GC movement.
  • Uses memmove over length * elementSize bytes, which correctly handles overlapping spans.

Given that both spans are Span<T> with the same T, relying on m_sizeOfElement from the source array is safe here.


148-183: NativeSpanConstructor stores raw pointers into managed arrays without confirmed retention or GC awareness

The implementation stores ptrToStartElement (a raw uintptr_t) via CreateInstanceWithStorage() but does not demonstrate that:

  1. The source array is pinned or retained as a managed reference to prevent collection
  2. The GC/runtime relocates m_StoragePointer during heap compaction (nanoCLR uses a compacting GC, and raw addresses are not automatically updated)

Without these safeguards, spans constructed from managed arrays risk:

  • Immediate collection if the source array has no other references (e.g., new Span<int>(new int[10], 2, 5))
  • Dangling pointers after heap compaction moves the source array

Additionally, range validation on start and length is now the caller's responsibility (no native-side bounds checking visible).

Verify that the source array lifetime is managed (pinned or held via a tracked reference in the span's backing structure) and that m_StoragePointer is relocated by the GC during compaction, or consider representing managed-array spans with explicit array references and offsets instead.

src/CLR/CorLib/corlib_native_System_ReadOnlySpan_1.cpp (2)

9-106: Pointer-based ReadOnlySpan<T>(void* pointer, int length) ctor matches Span and enforces non-reference T

This ctor mirrors the Span implementation: it validates the generic instantiation, rejects reference/contains-references element types, checks length and DATATYPE_PTR, and uses CreateInstanceWithStorage. That’s appropriate for an unsafe ReadOnlySpan over unmanaged memory.


126-143: NativeReadOnlySpanConstructor shares the same GC and compaction risks as NativeSpanConstructor

The span is backed by a storage-pointer array:

  • uintptr_t ptrToStartElement = (uintptr_t)sourceArray->GetElement(start);
  • CreateInstanceWithStorage(refArray, length, ptrToStartElement, sourceType);

Risks identified:

  • The backing array stores only m_StoragePointer into the source array's data without a managed reference to the source T[]
  • If the original array has no other references, it can be collected while the ReadOnlySpan still exists, leaving m_StoragePointer dangling
  • Heap compaction can move the source array, but m_StoragePointer is not a tracked reference and will not be updated

Verify whether REFLECTION_STORAGE_PTR arrays keep the source array alive and relocate m_StoragePointer during compaction. If not, consider representing spans over arrays with real managed T[] references (and offsets) or augmenting GC support for storage-backed arrays over managed memory.

@josesimoes josesimoes changed the title More work on generic support More work on supporting generics in type system and execution engine Dec 3, 2025
josesimoes and others added 6 commits December 3, 2025 20:10
- Update declaration and callers.
- Rework code to resolve VAR and MVAR of TypeSpec using it.
Automated fixes for code style.
…524bd-c4af-408e-8163-5940f64d18ea

Code style fixes for nanoframework/nf-interpreter PR#3242
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: dependencies Pull requests that update a dependency file(s) or version Type: enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants