# GDScript Parser Research

## 1. Codebase Architecture Analysis

### 1.1 Existing Parser Patterns

All parsers in `src/godotiq/parsers/` follow a consistent architecture:

- **TSCN Parser** (532 lines): Dataclass models, state machine, two-pass parsing (flat list → tree), multiline value accumulation with bracket-depth tracking, regex for section headers, lazy parsing for heavy binary data.
- **Value Parser** (371 lines): Pre-compiled regex patterns for type detection, comprehensive type coverage, fallback-to-string for unknowns, Transform3D decomposition into (position, rotation, scale).
- **Scene Resolver** (438 lines): Works on parsed TscnScene objects, recursive instance expansion with cycle detection and depth limits, world-space transform calculation.
- **Project Index** (280 lines): Cross-reference building, caching via FileCache, filesystem walk with filtering.

### 1.2 Data Model Pattern: Dataclasses

All parsers use `@dataclass` with `field()` defaults:
- Mutable defaults use `field(default_factory=...)`
- Simple strings default to `""`
- Lists default to `field(default_factory=list)`
- Type hints are comprehensive (including `|` for unions and `None`)
- Each dataclass has clear docstrings

### 1.3 Import Conventions

Absolute imports only: `from godotiq.parsers.tscn_parser import parse_tscn`
Never use `from src.godotiq...` or relative imports.

### 1.4 Current gd_parser.py Stub (50 lines)

```python
@dataclass
class GdSignal:
    name: str = ""
    args: list[str] = field(default_factory=list)

@dataclass
class GdFunction:
    name: str = ""
    args: list[str] = field(default_factory=list)
    return_type: str = ""

@dataclass
class GdScript:
    signals: list[GdSignal] = field(default_factory=list)
    functions: list[GdFunction] = field(default_factory=list)
    extends: str = ""

def parse_gd(content: str) -> GdScript:
    raise NotImplementedError("gd parser not yet implemented")
```

### 1.5 Available .gd Fixture Files

Six .gd files in `tests/fixtures/`:

| File | Lines | Key Features |
|------|-------|-------------|
| `economy_manager.gd` | 69 | class_name, extends, signals with typed args, functions with defaults/return types |
| `printer_manager.gd` | 354 | Complex signatures, lambdas, type casting, match statements, signal.connect() |
| `game_manager.gd` | 150 | Enums, _process(), get_tree().root.get_node_or_null() |
| `progression_manager.gd` | 267 | Const arrays, complex dicts, lambda in loops, level_up.connect() |
| `sample_project/scripts/entities/printer.gd` | 1 | Minimal: `extends Node3D` |
| `sample_project/scripts/entities/worker.gd` | 1 | Minimal: `extends CharacterBody3D` |

### 1.6 Testing Patterns

- Class-based organization by feature (TestSignalParsing, TestFunctionParsing, etc.)
- Method names: `test_<feature>_<scenario>()`
- Use real fixture files from `tests/fixtures/`
- `FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"`
- pytest.approx() for floating-point comparison
- `asyncio_mode = "auto"` in pyproject.toml

### 1.7 Package Structure

`src/godotiq/parsers/__init__.py` is currently empty (just docstring).

### 1.8 Server Integration

`server.py` uses `@mcp.tool()` decorator for tools. The constraint says NOT to implement MCP tools or update project_index.py — only implement gd_parser.py and tests.

---

## 2. GDScript 4.x Syntax Reference

### 2.1 class_name Declaration

```gdscript
class_name MyClass
class_name Item extends Node          # Combined with extends on same line
```
- Only one per file
- `@icon()` can precede it
- `@abstract` (Godot 4.5+) can precede it

### 2.2 extends Declaration

```gdscript
extends BaseClass
extends Node
extends "res://path/to/character.gd"
extends "somefile.gd".SomeInnerClass
```
- Can be combined with class_name on same line
- If omitted, implicitly extends RefCounted

### 2.3 enum Definitions

**Anonymous:**
```gdscript
enum {UNIT_NEUTRAL, UNIT_ENEMY, UNIT_ALLY}
```

**Named:**
```gdscript
enum Named {THING_1, THING_2, ANOTHER_THING = -1}
enum State {STATE_IDLE, STATE_JUMP = 5, STATE_SHOOT}
```
- Values auto-increment from 0 or previous value + 1
- Named enum values accessed as `Named.THING_1`
- Can span multiple lines

