Spaces:
Running on Zero
Running on Zero
| """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) | |
| 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}") | |