File size: 5,239 Bytes
777ea0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635e6fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777ea0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
"""Validate GDScript produced by the model using gdtoolkit (Scony's parser).

Pure-Python, CPU-only, Godot-4 (GDScript 2.0). Checks SYNTAX (gdparse) and STYLE
(gdlint); it does NOT check runtime/scene semantics (node paths, types against a
real project) — that needs the Godot engine.
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field

_FENCE_RE = re.compile(r"```(?:gdscript|gd|godot)?\s*\n(.*?)```", re.S | re.I)


@dataclass
class BlockResult:
    code: str
    ok: bool                      # parses (valid syntax)
    error: str = ""               # syntax error message (if any)
    lint: list[str] = field(default_factory=list)   # style/lint warnings
    formatted: str = ""           # gdformat output (if available)


def extract_gdscript_blocks(text: str) -> list[str]:
    """Pull fenced GDScript blocks from a model answer."""
    blocks = [m.group(1).strip() for m in _FENCE_RE.finditer(text or "")]
    return [b for b in blocks if b]


# ---------------------------------------------------------------------------
# gdtoolkit wrappers (imported lazily so the module loads even if absent)
# ---------------------------------------------------------------------------
def _parse(code: str) -> tuple[bool, str]:
    try:
        from gdtoolkit.parser import parser
    except Exception as e:                       # gdtoolkit not installed
        return True, f"(parser unavailable: {e})"
    try:
        parser.parse(code, gather_metadata=False)
        return True, ""
    except TypeError:
        # older/newer signature without gather_metadata
        try:
            parser.parse(code)
            return True, ""
        except Exception as e:
            return False, _fmt_err(e)
    except Exception as e:
        return False, _fmt_err(e)


def _fmt_err(e: Exception) -> str:
    line = getattr(e, "line", None)
    col = getattr(e, "column", None)
    msg = str(e).strip().splitlines()[0] if str(e).strip() else type(e).__name__
    if line is not None:
        return f"line {line}:{col or 0}: {msg}"
    return msg


def _lint(code: str) -> list[str]:
    try:
        from gdtoolkit.linter import lint_code
    except Exception:
        return []
    try:
        problems = lint_code(code)
    except Exception:
        return []
    out = []
    for p in problems:
        line = getattr(p, "line", "?")
        name = getattr(p, "name", "")
        desc = getattr(p, "description", str(p))
        out.append(f"line {line}: {desc}" + (f" [{name}]" if name else ""))
    return out


def _format(code: str) -> str:
    try:
        from gdtoolkit.formatter import format_code
        return format_code(code, max_line_length=100)
    except Exception:
        return ""


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def validate_code(code: str) -> BlockResult:
    ok, err = _parse(code)
    return BlockResult(
        code=code, ok=ok, error=err,
        lint=_lint(code) if ok else [],
        formatted=_format(code) if ok else "",
    )


def validate_answer(answer: str) -> list[BlockResult]:
    return [validate_code(b) for b in extract_gdscript_blocks(answer)]


def gdscript_block_spans(text: str) -> list[tuple[str, int, int]]:
    """Each fenced GDScript block as (stripped_code, match_start, match_end), in
    document order. The span covers the whole ```...``` fence so a caller can
    splice a corrected block back in place of the original."""
    out: list[tuple[str, int, int]] = []
    for m in _FENCE_RE.finditer(text or ""):
        code = m.group(1).strip()
        if code:
            out.append((code, m.start(), m.end()))
    return out


def first_gdscript_block(text: str) -> str:
    """First fenced GDScript block (stripped), or '' if none — used to pull the
    corrected code out of a fix generation."""
    blocks = extract_gdscript_blocks(text)
    return blocks[0] if blocks else ""


def render_report(results: list[BlockResult]) -> str:
    """Markdown summary for the UI."""
    if not results:
        return "_No GDScript code blocks detected to validate._"
    lines = []
    for i, r in enumerate(results, 1):
        if r.ok:
            badge = "✅ **valid GDScript** (syntax OK)"
            if r.lint:
                badge += f" · {len(r.lint)} lint note(s)"
        else:
            badge = f"❌ **syntax error** — {r.error}"
        lines.append(f"**Block {i}:** {badge}")
        for w in r.lint[:5]:
            lines.append(f"- ⚠ {w}")
    return "\n".join(lines)


def first_syntax_error(results: list[BlockResult]) -> tuple[str, str] | None:
    """Return (code, error) of the first block that failed to parse, else None."""
    for r in results:
        if not r.ok:
            return r.code, r.error
    return None


if __name__ == "__main__":
    good = "extends Node\n\n@export var speed: float = 5.0\n\nfunc _ready() -> void:\n\tprint(speed)\n"
    bad = "extends Node\n\nfunc _ready(\n\tprint('oops')\n"
    for label, code in (("GOOD", good), ("BAD", bad)):
        r = validate_code(code)
        print(f"== {label} == ok={r.ok} error={r.error!r} lint={r.lint}")