inscript-lang 1.7.1__tar.gz → 1.7.4__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.7.1 → inscript_lang-1.7.4}/PKG-INFO +1 -1
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/environment.py +7 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/errors.py +39 -17
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript.py +1 -1
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/interpreter.py +28 -9
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/parser.py +7 -2
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/pyproject.toml +1 -1
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/repl.py +14 -1
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/setup.py +1 -1
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/README.md +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/analyzer.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/ast_nodes.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/compiler.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript_fmt.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/inscript_test.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/lexer.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/pygame_backend.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/setup.cfg +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/stdlib.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/stdlib_extended.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/stdlib_game.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/stdlib_values.py +0 -0
- {inscript_lang-1.7.1 → inscript_lang-1.7.4}/vm.py +0 -0
|
@@ -16,9 +16,16 @@ class Environment:
|
|
|
16
16
|
self.name: str = name
|
|
17
17
|
self._store: Dict[str, Any] = {}
|
|
18
18
|
self._consts: set = set() # names that cannot be reassigned
|
|
19
|
+
# v1.7.4: when True, `let` re-declarations silently re-bind (REPL mode)
|
|
20
|
+
self._repl_mode: bool = False
|
|
19
21
|
|
|
20
22
|
# ── Define ────────────────────────────────────────────────────────────────
|
|
21
23
|
def define(self, name: str, value: Any, is_const: bool = False) -> None:
|
|
24
|
+
# v1.7.4: in REPL mode, re-declaring an existing non-const `let` just
|
|
25
|
+
# re-binds it. Attempting to re-declare a const is still an error.
|
|
26
|
+
if self._repl_mode and name in self._store and name in self._consts:
|
|
27
|
+
raise InScriptRuntimeError(
|
|
28
|
+
f"Cannot re-declare constant '{name}' in REPL", 0)
|
|
22
29
|
self._store[name] = value
|
|
23
30
|
if is_const:
|
|
24
31
|
self._consts.add(name)
|
|
@@ -53,6 +53,9 @@ ERROR_CODES = {
|
|
|
53
53
|
"MatchError": "E0047",
|
|
54
54
|
"PropertyError": "E0048",
|
|
55
55
|
"NilAccess": "E0049",
|
|
56
|
+
|
|
57
|
+
# Deprecated-keyword hard errors (v1.7.4)
|
|
58
|
+
"NullKeyword": "E0055",
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
DOCS_BASE = "https://docs.inscript.dev/errors"
|
|
@@ -103,11 +106,15 @@ class InScriptError(Exception):
|
|
|
103
106
|
if self.hint:
|
|
104
107
|
parts.append(f" Hint: {self.hint}")
|
|
105
108
|
|
|
106
|
-
# Call trace (runtime only)
|
|
109
|
+
# Call trace (runtime only) — v1.7.3: entries are (fn, file, line, src) 4-tuples
|
|
107
110
|
if self.call_trace:
|
|
108
111
|
parts.append("\nCall stack (most recent last):")
|
|
109
|
-
for
|
|
112
|
+
for entry in self.call_trace:
|
|
113
|
+
fn, file, ln = entry[0], entry[1], entry[2]
|
|
114
|
+
src = entry[3] if len(entry) > 3 else ""
|
|
110
115
|
parts.append(f" File \"{file}\", line {ln}, in {fn}")
|
|
116
|
+
if src:
|
|
117
|
+
parts.append(f" {src}")
|
|
111
118
|
|
|
112
119
|
# Docs link
|
|
113
120
|
parts.append(f" See: {DOCS_BASE}/{code}")
|
|
@@ -222,11 +229,12 @@ class InScriptWarning:
|
|
|
222
229
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
223
230
|
|
|
224
231
|
class CallFrame:
|
|
225
|
-
__slots__ = ("fn_name", "file", "line")
|
|
226
|
-
def __init__(self, fn_name: str, file: str, line: int):
|
|
227
|
-
self.fn_name
|
|
228
|
-
self.file
|
|
229
|
-
self.line
|
|
232
|
+
__slots__ = ("fn_name", "file", "line", "source_line")
|
|
233
|
+
def __init__(self, fn_name: str, file: str, line: int, source_line: str = ""):
|
|
234
|
+
self.fn_name = fn_name
|
|
235
|
+
self.file = file
|
|
236
|
+
self.line = line
|
|
237
|
+
self.source_line = source_line # v1.7.3: source snippet at this frame
|
|
230
238
|
|
|
231
239
|
def as_tuple(self) -> Tuple[str, str, int]:
|
|
232
240
|
return (self.fn_name, self.file, self.line)
|
|
@@ -239,40 +247,54 @@ class InScriptCallStack:
|
|
|
239
247
|
"""
|
|
240
248
|
MAX_FRAMES = 200
|
|
241
249
|
|
|
242
|
-
def __init__(self, filename: str = "<script>"):
|
|
250
|
+
def __init__(self, filename: str = "<script>", src_lines: list = None):
|
|
243
251
|
self._frames: List[CallFrame] = []
|
|
244
252
|
self._file = filename
|
|
253
|
+
self._src = src_lines or [] # v1.7.3: source lines for per-frame snippets
|
|
254
|
+
|
|
255
|
+
def _lookup_src(self, line: int) -> str:
|
|
256
|
+
"""Return stripped source line, or '' if unavailable."""
|
|
257
|
+
if self._src and 0 < line <= len(self._src):
|
|
258
|
+
return self._src[line - 1].strip()
|
|
259
|
+
return ""
|
|
245
260
|
|
|
246
261
|
def push(self, fn_name: str, line: int):
|
|
247
262
|
if len(self._frames) < self.MAX_FRAMES:
|
|
248
|
-
self._frames.append(
|
|
263
|
+
self._frames.append(
|
|
264
|
+
CallFrame(fn_name, self._file, line, self._lookup_src(line))
|
|
265
|
+
)
|
|
249
266
|
|
|
250
267
|
def pop(self):
|
|
251
268
|
if self._frames:
|
|
252
269
|
self._frames.pop()
|
|
253
270
|
|
|
254
271
|
def snapshot(self) -> List[Tuple[str, str, int]]:
|
|
255
|
-
"""Return a copy of current frames as
|
|
256
|
-
|
|
272
|
+
"""Return a copy of current frames as (fn, file, line, source_line) 4-tuples.
|
|
273
|
+
Backwards-compatible: consumers that only unpack 3 elements still work."""
|
|
274
|
+
return [(f.fn_name, f.file, f.line, f.source_line) for f in self._frames]
|
|
257
275
|
|
|
258
276
|
def update_top_line(self, line: int):
|
|
259
|
-
"""Keep top frame's line number current
|
|
260
|
-
if self._frames:
|
|
261
|
-
self._frames[-1]
|
|
277
|
+
"""v1.7.3: Keep top frame's line number AND source snippet current during execution."""
|
|
278
|
+
if self._frames and line:
|
|
279
|
+
f = self._frames[-1]
|
|
280
|
+
f.line = line
|
|
281
|
+
f.source_line = self._lookup_src(line)
|
|
262
282
|
|
|
263
283
|
def format(self) -> str:
|
|
264
284
|
if not self._frames:
|
|
265
285
|
return ""
|
|
266
|
-
lines
|
|
267
|
-
shown
|
|
286
|
+
lines = ["Call stack (most recent last):"]
|
|
287
|
+
shown = self._frames
|
|
268
288
|
truncated = 0
|
|
269
289
|
if len(shown) > 20:
|
|
270
290
|
truncated = len(shown) - 20
|
|
271
|
-
shown
|
|
291
|
+
shown = shown[-20:]
|
|
272
292
|
if truncated:
|
|
273
293
|
lines.append(f" ... {truncated} earlier frame(s) ...")
|
|
274
294
|
for f in shown:
|
|
275
295
|
lines.append(f' File "{f.file}", line {f.line}, in {f.fn_name}')
|
|
296
|
+
if f.source_line: # v1.7.3: source snippet per frame
|
|
297
|
+
lines.append(f" {f.source_line}")
|
|
276
298
|
return "\n".join(lines)
|
|
277
299
|
|
|
278
300
|
|
|
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
|
|
|
24
24
|
SemanticError, InScriptRuntimeError,
|
|
25
25
|
MultiError, InScriptWarning)
|
|
26
26
|
|
|
27
|
-
VERSION = "1.7.
|
|
27
|
+
VERSION = "1.7.4"
|
|
28
28
|
LANG = "InScript"
|
|
29
29
|
PACKAGES_DIR = os.path.join(os.path.expanduser("~"), ".inscript", "packages")
|
|
30
30
|
REGISTRY_URL = "https://raw.githubusercontent.com/authorss81/inscript-packages/main/registry.json"
|
|
@@ -39,7 +39,7 @@ class Interpreter(Visitor):
|
|
|
39
39
|
self._env = self._globals
|
|
40
40
|
self._call_depth = 0
|
|
41
41
|
self._MAX_CALL_DEPTH = 500
|
|
42
|
-
self._call_stack = InScriptCallStack(filename) #
|
|
42
|
+
self._call_stack = InScriptCallStack(filename, src_lines=source_lines) # v1.7.3
|
|
43
43
|
|
|
44
44
|
# v1.3.0: dispatch cache — build once, avoids getattr on every node visit
|
|
45
45
|
self._dispatch: dict = {}
|
|
@@ -75,6 +75,22 @@ class Interpreter(Visitor):
|
|
|
75
75
|
|
|
76
76
|
# ── entry point ───────────────────────────────────────────────────────────
|
|
77
77
|
|
|
78
|
+
# v1.7.3: Override Visitor.visit() to track current execution line in the
|
|
79
|
+
# top call-stack frame. This makes stack-trace line numbers point to the
|
|
80
|
+
# exact statement that was executing when an error occurred, rather than
|
|
81
|
+
# the call-site where the function was entered.
|
|
82
|
+
def visit(self, node) -> Any:
|
|
83
|
+
line = getattr(node, 'line', 0)
|
|
84
|
+
if line:
|
|
85
|
+
self._call_stack.update_top_line(line)
|
|
86
|
+
cls = type(node)
|
|
87
|
+
try:
|
|
88
|
+
return self._dispatch[cls](node)
|
|
89
|
+
except KeyError:
|
|
90
|
+
method = getattr(self, f"visit_{cls.__name__}", self.generic_visit)
|
|
91
|
+
self._dispatch[cls] = method
|
|
92
|
+
return method(node)
|
|
93
|
+
|
|
78
94
|
def run(self, program: Program) -> Any:
|
|
79
95
|
"""Execute a full program."""
|
|
80
96
|
return self.visit(program)
|
|
@@ -1288,16 +1304,18 @@ class Interpreter(Visitor):
|
|
|
1288
1304
|
|
|
1289
1305
|
def visit_ThrowStmt(self, node) -> Any:
|
|
1290
1306
|
val = self.visit(node.value)
|
|
1291
|
-
# BUG-11 fix: tag error with the InScript type name of the thrown value
|
|
1292
|
-
# so that `catch e: string` can match when `throw "hello"` is used
|
|
1293
1307
|
if isinstance(val, str): etype = 'string'
|
|
1294
1308
|
elif isinstance(val, bool): etype = 'bool'
|
|
1295
1309
|
elif isinstance(val, int): etype = 'int'
|
|
1296
1310
|
elif isinstance(val, float): etype = 'float'
|
|
1297
1311
|
else: etype = 'Error'
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
err
|
|
1312
|
+
# v1.7.3: capture full call stack at throw time
|
|
1313
|
+
trace = self._call_stack.snapshot()
|
|
1314
|
+
err = InScriptRuntimeError(str(val), node.line,
|
|
1315
|
+
source_line=self._src_line(node.line),
|
|
1316
|
+
call_trace=trace)
|
|
1317
|
+
err.error_type = etype
|
|
1318
|
+
err.thrown_value = val
|
|
1301
1319
|
raise err
|
|
1302
1320
|
|
|
1303
1321
|
def visit_TryExpr(self, node) -> Any:
|
|
@@ -1488,12 +1506,13 @@ class Interpreter(Visitor):
|
|
|
1488
1506
|
def visit_StringLiteralExpr(self,n): return n.value
|
|
1489
1507
|
def visit_BoolLiteralExpr(self, n): return n.value
|
|
1490
1508
|
def visit_NullLiteralExpr(self, n):
|
|
1491
|
-
# v1.7.4: 'null' keyword is a hard error; 'nil' is
|
|
1509
|
+
# v1.7.4: 'null' keyword is a hard error; 'nil' is correct
|
|
1492
1510
|
if getattr(n, '_is_null_keyword', False):
|
|
1493
|
-
|
|
1511
|
+
raise InScriptRuntimeError(
|
|
1494
1512
|
"'null' was removed in v1.7.4 — use 'nil' instead. "
|
|
1495
1513
|
"Run: inscript migrate <file> to auto-fix.",
|
|
1496
|
-
getattr(n, 'line', 0)
|
|
1514
|
+
getattr(n, 'line', 0),
|
|
1515
|
+
code="E0055",
|
|
1497
1516
|
)
|
|
1498
1517
|
return None
|
|
1499
1518
|
|
|
@@ -444,6 +444,9 @@ class Parser:
|
|
|
444
444
|
TT.STRING_TYPE, TT.VOID_TYPE, TT.IDENT):
|
|
445
445
|
name = tok.value
|
|
446
446
|
self.advance()
|
|
447
|
+
elif tok.type == TT.NIL: # v1.7.2: nil as type annotation
|
|
448
|
+
name = "nil"
|
|
449
|
+
self.advance()
|
|
447
450
|
else:
|
|
448
451
|
self._error(f"Expected type name, got '{tok.value}'")
|
|
449
452
|
ann = TypeAnnotation(name=name, line=line, col=col)
|
|
@@ -885,9 +888,11 @@ class Parser:
|
|
|
885
888
|
# e.g. items: [] or count: 0 or value: nil
|
|
886
889
|
type_ann = None
|
|
887
890
|
_type_starts = (TT.IDENT, TT.INT_TYPE, TT.FLOAT_TYPE, TT.BOOL_TYPE,
|
|
888
|
-
TT.STRING_TYPE, TT.VOID_TYPE, TT.LBRACE, TT.LPAREN
|
|
891
|
+
TT.STRING_TYPE, TT.VOID_TYPE, TT.LBRACE, TT.LPAREN,
|
|
892
|
+
TT.NIL) # v1.7.2: nil is a valid type (nullable/any)
|
|
889
893
|
# Tokens that unambiguously start a literal default (not a type name)
|
|
890
|
-
|
|
894
|
+
# Note: TT.NIL removed — could be type OR default; handled by _type_starts
|
|
895
|
+
_default_starts = (TT.NULL, TT.BOOL, TT.INT, TT.FLOAT, TT.STRING, TT.MINUS)
|
|
891
896
|
|
|
892
897
|
if self.check(TT.LBRACKET):
|
|
893
898
|
if self.peek.type != TT.RBRACKET:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inscript-lang"
|
|
7
|
-
version = "1.7.
|
|
7
|
+
version = "1.7.4"
|
|
8
8
|
description = "InScript — a game-focused scripting language with 59 game modules and a bytecode VM"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -40,7 +40,7 @@ sys.path.insert(0, str(Path(__file__).parent))
|
|
|
40
40
|
|
|
41
41
|
HISTORY_FILE = Path.home() / ".inscript" / "history"
|
|
42
42
|
HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
-
VERSION = "1.7.
|
|
43
|
+
VERSION = "1.7.4"
|
|
44
44
|
|
|
45
45
|
# ── ANSI colours ──────────────────────────────────────────────────────────────
|
|
46
46
|
def _c(code, text):
|
|
@@ -434,6 +434,9 @@ class EnhancedREPL:
|
|
|
434
434
|
self._history: List[str] = []
|
|
435
435
|
self._session: List[str] = []
|
|
436
436
|
self._last_src: str = ""
|
|
437
|
+
# v1.7.4: mark the root env as REPL-mode so `let` re-declarations
|
|
438
|
+
# silently re-bind instead of erroring ("let x=1" then "let x=2" works)
|
|
439
|
+
self._interp._env._repl_mode = True
|
|
437
440
|
self._setup_readline()
|
|
438
441
|
|
|
439
442
|
def _setup_readline(self):
|
|
@@ -457,6 +460,13 @@ class EnhancedREPL:
|
|
|
457
460
|
from parser import Parser
|
|
458
461
|
from errors import InScriptError
|
|
459
462
|
t0 = time.perf_counter()
|
|
463
|
+
|
|
464
|
+
# v1.7.4: snapshot the top-level env so a runtime error can't corrupt
|
|
465
|
+
# previously-defined globals. On error we restore the snapshot, which
|
|
466
|
+
# means any partial defines from the failed block are rolled back —
|
|
467
|
+
# already-defined names from prior REPL lines are always preserved.
|
|
468
|
+
env_snapshot = dict(self._interp._env._store)
|
|
469
|
+
|
|
460
470
|
try:
|
|
461
471
|
tokens = Lexer(source).tokenize()
|
|
462
472
|
program = Parser(tokens).parse()
|
|
@@ -474,8 +484,11 @@ class EnhancedREPL:
|
|
|
474
484
|
result = self._interp.run(program)
|
|
475
485
|
return result, None, (time.perf_counter() - t0) * 1000
|
|
476
486
|
except InScriptError as e:
|
|
487
|
+
# v1.7.4: restore globals so surviving names are untouched
|
|
488
|
+
self._interp._env._store = env_snapshot
|
|
477
489
|
return None, _format_error(str(e), source), (time.perf_counter() - t0) * 1000
|
|
478
490
|
except Exception as e:
|
|
491
|
+
self._interp._env._store = env_snapshot
|
|
479
492
|
import traceback as _tb
|
|
480
493
|
return None, f"Internal error: {e}\n{_tb.format_exc()}", (time.perf_counter() - t0) * 1000
|
|
481
494
|
|
|
@@ -11,7 +11,7 @@ from setuptools import setup, find_packages
|
|
|
11
11
|
|
|
12
12
|
setup(
|
|
13
13
|
name = "inscript-lang",
|
|
14
|
-
version = "1.7.
|
|
14
|
+
version = "1.7.2",
|
|
15
15
|
author = "Shreyasi Sarkar",
|
|
16
16
|
description = "InScript — a game-focused scripting language for 2D games",
|
|
17
17
|
long_description = open("README.md", encoding="utf-8").read(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|