### 2.4 const Declarations

```gdscript
const ANSWER = 42
const A: int = 5
const MAX_SPEED: float = 120.0
const MyClass = preload("myclass.gd")
const LEVEL_XP: Array[int] = [0, 40, 100, 200]
const UNLOCK_TABLE: Dictionary = { 1: {...}, 2: {...} }
```
- Must be computable at parse time
- `load()` cannot be used with `const` (only `preload()`)

### 2.5 Signal Declarations

```gdscript
signal my_signal                                    # No parameters
signal health_changed(old_value, new_value)         # Untyped
signal damage_taken(amount: int)                    # Typed
signal thing_happened(triggered_by: Node, msg: String)  # Multiple typed
signal released(player: Player)                     # Custom type
signal my_signal(custom_object: CustomClass)        # Custom type
```
- Signal parameter types are informational/documentary only (NOT type-checked at compile/runtime)

### 2.6 Variable Declarations

```gdscript
var a                           # Variant
var b = 5                       # Inferred int
var typed_var: int
var my_vector2: Vector2
var inferred_type := "String"   # Inferred via :=
var a: Array[int]               # Typed array
var d: Dictionary[String, int]  # Typed dictionary (4.x)
static var balance: int = 0     # Static variable
```

### 2.7 Function Signatures

```gdscript
func some_function(param1, param2, param3 = 123):
func my_function(a: int, b: String):
func my_function(int_arg := 42, String_arg := "string"):  # Default with type inference
func my_int_function() -> int:
func void_function() -> void:
static func sum2(a, b):
func square(a): return a * a     # One-liner
```

**Lambda functions:**
```gdscript
var lambda = func (x): print(x)
var lambda = func my_lambda(x): print(x)    # Named for stack traces
var lambda := func (x: int) -> void: print(x)
```

### 2.8 Inner Classes

```gdscript
class Something:
    var a = 10

class Circle extends Shape:
    func draw():
        print("Drawing a circle.")
```
- Accessed as `OuterScript.InnerClass`
- Can use `extends`

---

## 3. @export Annotations (Complete List)

### 3.1 Variable Annotations

| Annotation | Example |
|-----------|---------|
| `@onready` | `@onready var sprite := $Sprite2D` |
| `@export` | `@export var speed: float = 5.0` |
| `@export_enum()` | `@export_enum("Warrior", "Magician") var class: int` |
| `@export_range()` | `@export_range(0, 100, 1, "or_less") var hp: int` |
| `@export_file()` | `@export_file("*.txt") var f` |
| `@export_global_file()` | `@export_global_file("*.png") var img` |
| `@export_dir` | `@export_dir var f` |
| `@export_global_dir` | `@export_global_dir var tool_dir` |
| `@export_multiline` | `@export_multiline var text` |
| `@export_placeholder()` | `@export_placeholder("hint") var name: String` |
| `@export_color_no_alpha` | `@export_color_no_alpha var col: Color` |
| `@export_node_path()` | `@export_node_path("Button") var btn` |
| `@export_exp_easing` | `@export_exp_easing var speed` |
| `@export_flags()` | `@export_flags("Fire", "Water") var elems = 0` |
| `@export_flags_2d_physics` | `@export_flags_2d_physics var layers` |
| `@export_flags_2d_render` | `@export_flags_2d_render var layers` |
| `@export_flags_2d_navigation` | `@export_flags_2d_navigation var layers` |
| `@export_flags_3d_physics` | `@export_flags_3d_physics var layers` |
| `@export_flags_3d_render` | `@export_flags_3d_render var layers` |
| `@export_flags_3d_navigation` | `@export_flags_3d_navigation var layers` |
| `@export_flags_avoidance` | `@export_flags_avoidance var layers` |
| `@export_storage` | `@export_storage var b` |
| `@export_custom()` | `@export_custom(PROPERTY_HINT_NONE, "suffix:m") var alt: float` |
| `@export_tool_button()` | `@export_tool_button("Hello") var action = hello` |

### 3.2 Grouping Annotations (Standalone)

```gdscript
@export_group("My Properties")
@export_subgroup("Extra Properties")
@export_category("Main Category")
```

