Skip to content

Add nullable static types to GDScript. - #1335

Open
mcdubhghlas wants to merge 3 commits into
Redot-Engine:masterfrom
mcdubhghlas:nullsies
Open

Add nullable static types to GDScript.#1335
mcdubhghlas wants to merge 3 commits into
Redot-Engine:masterfrom
mcdubhghlas:nullsies

Conversation

@mcdubhghlas

@mcdubhghlas mcdubhghlas commented Jul 29, 2026

Copy link
Copy Markdown
Member

Title.

Summary by CodeRabbit

  • New Features
    • Added nullable type syntax (?) across value, array, dictionary, enum, callable, and return types.
    • Improved null-safety by recognizing non-null narrowing inside conditional and boolean expressions.
    • Typed nullable locals now remain null by default when initialized implicitly.
  • Bug Fixes
    • Runtime/typed opcode handling is now nullable-aware for assignments, returns, and typed containers.
    • Disassembly can mark typed operands with a ? indicator for nullable types.
  • Documentation
    • Added project setting to control the new unsafe nullable access warning.
  • Tests
    • Added extensive parser, analyzer, and runtime coverage for nullable behavior and warnings.

@mcdubhghlas
mcdubhghlas requested review from a team July 29, 2026 22:24
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Nullable type syntax, flow-sensitive non-null narrowing, nullable-aware bytecode execution, unsafe-access warnings, and extensive analyzer and runtime tests are added across GDScript.

Changes

Nullable GDScript support

