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.
Files changed (35) hide show
  1. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/PKG-INFO +1 -1
  2. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/ast_nodes.py +35 -0
  3. inscript_lang-2.10.0/hot_reload.py +340 -0
  4. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript.py +572 -6
  5. inscript_lang-2.10.0/inscript_dap.py +468 -0
  6. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/PKG-INFO +1 -1
  7. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/SOURCES.txt +4 -0
  8. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/top_level.txt +4 -0
  9. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/interpreter.py +47 -0
  10. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/lexer.py +10 -0
  11. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/parser.py +54 -0
  12. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/pygame_backend.py +19 -2
  13. inscript_lang-2.10.0/pyproject.toml +68 -0
  14. inscript_lang-2.10.0/scene_tree.py +501 -0
  15. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/setup.py +9 -1
  16. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib.py +6 -0
  17. inscript_lang-2.10.0/stdlib_assets.py +381 -0
  18. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib_game.py +461 -0
  19. inscript_lang-2.5.0/pyproject.toml +0 -51
  20. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/README.md +0 -0
  21. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/analyzer.py +0 -0
  22. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/compiler.py +0 -0
  23. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/environment.py +0 -0
  24. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/errors.py +0 -0
  25. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_fmt.py +0 -0
  26. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
  27. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/entry_points.txt +0 -0
  28. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_lang.egg-info/requires.txt +0 -0
  29. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/inscript_test.py +0 -0
  30. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/repl.py +0 -0
  31. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/setup.cfg +0 -0
  32. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib_extended.py +0 -0
  33. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib_extended_2.py +0 -0
  34. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/stdlib_values.py +0 -0
  35. {inscript_lang-2.5.0 → inscript_lang-2.10.0}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.5.0
3
+ Version: 2.10.0
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -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
+ )