### 3.3 Exported Arrays

```gdscript
@export var ints: Array[int] = [1, 2, 3]
@export var scenes: Array[PackedScene]
@export_range(-360, 360, 0.001, "degrees") var angles: Array[float] = []
@export_file("*.json") var skill_trees: Array[String] = []
```

### 3.4 Other Annotations

| Annotation | Purpose |
|-----------|---------|
| `@tool` | Script runs in editor (must be first line) |
| `@icon()` | Sets class icon |
| `@static_unload` | Allows static unloading |
| `@abstract` | Abstract class/func (4.5+) |
| `@warning_ignore()` | Suppress warnings |
| `@rpc()` | RPC/networking |

---

## 4. Signal Connection & Emission Patterns

### 4.1 Connection Patterns

**Direct method reference:**
```gdscript
timer.timeout.connect(_on_timer_timeout)
```

**With .bind() (extra arguments):**
```gdscript
signal_name.connect(_handler.bind(param1, param2))
signal_name.connect(_handler.bindv([params]))
```

**Lambda / anonymous function:**
```gdscript
my_button.pressed.connect(func(): print("pressed"))
my_button.pressed.connect(func():
    print("Button was pressed!")
    do_something()
)
```

**$Node references:**
```gdscript
$Timer.timeout.connect(_on_timer_timeout)
$UI/Button.pressed.connect(_on_button_pressed)
```

**Connect flags:**
- `CONNECT_DEFERRED = 1`
- `CONNECT_PERSIST = 2`
- `CONNECT_ONE_SHOT = 4`
- `CONNECT_REFERENCE_COUNTED = 8`

### 4.2 Emission Patterns

```gdscript
health_depleted.emit()                     # No args
health_changed.emit(old_health, health)    # 2 args
car_ready.emit("Mario", "Dodgers", 1)     # 3 args
```

Legacy (still valid): `emit_signal("health_depleted")`

### 4.3 Awaiting Signals

```gdscript
await get_tree().create_timer(1.0).timeout
await animation_player.animation_finished
```

---

## 5. Autoload / Singleton Patterns

**Direct access:**
```gdscript
PlayerVariables.health -= 10
EconomyManager.add_cash(100)
EventBus.new_points.emit(3)
```

**Check existence:**
```gdscript
if has_node("/root/PlayerVariables"):
    print("Autoload is present")

var node = get_tree().root.get_node_or_null("PlayerVariables")
```

---

## 6. Other Patterns

### 6.1 is_instance_valid()

```gdscript
if is_instance_valid(target_node):
    target_node.take_damage(10)

# Prune freed objects
for p in active_projectiles:
    if not is_instance_valid(p):
        active_projectiles.erase(p)
```

### 6.2 preload() vs load()

| Feature | `preload()` | `load()` |
|---------|------------|----------|
| Timing | Parse time | Runtime |
| Path | Must be string literal | Can be variable |
| Use with const | Yes | No |

```gdscript
const MyClass = preload("myclass.gd")
var resource = load("res://some_resource.tres")
```

---

## 7. Godot 3.x → 4.x Migration (Parsing-Relevant)

| Feature | 3.x | 4.x |
|---------|-----|-----|
| Signal connect | `connect("pressed", self, "_handler")` | `pressed.connect(_handler)` |
| Signal emit | `emit_signal("sig", arg)` | `sig.emit(arg)` |
| Export | `export(float) var speed` | `@export var speed: float` |
| Onready | `onready var x = $Node` | `@onready var x = $Node` |
| Tool | `tool` keyword | `@tool` annotation |
| Yield | `yield(obj, "signal")` | `await obj.signal` |
| Setget | `var x setget set_x, get_x` | Property `get:/set(value):` blocks |

**Key: The parser targets Godot 4.x ONLY — no need for 3.x patterns.**

---

## 8. Testing Setup

- **Framework:** pytest + pytest-asyncio
- **Config:** `asyncio_mode = "auto"` in pyproject.toml
- **Fixture path:** `FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"`
- **Pattern:** Class-based test organization by feature group
- **Naming:** `test_<feature>_<scenario>()`
- **Real data:** Tests use actual .gd fixture files for integration tests
- **Existing tests:** 127 tests across Sprint 0 and Sprint 1 (all passing)
