Skip to content

Reduce cases, branches, etc, with regard to Variant#1222

Open
VictorSohier wants to merge 13 commits intoRedot-Engine:masterfrom
VictorSohier:master
Open

Reduce cases, branches, etc, with regard to Variant#1222
VictorSohier wants to merge 13 commits intoRedot-Engine:masterfrom
VictorSohier:master

Conversation

@VictorSohier
Copy link

@VictorSohier VictorSohier commented Mar 12, 2026

Problem

I got nerd sniped by Tekk into making significant reductions to this file

What this PR changes

Significantly cleans up the variant implementation such that it is a lot more maintainable and anything with clear cases that reasonably could be compressed was made into clear cases that were reasonably compressed

Result

A much cleaner and smaller variant implementation file

Testing

All existing tests pass. No new tests were added nor were there old tests that were removed.

Summary by CodeRabbit

  • Refactor
    • Centralized type/name and conversion mappings, consolidated destruction and memory paths, and replaced many per-type branches with table-driven dispatch to simplify variant handling and accessors.
  • Bug Fixes
    • Streamlined and unified error reporting for variant operations; added checks for missing arguments, null instances, and const-method cases.
  • Chores
    • Replaced per-type deinitialization arrays with a compact bitmap-style representation for internal deinit handling.

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaced per-type branching in Variant with table-driven structures for type names, casts, strict casts, destructors, and bucket management. get_type_name, get_type_by_name, can_convert/can_convert_strict, and _clear_internal now dispatch via static tables. needs_deinit changed from a per-type array to a 64-bit bitmask.

Changes

Cohort / File(s) Summary
Core variant implementation
core/variant/variant.cpp
Introduced static tables: TYPE_STRING_TABLE, TYPE_CAST_TABLE, TYPE_STRICT_CAST_TABLE, DESTRUCTOR_TABLE, BUCKET_FREE_FN_TBL, BUCKET_TBL. Replaced large per-type switch/array logic for type-name lookup, conversion rules, strict conversion checks, destruction and bucket-freeing with table-driven dispatch. Consolidated error reporting and added <array>/<cstdint> includes.
Variant header / deinit bitmask
core/variant/variant.h
Replaced needs_deinit boolean array with a single static constexpr std::uint64_t needs_deinit bitmask; updated all deinit checks to use bitwise tests. Adjusted related _clear_internal and some _to_int/_to_float` return paths.
Accessors, conversion, and null/shared logic
core/variant/variant.cpp, core/variant/variant.h
Refactored get_type_name, get_type_by_name, can_convert, can_convert_strict, null/empty checks and is_type_shared to use table lookups or a static bitmask (IS_SHARED); moved several per-type branches to compact table or switch-based dispatch.
Public symbols / declarations
core/variant/variant.cpp, core/variant/variant.h
Added static table declarations (TYPE_STRING_TABLE, TYPE_CAST_TABLE, TYPE_STRICT_CAST_TABLE) and changed exported declaration shape for needs_deinit (array -> uint64_t bitmask). No public function signatures were added/removed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Reduce cases, branches, etc, with regard to Variant' accurately describes the main objective of the PR, which focuses on refactoring the Variant implementation to reduce cases, branches, and repetitive code.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/variant/variant.cpp`:
- Around line 2807-2822: Variant::_variant_call_error is missing messages for
several Callable::CallError cases so err can be empty; add explicit case arms
for Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS,
::CALL_ERROR_INSTANCE_IS_NULL, and ::CALL_ERROR_METHOD_NOT_CONST (and update the
default to produce a generic message) and set err to descriptive messages that
include p_method and Variant::get_type_name(type) or argument/index info as
appropriate so ERR_PRINT() never prints a blank line.
- Around line 453-455: Variant::is_null currently treats every non-OBJECT
variant as null because it negates (type == OBJECT && _get_obj().obj); change it
so only genuinely null/uninitialized variants or OBJECTs with a null internal
pointer are considered null: in Variant::is_null(), if type == OBJECT return
!_get_obj().obj, else if the variant represents the explicit
null/none/uninitialized kind return true, otherwise return false; update logic
referencing Variant::is_null, the type enum values (the explicit null/none kind)
and _get_obj() to implement this precise branching.
- Around line 720-730: The cleanup helpers are being called through incompatible
function pointer types (destroy<T> and bucketFree<T> are cast to Destructor and
BucketFree), which is UB; replace the casts with proper type-safe adapter
functions and use those adapters for the stored function pointers. Add
non-templated (or templated-but-instantiated) adapter functions such as
destroyAdapter<T>(void* p) { reinterpret_cast<T*>(p)->~T(); } and
bucketFreeAdapter<T>(void* bucket, void* mem) { static_cast<PagedAllocator<T,
true>*>(bucket)->free(static_cast<T*>(mem)); } and assign pointers to these
adapter functions to Destructor and BucketFree instead of casting the original
templated functions; refer to destroy, bucketFree, Destructor, BucketFree and
PagedAllocator<T, true> when making the change.