Layer / File(s) Summary
Nullable type contracts and parsing
modules/gdscript/gdscript_parser.*, modules/gdscript/gdscript_function.h, modules/gdscript/gdscript_analyzer.cpp
Nullable markers (?) are parsed and propagate through parser and runtime data types; typed container element types are validated (only containers can be nullable); and nil compatibility is established for nullable built-ins and enums.
Analyzer narrowing and nullable diagnostics
modules/gdscript/gdscript_analyzer.*, modules/gdscript/gdscript_warning.*
Conditional expressions narrow nullable identifiers to non-null within branches and boolean expressions; assignments invalidate prior narrowing and can re-establish it for known non-null sources; unsafe nullable operations (unary, binary, subscript, calls, returns, iteration) and nullable-to-non-nullable transfers emit the new UNSAFE_NULLABLE_ACCESS warning; type compatibility rules are tightened for nullable vs. non-nullable mismatches.
Nullable bytecode generation and compilation
modules/gdscript/gdscript_compiler.cpp, modules/gdscript/gdscript_byte_codegen.cpp, modules/gdscript/gdscript_disassembler.cpp
Compiler propagates nullable metadata into GDScriptDataType; fast-paths for builtin calls exclude nullable types; typed assignment and return opcodes emit an is_nullable flag; implicit initializers skip typed default construction for nullable typed fields; disassembler renders nullable markers in typed opcode output.
VM nullable execution
modules/gdscript/gdscript_vm.cpp
Typed assignment and return opcodes read an is_nullable flag; when set and the value is nil, Variant() is assigned directly; otherwise strict type/signature validation is enforced. Bytecode space calculations are updated accordingly.
Analyzer and parser test coverage
modules/gdscript/tests/scripts/analyzer/**, modules/gdscript/tests/scripts/parser/**
Analyzer test fixtures cover nullable container element restrictions, nullable Variant and void? syntax, narrowing branches and limits, unsafe operations on nullable values, and unsafe nullable transfers.
Runtime nullable behavior test coverage
modules/gdscript/tests/scripts/runtime/**
Runtime test fixtures cover nullable basic types, enums, objects, callables, narrowing mutations, typed containers, returns, and invalid nil or type flows reaching non-nullable targets.
Documentation
doc/classes/ProjectSettings.xml
New warning setting debug/gdscript/warnings/unsafe_nullable_access is documented.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GDScriptParser
  participant GDScriptAnalyzer
  participant GDScriptCompiler
  participant GDScriptVM
  GDScriptParser->>GDScriptAnalyzer: provide nullable type information
  GDScriptAnalyzer->>GDScriptAnalyzer: narrow identifiers and validate nullable access
  GDScriptAnalyzer->>GDScriptCompiler: provide checked nullable types
  GDScriptCompiler->>GDScriptVM: emit nullable assignment and return metadata
  GDScriptVM->>GDScriptVM: accept nil for nullable targets or enforce non-nullable validation
Loading

Possibly related PRs

  • Redot-Engine/redot-engine#1152: Both PRs modify modules/gdscript/gdscript_analyzer.cpp's type-compatibility logic—main PR for nullable-vs-non-nullable mismatches and related PR for struct compatibility.

Suggested labels: enhancement, topic:core, topic:gdscript

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding nullable static type support to GDScript.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/gdscript/gdscript_analyzer.cpp`:
- Around line 970-975: Update the nested-enum resolution paths for
Variant.SomeEnum and built-in nested enums so they pass through the existing
nullable finalization block guarded by p_type->is_nullable, allowing annotations
such as Vector2.Axis? to set result.is_nullable. Preserve the current Variant
rejection behavior, and add a regression test covering nullable nested enums.
- Around line 2090-2098: Update the IF handling in the non-null narrowing logic
to use the absence of an exiting false branch rather than requiring false_block
to be null. Replace the false_block-null condition in the true_exits branch with
the corresponding !false_exits check, preserving the existing symmetric branch
and retaining the non-null fact after an else whose path does not exit.

In `@modules/gdscript/gdscript_function.h`:
- Around line 85-87: Complete nullable handling across all listed sites: in
modules/gdscript/gdscript_function.h:85-87, accept nullable Variant::NIL before
switching on GDScriptDataType::kind; in
modules/gdscript/gdscript_analyzer.cpp:4971-4975 and :5098-5102, emit
nullable-access warnings for non-meta object/class bases in attribute and index
access; in :6386-6398, reject nullable sources assigned to non-nullable targets
before object polymorphism checks while preserving nil acceptance for nullable
targets. Add regressions covering Node? and script-class assignment,
parameter/return transfers, null values, and member access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eb70ba44-2981-44d8-bd62-91969edb0ad2

📥 Commits

Reviewing files that changed from the base of the PR and between 5264617 and b11c8ea.

⛔ Files ignored due to path filters (40)
  • modules/gdscript/tests/scripts/analyzer/errors/nullable_array_element.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/errors/nullable_dictionary_key_element.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/errors/nullable_dictionary_value_element.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/errors/nullable_variant.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_and_false_no_narrow.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_break_guard_no_narrow.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_compound_assignment.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_continue_guard_no_narrow.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_narrowing_limits.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_or_true_no_narrow.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_unsafe_operations.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_unsafe_transfer.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/analyzer/warnings/unsafe_nullable_access.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/parser/errors/nullable_void_return.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_enum_null_transfer.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_null_reaches_argument.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_null_reaches_typed_local.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_null_use.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_return_null_from_nonnull.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_array_assign_mismatch.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_array_reassign_mismatch.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_dictionary_assign_mismatch.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_dictionary_reassign_mismatch.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_dictionary_return_mismatch.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_return_mismatch.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_empty_callable.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_enum_raw_integer.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_enum_transfer.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_freed_object.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_multivar_and.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_multivar_or_guard.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_narrowing.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_narrowing_mutation.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_not_compound_or.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_renarrow_after_reassign.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_self_reference_assignment.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_typed_container_returns.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_types.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_variant_null_return.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_while_body_mutation.out is excluded by !**/*.out
