ADR-0006: ## @return-tuple(...) doc-tag and the synthesized Ty::Tuple

  • Status: Accepted
  • Date: 2026-07-02

Context

React-style GDScript libraries return fixed-shape pairs — ReactiveUI's useState returns [value, setter: Callable] — but GDScript has no tuple syntax: the best possible annotation, -> Array, erases the per-position types, so useState(0)[1] types as Variant and a typo'd setter method (.casll()) is uncheckable. Options considered: (a) hardcode the known library signatures in the analyzer (couples a standalone "Roslyn for Godot" to one third-party library); (b) infer function return shapes from return [a, b] bodies (violates the annotation-only return invariant and changes typing project-wide); (c) a declaration channel libraries opt into.

Decision

We will support a ## @return-tuple(T0, T1, …) doc-comment tag on any func — inert in Godot (a comment), so annotating never breaks a real build — parsed into the item tree and resolved by one shared mapping (resolve::resolve_tuple_return) on both the same-file and the cross-file (script member table) call paths. It produces a new Ty::Tuple(Vec<Ty>): a synthesized, source-only positional type no annotation can name, widen-only everywhere non-positional — it assigns exactly as its runtime Array[Variant] form, labels as Array, iterates as Variant, and exposes Array's methods — while a constant integer index projects the element's real type.

Consequences

Easier: sliced[1].casll() is checkable wherever the call resolves to the tagged signature (direct indexing, :=-inferred locals, cross-file member calls), and any library can adopt the convention without analyzer changes. Harder: an UNTYPED var s = useState(0) local is a Variant variable by GDScript semantics (only := infers) — Godot cannot check through it and neither do we until assignment-carried flow narrowing lands (tracked in TECH_DEBT.md); and the widen-only rule means a tuple never rejects anything its array form would accept, so the tag can sharpen but never break existing code.