Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion doc/gdcc_c_backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Usage and lifecycle rules:
- Primitive types are always by value.
- Object types are always internal fat pointers (raw pointers only at ABI/layout/helper edges).
- Only other built-in types change C type shape based on `ref`.
- For `String`, `StringName`, `NodePath`, `Callable`, `Signal`, `Packed*Array`:
- For `String`, `StringName`, `NodePath`, `Callable`, `Signal`:
- They are value-semantic wrapper structs that hold opaque engine-side state.
- Their C type shape follows the `ref` rule above:
- `ref=true` variable is a pointer to the wrapper struct.
Expand All @@ -132,6 +132,15 @@ Usage and lifecycle rules:
prematurely release the same engine-side state that the slot now refers to
- once a stable carrier has been consumed by the slot, it must not enter the ordinary temp-destroy path again
- When a value of these types are no longer used, call `godot_destroy_<TypeName>(TypeName* value)` to destroy them properly.
- For `Packed*Array` (all 10 families), the canonical storage is Variant-backed, NOT a wrapper struct:
- Storage/parameter/return C types are `godot_Variant` / `godot_Variant*` (internal ABI); the shared engine-side
`PackedArrayRef` gives Godot4 reference semantics (aliases observe each other's mutations).
- Copy is always `godot_new_Variant_with_Variant(...)`; destroy is always `godot_Variant_destroy(...)`.
- Builtin method calls take the receiver/arguments through the cached per-family internal pointer getter
(`gdcc_packed_<slug>_internal_ptr`); packed return values arrive as a native temp struct that is immediately
wrapped into a Variant (`gdcc_packed_<slug>_wrap_temp`) and the temp is destroyed.
- The only legal struct boundaries are the whitelisted named helpers in `gdcc_packed_ref.h`
(see "Packed*Array Variant-backed Storage" below for the full contract).
- For `Dictionary`, `Array` and `Variant`:
- They are wrapper structs with shared/ref-counted internals (not raw C pointers).
- Their C type shape also follows the `ref` rule above:
Expand Down Expand Up @@ -169,6 +178,48 @@ Usage and lifecycle rules:
`gdcc_object_from_godot_object_ptr(...)` if a wrapper pointer is required, then capture ID into
`gdcc_<Type>_fat_ptr` via `<Type>_fat_ptr_from_raw(...)` for internal use.

### Packed*Array Variant-backed Storage

- Canonical storage: every packed slot (locals, parameters, instance/static fields, coroutine frame fields,
lambda captures, signal arguments) is a `godot_Variant` holding a shared engine-side `PackedArrayRef`.
This matches the Godot 4.5 interpreter: `Packed*Array` has reference (shared) semantics at the language level.
- Core invariant: gdcc-held packed values must never round-trip through struct pack/unpack
(`godot_new_Packed*Array_with_*` / `godot_new_Variant_with_Packed*Array`) outside the whitelisted boundaries
below. Identity-carrying operations (assignment, aliasing, parameter passing, signals, lambda/coroutine
captures, call_func wrapper transit) use only `godot_new_Variant_with_Variant(...)`. The single exception is
the same-family `as` cast, which deliberately produces an independent COW copy through whitelist (d)
(`new_copy`) instead of sharing identity (see below).
- The whitelisted struct boundaries are centralized as named helpers in `include_451/gdcc/gdcc_packed_ref.h`:
- (a) ptrcall ABI boundary, both directions: inbound `gdcc_packed_<slug>_variant_from_struct` materializes the
raw struct argument slot into a Variant (destroyed after the call); outbound `gdcc_packed_<slug>_struct_from_variant`
copies the returned Variant back into the caller's raw struct slot.
- (b) empty construction: `gdcc_packed_<slug>_new_empty` builds the mandatory empty-array Variant default
(a nil Variant has no internal value pointer and would fail method calls).
- (c) builtin-method native return temps: `gdcc_packed_<slug>_wrap_temp` wraps the temp struct into a new
Variant and destroys the temp (e.g. `duplicate`, `slice` results).
- (d) explicit constructors: `gdcc_packed_<slug>_new_copy` (same-family, produces an independent new array,
also used by same-family `as` casts) and `gdcc_packed_<slug>_new_from_array` (cross-type from `Array`).
- Generated code outside `gdcc_packed_ref.h` must not call `godot_new_Packed*Array_with_*`,
`godot_new_Variant_with_Packed*Array`, or bare `godot_new_Packed*()`; `CCodegenTest` enforces this ban by
scanning all generated artifacts.
- Method receivers: the builtin wrapper signatures are unchanged; the call site passes the internal value pointer
obtained from the cached per-family `GDExtensionVariantGetInternalPtrFunc` getter (resolved once per
translation unit by `gdcc_packed_ref_init()`, fail-fast when unavailable).
- Operators: packed operands take the internal pointer, non-packed operands keep the evaluator's native ABI shape;
packed results are produced in a native temp struct and wrapped into a new Variant. `+`/`+=` must NOT be
optimized into an in-place `append_array` on the internal pointer (that would leak the mutation to old aliases;
`+=` is new-array + rebind).
- Documented exception (engine ABI limit, accepted): the **ptrcall ABI boundary** does not preserve identity.
Packed ptrcall parameters arrive as raw struct slots, so the callee materializes struct->Variant (a Vector-level
copy) and mutations stay isolated from the caller; packed ptrcall returns are copied Variant->struct on the way
out. Ordinary GDScript<->GDExtension calls go through `call_func` (Variant ABI) and DO preserve identity; only
ptrcall extension-to-extension paths are affected. Locked by runtime tests, see
`PackedRefStorageModelSmokeTest.ptrcallBoundaryShouldIsolateCallerIdentityInBothDirections` and the detailed
rationale in `module_impl/backend/packed_array_implementation.md`, which links back here.
- Documented behavior change: mutating calls on builtin engine properties (e.g. `poly.polygon.push_back(x)`)
are no longer written back, matching the interpreter (the getter returns a copy). Assignment routes on the same
property (`poly.polygon = p`, `poly.polygon[0] = v`) still write back (read-modify-write persists).

### Object Value Representation (Mandatory)

- Internal object storage, parameters, and returns always use `gdcc_<Type>_fat_ptr` once the static type is known.
Expand Down Expand Up @@ -327,6 +378,7 @@ Usage and lifecycle rules:
- this helper only answers the receiver-side runtime writeback gate for runtime-open `Variant` carriers
- it does not participate in callable resolution, receiver provenance, or owner-route reconstruction
- its false/true family matrix is owned by `gdcc_type_system.md` and `gdcc_helper.h`; backend must not drift into a second independent classification table
- all 10 packed kinds are explicitly listed as `false` (Variant-backed shared identity needs no writeback); unlisted future kinds keep the frozen default-`true` answer

### Variant Outward ABI Contract

Expand Down Expand Up @@ -589,6 +641,10 @@ Transform2D(1, 0, 0, 1, 0, 0), RID(), -99, "000000000000000000000000000000000000
- Regular builtin constructors are selected by exact `ExtensionBuiltinClass` constructor metadata
after frontend lowering has materialized any accepted argument boundary. The generated symbol is
`godot_new_<Type>[_with_<argType>...]`.
- `Packed*Array` constructors do NOT use `godot_new_Packed*` symbols; they route to the whitelisted
`gdcc_packed_ref.h` helpers: zero-arg and default values -> `gdcc_packed_<slug>_new_empty`, same-family
argument -> `gdcc_packed_<slug>_new_copy` (independent new array), `Array` argument ->
`gdcc_packed_<slug>_new_from_array`. Other combinations fail closed on constructor metadata validation.
- `Transform2D`, `Transform3D`, `Basis`, and `Projection` may use GDCC-owned helper-shim constructor
signatures when Godot API metadata has no exact constructor surface but the binding
naming contract already exposed the helper.
Expand Down
15 changes: 11 additions & 4 deletions doc/gdcc_lir_intrinsic.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ $<element_result> = call_intrinsic "gdcc.for_packed_<family>_iter.get" $<iter>;
- `should_continue`:result `bool`;arg0 为对应 state。
- `next`:result/arg0 均为对应 state。
- `get`:result 为 **typed element**(`int` / `float` / `String` / `Vector*` / `Color`),不是 `Variant`;arg0 为对应 state。
- state **不可**直接 struct 赋值;`copy` helper 为 `gdcc_for_packed_<family>_iter_copy`(COW 句柄 + 共享 typed base pointer)。
- state **不可**直接 struct 赋值;`copy` helper 为 `gdcc_for_packed_<family>_iter_copy`(Variant 持有者拷贝 + index,共享源数组身份)。

C backend 语义:

Expand All @@ -778,10 +778,17 @@ $target = gdcc_for_packed_<family>_iter_next(&$iter);
$target = gdcc_for_packed_<family>_iter_get(&$iter);
```

边界语义:
边界语义(活迭代,对齐 Godot 4.5 解释器):

- `from` 深拷贝 typed Packed*Array(COW),缓存 size/index,并缓存 typed 元素基址指针;snapshot 只读,故缓存基址安全。
- `get` 对 typed 基址做指针算术并返回 **typed element**(无 kind switch,无 per-element Variant 装箱)。
- state 持有源数组的 **Variant 持有者拷贝**(共享引擎侧 `PackedArrayRef` 身份)+ 当前 index;**不**持有
COW struct 快照、不缓存 size、不缓存 typed 元素基址指针。
- `from` 接收 `const godot_Variant*`(packed 源本身即 Variant-backed),做持有者拷贝并置 index=0。
- `next` 仅做持有者拷贝并递增 index;禁止复用任何快照式 copy(会把迭代锚定到 detach 后的数组上)。
- `should_continue` 每次经 `gdcc_packed_<slug>_internal_ptr` 求 **live size**:迭代期间 `push_back` 的新元素
会被本轮循环访问(解释器锁定的活迭代合同)。
- `get` 每次先以 live size 做越界检查(越界返回 family 默认值),再经内部指针 + `operator_index_const`
取 **typed element**(无 kind switch,无 per-element Variant 装箱);禁止跨迭代缓存元素基址——mutation
可能 realloc,缓存指针会悬垂。
- lowering 的 `ForLoopGetItem` 在 element 与 `exposedIteratorType` 兼容时可直接赋值,通常无需 `UnpackVariant`。

### `gdcc.for_float_iter.init`
Expand Down
18 changes: 16 additions & 2 deletions doc/gdcc_low_ir.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ $<result_id> = assign $<source_id>
Constructs a builtin of a specific type with arguments. The type is the same as the type
of the result variable.

For `Packed*Array` result types the backend does not emit `godot_new_Packed*` symbols; construction
routes to the whitelisted `gdcc_packed_ref.h` helpers (zero-arg -> `gdcc_packed_<slug>_new_empty`,
same-family argument -> `gdcc_packed_<slug>_new_copy`, `Array` argument ->
`gdcc_packed_<slug>_new_from_array`), and the result is a Variant-backed packed value.

```
$<result_id> = construct_builtin $<arg1_id> $<arg2_id> ...
```
Expand All @@ -162,6 +167,9 @@ Rules:
- If result variable type is `Packed*Array` (`GdPackedArrayType`):
- Construction type is inferred only from the result variable type.
- `class_name` must not be provided; providing it is invalid and should fail fast.
- The result is a Variant-backed packed value (empty-array `godot_Variant` via
`gdcc_packed_<slug>_new_empty`), never a bare packed struct; this is also the mandatory
default initialization for every packed slot.

```
$<result_id> = construct_array "<class_name>"?
Expand Down Expand Up @@ -295,7 +303,8 @@ Types can be destruct:
- Signal
- Dictionary
- Array
- Packed*Array
- Packed*Array (Variant-backed storage: destruction is `godot_Variant_destroy(...)`, releasing this
holder's share of the engine-side `PackedArrayRef`; there is no packed struct destructor)
- Object
- Variant
- `compiler::GdccCoroState` (destruction releases the owned state object reference —
Expand Down Expand Up @@ -476,7 +485,12 @@ Runtime builtin conversion for GDScript `as` when the target is a non-Object, no
non-Nil runtime builtin (including parameterized `Array[T]` / `Dictionary[K, V]`).
The same as Godot `Variant::construct` / `can_convert` semantics at the backend.
Parameterized containers keep full declared type text.
Result is required. Exact same-type and `as Variant` use `assign` / `pack_variant` instead.
Result is required. Exact same-type and `as Variant` use `assign` / `pack_variant` instead,
with one exception: a same-family `Packed*Array` `as` cast (e.g. `v as PackedInt32Array` where the
source is statically or dynamically `PackedInt32Array`) is NOT an identity-preserving assign — it
produces a COW copy with a fresh identity (`gdcc_packed_<slug>_new_copy`), matching the Godot 4.5
interpreter; the frontend therefore routes it here instead of `assign`. Packed `is` tests are exact
`Variant.get_type()` family matches (`gdcc_packed_ref_is`).

```
$<result_id:target_type> = builtin_cast "<target_type_name>" $<value_id>
Expand Down
29 changes: 27 additions & 2 deletions doc/gdcc_ownership_lifecycle_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ Implementation note:
- destroy old value when required,
- then assign.
- Such consolidation is a structural refactor and must not change copy/destroy semantics by itself.
- `Packed*Array` slots are Variant-backed: the copy step is `godot_new_Variant_with_Variant(...)` (a holder copy
that SHARES the engine-side `PackedArrayRef` identity with the source), and the destroy step is
`godot_Variant_destroy(...)` (releases this holder). They must never be shallow-copied as plain structs and
never round-trip through packed struct pack/unpack outside the whitelisted `gdcc_packed_ref.h` helpers
(see `gdcc_c_backend.md` "Packed*Array Variant-backed Storage").

### 3.3 Overwrite vs First Write

Expand Down Expand Up @@ -123,6 +128,9 @@ Implementation note:
- Discarding an `OWNED` object return value: must immediately `release` (or `try_release` variant).
- Discarding a `BORROWED` object value: no cleanup required.
- For non-object but `isDestroyable()==true` return values (String/Variant/Container, etc.), discarding must immediately `destroy`.
- `Packed*Array` temporaries are Variant carriers: discard destroys them via `godot_Variant_destroy(...)`
(builtin-method native return structs are already consumed by `gdcc_packed_<slug>_wrap_temp`, which
destroys the temp struct at the wrap site).

### 3.6 RefCounted Status Matrix

Expand Down Expand Up @@ -251,6 +259,10 @@ Parameters of a coroutine function:
ones copied). Parameter fields are destroyed exactly once, by `free_instance` — the cancel
path never touches them (after cancel-resume the coroutine is `MCO_DEAD` and flows into
the same single `free_instance` cleanup).
- Packed parameter/capture/return fields store `godot_Variant`: "copied" means a Variant holder copy
(`godot_new_Variant_with_Variant`) that shares the packed identity with the caller across `await`
suspension, and destruction is `godot_Variant_destroy(...)`. A struct copy-construct here would break
the interpreter-observable sharing.

Captures of a coroutine lambda follow the same per-call frame discipline:

Expand All @@ -259,12 +271,15 @@ Captures of a coroutine lambda follow the same per-call frame discipline:
them. Body capture operands map directly to frame fields.
- The start thunk copies each field out of the Callable-owned capture block before
`mco_create` (primitives assigned, objects retained from a BORROWED source, value types
copy-constructed); the capture block itself remains owned solely by the Callable userdata
copy-constructed, packed arrays Variant-holder-copied); the capture block itself remains owned solely by the Callable userdata
and is freed independently by its `free_func` — releasing the Callable while suspended
therefore never invalidates the frame.
- Capture fields are destroyed exactly once, by `free_instance` after the parameter fields;
the cancel path flows into the same single cleanup. Writes to a capture name inside the
lambda hit only that call's frame copy, matching copy-on-capture semantics.
lambda hit only that call's frame copy, matching copy-on-capture semantics. For packed captures,
copy-on-capture copies the Variant HOLDER, not the underlying packed data: the frame field and the
outer variable share the same packed identity, so mutations (not rebindings) are visible in both
directions — identical to interpreter lambda capture behavior.

Return-value storage state machine (must not be violated):

Expand Down Expand Up @@ -321,6 +336,9 @@ Cancel-resume (abandonment path, e.g. emitter death dropping the last reference)
- Keep `__prepare__` / `__finally__` framework unchanged.
- `_return_val` is still generated and managed by `CBodyBuilder`, and must not be moved into variable-table auto-destruction.
- Property initializer lowering may materialize helper-produced values, but constructor-time application of those values to backing fields remains a separate backend-owned route.
- Packed backing fields are Variant carriers: the initializer produces the packed value through the whitelisted
`gdcc_packed_ref.h` construction helpers (empty / same-family copy / from-`Array`), and the field write is an
ordinary Variant-holder slot write.
- Coroutine body functions reuse the same `__prepare__` / `__finally__` framework unchanged;
coroutine frame fields (typed parameter fields, typed return slot) are not ordinary C local
slots and stay outside the variable-table auto-destruction scope, exactly like `_return_val`.
Expand All @@ -338,11 +356,18 @@ Cancel-resume (abandonment path, e.g. emitter death dropping the last reference)
- local non-`void` return carrier `r`
- Cleanup rule for those locals is value-wrapper specific:
- destroyable non-object wrappers must be explicitly destroyed before the wrapper returns
- packed argument locals are Variant copies (`godot_new_Variant_with_Variant`) that share identity with the
caller, and are destroyed via `godot_Variant_destroy(...)`; no struct unpack/pack is involved, which is what
makes callee mutations visible to the GDScript caller
- `OWNED` object return carrier `r` must be released after Variant packing
(`release_object` / `try_release_object` per `RefCountedStatus`) so internal ownership
transfers net-zero into `r_return`
- object argument locals are BORROWED from Variant args and must not be released here
- primitives never need wrapper cleanup
- The ptrcall wrapper is the documented exception boundary: packed ptrcall arguments are materialized
struct->Variant (`gdcc_packed_<slug>_variant_from_struct`, destroyed after the call) and packed ptrcall
returns are copied Variant->struct (`gdcc_packed_<slug>_struct_from_variant`), so identity is NOT shared
across the ptrcall ABI (see `gdcc_c_backend.md` "Packed*Array Variant-backed Storage").
- Required success-path order:
1. publish `r_return`
2. destroy local `ret`
Expand Down
Loading
Loading