📒 Files selected for processing (51)
  • modules/gdscript/gdscript_analyzer.cpp
  • modules/gdscript/gdscript_analyzer.h
  • modules/gdscript/gdscript_byte_codegen.cpp
  • modules/gdscript/gdscript_compiler.cpp
  • modules/gdscript/gdscript_disassembler.cpp
  • modules/gdscript/gdscript_function.h
  • modules/gdscript/gdscript_parser.cpp
  • modules/gdscript/gdscript_parser.h
  • modules/gdscript/gdscript_vm.cpp
  • modules/gdscript/gdscript_warning.cpp
  • modules/gdscript/gdscript_warning.h
  • modules/gdscript/tests/scripts/analyzer/errors/nullable_array_element.gd
  • modules/gdscript/tests/scripts/analyzer/errors/nullable_dictionary_key_element.gd
  • modules/gdscript/tests/scripts/analyzer/errors/nullable_dictionary_value_element.gd
  • modules/gdscript/tests/scripts/analyzer/errors/nullable_variant.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_and_false_no_narrow.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_break_guard_no_narrow.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_compound_assignment.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_continue_guard_no_narrow.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_narrowing_limits.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_or_true_no_narrow.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_unsafe_operations.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/nullable_unsafe_transfer.gd
  • modules/gdscript/tests/scripts/analyzer/warnings/unsafe_nullable_access.gd
  • modules/gdscript/tests/scripts/parser/errors/nullable_void_return.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_enum_null_transfer.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_null_reaches_argument.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_null_reaches_typed_local.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_null_use.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_return_null_from_nonnull.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_array_assign_mismatch.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_array_reassign_mismatch.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_dictionary_assign_mismatch.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_dictionary_reassign_mismatch.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_dictionary_return_mismatch.gd
  • modules/gdscript/tests/scripts/runtime/errors/nullable_typed_return_mismatch.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_empty_callable.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_enum_raw_integer.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_enum_transfer.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_freed_object.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_multivar_and.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_multivar_or_guard.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_narrowing.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_narrowing_mutation.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_not_compound_or.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_renarrow_after_reassign.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_self_reference_assignment.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_typed_container_returns.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_types.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_variant_null_return.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_while_body_mutation.gd

Comment thread modules/gdscript/gdscript_analyzer.cpp
Comment thread modules/gdscript/gdscript_analyzer.cpp
Comment thread modules/gdscript/gdscript_function.h

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
modules/gdscript/gdscript_analyzer.cpp (1)

2079-2103: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore the complete narrowing state, not only its size.

These scopes snapshot narrowed_non_null by length and later call resize(), but invalidate_narrowing() can erase facts that existed before the snapshot. Resizing cannot restore those removed entries, so a nested assignment or boolean expression can permanently lose an outer non-null fact and produce false UNSAFE_NULLABLE_ACCESS warnings afterward. Save and restore the vector contents (or use an equivalent scoped snapshot), and add a regression covering an outer null check with a nested branch that reassigns the narrowed variable.

Also applies to: 2284-2392, 3015-3029, 3254-3260

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_analyzer.cpp` around lines 2079 - 2103, Restore the
full narrowed_non_null contents after each scoped narrowing operation, rather
than only resizing to the saved length. Update the snapshot/cleanup logic around
the shown suite loop and the corresponding scopes near the other referenced
locations, preserving facts removed by invalidate_narrowing() during nested
branches or expressions. Add a regression covering an outer null check whose
nested branch reassigns the narrowed variable, ensuring the outer non-null fact
remains available afterward.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@modules/gdscript/gdscript_analyzer.cpp`:
- Around line 2079-2103: Restore the full narrowed_non_null contents after each
scoped narrowing operation, rather than only resizing to the saved length.
Update the snapshot/cleanup logic around the shown suite loop and the
corresponding scopes near the other referenced locations, preserving facts
removed by invalidate_narrowing() during nested branches or expressions. Add a
regression covering an outer null check whose nested branch reassigns the
narrowed variable, ensuring the outer non-null fact remains available afterward.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a888371c-2741-4562-86b0-9680d6e02ded

📥 Commits

Reviewing files that changed from the base of the PR and between b11c8ea and 4e10d54.

⛔ Files ignored due to path filters (2)
  • modules/gdscript/tests/scripts/runtime/features/nullable_narrowing_else.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/runtime/features/nullable_nested_enum.out is excluded by !**/*.out
📒 Files selected for processing (4)
  • doc/classes/ProjectSettings.xml
  • modules/gdscript/gdscript_analyzer.cpp
  • modules/gdscript/tests/scripts/runtime/features/nullable_narrowing_else.gd
  • modules/gdscript/tests/scripts/runtime/features/nullable_nested_enum.gd

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.

1 participant