inscript-lang 1.0.23__tar.gz → 1.6.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-1.6.0/PKG-INFO +210 -0
- inscript_lang-1.6.0/README.md +179 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/analyzer.py +32 -11
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/ast_nodes.py +25 -2
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/compiler.py +113 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/errors.py +6 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/inscript.py +178 -8
- inscript_lang-1.6.0/inscript_lang.egg-info/PKG-INFO +210 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/interpreter.py +331 -78
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/lexer.py +6 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/parser.py +85 -24
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/pyproject.toml +1 -1
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/repl.py +32 -1
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/setup.cfg +4 -4
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/setup.py +1 -1
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/stdlib.py +147 -61
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/stdlib_game.py +276 -0
- inscript_lang-1.0.23/PKG-INFO +0 -166
- inscript_lang-1.0.23/README.md +0 -135
- inscript_lang-1.0.23/inscript_lang.egg-info/PKG-INFO +0 -166
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/environment.py +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/inscript_fmt.py +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/inscript_test.py +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/pygame_backend.py +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/stdlib_extended.py +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/stdlib_values.py +0 -0
- {inscript_lang-1.0.23 → inscript_lang-1.6.0}/vm.py +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inscript-lang
|
|
3
|
+
Version: 1.6.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/)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# InScript v1.1.0 — First Stable Release
|
|
2
|
+
|
|
3
|
+
> **839 tests passing** · **59 stdlib modules** · **Python 3.10+** · **Audit 9.5/10**
|
|
4
|
+
> `pip install inscript-lang` · **v1.1.0 is the first production-ready release**
|
|
5
|
+
|
|
6
|
+
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.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install inscript-lang # core
|
|
14
|
+
pip install "inscript-lang[game]" # with pygame
|
|
15
|
+
pip install "inscript-lang[all]" # pygame + LSP
|
|
16
|
+
inscript --version # InScript 1.1.0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
inscript --repl # interactive shell
|
|
25
|
+
inscript game.ins # run a file
|
|
26
|
+
inscript --watch game.ins # auto-rerun on save
|
|
27
|
+
inscript --fmt game.ins # format code
|
|
28
|
+
inscript --test # run test_*.ins files
|
|
29
|
+
inscript --check game.ins # static analysis only
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Language
|
|
35
|
+
|
|
36
|
+
```inscript
|
|
37
|
+
// Arrow functions
|
|
38
|
+
let evens = [1,2,3,4,5].filter(fn(x) => x % 2 == 0)
|
|
39
|
+
let names = users.map(fn(u) => u.name).sorted()
|
|
40
|
+
|
|
41
|
+
// ADT enums + exhaustive pattern matching
|
|
42
|
+
enum Shape { Circle(r: float) Rect(w: float, h: float) }
|
|
43
|
+
|
|
44
|
+
fn area(s: Shape) -> float {
|
|
45
|
+
return match s {
|
|
46
|
+
case Shape.Circle(r) if r > 10.0 { "large" }
|
|
47
|
+
case Shape.Circle(r) { 3.14159 * r * r }
|
|
48
|
+
case Shape.Rect(w, h) { w * h }
|
|
49
|
+
case 0..=5 { 0.0 }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Structs with priv, super, decorators
|
|
54
|
+
struct Entity {
|
|
55
|
+
priv id: int = 0
|
|
56
|
+
name: string
|
|
57
|
+
fn update(dt: float) { }
|
|
58
|
+
}
|
|
59
|
+
struct Player extends Entity {
|
|
60
|
+
fn update(dt: float) {
|
|
61
|
+
super.update(dt)
|
|
62
|
+
self.move(dt)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Result type chaining
|
|
67
|
+
fn divide(a: float, b: float) -> Result {
|
|
68
|
+
if b == 0.0 { return Err("zero") }
|
|
69
|
+
return Ok(a / b)
|
|
70
|
+
}
|
|
71
|
+
let r = divide(10.0, 2.0)
|
|
72
|
+
.map(fn(v) => v * 3.0)
|
|
73
|
+
.unwrap_or(0.0)
|
|
74
|
+
|
|
75
|
+
// Type aliases + nullable + union
|
|
76
|
+
type PlayerID = int
|
|
77
|
+
fn find(id: PlayerID?) -> string|nil { ... }
|
|
78
|
+
|
|
79
|
+
// Rest destructuring
|
|
80
|
+
let [first, ...rest] = [1,2,3,4,5]
|
|
81
|
+
print(rest) // [2, 3, 4, 5]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Developer Tools
|
|
87
|
+
|
|
88
|
+
| Tool | Command |
|
|
89
|
+
|------|---------|
|
|
90
|
+
| Format | `inscript --fmt file.ins` · `--fmt-check` for CI |
|
|
91
|
+
| Watch | `inscript --watch file.ins` (auto-rerun on save) |
|
|
92
|
+
| Test | `inscript --test` (discovers `test_*.ins`) |
|
|
93
|
+
| REPL | `inscript --repl` · `.doc math` for live module docs |
|
|
94
|
+
| Check | `inscript --check file.ins` (static analysis) |
|
|
95
|
+
| LSP | `inscript --lsp` (VS Code extension available) |
|
|
96
|
+
| Playground | [authorss81.github.io/inscript/playground.html](https://authorss81.github.io/inscript/playground.html) |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 59 Game Modules
|
|
101
|
+
|
|
102
|
+
```inscript
|
|
103
|
+
import "physics2d" as P // RigidBody, World, collision callbacks
|
|
104
|
+
import "ecs" as E // Entity-Component-System
|
|
105
|
+
import "pathfind" as N // A*, Dijkstra, flow fields
|
|
106
|
+
import "tilemap" as T // load Tiled maps
|
|
107
|
+
import "camera2d" as C // follow, shake, zoom
|
|
108
|
+
import "particle" as FX // emitter, burst, continuous
|
|
109
|
+
import "fsm" as SM // state machine with guards
|
|
110
|
+
import "save" as S // save slots
|
|
111
|
+
import "audio" as A // load/play/stop/volume
|
|
112
|
+
import "localize" as L // multi-language strings
|
|
113
|
+
// + 49 more: animation, ecs, net_game, image, atlas, shader, ...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Testing Your Code
|
|
119
|
+
|
|
120
|
+
```inscript
|
|
121
|
+
// test_game.ins
|
|
122
|
+
test "player moves right" {
|
|
123
|
+
let p = Player{x: 0.0, y: 0.0}
|
|
124
|
+
p.move(1.0, 0.0)
|
|
125
|
+
assert(p.x == 5.0, "moved right")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
test "score increases" {
|
|
129
|
+
let g = Game{}
|
|
130
|
+
g.add_score(100)
|
|
131
|
+
assert(g.score == 100, "score added")
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
inscript --test # ✅ 2/2 tests passed 12ms
|
|
137
|
+
inscript --test --verbose # shows each test name
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## GitHub Actions (auto-publish)
|
|
143
|
+
|
|
144
|
+
The `.github/workflows/publish.yml` workflow automatically publishes to PyPI when you push a version tag:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
git tag -a v1.2.0 -m "InScript v1.2.0"
|
|
148
|
+
git push origin v1.2.0
|
|
149
|
+
# → tests run, then auto-uploads to PyPI
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Setup: add `PYPI_API_TOKEN` to GitHub repo secrets.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Upgrading
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pip install --upgrade inscript-lang # from v1.0.x
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
All v1.0.x syntax is fully backward compatible. No changes needed.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Roadmap
|
|
167
|
+
|
|
168
|
+
| Version | Focus |
|
|
169
|
+
|---------|-------|
|
|
170
|
+
| **v1.1.0** ← you are here | First stable — all tooling complete |
|
|
171
|
+
| v1.2.0 | Type safety — generic enforcement, type narrowing |
|
|
172
|
+
| v1.3.0 | Performance — C extension (5-15× speedup) |
|
|
173
|
+
| v2.0.0 | Ecosystem — package registry, Studio IDE, WASM |
|
|
174
|
+
|
|
175
|
+
[ROADMAP.md](ROADMAP.md) · [Audit (9.5/10)](InScript_Language_Audit.md) · [Docs](https://authorss81.github.io/inscript/docs/)
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
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
|
-
|
|
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
|
-
|
|
680
|
+
terminated = False # True after return/break/continue/throw
|
|
661
681
|
for stmt in node.body:
|
|
662
|
-
if
|
|
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",
|
|
667
|
-
|
|
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
|
-
|
|
670
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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"""
|
|
@@ -217,12 +217,113 @@ class Compiler:
|
|
|
217
217
|
def _patch_a(self, idx): self._scope.proto.code[idx].a = len(self._scope.proto.code)-idx-1
|
|
218
218
|
def _patch_c(self, idx, val): self._scope.proto.code[idx].c = val
|
|
219
219
|
|
|
220
|
+
# v1.3.0: constant folding helpers ─────────────────────────────────────────
|
|
221
|
+
def _try_fold_literal(self, node):
|
|
222
|
+
"""Return (node_type, python_value) if node is a compile-time constant, else None."""
|
|
223
|
+
from ast_nodes import (IntLiteralExpr, FloatLiteralExpr, StringLiteralExpr,
|
|
224
|
+
BoolLiteralExpr, NullLiteralExpr, UnaryExpr)
|
|
225
|
+
if isinstance(node, IntLiteralExpr): return ('int', node.value)
|
|
226
|
+
if isinstance(node, FloatLiteralExpr): return ('float', node.value)
|
|
227
|
+
if isinstance(node, StringLiteralExpr):return ('str', node.value)
|
|
228
|
+
if isinstance(node, BoolLiteralExpr): return ('bool', node.value)
|
|
229
|
+
if isinstance(node, NullLiteralExpr): return ('nil', None)
|
|
230
|
+
# Fold unary minus on literals: -5 → -5
|
|
231
|
+
if isinstance(node, UnaryExpr) and node.op == '-':
|
|
232
|
+
inner = self._try_fold_literal(node.operand)
|
|
233
|
+
if inner and inner[0] in ('int', 'float'):
|
|
234
|
+
return (inner[0], -inner[1])
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def _fold_op(self, left, op, right):
|
|
238
|
+
"""Fold a binary operation on Python literals. Returns result or None if unsafe."""
|
|
239
|
+
try:
|
|
240
|
+
if op == '+':
|
|
241
|
+
if isinstance(left, str) or isinstance(right, str):
|
|
242
|
+
return str(left) + str(right)
|
|
243
|
+
return left + right
|
|
244
|
+
if op == '-': return left - right
|
|
245
|
+
if op == '*': return left * right
|
|
246
|
+
if op == '/':
|
|
247
|
+
if right == 0: return None # keep runtime div-by-zero error
|
|
248
|
+
return left / right
|
|
249
|
+
if op == '//':
|
|
250
|
+
if right == 0: return None
|
|
251
|
+
return int(left // right)
|
|
252
|
+
if op == '%':
|
|
253
|
+
if right == 0: return None
|
|
254
|
+
return left % right
|
|
255
|
+
if op == '**':
|
|
256
|
+
# guard against huge exponents
|
|
257
|
+
if isinstance(right, (int, float)) and right > 64: return None
|
|
258
|
+
return left ** right
|
|
259
|
+
if op == '==': return left == right
|
|
260
|
+
if op == '!=': return left != right
|
|
261
|
+
if op == '<': return left < right
|
|
262
|
+
if op == '>': return left > right
|
|
263
|
+
if op == '<=': return left <= right
|
|
264
|
+
if op == '>=': return left >= right
|
|
265
|
+
if op == '++':
|
|
266
|
+
return str(left) + str(right)
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
return None
|
|
270
|
+
|
|
220
271
|
# ── compile entry ─────────────────────────────────────────────────────────
|
|
221
272
|
def compile(self, prog):
|
|
222
273
|
for node in prog.body: self._stmt(node)
|
|
223
274
|
self._e(Op.RETURN, NIL_REG)
|
|
275
|
+
self._eliminate_dead_code(self._proto)
|
|
224
276
|
return self._proto
|
|
225
277
|
|
|
278
|
+
def _eliminate_dead_code(self, proto):
|
|
279
|
+
"""v1.3.0: remove unreachable instructions after unconditional JUMP/RETURN."""
|
|
280
|
+
code = proto.code
|
|
281
|
+
reachable = set()
|
|
282
|
+
worklist = [0]
|
|
283
|
+
while worklist:
|
|
284
|
+
pc = worklist.pop()
|
|
285
|
+
if pc < 0 or pc >= len(code) or pc in reachable:
|
|
286
|
+
continue
|
|
287
|
+
reachable.add(pc)
|
|
288
|
+
ins = code[pc]
|
|
289
|
+
|
|
290
|
+
if ins.op == Op.RETURN:
|
|
291
|
+
continue # terminal
|
|
292
|
+
|
|
293
|
+
if ins.op == Op.JUMP:
|
|
294
|
+
target = pc + 1 + ins.b
|
|
295
|
+
worklist.append(target)
|
|
296
|
+
continue # unconditional — don't fall through
|
|
297
|
+
|
|
298
|
+
if ins.op in (Op.JUMP_IF_FALSE, Op.JUMP_IF_TRUE, Op.JUMP_IF_NIL):
|
|
299
|
+
worklist.append(pc + 1) # fall-through
|
|
300
|
+
worklist.append(pc + 1 + ins.b) # branch target
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# PUSH_HANDLER: exception can jump to (pc + 1 + a) at any time
|
|
304
|
+
if ins.op == Op.PUSH_HANDLER:
|
|
305
|
+
worklist.append(pc + 1) # normal path continues
|
|
306
|
+
worklist.append(pc + 1 + ins.a) # catch handler target
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# ITER_NEXT: on exhaustion jumps forward by c; otherwise falls through
|
|
310
|
+
if ins.op == Op.ITER_NEXT:
|
|
311
|
+
worklist.append(pc + 1) # loop body
|
|
312
|
+
worklist.append(pc + 1 + ins.c) # past the loop
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
worklist.append(pc + 1) # normal fall-through
|
|
316
|
+
|
|
317
|
+
# Replace unreachable instructions with NOP (MOVE r0,r0)
|
|
318
|
+
for i, ins in enumerate(code):
|
|
319
|
+
if i not in reachable:
|
|
320
|
+
ins.op = Op.MOVE; ins.a = 0; ins.b = 0; ins.c = 0
|
|
321
|
+
|
|
322
|
+
# Recurse into nested function protos
|
|
323
|
+
for c in proto.consts:
|
|
324
|
+
if hasattr(c, 'code'): # it's a FnProto
|
|
325
|
+
self._eliminate_dead_code(c)
|
|
326
|
+
|
|
226
327
|
# ── statements ────────────────────────────────────────────────────────────
|
|
227
328
|
def _stmt(self, node):
|
|
228
329
|
if node is None: return
|
|
@@ -902,6 +1003,18 @@ class Compiler:
|
|
|
902
1003
|
# ── binary ────────────────────────────────────────────────────────────────
|
|
903
1004
|
def _binary(self, node):
|
|
904
1005
|
op = node.op; dst = self._alloc()
|
|
1006
|
+
|
|
1007
|
+
# v1.3.0: constant folding — evaluate at compile time if both operands are literals
|
|
1008
|
+
if op not in ('&&', '||') and not op.endswith('='):
|
|
1009
|
+
lv_node, rv_node = node.left, node.right
|
|
1010
|
+
lv_lit = self._try_fold_literal(lv_node)
|
|
1011
|
+
rv_lit = self._try_fold_literal(rv_node)
|
|
1012
|
+
if lv_lit is not None and rv_lit is not None:
|
|
1013
|
+
folded = self._fold_op(lv_lit[1], op, rv_lit[1])
|
|
1014
|
+
if folded is not None:
|
|
1015
|
+
self._load_lit(folded, dst)
|
|
1016
|
+
return dst
|
|
1017
|
+
|
|
905
1018
|
if op == '&&':
|
|
906
1019
|
l = self._expr(node.left); self._e(Op.MOVE, dst, l); self._free(l)
|
|
907
1020
|
jf = self._e(Op.JUMP_IF_FALSE, dst, 0)
|
|
@@ -349,3 +349,9 @@ class YieldSignal(Exception):
|
|
|
349
349
|
class PropagateSignal(Exception):
|
|
350
350
|
def __init__(self, err_val):
|
|
351
351
|
self.err_val = err_val
|
|
352
|
+
|
|
353
|
+
class TailCallSignal(Exception):
|
|
354
|
+
"""v1.3.0: raised by a self-recursive tail call to trampoline in _call_function."""
|
|
355
|
+
__slots__ = ("arg_vals",)
|
|
356
|
+
def __init__(self, arg_vals):
|
|
357
|
+
self.arg_vals = arg_vals
|