inscript-lang 1.1.0__tar.gz → 1.7.1__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 (30) hide show
  1. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/PKG-INFO +210 -210
  2. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/analyzer.py +32 -11
  3. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/ast_nodes.py +25 -2
  4. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/compiler.py +113 -0
  5. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/errors.py +6 -0
  6. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript.py +227 -8
  7. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript_lang.egg-info/PKG-INFO +210 -210
  8. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/interpreter.py +380 -89
  9. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/lexer.py +31 -13
  10. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/parser.py +98 -31
  11. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/pyproject.toml +1 -1
  12. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/repl.py +32 -1
  13. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/setup.cfg +4 -4
  14. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/setup.py +1 -1
  15. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/stdlib.py +147 -61
  16. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/stdlib_game.py +276 -0
  17. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/README.md +0 -0
  18. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/environment.py +0 -0
  19. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript_fmt.py +0 -0
  20. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript_lang.egg-info/SOURCES.txt +0 -0
  21. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript_lang.egg-info/dependency_links.txt +0 -0
  22. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript_lang.egg-info/entry_points.txt +0 -0
  23. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript_lang.egg-info/requires.txt +0 -0
  24. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript_lang.egg-info/top_level.txt +0 -0
  25. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/inscript_test.py +0 -0
  26. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/pygame_backend.py +0 -0
  27. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/stdlib_extended.py +0 -0
  28. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/stdlib_extended_2.py +0 -0
  29. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/stdlib_values.py +0 -0
  30. {inscript_lang-1.1.0 → inscript_lang-1.7.1}/vm.py +0 -0