In `@core/variant/variant.h`:
- Around line 283-305: The needs_deinit bitmask is missing Variant::RID so
_clear_internal()'s RID destructor path is skipped; add (1ull << Variant::RID)
into the static constexpr std::uint64_t needs_deinit mask so that RID cleanup is
performed when clear() or ~Variant() route teardown through that mask (update
the mask near the declaration of needs_deinit in variant.h to include
Variant::RID).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0332c04f-160b-40bc-8223-122db4d6087b

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff8093 and 7423ce4.

📒 Files selected for processing (2)
  • core/variant/variant.cpp
  • core/variant/variant.h

Copy link

@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

♻️ Duplicate comments (4)
core/variant/variant.h (1)

283-305: ⚠️ Potential issue | 🟠 Major

Restore Variant::RID in needs_deinit.

_clear_internal() still has a RID teardown path, but clear() / ~Variant() now route all cleanup through this mask. Leaving RID out skips RID destruction entirely.

Suggested fix
 static constexpr std::uint64_t needs_deinit = (1ull << Variant::STRING) |
 			(1ull << Variant::TRANSFORM2D) |
 			(1ull << Variant::AABB) |
 			(1ull << Variant::BASIS) |
 			(1ull << Variant::TRANSFORM3D) |
 			(1ull << Variant::PROJECTION) |
 			(1ull << Variant::STRING_NAME) |
 			(1ull << Variant::NODE_PATH) |
+			(1ull << Variant::RID) |
 			(1ull << Variant::OBJECT) |
 			(1ull << Variant::CALLABLE) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/variant/variant.h` around lines 283 - 305, The needs_deinit mask omits
Variant::RID causing RID cleanup to be skipped when clear() / ~Variant() route
teardown through this mask; add (1ull << Variant::RID) back into the static
constexpr std::uint64_t needs_deinit definition so _clear_internal()'s RID
teardown path is reached during Variant destruction/clear, ensuring Variant::RID
is included alongside the other types in needs_deinit.
core/variant/variant.cpp (3)

2859-2874: ⚠️ Potential issue | 🟡 Minor

Populate the remaining CallError branches here.

CALL_ERROR_TOO_FEW_ARGUMENTS, CALL_ERROR_INSTANCE_IS_NULL, and CALL_ERROR_METHOD_NOT_CONST still leave err empty, so ERR_PRINT() emits a blank line for those failures.

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

In `@core/variant/variant.cpp` around lines 2859 - 2874, The error-handling switch
in Variant::_variant_call_error leaves several Callable::CallError branches
empty; implement messages for CALL_ERROR_TOO_FEW_ARGUMENTS (e.g., "Too few
arguments for method 'p_method', expected at least X, got Y" using
error.argument/expected), CALL_ERROR_INSTANCE_IS_NULL (e.g., "Instance is null
for method 'p_method' on type 'Variant::get_type_name(type)'"), and
CALL_ERROR_METHOD_NOT_CONST (e.g., "Method 'p_method' is not const and cannot be
called on a const instance"); ensure you use the existing helpers itos(),
Variant::get_type_name(), p_method and type, populate err in each branch, and
keep the default branch producing a sensible fallback message before calling
ERR_PRINT(err.utf8().get_data()).

450-452: ⚠️ Potential issue | 🔴 Critical

is_null() now treats every non-OBJECT value as null.

This expression only returns false for a live OBJECT. INT, STRING, ARRAY, etc. all become “null”, which is a breaking semantic change.

Suggested fix
 bool Variant::is_null() const {
-	return !(type == OBJECT && _get_obj().obj);
+	return type == NIL || (type == OBJECT && _get_obj().obj == nullptr);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/variant/variant.cpp` around lines 450 - 452, Variant::is_null currently
treats every non-OBJECT type as null; update its logic so it only reports null
for the explicit null variant type or for an OBJECT whose internal pointer is
null. Specifically, change Variant::is_null to check the variant's null
discriminator (e.g., the explicit NULL/NIL enum value) OR (type == OBJECT &&
!_get_obj().obj), instead of negating (type == OBJECT && _get_obj().obj); use
the existing type enum and _get_obj().obj accessor to implement this.

718-729: ⚠️ Potential issue | 🟠 Major

Use type-safe adapters in the destructor tables.

destroy<T> and bucketFree<T> are cast to unrelated function pointer types and then invoked through Destructor / BucketFree. That is undefined behavior in C++ and makes teardown ABI-dependent.

