inscript-lang 2.5.0__tar.gz → 2.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/PKG-INFO +1 -1
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/ast_nodes.py +35 -0
- inscript_lang-2.10.0/hot_reload.py +340 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript.py +572 -6
- inscript_lang-2.10.0/inscript_dap.py +468 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/SOURCES.txt +4 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/top_level.txt +4 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/interpreter.py +47 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/lexer.py +10 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/parser.py +54 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/pygame_backend.py +19 -2
- inscript_lang-2.10.0/pyproject.toml +68 -0
- inscript_lang-2.10.0/scene_tree.py +501 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/setup.py +9 -1
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib.py +6 -0
- inscript_lang-2.10.0/stdlib_assets.py +381 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib_game.py +461 -0
- inscript_lang-2.5.0/pyproject.toml +0 -51
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/README.md +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/analyzer.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/compiler.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/environment.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/errors.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_fmt.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_test.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/repl.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/setup.cfg +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib_extended.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib_extended_2.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib_values.py +0 -0
- {inscript_lang-2.5.0 → inscript_lang-2.10.0}/vm.py +0 -0
|
@@ -382,6 +382,41 @@ class SceneDecl(Node):
|
|
|
382
382
|
methods: List[FunctionDecl]
|
|
383
383
|
|
|
384
384
|
|
|
385
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
386
|
+
# v2.7.0 — NODE DECLARATION
|
|
387
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
@dataclass
|
|
390
|
+
class NodeLifecycleHook(Node):
|
|
391
|
+
"""
|
|
392
|
+
_ready() — called once when node enters the scene tree
|
|
393
|
+
_update(dt) — called every frame with delta-time
|
|
394
|
+
_draw() — called every frame for rendering
|
|
395
|
+
"""
|
|
396
|
+
hook_type: str # "_ready" | "_update" | "_draw"
|
|
397
|
+
params: List # list of Param nodes
|
|
398
|
+
body: "Block"
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@dataclass
|
|
402
|
+
class NodeDecl(Node):
|
|
403
|
+
"""
|
|
404
|
+
v2.7.0: node PlayerNode {
|
|
405
|
+
let hp: int = 100
|
|
406
|
+
_ready() { ... }
|
|
407
|
+
_update(dt) { ... }
|
|
408
|
+
_draw() { ... }
|
|
409
|
+
fn shoot() { ... }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
Compiles to an InScript class with lifecycle methods and tree participation.
|
|
413
|
+
"""
|
|
414
|
+
name: str
|
|
415
|
+
vars: List[VarDecl]
|
|
416
|
+
hooks: List[NodeLifecycleHook]
|
|
417
|
+
methods: List[FunctionDecl]
|
|
418
|
+
|
|
419
|
+
|
|
385
420
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
386
421
|
# AI DECLARATION
|
|
387
422
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
hot_reload.py — InScript v2.9.0 Hot Reload Engine
|
|
4
|
+
================================================================
|
|
5
|
+
Provides state-preserving hot reload:
|
|
6
|
+
|
|
7
|
+
• Snapshot the live interpreter's global scope (scalar values / asset
|
|
8
|
+
handles / node instances) before reloading.
|
|
9
|
+
• Re-parse the changed source.
|
|
10
|
+
• Replay only declaration statements: fn, struct, node, const, @decorators.
|
|
11
|
+
• Patch callables in the live env without resetting runtime state (score,
|
|
12
|
+
position, health, etc. survive the reload).
|
|
13
|
+
• Restore non-callable values that existed before the reload.
|
|
14
|
+
• Call on_reload() if defined, so game code can react (re-register assets,
|
|
15
|
+
reset physics contacts, etc.)
|
|
16
|
+
• Collect and report parse/type errors; keep old code running on failure.
|
|
17
|
+
|
|
18
|
+
@hot_reload annotation
|
|
19
|
+
──────────────────────
|
|
20
|
+
Mark a function as stable so the hot-reload engine skips patching it:
|
|
21
|
+
|
|
22
|
+
@hot_reload
|
|
23
|
+
fn expensive_precompute(data) { ... }
|
|
24
|
+
|
|
25
|
+
The function is patched only on first load; subsequent reloads leave it
|
|
26
|
+
unchanged even if the source changed. Useful for computationally expensive
|
|
27
|
+
setup that should not re-run every save.
|
|
28
|
+
|
|
29
|
+
Usage
|
|
30
|
+
─────
|
|
31
|
+
from hot_reload import HotReloader
|
|
32
|
+
reloader = HotReloader(source_path, interp)
|
|
33
|
+
reloader.start() # initial load
|
|
34
|
+
changed = reloader.tick() # call every save (from --watch loop)
|
|
35
|
+
reloader.stop()
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
import os, time
|
|
40
|
+
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from interpreter import Interpreter
|
|
44
|
+
|
|
45
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
# Helpers
|
|
47
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
_DECLARATION_NODE_TYPES = frozenset({
|
|
50
|
+
"FunctionDecl", "StructDecl", "NodeDecl",
|
|
51
|
+
"ExportDecl", "DecoratedDecl",
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
_SKIP_RESTORE_TYPES = frozenset({
|
|
55
|
+
# Never restore these from snapshot — they're always re-declared fresh
|
|
56
|
+
"InScriptFunction", "NodeBlueprint",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
# Primitive types safe to snapshot/restore across a reload
|
|
60
|
+
_SCALAR_TYPES = (int, float, str, bool, type(None), list, dict, tuple)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_callable_value(v: Any) -> bool:
|
|
64
|
+
"""True for InScript functions, structs, node blueprints, Python callables."""
|
|
65
|
+
from ast_nodes import StructDecl
|
|
66
|
+
try:
|
|
67
|
+
from scene_tree import NodeBlueprint
|
|
68
|
+
if isinstance(v, NodeBlueprint):
|
|
69
|
+
return True
|
|
70
|
+
except ImportError:
|
|
71
|
+
pass
|
|
72
|
+
try:
|
|
73
|
+
from interpreter import InScriptFunction
|
|
74
|
+
if isinstance(v, InScriptFunction):
|
|
75
|
+
return True
|
|
76
|
+
except ImportError:
|
|
77
|
+
pass
|
|
78
|
+
if isinstance(v, StructDecl):
|
|
79
|
+
return True
|
|
80
|
+
return callable(v)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _snapshot_globals(interp: "Interpreter") -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Capture non-callable, non-private names from the global scope.
|
|
86
|
+
These are the values that must survive the reload (game state).
|
|
87
|
+
"""
|
|
88
|
+
snap = {}
|
|
89
|
+
for name, val in list(interp._globals._store.items()):
|
|
90
|
+
if name.startswith("_"):
|
|
91
|
+
continue
|
|
92
|
+
if _is_callable_value(val):
|
|
93
|
+
continue
|
|
94
|
+
snap[name] = val
|
|
95
|
+
return snap
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _restore_snapshot(interp: "Interpreter",
|
|
99
|
+
snap: Dict[str, Any],
|
|
100
|
+
patched_callables: Set[str]) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Re-inject snapshot values for names that were NOT redeclared by the reload
|
|
103
|
+
and whose values are scalar (i.e. game state, not code).
|
|
104
|
+
"""
|
|
105
|
+
for name, val in snap.items():
|
|
106
|
+
if name in patched_callables:
|
|
107
|
+
continue # This name was re-declared; new version wins
|
|
108
|
+
try:
|
|
109
|
+
interp._globals.set(name, val)
|
|
110
|
+
except Exception:
|
|
111
|
+
try:
|
|
112
|
+
interp._globals.define(name, val)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _collect_declared_names(program) -> Set[str]:
|
|
118
|
+
"""Walk the top-level AST and collect every declared name."""
|
|
119
|
+
names = set()
|
|
120
|
+
for stmt in program.body:
|
|
121
|
+
cls = type(stmt).__name__
|
|
122
|
+
if cls == "FunctionDecl":
|
|
123
|
+
names.add(stmt.name)
|
|
124
|
+
elif cls == "StructDecl":
|
|
125
|
+
names.add(stmt.name)
|
|
126
|
+
elif cls == "NodeDecl":
|
|
127
|
+
names.add(stmt.name)
|
|
128
|
+
elif cls == "ExportDecl":
|
|
129
|
+
inner = getattr(stmt, "decl", None)
|
|
130
|
+
if inner is not None:
|
|
131
|
+
names.add(getattr(inner, "name", None) or "")
|
|
132
|
+
elif cls == "DecoratedDecl":
|
|
133
|
+
inner = getattr(stmt, "target", None)
|
|
134
|
+
inner2 = getattr(inner, "decl", inner)
|
|
135
|
+
names.add(getattr(inner2, "name", None) or "")
|
|
136
|
+
elif cls == "VarDecl" and getattr(stmt, "name", None):
|
|
137
|
+
names.add(stmt.name)
|
|
138
|
+
names.discard("")
|
|
139
|
+
return names
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
# ReloadResult
|
|
144
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
class ReloadResult:
|
|
147
|
+
"""Returned by HotReloader.tick() describing what happened."""
|
|
148
|
+
def __init__(self, changed: bool, success: bool,
|
|
149
|
+
patched: List[str], restored: List[str],
|
|
150
|
+
errors: List[str], elapsed_ms: float):
|
|
151
|
+
self.changed = changed # True if the source file changed
|
|
152
|
+
self.success = success # True if reload completed without errors
|
|
153
|
+
self.patched = patched # names that were re-declared
|
|
154
|
+
self.restored = restored # names restored from snapshot
|
|
155
|
+
self.errors = errors # parse/runtime error strings (if any)
|
|
156
|
+
self.elapsed_ms = elapsed_ms # wall-clock time in milliseconds
|
|
157
|
+
|
|
158
|
+
def __repr__(self):
|
|
159
|
+
status = "OK" if self.success else "ERR"
|
|
160
|
+
return (f"<ReloadResult {status} patched={self.patched} "
|
|
161
|
+
f"restored={len(self.restored)} errors={len(self.errors)} "
|
|
162
|
+
f"{self.elapsed_ms:.1f}ms>")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
# HotReloader
|
|
167
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
class HotReloader:
|
|
170
|
+
"""
|
|
171
|
+
Manages hot reload for a single .ins source file against a live interpreter.
|
|
172
|
+
|
|
173
|
+
Typical usage (headless / test):
|
|
174
|
+
reloader = HotReloader("game.ins", interp)
|
|
175
|
+
reloader.start()
|
|
176
|
+
# ... mutate source file ...
|
|
177
|
+
result = reloader.tick() # detects change, reloads, returns ReloadResult
|
|
178
|
+
|
|
179
|
+
Typical usage (game loop / --watch):
|
|
180
|
+
reloader = HotReloader("game.ins", interp)
|
|
181
|
+
reloader.start()
|
|
182
|
+
while running:
|
|
183
|
+
result = reloader.tick()
|
|
184
|
+
if result.changed and not result.success:
|
|
185
|
+
show_error_overlay(result.errors)
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, source_path: str, interp: "Interpreter"):
|
|
189
|
+
self.source_path = source_path
|
|
190
|
+
self._interp = interp
|
|
191
|
+
self._last_mtime: Optional[float] = None
|
|
192
|
+
self._stable_names: Set[str] = set() # @hot_reload-marked names
|
|
193
|
+
self._reload_count = 0
|
|
194
|
+
|
|
195
|
+
# ── public API ─────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
def start(self) -> ReloadResult:
|
|
198
|
+
"""Initial load. Equivalent to a cold run with snapshot setup."""
|
|
199
|
+
return self._do_reload(initial=True)
|
|
200
|
+
|
|
201
|
+
def tick(self) -> ReloadResult:
|
|
202
|
+
"""
|
|
203
|
+
Check if source file changed; reload if so.
|
|
204
|
+
Returns a ReloadResult with changed=False if nothing changed.
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
mtime = os.path.getmtime(self.source_path)
|
|
208
|
+
except FileNotFoundError:
|
|
209
|
+
return ReloadResult(False, False, [], [], [f"File not found: {self.source_path}"], 0.0)
|
|
210
|
+
|
|
211
|
+
if self._last_mtime is not None and mtime <= self._last_mtime:
|
|
212
|
+
return ReloadResult(False, True, [], [], [], 0.0)
|
|
213
|
+
|
|
214
|
+
return self._do_reload(initial=False)
|
|
215
|
+
|
|
216
|
+
def stop(self):
|
|
217
|
+
"""No-op; hooks into game loop cleanup."""
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def reload_count(self) -> int:
|
|
222
|
+
return self._reload_count
|
|
223
|
+
|
|
224
|
+
# ── internals ──────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
def _do_reload(self, initial: bool) -> ReloadResult:
|
|
227
|
+
t0 = time.perf_counter()
|
|
228
|
+
errors: List[str] = []
|
|
229
|
+
patched: List[str] = []
|
|
230
|
+
restored: List[str] = []
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
mtime = os.path.getmtime(self.source_path)
|
|
234
|
+
except FileNotFoundError:
|
|
235
|
+
return ReloadResult(True, False, [], [],
|
|
236
|
+
[f"File not found: {self.source_path}"], 0.0)
|
|
237
|
+
|
|
238
|
+
# 1. Read source
|
|
239
|
+
try:
|
|
240
|
+
with open(self.source_path, encoding="utf-8") as f:
|
|
241
|
+
source = f.read()
|
|
242
|
+
except OSError as e:
|
|
243
|
+
return ReloadResult(True, False, [], [], [str(e)], 0.0)
|
|
244
|
+
|
|
245
|
+
# 2. Parse (fail-safe: keep old code running on syntax error)
|
|
246
|
+
try:
|
|
247
|
+
from parser import parse as _parse
|
|
248
|
+
program = _parse(source)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
errors.append(f"Parse error: {e}")
|
|
251
|
+
elapsed = (time.perf_counter() - t0) * 1000
|
|
252
|
+
return ReloadResult(True, False, patched, restored, errors, elapsed)
|
|
253
|
+
|
|
254
|
+
# 3. Snapshot current game state (skip on initial load)
|
|
255
|
+
snap: Dict[str, Any] = {}
|
|
256
|
+
if not initial:
|
|
257
|
+
snap = _snapshot_globals(self._interp)
|
|
258
|
+
|
|
259
|
+
# 4. Determine which names will be patched
|
|
260
|
+
declared = _collect_declared_names(program)
|
|
261
|
+
|
|
262
|
+
# 5. Execute only declaration nodes (fn, struct, node, const, decorated)
|
|
263
|
+
# Skip @hot_reload-marked stable names (unless initial load)
|
|
264
|
+
for stmt in program.body:
|
|
265
|
+
cls = type(stmt).__name__
|
|
266
|
+
if not initial:
|
|
267
|
+
# Hot reload: only patch code declarations, never re-run VarDecl
|
|
268
|
+
# (that would reset game state like score, player position, etc.)
|
|
269
|
+
if cls not in _DECLARATION_NODE_TYPES:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
# Determine the name of this declaration
|
|
273
|
+
name = None
|
|
274
|
+
if cls == "FunctionDecl":
|
|
275
|
+
name = stmt.name
|
|
276
|
+
elif cls in ("StructDecl", "NodeDecl"):
|
|
277
|
+
name = stmt.name
|
|
278
|
+
elif cls == "DecoratedDecl":
|
|
279
|
+
inner = getattr(getattr(stmt, "target", None), "decl",
|
|
280
|
+
stmt.target)
|
|
281
|
+
name = getattr(inner, "name", None)
|
|
282
|
+
# Detect @hot_reload — register as stable
|
|
283
|
+
for dec_name, _ in stmt.decorators:
|
|
284
|
+
if dec_name == "hot_reload":
|
|
285
|
+
if name:
|
|
286
|
+
self._stable_names.add(name)
|
|
287
|
+
|
|
288
|
+
# Skip stable names on subsequent reloads
|
|
289
|
+
if not initial and name and name in self._stable_names:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Execute the declaration
|
|
293
|
+
try:
|
|
294
|
+
self._interp.visit(stmt)
|
|
295
|
+
if name:
|
|
296
|
+
patched.append(name)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
errors.append(f"Reload error in '{name or cls}': {e}")
|
|
299
|
+
|
|
300
|
+
# 6. Restore snapshot values for names NOT re-declared
|
|
301
|
+
if not initial and snap:
|
|
302
|
+
for k, v in snap.items():
|
|
303
|
+
if k not in declared:
|
|
304
|
+
try:
|
|
305
|
+
self._interp._globals.set(k, v)
|
|
306
|
+
except Exception:
|
|
307
|
+
try:
|
|
308
|
+
self._interp._globals.define(k, v)
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
restored.append(k)
|
|
312
|
+
|
|
313
|
+
# 7. Call on_reload() if defined
|
|
314
|
+
if not initial:
|
|
315
|
+
try:
|
|
316
|
+
on_reload_fn = self._interp._globals._store.get("on_reload")
|
|
317
|
+
if on_reload_fn is not None and callable(on_reload_fn):
|
|
318
|
+
self._interp._call_fn(on_reload_fn, [])
|
|
319
|
+
except Exception as e:
|
|
320
|
+
errors.append(f"on_reload() error: {e}")
|
|
321
|
+
|
|
322
|
+
self._last_mtime = mtime
|
|
323
|
+
self._reload_count += 1
|
|
324
|
+
elapsed = (time.perf_counter() - t0) * 1000
|
|
325
|
+
|
|
326
|
+
success = len(errors) == 0
|
|
327
|
+
if not initial:
|
|
328
|
+
status = "✅" if success else "❌"
|
|
329
|
+
print(f"[hot_reload] {status} #{self._reload_count} "
|
|
330
|
+
f"patched={patched} restored={len(restored)} "
|
|
331
|
+
f"{elapsed:.1f}ms")
|
|
332
|
+
|
|
333
|
+
return ReloadResult(
|
|
334
|
+
changed = True,
|
|
335
|
+
success = success,
|
|
336
|
+
patched = patched,
|
|
337
|
+
restored = restored,
|
|
338
|
+
errors = errors,
|
|
339
|
+
elapsed_ms = elapsed,
|
|
340
|
+
)
|