@@ -1,210 +1,210 @@
1
- Metadata-Version: 2.4
2
- Name: inscript-lang
3
- Version: 1.1.0
4
- Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
- Author: Shreyasi Sarkar
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/authorss81/inscript
8
- Project-URL: Repository, https://github.com/authorss81/inscript
9
- Project-URL: Bug Tracker, https://github.com/authorss81/inscript/issues
10
- Project-URL: Documentation, https://authorss81.github.io/inscript/docs/
11
- Keywords: game,scripting,language,gamedev,gdscript
12
- Classifier: Development Status :: 4 - Beta
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Topic :: Games/Entertainment
20
- Classifier: Topic :: Software Development :: Interpreters
21
- Requires-Python: >=3.10
22
- Description-Content-Type: text/markdown
23
- Provides-Extra: game
24
- Requires-Dist: pygame>=2.0; extra == "game"
25
- Provides-Extra: lsp
26
- Requires-Dist: pygls>=1.0; extra == "lsp"
27
- Provides-Extra: all
28
- Requires-Dist: pygame>=2.0; extra == "all"
29
- Requires-Dist: pygls>=1.0; extra == "all"
30
- Dynamic: requires-python
31
-
32
- # InScript v1.1.0 — First Stable Release
33
-
34
- > **839 tests passing** · **59 stdlib modules** · **Python 3.10+** · **Audit 9.5/10**
35
- > `pip install inscript-lang` · **v1.1.0 is the first production-ready release**
36
-
37
- InScript is a statically-typed scripting language for 2D games — a readable, safe alternative to GDScript with 59 built-in game modules and a complete bytecode VM.
38
-
39
- ---
40
-
41
- ## Install
42
-
43
- ```bash
44
- pip install inscript-lang # core
45
- pip install "inscript-lang[game]" # with pygame
46
- pip install "inscript-lang[all]" # pygame + LSP
47
- inscript --version # InScript 1.1.0
48
- ```
49
-
50
- ---
51
-
52
- ## Quick Start
53
-
54
- ```bash
55
- inscript --repl # interactive shell
56
- inscript game.ins # run a file
57
- inscript --watch game.ins # auto-rerun on save
58
- inscript --fmt game.ins # format code
59
- inscript --test # run test_*.ins files
60
- inscript --check game.ins # static analysis only
61
- ```
62
-
63
- ---
64
-
65
- ## Language
66
-
67
- ```inscript
68
- // Arrow functions
69
- let evens = [1,2,3,4,5].filter(fn(x) => x % 2 == 0)
70
- let names = users.map(fn(u) => u.name).sorted()
71
-
72
- // ADT enums + exhaustive pattern matching
73
- enum Shape { Circle(r: float) Rect(w: float, h: float) }
74
-
75
- fn area(s: Shape) -> float {
76
- return match s {
77
- case Shape.Circle(r) if r > 10.0 { "large" }
78
- case Shape.Circle(r) { 3.14159 * r * r }
79
- case Shape.Rect(w, h) { w * h }
80
- case 0..=5 { 0.0 }
81
- }
82
- }
83
-
84
- // Structs with priv, super, decorators
85
- struct Entity {
86
- priv id: int = 0
87
- name: string
88
- fn update(dt: float) { }
89
- }
90
- struct Player extends Entity {
91
- fn update(dt: float) {
92
- super.update(dt)
93
- self.move(dt)
94
- }
95
- }
96
-
97
- // Result type chaining
98
- fn divide(a: float, b: float) -> Result {
99
- if b == 0.0 { return Err("zero") }
100
- return Ok(a / b)
101
- }
102
- let r = divide(10.0, 2.0)
103
- .map(fn(v) => v * 3.0)
104
- .unwrap_or(0.0)
105
-
106
- // Type aliases + nullable + union
107
- type PlayerID = int
108
- fn find(id: PlayerID?) -> string|nil { ... }
109
-
110
- // Rest destructuring
111
- let [first, ...rest] = [1,2,3,4,5]
112
- print(rest) // [2, 3, 4, 5]
113
- ```
114
-
115
- ---
116
-
117
- ## Developer Tools
118
-
119
- | Tool | Command |
120
- |------|---------|
121
- | Format | `inscript --fmt file.ins` · `--fmt-check` for CI |
122
- | Watch | `inscript --watch file.ins` (auto-rerun on save) |
123
- | Test | `inscript --test` (discovers `test_*.ins`) |
124
- | REPL | `inscript --repl` · `.doc math` for live module docs |
125
- | Check | `inscript --check file.ins` (static analysis) |
126
- | LSP | `inscript --lsp` (VS Code extension available) |
127
- | Playground | [authorss81.github.io/inscript/playground.html](https://authorss81.github.io/inscript/playground.html) |
128
-
129
- ---
130
-
131
- ## 59 Game Modules
132
-
133
- ```inscript
134
- import "physics2d" as P // RigidBody, World, collision callbacks
135
- import "ecs" as E // Entity-Component-System
136
- import "pathfind" as N // A*, Dijkstra, flow fields
137
- import "tilemap" as T // load Tiled maps
138
- import "camera2d" as C // follow, shake, zoom
139
- import "particle" as FX // emitter, burst, continuous
140
- import "fsm" as SM // state machine with guards
141
- import "save" as S // save slots
142
- import "audio" as A // load/play/stop/volume
143
- import "localize" as L // multi-language strings
144
- // + 49 more: animation, ecs, net_game, image, atlas, shader, ...
145
- ```
146
-
147
- ---
148
-
149
- ## Testing Your Code
150
-
151
- ```inscript
152
- // test_game.ins
153
- test "player moves right" {
154
- let p = Player{x: 0.0, y: 0.0}
155
- p.move(1.0, 0.0)
156
- assert(p.x == 5.0, "moved right")
157
- }
158
-
159
- test "score increases" {
160
- let g = Game{}
161
- g.add_score(100)
162
- assert(g.score == 100, "score added")
163
- }
164
- ```
165
-
166
- ```bash
167
- inscript --test # ✅ 2/2 tests passed 12ms
168
- inscript --test --verbose # shows each test name
169
- ```
170
-
171
- ---
172
-
173
- ## GitHub Actions (auto-publish)
174
-
175
- The `.github/workflows/publish.yml` workflow automatically publishes to PyPI when you push a version tag:
176
-
177
- ```bash
178
- git tag -a v1.2.0 -m "InScript v1.2.0"
179
- git push origin v1.2.0
180
- # → tests run, then auto-uploads to PyPI
181
- ```
182
-
183
- Setup: add `PYPI_API_TOKEN` to GitHub repo secrets.
184
-
185
- ---
186
-
187
- ## Upgrading
188
-
189
- ```bash
190
- pip install --upgrade inscript-lang # from v1.0.x
191
- ```
192
-
193
- All v1.0.x syntax is fully backward compatible. No changes needed.
194
-
195
- ---
196
-
197
- ## Roadmap
198
-
199
- | Version | Focus |
200
- |---------|-------|
201
- | **v1.1.0** ← you are here | First stable — all tooling complete |
202
- | v1.2.0 | Type safety — generic enforcement, type narrowing |
203
- | v1.3.0 | Performance — C extension (5-15× speedup) |
204
- | v2.0.0 | Ecosystem — package registry, Studio IDE, WASM |
205
-
206
- [ROADMAP.md](ROADMAP.md) · [Audit (9.5/10)](InScript_Language_Audit.md) · [Docs](https://authorss81.github.io/inscript/docs/)
207
-
208
- ---
209
-
210
- MIT License · [GitHub](https://github.com/authorss81/inscript) · [PyPI](https://pypi.org/project/inscript-lang/)
1
+ Metadata-Version: 2.4
2
+ Name: inscript-lang
3
+ Version: 1.7.1
4
+ Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
+ Author: Shreyasi Sarkar
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/authorss81/inscript
8
+ Project-URL: Repository, https://github.com/authorss81/inscript
9
+ Project-URL: Bug Tracker, https://github.com/authorss81/inscript/issues
10
+ Project-URL: Documentation, https://authorss81.github.io/inscript/docs/
11
+ Keywords: game,scripting,language,gamedev,gdscript
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Games/Entertainment
20
+ Classifier: Topic :: Software Development :: Interpreters
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: game
24
+ Requires-Dist: pygame>=2.0; extra == "game"
25
+ Provides-Extra: lsp
26
+ Requires-Dist: pygls>=1.0; extra == "lsp"
27
+ Provides-Extra: all
28
+ Requires-Dist: pygame>=2.0; extra == "all"
29
+ Requires-Dist: pygls>=1.0; extra == "all"
30
+ Dynamic: requires-python
31
+
32
+ # InScript v1.1.0 — First Stable Release
33
+
34
+ > **839 tests passing** · **59 stdlib modules** · **Python 3.10+** · **Audit 9.5/10**
35
+ > `pip install inscript-lang` · **v1.1.0 is the first production-ready release**
36
+
37
+ InScript is a statically-typed scripting language for 2D games — a readable, safe alternative to GDScript with 59 built-in game modules and a complete bytecode VM.
38
+
39
+ ---
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install inscript-lang # core
45
+ pip install "inscript-lang[game]" # with pygame
46
+ pip install "inscript-lang[all]" # pygame + LSP
47
+ inscript --version # InScript 1.1.0
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Quick Start
53
+
54
+ ```bash
55
+ inscript --repl # interactive shell
56
+ inscript game.ins # run a file
57
+ inscript --watch game.ins # auto-rerun on save
58
+ inscript --fmt game.ins # format code
59
+ inscript --test # run test_*.ins files
60
+ inscript --check game.ins # static analysis only
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Language
66
+
67
+ ```inscript
68
+ // Arrow functions
69
+ let evens = [1,2,3,4,5].filter(fn(x) => x % 2 == 0)
70
+ let names = users.map(fn(u) => u.name).sorted()
71
+
72
+ // ADT enums + exhaustive pattern matching
73
+ enum Shape { Circle(r: float) Rect(w: float, h: float) }
74
+
75
+ fn area(s: Shape) -> float {
76
+ return match s {
77
+ case Shape.Circle(r) if r > 10.0 { "large" }
78
+ case Shape.Circle(r) { 3.14159 * r * r }
79
+ case Shape.Rect(w, h) { w * h }
80
+ case 0..=5 { 0.0 }
81
+ }
82
+ }
83
+
84
+ // Structs with priv, super, decorators
85
+ struct Entity {
86
+ priv id: int = 0
87
+ name: string
88
+ fn update(dt: float) { }
89
+ }
90
+ struct Player extends Entity {
91
+ fn update(dt: float) {
92
+ super.update(dt)
93
+ self.move(dt)
94
+ }
95
+ }
96
+
97
+ // Result type chaining
98
+ fn divide(a: float, b: float) -> Result {
99
+ if b == 0.0 { return Err("zero") }
100
+ return Ok(a / b)
101
+ }
102
+ let r = divide(10.0, 2.0)
103
+ .map(fn(v) => v * 3.0)
104
+ .unwrap_or(0.0)
105
+
106
+ // Type aliases + nullable + union
107
+ type PlayerID = int
108
+ fn find(id: PlayerID?) -> string|nil { ... }
109
+
110
+ // Rest destructuring
111
+ let [first, ...rest] = [1,2,3,4,5]
112
+ print(rest) // [2, 3, 4, 5]
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Developer Tools
118
+
119
+ | Tool | Command |
120
+ |------|---------|
121
+ | Format | `inscript --fmt file.ins` · `--fmt-check` for CI |
122
+ | Watch | `inscript --watch file.ins` (auto-rerun on save) |
123
+ | Test | `inscript --test` (discovers `test_*.ins`) |
124
+ | REPL | `inscript --repl` · `.doc math` for live module docs |
125
+ | Check | `inscript --check file.ins` (static analysis) |
126
+ | LSP | `inscript --lsp` (VS Code extension available) |
127
+ | Playground | [authorss81.github.io/inscript/playground.html](https://authorss81.github.io/inscript/playground.html) |
128
+
129
+ ---
130
+
131
+ ## 59 Game Modules
132
+
133
+ ```inscript
134
+ import "physics2d" as P // RigidBody, World, collision callbacks
135
+ import "ecs" as E // Entity-Component-System
136
+ import "pathfind" as N // A*, Dijkstra, flow fields
137
+ import "tilemap" as T // load Tiled maps
138
+ import "camera2d" as C // follow, shake, zoom
139
+ import "particle" as FX // emitter, burst, continuous
140
+ import "fsm" as SM // state machine with guards
141
+ import "save" as S // save slots
142
+ import "audio" as A // load/play/stop/volume
143
+ import "localize" as L // multi-language strings
144
+ // + 49 more: animation, ecs, net_game, image, atlas, shader, ...
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Testing Your Code
150
+
151
+ ```inscript
152
+ // test_game.ins
153
+ test "player moves right" {
154
+ let p = Player{x: 0.0, y: 0.0}
155
+ p.move(1.0, 0.0)
156
+ assert(p.x == 5.0, "moved right")
157
+ }
158
+
159
+ test "score increases" {
160
+ let g = Game{}
161
+ g.add_score(100)
162
+ assert(g.score == 100, "score added")
163
+ }
164
+ ```
165
+
166
+ ```bash
167
+ inscript --test # ✅ 2/2 tests passed 12ms
168
+ inscript --test --verbose # shows each test name
169
+ ```
170
+
171
+ ---
172
+
173
+ ## GitHub Actions (auto-publish)
174
+
175
+ The `.github/workflows/publish.yml` workflow automatically publishes to PyPI when you push a version tag:
176
+
177
+ ```bash
178
+ git tag -a v1.2.0 -m "InScript v1.2.0"
179
+ git push origin v1.2.0
180
+ # → tests run, then auto-uploads to PyPI
181
+ ```
182
+
183
+ Setup: add `PYPI_API_TOKEN` to GitHub repo secrets.
184
+
185
+ ---
186
+
187
+ ## Upgrading
188
+
189
+ ```bash
190
+ pip install --upgrade inscript-lang # from v1.0.x
191
+ ```
192
+
193
+ All v1.0.x syntax is fully backward compatible. No changes needed.
194
+
195
+ ---
196
+
197
+ ## Roadmap
198
+
199
+ | Version | Focus |
200
+ |---------|-------|
201
+ | **v1.1.0** ← you are here | First stable — all tooling complete |
202
+ | v1.2.0 | Type safety — generic enforcement, type narrowing |
203
+ | v1.3.0 | Performance — C extension (5-15× speedup) |
204
+ | v2.0.0 | Ecosystem — package registry, Studio IDE, WASM |
205
+
206
+ [ROADMAP.md](ROADMAP.md) · [Audit (9.5/10)](InScript_Language_Audit.md) · [Docs](https://authorss81.github.io/inscript/docs/)
207
+
208
+ ---
209
+
210
+ MIT License · [GitHub](https://github.com/authorss81/inscript) · [PyPI](https://pypi.org/project/inscript-lang/)
@@ -133,6 +133,7 @@ class Symbol:
133
133
  self.struct_node = struct_node # for structs: the AST node
134
134
  self.line = line
135
135
  self.col = col
136
+ self.used = False # v1.2.0: unused-variable tracking
136
137
 
137
138
 
138
139
  # ─────────────────────────────────────────────────────────────────────────────
@@ -180,7 +181,8 @@ class Analyzer(Visitor):
180
181
  def __init__(self, source_lines: List[str] = None,
181
182
  multi_error: bool = True,
182
183
  warn_as_error: bool = False,
183
- no_warn: bool = False):
184
+ no_warn: bool = False,
185
+ no_warn_unused: bool = False):
184
186
  self._src = source_lines or []
185
187
  self._scope = Scope(kind="global")
186
188
  self._errors: List[SemanticError] = [] # collected (multi-error)
@@ -188,6 +190,8 @@ class Analyzer(Visitor):
188
190
  self._multi_error = multi_error
189
191
  self._warn_as_error = warn_as_error
190
192
  self._no_warn = no_warn
193
+ self._no_warn_unused = no_warn_unused
194
+ self._dispatch: dict = {} # v1.3.0: cached dispatch
191
195
 
192
196
  # State for context-sensitive checks
193
197
  self._current_fn_return_type: Optional[InScriptType] = None
@@ -261,6 +265,7 @@ class Analyzer(Visitor):
261
265
  candidates = list(self._all_visible_names())
262
266
  self._error(f"Undefined name: '{name}'", line, col, candidates)
263
267
  return Symbol(name, T_ANY, kind="var") # dummy to continue analysis
268
+ sym.used = True # v1.2.0: mark as used
264
269
  return sym
265
270
 
266
271
  def _all_visible_names(self) -> List[str]:
@@ -307,6 +312,19 @@ class Analyzer(Visitor):
307
312
 
308
313
  def _pop_scope(self) -> Scope:
309
314
  old = self._scope
315
+ # v1.2.0: warn about unused locals in fn/block scopes
316
+ if (not self._no_warn and not self._no_warn_unused
317
+ and old.kind in ("fn", "block", "match_arm")):
318
+ for sym in old.symbols.values():
319
+ if (sym.kind in ("var", "const")
320
+ and not sym.used
321
+ and not sym.name.startswith("_")):
322
+ self._warn(
323
+ "unused",
324
+ f"Variable '{sym.name}' is defined but never used "
325
+ f"(prefix with '_' to suppress)",
326
+ sym.line,
327
+ )
310
328
  self._scope = self._scope.parent
311
329
  return old
312
330
 
@@ -523,13 +541,15 @@ class Analyzer(Visitor):
523
541
  prev_ret = self._current_fn_return_type
524
542
  self._current_fn_return_type = ret_type
525
543
 
526
- # Register parameters
544
+ # Register parameters — mark used=True so we never warn about unused params
527
545
  for param in node.params:
528
546
  p_type = self._resolve_type_ann(param.type_ann)
529
- self._define(Symbol(
547
+ sym = Symbol(
530
548
  param.name, p_type, kind="var",
531
549
  line=param.line, col=param.col
532
- ))
550
+ )
551
+ sym.used = True # params are always "used" by the caller
552
+ self._define(sym)
533
553
 
534
554
  # Analyze body
535
555
  self.visit(node.body)
@@ -657,17 +677,18 @@ class Analyzer(Visitor):
657
677
 
658
678
  def visit_BlockStmt(self, node: BlockStmt) -> InScriptType:
659
679
  self._push_scope("block")
660
- returned = False
680
+ terminated = False # True after return/break/continue/throw
661
681
  for stmt in node.body:
662
- if returned:
663
- # 3.6: warn about unreachable code after return/break/continue
682
+ if terminated and not self._no_warn:
664
683
  line = getattr(stmt, "line", 0)
665
684
  self._warn("unreachable",
666
- "Unreachable code after return/break/continue", line)
667
- break
685
+ "Unreachable code after return/break/continue/throw",
686
+ line)
687
+ # Still visit so inner undefined-name errors are caught
668
688
  self.visit(stmt)
669
- if isinstance(stmt, (ReturnStmt, BreakStmt, ContinueStmt)):
670
- returned = True
689
+ from ast_nodes import ThrowStmt as ThrowStmt_
690
+ if isinstance(stmt, (ReturnStmt, BreakStmt, ContinueStmt, ThrowStmt_)):
691
+ terminated = True
671
692
  self._pop_scope()
672
693
  return T_VOID
673
694
 
@@ -344,6 +344,12 @@ class MatchArm(Node):
344
344
  guard: Optional[Node] = None # if condition: case x if x > 0 { ... }
345
345
  binding: Optional[str] = None # case h if h <= 0 { ... } — 'h' bound to subject
346
346
 
347
+ @dataclass
348
+ class TypePattern(Node):
349
+ """v1.4.0: type-narrowing match pattern — case int x { } or case Vec2 v { }"""
350
+ type_name: str # "int", "float", "string", "bool", or struct name
351
+ var_name: str # variable bound to the narrowed value
352
+
347
353
  @dataclass
348
354
  class MatchStmt(Node):
349
355
  subject: Node
@@ -429,8 +435,14 @@ class Visitor:
429
435
  """
430
436
 
431
437
  def visit(self, node: Node) -> Any:
432
- method = getattr(self, f"visit_{type(node).__name__}", self.generic_visit)
433
- return method(node)
438
+ # v1.3.0: cache dispatch table per visitor class to avoid getattr on every call
439
+ cls = type(node)
440
+ try:
441
+ return self._dispatch[cls](node)
442
+ except KeyError:
443
+ method = getattr(self, f"visit_{cls.__name__}", self.generic_visit)
444
+ self._dispatch[cls] = method
445
+ return method(node)
434
446
 
435
447
  def generic_visit(self, node: Node) -> Any:
436
448
  raise NotImplementedError(
@@ -447,6 +459,17 @@ class ThrowStmt(Node):
447
459
  """throw ErrorExpr"""
448
460
  value: Node
449
461
 
462
+ @dataclass
463
+ class DeferStmt(Node):
464
+ """v1.4.0: defer expr_or_call — runs at end of enclosing function/block"""
465
+ expr: Node
466
+
467
+ @dataclass
468
+ class RepeatUntilStmt(Node):
469
+ """v1.4.0: repeat { body } until condition — do-while equivalent"""
470
+ body: "BlockStmt"
471
+ condition: Node
472
+
450
473
  @dataclass
451
474
  class TryCatchStmt(Node):
452
475
  """try { body } catch(e: T) { ... } catch e { ... } — multiple catch clauses supported"""