Suggested fix
-template <typename T>
-void destroy(T *s) {
-	reinterpret_cast<T *>(s)->~T();
+template <typename T>
+static void destroy_adapter(void *p_mem) {
+	static_cast<T *>(p_mem)->~T();
 }
 
 template <typename T>
-void bucketFree(PagedAllocator<T, true> *bucket, void *mem) {
-	((PagedAllocator<T, true> *)bucket)->free((T *)mem);
+static void bucket_free_adapter(void *p_bucket, void *p_mem) {
+	static_cast<PagedAllocator<T, true> *>(p_bucket)->free(static_cast<T *>(p_mem));
 }
@@
-		(Destructor)destroy<String>,
+		destroy_adapter<String>,
@@
-		(Destructor)destroy<Transform2D>,
+		destroy_adapter<Transform2D>,
@@
-		(Destructor)destroy<::AABB>,
-		(Destructor)destroy<Basis>,
-		(Destructor)destroy<Transform3D>,
-		(Destructor)destroy<Projection>,
+		destroy_adapter<::AABB>,
+		destroy_adapter<Basis>,
+		destroy_adapter<Transform3D>,
+		destroy_adapter<Projection>,
@@
-		(Destructor)destroy<StringName>,
-		(Destructor)destroy<NodePath>,
-		(Destructor)destroy<::RID>,
+		destroy_adapter<StringName>,
+		destroy_adapter<NodePath>,
+		destroy_adapter<::RID>,
@@
-		(Destructor)destroy<Callable>,
-		(Destructor)destroy<Signal>,
-		(Destructor)destroy<Dictionary>,
-		(Destructor)destroy<Array>,
+		destroy_adapter<Callable>,
+		destroy_adapter<Signal>,
+		destroy_adapter<Dictionary>,
+		destroy_adapter<Array>,
@@
-		(BucketFree)bucketFree<Variant::Pools::BucketSmall>,
+		bucket_free_adapter<Variant::Pools::BucketSmall>,
@@
-		(BucketFree)bucketFree<Variant::Pools::BucketSmall>,
-		(BucketFree)bucketFree<Variant::Pools::BucketMedium>,
-		(BucketFree)bucketFree<Variant::Pools::BucketMedium>,
-		(BucketFree)bucketFree<Variant::Pools::BucketLarge>,
+		bucket_free_adapter<Variant::Pools::BucketSmall>,
+		bucket_free_adapter<Variant::Pools::BucketMedium>,
+		bucket_free_adapter<Variant::Pools::BucketMedium>,
+		bucket_free_adapter<Variant::Pools::BucketLarge>,
#!/bin/bash
# Verify the helper signatures and the casted entries stored in the cleanup dispatch tables.
sed -n '718,845p' core/variant/variant.cpp

Also applies to: 732-845

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

In `@core/variant/variant.cpp` around lines 718 - 729, The destructor tables store
pointers to destroy<T> and bucketFree<T> via reinterpret_cast to the generic
typedefs Destructor and BucketFree which is UB; replace those casts with
type-safe adapter functions that have the exact signatures used in the tables
(e.g., a non-template void destroy_adapter(void *p) { static_cast<T*>(p)->~T();
} and void bucket_free_adapter(void *bucket, void *mem) {
static_cast<PagedAllocator<T, true>*>(bucket)->free(static_cast<T*>(mem)); })
and store pointers to those adapters instead of casting
destroy<T>/bucketFree<T>; update all places referencing Destructor and
BucketFree to expect the adapter signatures and remove the reinterpret_casts
involving destroy<T> and bucketFree<T>.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/variant/variant.cpp`:
- Around line 2836-2841: IS_SHARED currently omits packed array types so
Variant::is_type_shared/ is_shared returns false for PACKED_*_ARRAYs; update the
IS_SHARED bitmask to include all packed-array Variant enum values by OR-ing
(1ull << Variant::PACKED_..._ARRAY) for each PACKED_*_ARRAY variant (i.e., add
the PACKED array enum entries to the existing mask) so
Variant::is_type_shared(Variant::Type p_type) correctly reports them as shared.
- Around line 202-205: The code indexes TYPE_STRING_TABLE and uses bit shifts
with Variant::Type without checking for negative values; update
Variant::get_type_name (and the similar helper in lines 221-234) to validate
that p_type is within the full valid range (p_type >= 0 && p_type < VARIANT_MAX)
before indexing or shifting, returning a safe fallback string (e.g., "Invalid")
or handling out-of-range values; additionally, at the call sites where signed
ints are cast to Variant::Type (the places using error.expected and
ce.expected), validate the signed int is within [0, VARIANT_MAX-1] before
casting or pass a sanitized/validated value to the helper to prevent negative
indices and UB.

---

Duplicate comments:
In `@core/variant/variant.cpp`:
- Around line 2859-2874: The error-handling switch in
Variant::_variant_call_error leaves several Callable::CallError branches empty;
implement messages for CALL_ERROR_TOO_FEW_ARGUMENTS (e.g., "Too few arguments
for method 'p_method', expected at least X, got Y" using
error.argument/expected), CALL_ERROR_INSTANCE_IS_NULL (e.g., "Instance is null
for method 'p_method' on type 'Variant::get_type_name(type)'"), and
CALL_ERROR_METHOD_NOT_CONST (e.g., "Method 'p_method' is not const and cannot be
called on a const instance"); ensure you use the existing helpers itos(),
Variant::get_type_name(), p_method and type, populate err in each branch, and
keep the default branch producing a sensible fallback message before calling
ERR_PRINT(err.utf8().get_data()).
- Around line 450-452: Variant::is_null currently treats every non-OBJECT type
as null; update its logic so it only reports null for the explicit null variant
type or for an OBJECT whose internal pointer is null. Specifically, change
Variant::is_null to check the variant's null discriminator (e.g., the explicit
NULL/NIL enum value) OR (type == OBJECT && !_get_obj().obj), instead of negating
(type == OBJECT && _get_obj().obj); use the existing type enum and
_get_obj().obj accessor to implement this.
- Around line 718-729: The destructor tables store pointers to destroy<T> and
bucketFree<T> via reinterpret_cast to the generic typedefs Destructor and
BucketFree which is UB; replace those casts with type-safe adapter functions
that have the exact signatures used in the tables (e.g., a non-template void
destroy_adapter(void *p) { static_cast<T*>(p)->~T(); } and void
bucket_free_adapter(void *bucket, void *mem) { static_cast<PagedAllocator<T,
true>*>(bucket)->free(static_cast<T*>(mem)); }) and store pointers to those
adapters instead of casting destroy<T>/bucketFree<T>; update all places
referencing Destructor and BucketFree to expect the adapter signatures and
remove the reinterpret_casts involving destroy<T> and bucketFree<T>.

In `@core/variant/variant.h`:
- Around line 283-305: The needs_deinit mask omits Variant::RID causing RID
cleanup to be skipped when clear() / ~Variant() route teardown through this
mask; add (1ull << Variant::RID) back into the static constexpr std::uint64_t
needs_deinit definition so _clear_internal()'s RID teardown path is reached
during Variant destruction/clear, ensuring Variant::RID is included alongside
the other types in needs_deinit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d30bc85b-490e-41e9-a2c1-d3b083bda62c

📥 Commits

Reviewing files that changed from the base of the PR and between 7423ce4 and b718418.

📒 Files selected for processing (2)
  • core/variant/variant.cpp
  • core/variant/variant.h

Copy link

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

♻️ Duplicate comments (2)
core/variant/variant.cpp (2)

2859-2874: ⚠️ Potential issue | 🟡 Minor

ERR_PRINT called unconditionally may print empty error messages.

When error codes like CALL_ERROR_TOO_FEW_ARGUMENTS, CALL_ERROR_INSTANCE_IS_NULL, or CALL_ERROR_METHOD_NOT_CONST hit the default case, err remains empty, but ERR_PRINT is still called. This could emit blank error lines depending on how ERR_PRINT handles empty strings.

Proposed fix to guard the print
-	ERR_PRINT(err.utf8().get_data());
+	if (!err.is_empty()) {
+		ERR_PRINT(err.utf8().get_data());
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/variant/variant.cpp` around lines 2859 - 2874, The
Variant::_variant_call_error function unconditionally calls ERR_PRINT which can
emit empty lines when Callable::CallError falls into unhandled default cases;
update this function (Variant::_variant_call_error) to only call ERR_PRINT when
the constructed err string is non-empty (or alternatively set meaningful
messages for the missing error cases such as CALL_ERROR_TOO_FEW_ARGUMENTS,
CALL_ERROR_INSTANCE_IS_NULL, CALL_ERROR_METHOD_NOT_CONST), ensuring you
reference Callable::CallError::CALL_ERROR_* enums and
Variant::get_type_name/type/p_method when building the message so ERR_PRINT is
invoked only with a non-empty, informative error string.

221-234: ⚠️ Potential issue | 🟠 Major

p_type_from is not validated before the bit shift, leading to potential undefined behavior.

While p_type_to is now validated (per prior review feedback), p_type_from is still used directly in 1ull << p_type_from without validation. If p_type_from is negative or ≥ 64, this is undefined behavior.

Proposed fix to validate both parameters
 bool Variant::can_convert(Variant::Type p_type_from, Variant::Type p_type_to) {
+	if ((p_type_from < NIL) || (p_type_from >= VARIANT_MAX)) {
+		return false;
+	}
 	std::uint64_t valid_types = 0;
 	if ((p_type_to >= NIL) && (p_type_to < VARIANT_MAX)) {
 		valid_types = TYPE_CAST_TABLE[p_type_to];
 	}
 	return (valid_types & (1ull << p_type_from)) != 0;
 }
 
 bool Variant::can_convert_strict(Variant::Type p_type_from, Variant::Type p_type_to) {
+	if ((p_type_from < NIL) || (p_type_from >= VARIANT_MAX)) {
+		return false;
+	}
 	std::uint64_t valid_types = 0;
 	if ((p_type_to >= NIL) && (p_type_to < VARIANT_MAX)) {
 		valid_types = TYPE_STRICT_CAST_TABLE[p_type_to];
 	}
 	return (valid_types & (1ull << p_type_from)) != 0;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/variant/variant.cpp` around lines 221 - 234, Both Variant::can_convert
and Variant::can_convert_strict validate p_type_to but use p_type_from unchecked
in the bit-shift (1ull << p_type_from), which can cause UB for negative or >=64
values; update both functions to validate p_type_from (ensure p_type_from >= NIL
and p_type_from < VARIANT_MAX and also < 64) before performing the shift, only
compute the mask when both indices are valid, and keep using TYPE_CAST_TABLE and
TYPE_STRICT_CAST_TABLE for the lookup when p_type_to is valid.
🧹 Nitpick comments (2)
core/variant/variant.cpp (2)

2810-2829: Unreachable code at line 2829.

Line 2829 (return p_lhs < p_rhs;) is unreachable because all code paths through the two switches either return explicitly or hit a default case that returns.

Proposed fix to remove dead code
 	switch (p_rhs.get_type()) {
 		case Variant::STRING:
 			return StringName::AlphCompare::compare(lhs, *VariantInternal::get_string(&p_rhs));
 		case Variant::STRING_NAME:
 			return StringName::AlphCompare::compare(lhs, *VariantInternal::get_string_name(&p_rhs));
 		default:
 			return p_lhs < p_rhs;
 	}
-	return p_lhs < p_rhs;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/variant/variant.cpp` around lines 2810 - 2829, The final unconditional
"return p_lhs < p_rhs;" is unreachable because both switch blocks on
p_lhs.get_type() and p_rhs.get_type() always return (including their default
cases); remove that dead return at the end of the function (after the second
switch) so only the meaningful returns remain; locate the code around
variables/identifiers lhs, p_lhs, p_rhs, Variant::STRING, Variant::STRING_NAME,
VariantInternal::get_string/get_string_name and StringName::AlphCompare::compare
to make this change.

90-144: Typo in comment: "callbale" should be "callable".

Line 120 and 176 have "callbale" instead of "callable".

Also, consider using 1ull << consistently throughout the table (even for types < 32) to avoid potential issues if enum values change in the future.

Proposed fix for typos
-	(1 << Variant::CALLABLE), // callbale
+	(1 << Variant::CALLABLE), // callable
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/variant/variant.cpp` around lines 90 - 144, Fix the typo and make
bit-shifts consistent: update the comment labeling the entry for
Variant::CALLABLE in TYPE_CAST_TABLE from "callbale" to "callable" and change
any plain (1 << Variant::...) shifts in TYPE_CAST_TABLE to use 1ull <<
Variant::... (e.g., replace (1 << Variant::BOOL) with (1ull << Variant::BOOL))
so all shifts are performed with unsigned 64-bit literals; verify entries
referencing Variant::CALLABLE, Variant::SIGNAL, Variant::DICTIONARY, etc., are
updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@core/variant/variant.cpp`:
- Around line 2859-2874: The Variant::_variant_call_error function
unconditionally calls ERR_PRINT which can emit empty lines when
Callable::CallError falls into unhandled default cases; update this function
(Variant::_variant_call_error) to only call ERR_PRINT when the constructed err
string is non-empty (or alternatively set meaningful messages for the missing
error cases such as CALL_ERROR_TOO_FEW_ARGUMENTS, CALL_ERROR_INSTANCE_IS_NULL,
CALL_ERROR_METHOD_NOT_CONST), ensuring you reference
Callable::CallError::CALL_ERROR_* enums and Variant::get_type_name/type/p_method
when building the message so ERR_PRINT is invoked only with a non-empty,
informative error string.
- Around line 221-234: Both Variant::can_convert and Variant::can_convert_strict
validate p_type_to but use p_type_from unchecked in the bit-shift (1ull <<
p_type_from), which can cause UB for negative or >=64 values; update both
functions to validate p_type_from (ensure p_type_from >= NIL and p_type_from <
VARIANT_MAX and also < 64) before performing the shift, only compute the mask
when both indices are valid, and keep using TYPE_CAST_TABLE and
TYPE_STRICT_CAST_TABLE for the lookup when p_type_to is valid.

---

Nitpick comments:
In `@core/variant/variant.cpp`:
- Around line 2810-2829: The final unconditional "return p_lhs < p_rhs;" is
unreachable because both switch blocks on p_lhs.get_type() and p_rhs.get_type()
always return (including their default cases); remove that dead return at the
end of the function (after the second switch) so only the meaningful returns
remain; locate the code around variables/identifiers lhs, p_lhs, p_rhs,
Variant::STRING, Variant::STRING_NAME,
VariantInternal::get_string/get_string_name and StringName::AlphCompare::compare
to make this change.
- Around line 90-144: Fix the typo and make bit-shifts consistent: update the
comment labeling the entry for Variant::CALLABLE in TYPE_CAST_TABLE from
"callbale" to "callable" and change any plain (1 << Variant::...) shifts in
TYPE_CAST_TABLE to use 1ull << Variant::... (e.g., replace (1 << Variant::BOOL)
with (1ull << Variant::BOOL)) so all shifts are performed with unsigned 64-bit
literals; verify entries referencing Variant::CALLABLE, Variant::SIGNAL,
Variant::DICTIONARY, etc., are updated accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c273a6c7-a60a-4e66-8912-ebd20728852f

📥 Commits

Reviewing files that changed from the base of the PR and between b718418 and ac618b9.

📒 Files selected for processing (1)
  • core/variant/variant.cpp

Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/variant/variant.cpp`:
- Line 176: Fix the typo in the inline comment next to the Variant::CALLABLE
flag: change "callbale" to "callable" so the comment reads correctly; locate the
bitmask entry "(1 << Variant::CALLABLE), // callbale" in variant.cpp and update
the trailing comment to "// callable".
- Around line 221-235: The functions Variant::can_convert and
Variant::can_convert_strict validate p_type_to but still perform undefined
shifts with p_type_from; add a range check for p_type_from before doing (1ull <<
p_type_from). Specifically, ensure p_type_from is within the valid enum range
(e.g., p_type_from >= NIL && p_type_from < VARIANT_MAX) and also less than 64
(or use VARIANT_MAX <= 64 assumption) and only then perform the shift and mask
against TYPE_CAST_TABLE / TYPE_STRICT_CAST_TABLE; if the check fails return
false. Apply the same guard in both can_convert and can_convert_strict and use a
uint64_t temporary for the shifted bit after validation.
- Around line 2858-2876: The switch in Variant::_variant_call_error falls
through due to missing break statements and returns in default so ERR_PRINT
never runs; update each case in Variant::_variant_call_error to assign the
correct message and terminate the case with a break, fix the
CALL_ERROR_TOO_FEW_ARGUMENTS message to "Too few arguments for method
'p_method'" (or similar), and change the default so it does not return
immediately but sets a fallback error string (e.g. "Unknown call error") before
the final ERR_PRINT call; ensure you reference Callable::CallError enum values
(CALL_ERROR_INVALID_ARGUMENT, CALL_ERROR_INVALID_METHOD,
CALL_ERROR_TOO_MANY_ARGUMENTS, CALL_ERROR_TOO_FEW_ARGUMENTS,
CALL_ERROR_INSTANCE_IS_NULL, CALL_ERROR_METHOD_NOT_CONST) and the function
Variant::get_type_name/type as needed when composing messages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 90dd6f71-a7b0-4d23-88e3-f4011898a908

📥 Commits

Reviewing files that changed from the base of the PR and between ac618b9 and f9ed091.

📒 Files selected for processing (1)
  • core/variant/variant.cpp

Copy link

@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

♻️ Duplicate comments (4)
core/variant/variant.cpp (4)

221-227: ⚠️ Potential issue | 🟠 Major

Validate p_type_from before shifting.

Only p_type_to is range-checked. Lines 226 and 234 still do 1ull << p_type_from on an unchecked enum, so negative or out-of-range values hit undefined behavior.

Suggested fix
 bool Variant::can_convert(Variant::Type p_type_from, Variant::Type p_type_to) {
-	std::uint64_t valid_types = 0;
-	if ((p_type_to >= NIL) && (p_type_to < VARIANT_MAX)) {
-		valid_types = TYPE_CAST_TABLE[p_type_to];
-	}
-	return (valid_types & (1ull << p_type_from)) != 0;
+	if ((p_type_from < NIL) || (p_type_from >= VARIANT_MAX) ||
+			(p_type_to < NIL) || (p_type_to >= VARIANT_MAX)) {
+		return false;
+	}
+	return (TYPE_CAST_TABLE[p_type_to] & (1ull << p_type_from)) != 0;
 }
 
 bool Variant::can_convert_strict(Variant::Type p_type_from, Variant::Type p_type_to) {
-	std::uint64_t valid_types = 0;
-	if ((p_type_to >= NIL) && (p_type_to < VARIANT_MAX)) {
-		valid_types = TYPE_STRICT_CAST_TABLE[p_type_to];
-	}
-	return (valid_types & (1ull << p_type_from)) != 0;
+	if ((p_type_from < NIL) || (p_type_from >= VARIANT_MAX) ||
+			(p_type_to < NIL) || (p_type_to >= VARIANT_MAX)) {
+		return false;
+	}
+	return (TYPE_STRICT_CAST_TABLE[p_type_to] & (1ull << p_type_from)) != 0;
 }

Also applies to: 229-235

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

In `@core/variant/variant.cpp` around lines 221 - 227, The code in
Variant::can_convert uses (1ull << p_type_from) without validating p_type_from,
causing UB for negative or out-of-range enum values; update Variant::can_convert
to range-check p_type_from (e.g., ensure p_type_from >= NIL && p_type_from <
VARIANT_MAX) before performing the left shift and return false if out of range;
apply the same validation to the duplicate block referenced around lines 229-235
that also shifts by p_type_from and ensure you use TYPE_CAST_TABLE[p_type_to]
only after both p_type_to and p_type_from are validated.

2867-2870: ⚠️ Potential issue | 🟡 Minor

Use the correct message for CALL_ERROR_TOO_FEW_ARGUMENTS.

Line 2868 still falls into the "Too many arguments" text, so the logged diagnosis is inverted for that error.

Suggested fix
 		case Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS:
-		case Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS:
 			err = "Too many arguments for method '" + p_method + "'";
 			break;
+		case Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS:
+			err = "Too few arguments for method '" + p_method + "'";
+			break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/variant/variant.cpp` around lines 2867 - 2870, The error string for
Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS is incorrect—both cases set
err to "Too many arguments for method '" + p_method + "'". Update the branch
handling CALL_ERROR_TOO_FEW_ARGUMENTS (the switch case for
Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS) to set err to "Too few
arguments for method '" + p_method + "'" (or otherwise produce the correct "Too
few" message) so err reflects the actual error; keep the
CALL_ERROR_TOO_MANY_ARGUMENTS case returning the "Too many arguments..." message
and ensure p_method is used in both.

2835-2840: ⚠️ Potential issue | 🟠 Major

Packed arrays still need to be marked shared.

Packed arrays are reference-counted and assignments in this file keep shared backing storage, so is_type_shared() / is_shared() now incorrectly report false for every PACKED_*_ARRAY.

Suggested fix
 static const std::uint64_t IS_SHARED = (1ull << Variant::OBJECT) |
 		(1ull << Variant::ARRAY) |
-		(1ull << Variant::DICTIONARY);
+		(1ull << Variant::DICTIONARY) |
+		(1ull << Variant::PACKED_BYTE_ARRAY) |
+		(1ull << Variant::PACKED_INT32_ARRAY) |
+		(1ull << Variant::PACKED_INT64_ARRAY) |
+		(1ull << Variant::PACKED_FLOAT32_ARRAY) |
+		(1ull << Variant::PACKED_FLOAT64_ARRAY) |
+		(1ull << Variant::PACKED_STRING_ARRAY) |
+		(1ull << Variant::PACKED_VECTOR2_ARRAY) |
+		(1ull << Variant::PACKED_VECTOR3_ARRAY) |
+		(1ull << Variant::PACKED_COLOR_ARRAY) |
+		(1ull << Variant::PACKED_VECTOR4_ARRAY);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/variant/variant.cpp` around lines 2835 - 2840, IS_SHARED mask omits
packed array types so Variant::is_type_shared (and is_shared()) returns false
for PACKED_*_ARRAYs; update the IS_SHARED bitmask to include all PACKED_*_ARRAY
Variant::Type values (e.g., PACKED_INT_ARRAY, PACKED_REAL_ARRAY,
PACKED_VECTOR2_ARRAY, PACKED_VECTOR3_ARRAY, PACKED_COLOR_ARRAY,
PACKED_STRING_ARRAY, PACKED_NODE_ARRAY, PACKED_RAW_ARRAY or whatever
PACKED_*_ARRAY enum names exist) by OR-ing (1ull << Variant::PACKED_... ) for
each packed array type inside the IS_SHARED initializer so packed arrays are
treated as shared. Ensure you reference the Variant::Type enum names exactly as
declared when adding them.

718-729: ⚠️ Potential issue | 🔴 Critical

Avoid calling cleanup helpers through casted function pointers.

The destroy<T> / bucketFree<T> functions are cast to incompatible erased function-pointer types and then invoked through those casted types. That's undefined behavior in C++, so _clear_internal() becomes ABI-dependent.

Suggested fix
 template <typename T>
-void destroy(T *s) {
-	reinterpret_cast<T *>(s)->~T();
+static void destroy_adapter(void *p_mem) {
+	static_cast<T *>(p_mem)->~T();
 }
 
 template <typename T>
-void bucketFree(PagedAllocator<T, true> *bucket, void *mem) {
-	((PagedAllocator<T, true> *)bucket)->free((T *)mem);
+static void bucket_free_adapter(void *p_bucket, void *p_mem) {
+	static_cast<PagedAllocator<T, true> *>(p_bucket)->free(static_cast<T *>(p_mem));
 }
@@
-		(Destructor)destroy<String>,
+		destroy_adapter<String>,
@@
-		(Destructor)destroy<Transform2D>,
+		destroy_adapter<Transform2D>,
@@
-		(BucketFree)bucketFree<Variant::Pools::BucketSmall>,
+		bucket_free_adapter<Variant::Pools::BucketSmall>,
@@
-		(BucketFree)bucketFree<Variant::Pools::BucketMedium>,
+		bucket_free_adapter<Variant::Pools::BucketMedium>,
@@
-		(BucketFree)bucketFree<Variant::Pools::BucketLarge>,
+		bucket_free_adapter<Variant::Pools::BucketLarge>,

Also applies to: 732-845

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

In `@core/variant/variant.cpp` around lines 718 - 729, The code currently casts
template functions destroy<T> and bucketFree<T> to the erased types Destructor
and BucketFree which is UB; instead, implement non-templated trampoline
functions with the exact erased signatures (e.g. destroy_trampoline(void* p) and
bucketFree_trampoline(void* bucket, void* mem)) that inside cast to T* and call
the destructor or bucket->free, and assign those trampolines to Destructor and
BucketFree; update wherever destroy<T>/bucketFree<T> were cast (notably where
Destructor/BucketFree are stored/used and in _clear_internal) to use these
trampolines so no incompatible function-pointer casts occur.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/variant/variant.cpp`:
- Around line 209-218: The static HashMap<string, Variant::Type> type_names in
Variant::get_type_by_name is lazily populated with mutation on first use (via
is_empty()), creating a data race; change to build and populate type_names in a
thread-safe static initializer (e.g. initialize the static via a function-local
static constructed from a lambda or helper that fills the map by iterating
get_type_name((Type)i) for i from NIL to VARIANT_MAX) so no concurrent writes
occur in Variant::get_type_by_name; keep the lookup logic using
type_names.getptr(p_type_name) and return VARIANT_MAX when not found.

---

Duplicate comments:
In `@core/variant/variant.cpp`:
- Around line 221-227: The code in Variant::can_convert uses (1ull <<
p_type_from) without validating p_type_from, causing UB for negative or
out-of-range enum values; update Variant::can_convert to range-check p_type_from
(e.g., ensure p_type_from >= NIL && p_type_from < VARIANT_MAX) before performing
the left shift and return false if out of range; apply the same validation to
the duplicate block referenced around lines 229-235 that also shifts by
p_type_from and ensure you use TYPE_CAST_TABLE[p_type_to] only after both
p_type_to and p_type_from are validated.
- Around line 2867-2870: The error string for
Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS is incorrect—both cases set
err to "Too many arguments for method '" + p_method + "'". Update the branch
handling CALL_ERROR_TOO_FEW_ARGUMENTS (the switch case for
Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS) to set err to "Too few
arguments for method '" + p_method + "'" (or otherwise produce the correct "Too
few" message) so err reflects the actual error; keep the
CALL_ERROR_TOO_MANY_ARGUMENTS case returning the "Too many arguments..." message
and ensure p_method is used in both.
- Around line 2835-2840: IS_SHARED mask omits packed array types so
Variant::is_type_shared (and is_shared()) returns false for PACKED_*_ARRAYs;
update the IS_SHARED bitmask to include all PACKED_*_ARRAY Variant::Type values
(e.g., PACKED_INT_ARRAY, PACKED_REAL_ARRAY, PACKED_VECTOR2_ARRAY,
PACKED_VECTOR3_ARRAY, PACKED_COLOR_ARRAY, PACKED_STRING_ARRAY,
PACKED_NODE_ARRAY, PACKED_RAW_ARRAY or whatever PACKED_*_ARRAY enum names exist)
by OR-ing (1ull << Variant::PACKED_... ) for each packed array type inside the
IS_SHARED initializer so packed arrays are treated as shared. Ensure you
reference the Variant::Type enum names exactly as declared when adding them.
- Around line 718-729: The code currently casts template functions destroy<T>
and bucketFree<T> to the erased types Destructor and BucketFree which is UB;
instead, implement non-templated trampoline functions with the exact erased
signatures (e.g. destroy_trampoline(void* p) and bucketFree_trampoline(void*
bucket, void* mem)) that inside cast to T* and call the destructor or
bucket->free, and assign those trampolines to Destructor and BucketFree; update
wherever destroy<T>/bucketFree<T> were cast (notably where Destructor/BucketFree
are stored/used and in _clear_internal) to use these trampolines so no
incompatible function-pointer casts occur.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8c612872-cf9b-42a3-b90c-f89b0cc7390b

📥 Commits

Reviewing files that changed from the base of the PR and between f9ed091 and 7913061.

📒 Files selected for processing (1)
  • core/variant/variant.cpp

JoltedJon
JoltedJon previously approved these changes Mar 13, 2026
JoltedJon
JoltedJon previously approved these changes Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants