inscript-lang 1.6.0__tar.gz → 1.7.3__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 → inscript_lang-1.7.3}/PKG-INFO +1 -1
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/errors.py +39 -17
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript.py +50 -1
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/interpreter.py +74 -17
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/lexer.py +25 -13
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/parser.py +18 -7
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/pyproject.toml +1 -1
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/repl.py +14 -1
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/setup.py +1 -1
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/README.md +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/analyzer.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/ast_nodes.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/compiler.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/environment.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript_fmt.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/inscript_test.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/pygame_backend.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/setup.cfg +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/stdlib.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/stdlib_extended.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/stdlib_extended_2.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/stdlib_game.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/stdlib_values.py +0 -0
- {inscript_lang-1.6.0 → inscript_lang-1.7.3}/vm.py +0 -0
|
@@ -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.
|
|
27
|
+
VERSION = "1.7.3"
|
|
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"
|
|
@@ -245,6 +245,51 @@ def _check_all_files(directory: str, strict: bool = False) -> int:
|
|
|
245
245
|
return 1 if errors else 0
|
|
246
246
|
|
|
247
247
|
|
|
248
|
+
def _migrate_files(path: str) -> int:
|
|
249
|
+
"""v1.7.4: Auto-migrate deprecated InScript syntax in-place."""
|
|
250
|
+
import re, os
|
|
251
|
+
target = os.path.abspath(path)
|
|
252
|
+
files = []
|
|
253
|
+
if os.path.isfile(target):
|
|
254
|
+
files = [target]
|
|
255
|
+
elif os.path.isdir(target):
|
|
256
|
+
files = list(_find_ins_files(target))
|
|
257
|
+
else:
|
|
258
|
+
print(f"[InScript migrate] Not found: '{path}'", file=sys.stderr)
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
changed = errors = 0
|
|
262
|
+
for fpath in files:
|
|
263
|
+
try:
|
|
264
|
+
with open(fpath, encoding="utf-8") as f:
|
|
265
|
+
original = f.read()
|
|
266
|
+
src = original
|
|
267
|
+
# null → nil
|
|
268
|
+
src = re.sub(r'\bnull\b', 'nil', src)
|
|
269
|
+
# x div y → x // y (only bare `div` between expressions)
|
|
270
|
+
src = re.sub(r'\bdiv\b', '//', src)
|
|
271
|
+
# bare [] type annotation → array (e.g. `: []` → `: array`)
|
|
272
|
+
src = re.sub(r':\s*\[\]', ': array', src)
|
|
273
|
+
if src != original:
|
|
274
|
+
with open(fpath, "w", encoding="utf-8") as f:
|
|
275
|
+
f.write(src)
|
|
276
|
+
print(f" MIGRATED {fpath}")
|
|
277
|
+
changed += 1
|
|
278
|
+
else:
|
|
279
|
+
print(f" OK {fpath}")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
errors += 1
|
|
282
|
+
print(f" ERR {fpath}: {e}", file=sys.stderr)
|
|
283
|
+
|
|
284
|
+
sep = "-" * 50
|
|
285
|
+
print(f"\n{sep}")
|
|
286
|
+
print(f" Migrated {changed} file{'s' if changed != 1 else ''}, "
|
|
287
|
+
f"{len(files)-changed-errors} already clean, "
|
|
288
|
+
f"{errors} error{'s' if errors != 1 else ''}")
|
|
289
|
+
print(sep)
|
|
290
|
+
return 1 if errors else 0
|
|
291
|
+
|
|
292
|
+
|
|
248
293
|
def _fmt_all_files(directory: str) -> int:
|
|
249
294
|
"""v1.6.0: Format all .ins files in directory in-place. Returns exit code."""
|
|
250
295
|
files = list(_find_ins_files(directory))
|
|
@@ -568,6 +613,8 @@ Examples:
|
|
|
568
613
|
parser.add_argument("--check", action="store_true", help="Type-check only, don't run")
|
|
569
614
|
parser.add_argument("--check-all", metavar="DIR",
|
|
570
615
|
help="v1.6.0: Check all .ins files in DIR recursively, exit 1 if any errors")
|
|
616
|
+
parser.add_argument("--migrate", metavar="DIR_OR_FILE",
|
|
617
|
+
help="v1.7.4: Auto-migrate deprecated syntax (null→nil, div→//)")
|
|
571
618
|
parser.add_argument("--strict", action="store_true",
|
|
572
619
|
help="v1.6.0: Strict mode — all warnings become errors, no implicit any")
|
|
573
620
|
parser.add_argument("--tokens", action="store_true", help="Print lexer token stream")
|
|
@@ -721,6 +768,8 @@ Examples:
|
|
|
721
768
|
strict=getattr(args, 'strict', False))
|
|
722
769
|
if getattr(args, 'fmt_all', None):
|
|
723
770
|
return _fmt_all_files(args.fmt_all)
|
|
771
|
+
if getattr(args, 'migrate', None):
|
|
772
|
+
return _migrate_files(args.migrate)
|
|
724
773
|
|
|
725
774
|
if not args.file:
|
|
726
775
|
parser.print_help()
|
|
@@ -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:
|
|
@@ -1487,12 +1505,15 @@ class Interpreter(Visitor):
|
|
|
1487
1505
|
def visit_FloatLiteralExpr(self, n): return n.value
|
|
1488
1506
|
def visit_StringLiteralExpr(self,n): return n.value
|
|
1489
1507
|
def visit_BoolLiteralExpr(self, n): return n.value
|
|
1490
|
-
def visit_NullLiteralExpr(self,
|
|
1491
|
-
#
|
|
1492
|
-
if
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1508
|
+
def visit_NullLiteralExpr(self, n):
|
|
1509
|
+
# v1.7.4: 'null' keyword is a hard error; 'nil' is correct
|
|
1510
|
+
if getattr(n, '_is_null_keyword', False):
|
|
1511
|
+
raise InScriptRuntimeError(
|
|
1512
|
+
"'null' was removed in v1.7.4 — use 'nil' instead. "
|
|
1513
|
+
"Run: inscript migrate <file> to auto-fix.",
|
|
1514
|
+
getattr(n, 'line', 0),
|
|
1515
|
+
code="E0055",
|
|
1516
|
+
)
|
|
1496
1517
|
return None
|
|
1497
1518
|
|
|
1498
1519
|
def visit_IdentExpr(self, node: IdentExpr) -> Any:
|
|
@@ -3292,6 +3313,46 @@ def _maybe_copy_value(val):
|
|
|
3292
3313
|
return val
|
|
3293
3314
|
|
|
3294
3315
|
|
|
3316
|
+
def _format_float(val: float) -> str:
|
|
3317
|
+
"""v1.7.1: Clean float display.
|
|
3318
|
+
0.30000000000000004 → '0.3', 1.0 → '1.0',
|
|
3319
|
+
1/3 → '0.3333333333333333', sqrt(2) → '1.4142135623730951'
|
|
3320
|
+
"""
|
|
3321
|
+
if math.isinf(val): return "Infinity" if val > 0 else "-Infinity"
|
|
3322
|
+
if math.isnan(val): return "NaN"
|
|
3323
|
+
if val == int(val) and abs(val) < 1e15:
|
|
3324
|
+
return f"{int(val)}.0"
|
|
3325
|
+
|
|
3326
|
+
# Get the definitive repr (always round-trips correctly)
|
|
3327
|
+
s_repr = repr(val)
|
|
3328
|
+
|
|
3329
|
+
# Try 15 sig figs — may produce a shorter/cleaner string for float-noise values
|
|
3330
|
+
s15 = f"{val:.15g}"
|
|
3331
|
+
try:
|
|
3332
|
+
v15 = float(s15)
|
|
3333
|
+
# Only use 15g result if it's strictly shorter than repr AND round-trips close enough
|
|
3334
|
+
# "Shorter" means fewer significant digits → it genuinely simplified the value
|
|
3335
|
+
denom = abs(val) if val != 0 else 1.0
|
|
3336
|
+
rel_err = abs(v15 - val) / denom
|
|
3337
|
+
if rel_err < 2e-15 and len(s15.rstrip('0').rstrip('.')) <= len(s_repr) - 2:
|
|
3338
|
+
s = s15
|
|
3339
|
+
if '.' in s and 'e' not in s and 'E' not in s:
|
|
3340
|
+
s = s.rstrip('0')
|
|
3341
|
+
if s.endswith('.'):
|
|
3342
|
+
s += '0'
|
|
3343
|
+
return s
|
|
3344
|
+
except Exception:
|
|
3345
|
+
pass
|
|
3346
|
+
|
|
3347
|
+
# Use repr — strip redundant trailing zeros but keep all significant digits
|
|
3348
|
+
s = s_repr
|
|
3349
|
+
if '.' in s and 'e' not in s and 'E' not in s:
|
|
3350
|
+
s = s.rstrip('0')
|
|
3351
|
+
if s.endswith('.'):
|
|
3352
|
+
s += '0'
|
|
3353
|
+
return s
|
|
3354
|
+
|
|
3355
|
+
|
|
3295
3356
|
def _inscript_str(val) -> str:
|
|
3296
3357
|
if val is None: return "nil"
|
|
3297
3358
|
if val is True: return "true"
|
|
@@ -3299,11 +3360,7 @@ def _inscript_str(val) -> str:
|
|
|
3299
3360
|
if isinstance(val, list):
|
|
3300
3361
|
return "[" + ", ".join(_inscript_repr(v) for v in val) + "]"
|
|
3301
3362
|
if isinstance(val, float):
|
|
3302
|
-
|
|
3303
|
-
if math.isnan(val): return "NaN"
|
|
3304
|
-
if val == int(val):
|
|
3305
|
-
return f"{int(val)}.0"
|
|
3306
|
-
return str(val)
|
|
3363
|
+
return _format_float(val)
|
|
3307
3364
|
if isinstance(val, str):
|
|
3308
3365
|
return val # bare string — no quotes when printing top-level
|
|
3309
3366
|
# InScript struct instance — show data fields only, no methods
|
|
@@ -15,7 +15,8 @@ class TT(Enum):
|
|
|
15
15
|
FLOAT = auto()
|
|
16
16
|
STRING = auto()
|
|
17
17
|
BOOL = auto()
|
|
18
|
-
NULL = auto()
|
|
18
|
+
NULL = auto() # null (removed — hard error)
|
|
19
|
+
NIL = auto() # nil (the correct nil literal)
|
|
19
20
|
|
|
20
21
|
# ── Identifiers & Keywords ────────────────────
|
|
21
22
|
IDENT = auto()
|
|
@@ -175,7 +176,7 @@ KEYWORDS: dict = {
|
|
|
175
176
|
"true": TT.BOOL,
|
|
176
177
|
"false": TT.BOOL,
|
|
177
178
|
"null": TT.NULL,
|
|
178
|
-
"nil": TT.
|
|
179
|
+
"nil": TT.NIL,
|
|
179
180
|
"import": TT.IMPORT,
|
|
180
181
|
"from": TT.FROM,
|
|
181
182
|
"as": TT.AS,
|
|
@@ -291,16 +292,26 @@ class Lexer:
|
|
|
291
292
|
if ch in (" ", "\t", "\r", "\n"):
|
|
292
293
|
return
|
|
293
294
|
|
|
294
|
-
# // — floor-division or line comment
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
300
|
-
# Phase 1.2: // is now ALWAYS a line comment. Floor division uses 'div' keyword.
|
|
295
|
+
# // — floor-division operator or line comment
|
|
296
|
+
# v1.7.1: `//` is now the integer division operator (10 // 3 → 3)
|
|
297
|
+
# Line comment: `let x = 5 // this is a comment` — space before //
|
|
298
|
+
# Floor division: `10 // 3`, `x//2`, `(a+b) // c`
|
|
299
|
+
# Rule: if the character BEFORE // was a digit, ), ], or identifier char → floor div
|
|
300
|
+
# otherwise → line comment
|
|
301
301
|
if ch == "/" and self.current == "/":
|
|
302
302
|
self.advance() # consume second /
|
|
303
|
-
|
|
303
|
+
# Determine: floor-div or line comment?
|
|
304
|
+
# Rule: the character IMMEDIATELY before first '/' (no whitespace allowed)
|
|
305
|
+
# must be digit/identifier/closing bracket to be floor-div.
|
|
306
|
+
# Any space/tab before the // → line comment.
|
|
307
|
+
first_slash_pos = self.pos - 2
|
|
308
|
+
prev = self.source[first_slash_pos - 1] if first_slash_pos > 0 else ''
|
|
309
|
+
if prev and (prev.isdigit() or prev.isalpha() or prev in ')]}'):
|
|
310
|
+
# `10//3`, `x//2`, `(a+b)//c` — no space → floor division
|
|
311
|
+
self._emit(TT.SLASH_SLASH, "//", sl, sc)
|
|
312
|
+
else:
|
|
313
|
+
# `10 // 3` with spaces, `5 // comment`, `// standalone` → comment
|
|
314
|
+
self._skip_line_comment()
|
|
304
315
|
return
|
|
305
316
|
if ch == "/" and self.current == "*":
|
|
306
317
|
self._skip_block_comment(sl, sc); return
|
|
@@ -497,7 +508,8 @@ class Lexer:
|
|
|
497
508
|
text = "".join(chars)
|
|
498
509
|
tt = KEYWORDS.get(text, TT.IDENT)
|
|
499
510
|
if tt == TT.BOOL: value = (text == "true")
|
|
500
|
-
elif tt == TT.
|
|
511
|
+
elif tt == TT.NIL: value = None # nil literal → Python None
|
|
512
|
+
elif tt == TT.NULL: value = None # null (removed keyword — still lex it so error can fire)
|
|
501
513
|
else: value = text
|
|
502
514
|
self._emit(tt, value, sl, sc)
|
|
503
515
|
|
|
@@ -522,8 +534,8 @@ class Lexer:
|
|
|
522
534
|
elif self.match("="): emit(TT.STAR_EQ, "*=")
|
|
523
535
|
else: emit(TT.STAR, "*")
|
|
524
536
|
elif ch == "/":
|
|
525
|
-
if self.match("="):
|
|
526
|
-
else:
|
|
537
|
+
if self.match("="): emit(TT.SLASH_EQ, "/=")
|
|
538
|
+
else: emit(TT.SLASH, "/")
|
|
527
539
|
elif ch == "%":
|
|
528
540
|
if self.match("="): emit(TT.PERCENT_EQ, "%=")
|
|
529
541
|
else: emit(TT.PERCENT, "%")
|
|
@@ -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,8 +888,10 @@ 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)
|
|
894
|
+
# Note: TT.NIL removed — could be type OR default; handled by _type_starts
|
|
890
895
|
_default_starts = (TT.NULL, TT.BOOL, TT.INT, TT.FLOAT, TT.STRING, TT.MINUS)
|
|
891
896
|
|
|
892
897
|
if self.check(TT.LBRACKET):
|
|
@@ -1623,7 +1628,7 @@ class Parser:
|
|
|
1623
1628
|
attr_tok = self.current
|
|
1624
1629
|
# Accept any identifier OR keyword as attribute name (e.g. regex.match, uuid.nil)
|
|
1625
1630
|
_KEYWORD_TYPES = {
|
|
1626
|
-
TT.NULL, TT.MATCH, TT.IN, TT.FOR, TT.IF, TT.ELSE, TT.WHILE,
|
|
1631
|
+
TT.NIL, TT.NULL, TT.MATCH, TT.IN, TT.FOR, TT.IF, TT.ELSE, TT.WHILE,
|
|
1627
1632
|
TT.RETURN, TT.BREAK, TT.CONTINUE, TT.LET, TT.CONST, TT.FN,
|
|
1628
1633
|
TT.STRUCT, TT.ENUM, TT.IMPORT, TT.EXPORT, TT.FROM, TT.ASYNC,
|
|
1629
1634
|
TT.AWAIT, TT.ABSTRACT, TT.SELECT, TT.CASE, TT.TRY, TT.CATCH,
|
|
@@ -1634,7 +1639,7 @@ class Parser:
|
|
|
1634
1639
|
}
|
|
1635
1640
|
if attr_tok.type == TT.IDENT or attr_tok.type in _KEYWORD_TYPES:
|
|
1636
1641
|
# For NULL token use "nil"; for BOOL use the literal text ("true"/"false")
|
|
1637
|
-
if attr_tok.type
|
|
1642
|
+
if attr_tok.type in (TT.NIL, TT.NULL):
|
|
1638
1643
|
attr = "nil"
|
|
1639
1644
|
elif attr_tok.type == TT.BOOL:
|
|
1640
1645
|
attr = "true" if attr_tok.value else "false"
|
|
@@ -1694,7 +1699,7 @@ class Parser:
|
|
|
1694
1699
|
# Propagate ? is followed by newline, }, ), ,, ;, EOF
|
|
1695
1700
|
elif self.check(TT.QUESTION):
|
|
1696
1701
|
_EXPR_START = {
|
|
1697
|
-
TT.STRING, TT.INT, TT.FLOAT, TT.BOOL, TT.NULL,
|
|
1702
|
+
TT.STRING, TT.INT, TT.FLOAT, TT.BOOL, TT.NIL, TT.NULL,
|
|
1698
1703
|
TT.IDENT, TT.LPAREN, TT.LBRACKET, TT.LBRACE,
|
|
1699
1704
|
TT.BIT_OR, TT.OR, TT.NOT, TT.MINUS, TT.FSTRING,
|
|
1700
1705
|
TT.INT_TYPE, TT.FLOAT_TYPE, TT.STRING_TYPE, TT.BOOL_TYPE,
|
|
@@ -1735,7 +1740,7 @@ class Parser:
|
|
|
1735
1740
|
_CHECK_TYPES = {
|
|
1736
1741
|
TT.INT_TYPE: "int", TT.FLOAT_TYPE: "float",
|
|
1737
1742
|
TT.STRING_TYPE: "string", TT.BOOL_TYPE: "bool",
|
|
1738
|
-
TT.VOID_TYPE: "void", TT.NULL: "nil",
|
|
1743
|
+
TT.VOID_TYPE: "void", TT.NULL: "nil", TT.NIL: "nil",
|
|
1739
1744
|
TT.IDENT: None,
|
|
1740
1745
|
}
|
|
1741
1746
|
if check_tok.type in _CHECK_TYPES:
|
|
@@ -1813,10 +1818,16 @@ class Parser:
|
|
|
1813
1818
|
self.advance()
|
|
1814
1819
|
return BoolLiteralExpr(value=tok.value, line=line, col=col)
|
|
1815
1820
|
|
|
1816
|
-
#
|
|
1817
|
-
if tok.type == TT.
|
|
1821
|
+
# nil / null
|
|
1822
|
+
if tok.type == TT.NIL:
|
|
1818
1823
|
self.advance()
|
|
1819
1824
|
return NullLiteralExpr(line=line, col=col)
|
|
1825
|
+
if tok.type == TT.NULL:
|
|
1826
|
+
self.advance()
|
|
1827
|
+
# Keep the node but tag it as the removed 'null' keyword
|
|
1828
|
+
node = NullLiteralExpr(line=line, col=col)
|
|
1829
|
+
node._is_null_keyword = True # interpreter will hard-error on this
|
|
1830
|
+
return node
|
|
1820
1831
|
|
|
1821
1832
|
# Grouped expression: (expr) OR Tuple: (expr, expr, ...)
|
|
1822
1833
|
if tok.type == TT.LPAREN:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inscript-lang"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.3"
|
|
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.
|
|
43
|
+
VERSION = "1.7.3"
|
|
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.
|
|
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
|