inscript-lang 2.0.1__tar.gz → 2.0.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.
Files changed (30) hide show
  1. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/PKG-INFO +1 -1
  2. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript.py +1 -1
  3. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript_lang.egg-info/PKG-INFO +1 -1
  4. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/interpreter.py +90 -39
  5. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/parser.py +21 -3
  6. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/pyproject.toml +1 -1
  7. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/repl.py +1 -1
  8. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/README.md +0 -0
  9. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/analyzer.py +0 -0
  10. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/ast_nodes.py +0 -0
  11. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/compiler.py +0 -0
  12. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/environment.py +0 -0
  13. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/errors.py +0 -0
  14. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript_fmt.py +0 -0
  15. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript_lang.egg-info/SOURCES.txt +0 -0
  16. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript_lang.egg-info/dependency_links.txt +0 -0
  17. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript_lang.egg-info/entry_points.txt +0 -0
  18. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript_lang.egg-info/requires.txt +0 -0
  19. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript_lang.egg-info/top_level.txt +0 -0
  20. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/inscript_test.py +0 -0
  21. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/lexer.py +0 -0
  22. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/pygame_backend.py +0 -0
  23. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/setup.cfg +0 -0
  24. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/setup.py +0 -0
  25. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/stdlib.py +0 -0
  26. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/stdlib_extended.py +0 -0
  27. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/stdlib_extended_2.py +0 -0
  28. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/stdlib_game.py +0 -0
  29. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/stdlib_values.py +0 -0
  30. {inscript_lang-2.0.1 → inscript_lang-2.0.3}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
24
24
  SemanticError, InScriptRuntimeError,
25
25
  MultiError, InScriptWarning)
26
26
 
27
- VERSION = "2.0.1"
27
+ VERSION = "2.0.3"
28
28
 
29
29
  MANIFEST_FILENAME = "inscript.toml"
30
30
  LOCK_FILENAME = "inscript.lock"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -993,7 +993,17 @@ class Interpreter(Visitor):
993
993
  return self.visit(node.expr)
994
994
 
995
995
  def visit_PrintStmt(self, node: PrintStmt) -> Any:
996
- parts = [_inscript_str(self.visit(a)) for a in node.args]
996
+ # v2.0.3: spread args in print(...arr)
997
+ parts = []
998
+ for a in node.args:
999
+ if isinstance(a, SpreadExpr):
1000
+ val = self.visit(a.expr)
1001
+ if isinstance(val, list):
1002
+ parts.extend(_inscript_str(v) for v in val)
1003
+ else:
1004
+ parts.append(_inscript_str(val))
1005
+ else:
1006
+ parts.append(_inscript_str(self.visit(a)))
997
1007
  print(" ".join(parts))
998
1008
  return None
999
1009
 
@@ -2425,10 +2435,22 @@ class Interpreter(Visitor):
2425
2435
  fields[field.name] = self.visit(field.default) if field.default else None
2426
2436
 
2427
2437
  # Then overrides from initializer
2438
+ # v2.0.3: spread syntax — P{...defaults, x: 5}
2428
2439
  provided_names = set()
2429
2440
  for name, value_node in node.fields:
2430
- fields[name] = self.visit(value_node)
2431
- provided_names.add(name)
2441
+ if name == "..." and isinstance(value_node, SpreadExpr):
2442
+ # Spread a dict, struct instance, or dict-like object
2443
+ spread_val = self.visit(value_node.expr)
2444
+ if isinstance(spread_val, InScriptInstance):
2445
+ for k, v in spread_val.fields.items():
2446
+ fields[k] = v; provided_names.add(k)
2447
+ elif isinstance(spread_val, dict):
2448
+ for k, v in spread_val.items():
2449
+ if not str(k).startswith("_"):
2450
+ fields[k] = v; provided_names.add(k)
2451
+ else:
2452
+ fields[name] = self.visit(value_node)
2453
+ provided_names.add(name)
2432
2454
 
2433
2455
  # BUG-16 fix: warn about required fields (no default) that were not provided
