inscript-lang 2.0.0__tar.gz → 2.0.2__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-2.0.0 → inscript_lang-2.0.2}/PKG-INFO +1 -1
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/analyzer.py +6 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/ast_nodes.py +6 -4
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript.py +1 -1
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/interpreter.py +72 -37
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/parser.py +8 -2
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/pyproject.toml +1 -1
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/repl.py +1 -1
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/stdlib_values.py +13 -4
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/README.md +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/compiler.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/environment.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/errors.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript_fmt.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/inscript_test.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/lexer.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/pygame_backend.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/setup.cfg +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/setup.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/stdlib.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/stdlib_extended.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/stdlib_extended_2.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/stdlib_game.py +0 -0
- {inscript_lang-2.0.0 → inscript_lang-2.0.2}/vm.py +0 -0
|
@@ -1772,6 +1772,12 @@ class Analyzer(Visitor):
|
|
|
1772
1772
|
if s not in (T_INT, T_ANY) or e not in (T_INT, T_ANY):
|
|
1773
1773
|
self._error(f"Range bounds must be int, got '{s}' and '{e}'",
|
|
1774
1774
|
node.line, node.col)
|
|
1775
|
+
# v2.0.1: validate optional step expression
|
|
1776
|
+
if node.step is not None:
|
|
1777
|
+
st = self.visit(node.step)
|
|
1778
|
+
if st not in (T_INT, T_ANY):
|
|
1779
|
+
self._error(f"Range step must be int, got '{st}'",
|
|
1780
|
+
node.line, node.col)
|
|
1775
1781
|
return InScriptType("Range", [T_INT])
|
|
1776
1782
|
|
|
1777
1783
|
def visit_LambdaExpr(self, node: LambdaExpr) -> InScriptType:
|
|
@@ -195,10 +195,11 @@ class LambdaExpr(Node):
|
|
|
195
195
|
|
|
196
196
|
@dataclass
|
|
197
197
|
class RangeExpr(Node):
|
|
198
|
-
"""0..10 (exclusive) | 0..=10 (inclusive)"""
|
|
198
|
+
"""0..10 (exclusive) | 0..=10 (inclusive) | 0..10 step 2 (v2.0.1)"""
|
|
199
199
|
start: Node
|
|
200
200
|
end: Node
|
|
201
|
-
inclusive: bool
|
|
201
|
+
inclusive: bool = False
|
|
202
|
+
step: object = None # None = auto (1, or -1 if start > end)
|
|
202
203
|
|
|
203
204
|
|
|
204
205
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -560,10 +561,11 @@ class PropertyDecl(Node):
|
|
|
560
561
|
|
|
561
562
|
@dataclass
|
|
562
563
|
class RangeExpr(Node):
|
|
563
|
-
"""0..10 (exclusive) | 0..=10 (inclusive)"""
|
|
564
|
+
"""0..10 (exclusive) | 0..=10 (inclusive) | 0..10 step 2 (v2.0.1)"""
|
|
564
565
|
start: Node
|
|
565
566
|
end: Node
|
|
566
|
-
inclusive: bool
|
|
567
|
+
inclusive: bool = False
|
|
568
|
+
step: object = None # None = auto (1 ascending, -1 descending)
|
|
567
569
|
|
|
568
570
|
@dataclass
|
|
569
571
|
class LambdaExpr(Node):
|
|
@@ -2470,7 +2470,13 @@ class Interpreter(Visitor):
|
|
|
2470
2470
|
def visit_RangeExpr(self, node: RangeExpr) -> Any:
|
|
2471
2471
|
start = self.visit(node.start)
|
|
2472
2472
|
end = self.visit(node.end)
|
|
2473
|
-
|
|
2473
|
+
# v2.0.1: optional `step` expression
|
|
2474
|
+
if node.step is not None:
|
|
2475
|
+
step = self.visit(node.step)
|
|
2476
|
+
else:
|
|
2477
|
+
# Auto-descend: if start > end, default step to -1
|
|
2478
|
+
step = -1 if int(start) > int(end) else 1
|
|
2479
|
+
return InScriptRange(start, end, step=step, inclusive=node.inclusive)
|
|
2474
2480
|
|
|
2475
2481
|
def visit_AwaitExpr(self, node: AwaitExpr) -> Any:
|
|
2476
2482
|
"""
|
|
@@ -2510,41 +2516,76 @@ class Interpreter(Visitor):
|
|
|
2510
2516
|
cond = self.visit(node.condition)
|
|
2511
2517
|
return self.visit(node.then_expr) if cond else self.visit(node.else_expr)
|
|
2512
2518
|
|
|
2519
|
+
@staticmethod
|
|
2520
|
+
def _fstring_segments(template: str):
|
|
2521
|
+
"""v2.0.2: brace-depth-aware f-string segment splitter.
|
|
2522
|
+
Returns list of (kind, text) where kind is "lit" or "expr".
|
|
2523
|
+
Handles nested braces (match, for, object literals) correctly.
|
|
2524
|
+
{{ and }} in source are stored as sentinel \x00{ and }\x00.
|
|
2525
|
+
"""
|
|
2526
|
+
segments = []
|
|
2527
|
+
i = 0
|
|
2528
|
+
n = len(template)
|
|
2529
|
+
lit_buf = []
|
|
2530
|
+
while i < n:
|
|
2531
|
+
ch = template[i]
|
|
2532
|
+
# Sentinel escaped braces: \x00{ = literal {, }\x00 = literal }
|
|
2533
|
+
if ch == "\x00" and i + 1 < n and template[i + 1] == "{":
|
|
2534
|
+
lit_buf.append("{"); i += 2
|
|
2535
|
+
elif ch == "}" and i + 1 < n and template[i + 1] == "\x00":
|
|
2536
|
+
lit_buf.append("}"); i += 2
|
|
2537
|
+
elif ch == "{":
|
|
2538
|
+
# Start of an expression — find matching } with depth tracking
|
|
2539
|
+
if lit_buf:
|
|
2540
|
+
segments.append(("lit", "".join(lit_buf))); lit_buf = []
|
|
2541
|
+
depth = 1; j = i + 1; in_str = None
|
|
2542
|
+
while j < n and depth > 0:
|
|
2543
|
+
c2 = template[j]
|
|
2544
|
+
if in_str:
|
|
2545
|
+
if c2 == "\\" and j + 1 < n: j += 2; continue
|
|
2546
|
+
if c2 == in_str: in_str = None
|
|
2547
|
+
elif c2 in ('"', "\'"):
|
|
2548
|
+
in_str = c2
|
|
2549
|
+
elif c2 == "{":
|
|
2550
|
+
depth += 1
|
|
2551
|
+
elif c2 == "}":
|
|
2552
|
+
depth -= 1
|
|
2553
|
+
if depth == 0: break
|
|
2554
|
+
j += 1
|
|
2555
|
+
segments.append(("expr", template[i + 1:j]))
|
|
2556
|
+
i = j + 1
|
|
2557
|
+
else:
|
|
2558
|
+
lit_buf.append(ch); i += 1
|
|
2559
|
+
if lit_buf:
|
|
2560
|
+
segments.append(("lit", "".join(lit_buf)))
|
|
2561
|
+
return segments
|
|
2562
|
+
|
|
2513
2563
|
def visit_FStringExpr(self, node: FStringExpr) -> Any:
|
|
2514
2564
|
"""f"Hello {name}, score={score:.1f}" — evaluate {expr} segments at runtime.
|
|
2565
|
+
v2.0.2: brace-depth-aware splitter handles match/for/object literals inside {}.
|
|
2515
2566
|
Supports Python-style format specs: {x:.2f}, {n:06d}, {s:>20} etc.
|
|
2516
|
-
{{ and }} in source produce literal braces
|
|
2517
|
-
import re
|
|
2567
|
+
{{ and }} in source produce literal braces."""
|
|
2518
2568
|
result = []
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
inner =
|
|
2524
|
-
|
|
2525
|
-
# Split off format spec: {expr:spec}
|
|
2526
|
-
# Key: only split on ':' that is NOT inside brackets AND NOT after '?'
|
|
2527
|
-
# (ternary x ? a : b uses ':' but it's not a format spec)
|
|
2569
|
+
for kind, text in self._fstring_segments(node.template):
|
|
2570
|
+
if kind == "lit":
|
|
2571
|
+
result.append(text)
|
|
2572
|
+
continue
|
|
2573
|
+
inner = text.strip()
|
|
2574
|
+
# Split off format spec {expr:spec}, respecting brackets and ternary
|
|
2528
2575
|
fmt_spec = None
|
|
2529
|
-
depth = 0
|
|
2530
|
-
ternary_depth = 0
|
|
2531
|
-
split_at = -1
|
|
2576
|
+
depth = 0; ternary_depth = 0; split_at = -1
|
|
2532
2577
|
for i, ch in enumerate(inner):
|
|
2533
|
-
if ch in
|
|
2534
|
-
elif ch in
|
|
2535
|
-
elif ch ==
|
|
2536
|
-
elif ch ==
|
|
2537
|
-
if ternary_depth > 0:
|
|
2538
|
-
|
|
2539
|
-
else:
|
|
2540
|
-
split_at = i
|
|
2541
|
-
break
|
|
2578
|
+
if ch in "([{": depth += 1
|
|
2579
|
+
elif ch in ")]}": depth -= 1
|
|
2580
|
+
elif ch == "?" and depth == 0: ternary_depth += 1
|
|
2581
|
+
elif ch == ":" and depth == 0:
|
|
2582
|
+
if ternary_depth > 0: ternary_depth -= 1
|
|
2583
|
+
else: split_at = i; break
|
|
2542
2584
|
if split_at > 0:
|
|
2543
|
-
expr_src
|
|
2544
|
-
fmt_spec
|
|
2585
|
+
expr_src = inner[:split_at].strip()
|
|
2586
|
+
fmt_spec = inner[split_at + 1:].strip()
|
|
2545
2587
|
else:
|
|
2546
2588
|
expr_src = inner
|
|
2547
|
-
|
|
2548
2589
|
try:
|
|
2549
2590
|
from parser import parse
|
|
2550
2591
|
prog = parse(expr_src)
|
|
@@ -2552,19 +2593,13 @@ class Interpreter(Visitor):
|
|
|
2552
2593
|
for stmt in prog.body:
|
|
2553
2594
|
val = self.visit(stmt)
|
|
2554
2595
|
if fmt_spec:
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
result.append(format(val, fmt_spec))
|
|
2558
|
-
except Exception:
|
|
2559
|
-
result.append(_inscript_str(val))
|
|
2596
|
+
try: result.append(format(val, fmt_spec))
|
|
2597
|
+
except: result.append(_inscript_str(val))
|
|
2560
2598
|
else:
|
|
2561
2599
|
result.append(_inscript_str(val))
|
|
2562
2600
|
except Exception:
|
|
2563
|
-
result.append(
|
|
2564
|
-
|
|
2565
|
-
result.append(template[pos:])
|
|
2566
|
-
final = "".join(result).replace("\x00{", "{").replace("}\x00", "}")
|
|
2567
|
-
return final
|
|
2601
|
+
result.append("{" + inner + "}")
|
|
2602
|
+
return "".join(result)
|
|
2568
2603
|
|
|
2569
2604
|
def visit_DestructureDecl(self, node: DestructureDecl) -> Any:
|
|
2570
2605
|
"""let [a, b, c] = arr | let {x, y} = point | let [[a,b],[c,d]] = nested"""
|
|
@@ -1555,13 +1555,19 @@ class Parser:
|
|
|
1555
1555
|
line, col = self._pos()
|
|
1556
1556
|
left = self.parse_shift()
|
|
1557
1557
|
|
|
1558
|
-
# Range: start..end or start..=end
|
|
1558
|
+
# Range: start..end or start..=end or start..end step n (v2.0.1)
|
|
1559
1559
|
if self.check(TT.DOTDOT, TT.DOTDOT_EQ):
|
|
1560
1560
|
inclusive = (self.current.type == TT.DOTDOT_EQ)
|
|
1561
1561
|
self.advance()
|
|
1562
1562
|
right = self.parse_shift()
|
|
1563
|
+
# Optional `step` suffix: 0..20 step 2
|
|
1564
|
+
step_expr = None
|
|
1565
|
+
if (self.current and self.current.type == TT.IDENT
|
|
1566
|
+
and self.current.value == "step"):
|
|
1567
|
+
self.advance() # consume 'step'
|
|
1568
|
+
step_expr = self.parse_shift()
|
|
1563
1569
|
return RangeExpr(start=left, end=right, inclusive=inclusive,
|
|
1564
|
-
line=line, col=col)
|
|
1570
|
+
step=step_expr, line=line, col=col)
|
|
1565
1571
|
|
|
1566
1572
|
# Membership: x in collection / x not in collection
|
|
1567
1573
|
if self.check(TT.IN):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inscript-lang"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.2"
|
|
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 = "2.0.
|
|
43
|
+
VERSION = "2.0.2"
|
|
44
44
|
|
|
45
45
|
# ── ANSI colours ──────────────────────────────────────────────────────────────
|
|
46
46
|
def _c(code, text):
|
|
@@ -281,16 +281,25 @@ class InScriptRange:
|
|
|
281
281
|
self.inclusive = inclusive
|
|
282
282
|
|
|
283
283
|
def __iter__(self):
|
|
284
|
-
|
|
284
|
+
# v2.0.1: support negative step (descending ranges)
|
|
285
|
+
if self.step > 0:
|
|
286
|
+
stop = self.end + 1 if self.inclusive else self.end
|
|
287
|
+
else:
|
|
288
|
+
stop = self.end - 1 if self.inclusive else self.end
|
|
285
289
|
return iter(range(self.start, stop, self.step))
|
|
286
290
|
|
|
287
291
|
def __len__(self):
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
if self.step > 0:
|
|
293
|
+
stop = self.end + 1 if self.inclusive else self.end
|
|
294
|
+
return max(0, (stop - self.start + self.step - 1) // self.step)
|
|
295
|
+
else:
|
|
296
|
+
stop = self.end - 1 if self.inclusive else self.end
|
|
297
|
+
return max(0, (self.start - stop + (-self.step) - 1) // (-self.step))
|
|
290
298
|
|
|
291
299
|
def __repr__(self):
|
|
292
300
|
op = "..=" if self.inclusive else ".."
|
|
293
|
-
|
|
301
|
+
s = f" step {self.step}" if self.step not in (1, -1) else ("" if self.step == 1 else " step -1")
|
|
302
|
+
return f"{self.start}{op}{self.end}{s}"
|
|
294
303
|
|
|
295
304
|
|
|
296
305
|
class InScriptGenerator:
|
|
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
|