2434
2456
  missing_required = [
@@ -2516,41 +2538,76 @@ class Interpreter(Visitor):
2516
2538
  cond = self.visit(node.condition)
2517
2539
  return self.visit(node.then_expr) if cond else self.visit(node.else_expr)
2518
2540
 
2541
+ @staticmethod
2542
+ def _fstring_segments(template: str):
2543
+ """v2.0.2: brace-depth-aware f-string segment splitter.
2544
+ Returns list of (kind, text) where kind is "lit" or "expr".
2545
+ Handles nested braces (match, for, object literals) correctly.
2546
+ {{ and }} in source are stored as sentinel \x00{ and }\x00.
2547
+ """
2548
+ segments = []
2549
+ i = 0
2550
+ n = len(template)
2551
+ lit_buf = []
2552
+ while i < n:
2553
+ ch = template[i]
2554
+ # Sentinel escaped braces: \x00{ = literal {, }\x00 = literal }
2555
+ if ch == "\x00" and i + 1 < n and template[i + 1] == "{":
2556
+ lit_buf.append("{"); i += 2
2557
+ elif ch == "}" and i + 1 < n and template[i + 1] == "\x00":
2558
+ lit_buf.append("}"); i += 2
2559
+ elif ch == "{":
2560
+ # Start of an expression — find matching } with depth tracking
2561
+ if lit_buf:
2562
+ segments.append(("lit", "".join(lit_buf))); lit_buf = []
2563
+ depth = 1; j = i + 1; in_str = None
2564
+ while j < n and depth > 0:
2565
+ c2 = template[j]
2566
+ if in_str:
2567
+ if c2 == "\\" and j + 1 < n: j += 2; continue
2568
+ if c2 == in_str: in_str = None
2569
+ elif c2 in ('"', "\'"):
2570
+ in_str = c2
2571
+ elif c2 == "{":
2572
+ depth += 1
2573
+ elif c2 == "}":
2574
+ depth -= 1
2575
+ if depth == 0: break
2576
+ j += 1
2577
+ segments.append(("expr", template[i + 1:j]))
2578
+ i = j + 1
2579
+ else:
2580
+ lit_buf.append(ch); i += 1
2581
+ if lit_buf:
2582
+ segments.append(("lit", "".join(lit_buf)))
2583
+ return segments
2584
+
2519
2585
  def visit_FStringExpr(self, node: FStringExpr) -> Any:
2520
2586
  """f"Hello {name}, score={score:.1f}" — evaluate {expr} segments at runtime.
2587
+ v2.0.2: brace-depth-aware splitter handles match/for/object literals inside {}.
2521
2588
  Supports Python-style format specs: {x:.2f}, {n:06d}, {s:>20} etc.
2522
- {{ and }} in source produce literal braces (stored as \x00{ and }\x00 sentinels)."""
2523
- import re
2589
+ {{ and }} in source produce literal braces."""
2524
2590
  result = []
2525
- template = node.template
2526
- pos = 0
2527
- for m in re.finditer(r'(?<!\x00)\{([^}\x00][^}]*)\}', template):
2528
- result.append(template[pos:m.start()])
2529
- inner = m.group(1).strip()
2530
-
2531
- # Split off format spec: {expr:spec}
2532
- # Key: only split on ':' that is NOT inside brackets AND NOT after '?'
2533
- # (ternary x ? a : b uses ':' but it's not a format spec)
2591
+ for kind, text in self._fstring_segments(node.template):
2592
+ if kind == "lit":
2593
+ result.append(text)
2594
+ continue
2595
+ inner = text.strip()
2596
+ # Split off format spec {expr:spec}, respecting brackets and ternary
2534
2597
  fmt_spec = None
2535
- depth = 0
2536
- ternary_depth = 0
2537
- split_at = -1
2598
+ depth = 0; ternary_depth = 0; split_at = -1
2538
2599
  for i, ch in enumerate(inner):
2539
- if ch in '([{': depth += 1
2540
- elif ch in ')]}': depth -= 1
2541
- elif ch == '?' and depth == 0: ternary_depth += 1
2542
- elif ch == ':' and depth == 0:
2543
- if ternary_depth > 0:
2544
- ternary_depth -= 1 # this ':' closes the ternary
2545
- else:
2546
- split_at = i
2547
- break
2600
+ if ch in "([{": depth += 1
2601
+ elif ch in ")]}": depth -= 1
2602
+ elif ch == "?" and depth == 0: ternary_depth += 1
2603
+ elif ch == ":" and depth == 0:
2604
+ if ternary_depth > 0: ternary_depth -= 1
2605
+ else: split_at = i; break
2548
2606
  if split_at > 0:
2549
- expr_src = inner[:split_at].strip()
2550
- fmt_spec = inner[split_at+1:].strip()
2607
+ expr_src = inner[:split_at].strip()
2608
+ fmt_spec = inner[split_at + 1:].strip()
2551
2609
  else:
2552
2610
  expr_src = inner
2553
-
2554
2611
  try:
2555
2612
  from parser import parse
2556
2613
  prog = parse(expr_src)
@@ -2558,19 +2615,13 @@ class Interpreter(Visitor):
2558
2615
  for stmt in prog.body:
2559
2616
  val = self.visit(stmt)
2560
2617
  if fmt_spec:
2561
- # Apply Python format spec directly — supports .2f, 06d, >10s etc.
2562
- try:
2563
- result.append(format(val, fmt_spec))
2564
- except Exception:
2565
- result.append(_inscript_str(val))
2618
+ try: result.append(format(val, fmt_spec))
2619
+ except: result.append(_inscript_str(val))
2566
2620
  else:
2567
2621
  result.append(_inscript_str(val))
2568
2622
  except Exception:
2569
- result.append(f"{{{inner}}}")
2570
- pos = m.end()
2571
- result.append(template[pos:])
2572
- final = "".join(result).replace("\x00{", "{").replace("}\x00", "}")
2573
- return final
2623
+ result.append("{" + inner + "}")
2624
+ return "".join(result)
2574
2625
 
2575
2626
  def visit_DestructureDecl(self, node: DestructureDecl) -> Any:
2576
2627
  """let [a, b, c] = arr | let {x, y} = point | let [[a,b],[c,d]] = nested"""
@@ -1369,12 +1369,19 @@ class Parser:
1369
1369
 
1370
1370
  self.expect(TT.LPAREN, "Expected '(' after 'print'")
1371
1371
  args = []
1372
+ def _parse_print_arg():
1373
+ # v2.0.3: allow spread in print(...args)
1374
+ if self.check(TT.ELLIPSIS):
1375
+ sl2, sc2 = self._pos()
1376
+ self.advance()
1377
+ return SpreadExpr(expr=self.parse_expr(), line=sl2, col=sc2)
1378
+ return self.parse_expr()
1372
1379
  if not self.check(TT.RPAREN):
1373
- args.append(self.parse_expr())
1380
+ args.append(_parse_print_arg())
1374
1381
  while self.match(TT.COMMA):
1375
1382
  if self.check(TT.RPAREN):
1376
1383
  break
1377
- args.append(self.parse_expr())
1384
+ args.append(_parse_print_arg())
1378
1385
  self.expect(TT.RPAREN, "Expected ')' after print arguments")
1379
1386
  self.match(TT.SEMICOLON)
1380
1387
  return PrintStmt(args=args, line=line, col=col)
@@ -2072,18 +2079,29 @@ class Parser:
2072
2079
  result = (
2073
2080
  self.check(TT.RBRACE) # empty Counter {}
2074
2081
  or (self.current.type == TT.IDENT and self.peek.type == TT.COLON)
2082
+ or self.current.type == TT.ELLIPSIS # v2.0.3: spread P{...d}
2075
2083
  )
2076
2084
  self.pos = saved
2077
2085
  return result
2078
2086
 
2079
2087
  def parse_struct_init(self, name: str, line: int, col: int) -> StructInitExpr:
2080
- """Player { pos: Vec2(0,0), health: 100 }"""
2088
+ """Player { pos: Vec2(0,0), health: 100 }
2089
+ v2.0.3: spread syntax: Player { ...defaults, health: 100 }
2090
+ """
2081
2091
  self.advance() # consume '{'
2082
2092
  fields = []
2083
2093
 
2084
2094
  while not self.check(TT.RBRACE) and not self.is_at_end():
2085
2095
  if self.match(TT.SEMICOLON) or self.match(TT.COMMA):
2086
2096
  continue
2097
+ # v2.0.3: spread field — P{...defaults, x: 5}
2098
+ if self.check(TT.ELLIPSIS):
2099
+ sl2, sc2 = self._pos()
2100
+ self.advance() # consume '...'
2101
+ spread_expr = self.parse_expr()
2102
+ fields.append(("...", SpreadExpr(expr=spread_expr, line=sl2, col=sc2)))
2103
+ self.match(TT.COMMA)
2104
+ continue
2087
2105
  field_name = self.expect_ident("Expected field name in struct initializer")
2088
2106
  self.expect(TT.COLON, f"Expected ':' after field name '{field_name}'")
2089
2107
  value = self.parse_expr()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inscript-lang"
7
- version = "2.0.1"
7
+ version = "2.0.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 = "2.0.1"
43
+ VERSION = "2.0.3"
44
44
 
45
45
  # ── ANSI colours ──────────────────────────────────────────────────────────────
46
46
  def _c(code, text):
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