inscript-lang 2.1.1__tar.gz → 2.2.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.
Files changed (30) hide show
  1. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/PKG-INFO +1 -1
  2. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/ast_nodes.py +16 -0
  3. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript.py +1 -1
  4. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/PKG-INFO +1 -1
  5. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/interpreter.py +46 -1
  6. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/lexer.py +4 -1
  7. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/parser.py +131 -8
  8. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/pyproject.toml +1 -1
  9. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/repl.py +1 -1
  10. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/README.md +0 -0
  11. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/analyzer.py +0 -0
  12. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/compiler.py +0 -0
  13. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/environment.py +0 -0
  14. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/errors.py +0 -0
  15. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_fmt.py +0 -0
  16. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/SOURCES.txt +0 -0
  17. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
  18. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/entry_points.txt +0 -0
  19. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/requires.txt +0 -0
  20. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/top_level.txt +0 -0
  21. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_test.py +0 -0
  22. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/pygame_backend.py +0 -0
  23. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/setup.cfg +0 -0
  24. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/setup.py +0 -0
  25. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib.py +0 -0
  26. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib_extended.py +0 -0
  27. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib_extended_2.py +0 -0
  28. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib_game.py +0 -0
  29. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib_values.py +0 -0
  30. {inscript_lang-2.1.1 → inscript_lang-2.2.0}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.1.1
3
+ Version: 2.2.0
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
@@ -735,3 +735,19 @@ class SelectStmt(Node):
735
735
  Each clause: {"kind": "recv"/"send"/"timeout", "var":str|None, "channel":expr|None, "value":expr|None, "duration":expr|None, "body":BlockStmt}
736
736
  """
737
737
  clauses: list # list of clause dicts
738
+
739
+ # ── v2.2.0 new nodes ─────────────────────────────────────────────────────────
740
+
741
+ @dataclass
742
+ class WithExpr(Node):
743
+ """with expr { .field = val, ... } — clone-and-modify pattern."""
744
+ obj: object # expression producing the base object
745
+ fields: list # list of (str field_name, expr value)
746
+
747
+ @dataclass
748
+ class DestructParam(Node):
749
+ """Destructuring parameter: fn f({x, y}: Point) or fn f([head, ...tail]: [])"""
750
+ kind: str # 'dict' or 'array'
751
+ names: list # list of str (field/element names to bind)
752
+ rest: object # str name for rest element (...tail), or None
753
+ type_ann: object # TypeAnnotation or None
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
24
24
  SemanticError, InScriptRuntimeError,
25
25
  MultiError, InScriptWarning)
26
26
 
27
- VERSION = "2.1.1"
27
+ VERSION = "2.2.0"
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.1.1
3
+ Version: 2.2.0
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
@@ -1950,7 +1950,13 @@ class Interpreter(Visitor):
1950
1950
  op = node.op
1951
1951
  target = node.target
1952
1952
 
1953
- if op != "=":
1953
+ if op == "??=":
1954
+ # v2.2.0: null-coalescing assignment — only assign if current value is nil
1955
+ cur = self.visit(target)
1956
+ if cur is not None:
1957
+ return cur # already non-nil, do nothing
1958
+ # fall through to write val
1959
+ elif op != "=":
1954
1960
  # Compound assignment: strip '=' to get operator ("+=" → "+", "**=" → "**")
1955
1961
  cur = self.visit(target)
1956
1962
  sym_op = op.rstrip("=")
@@ -2314,6 +2320,22 @@ class Interpreter(Visitor):
2314
2320
  if getattr(param, 'is_variadic', False):
2315
2321
  env.define(param.name, avs[i:]); break
2316
2322
  val = avs[i] if i < len(avs) else (self.visit(param.default) if param.default else None)
2323
+ # v2.2.0: DestructParam — dict or array destructuring
2324
+ from ast_nodes import DestructParam as _DP
2325
+ if isinstance(param, _DP):
2326
+ if param.kind == 'dict':
2327
+ src = val if isinstance(val, dict) else (val.__dict__ if hasattr(val, '__dict__') else {})
2328
+ for fname in param.names:
2329
+ env.define(fname, src.get(fname))
2330
+ if param.rest:
2331
+ env.define(param.rest, {k: v for k, v in src.items() if k not in param.names})
2332
+ else: # array
2333
+ src = list(val) if val is not None else []
2334
+ for j, fname in enumerate(param.names):
2335
+ env.define(fname, src[j] if j < len(src) else None)
2336
+ if param.rest:
2337
+ env.define(param.rest, src[len(param.names):])
2338
+ continue
2317
2339
  if param.type_ann and val is not None:
2318
2340
  ann_name = param.type_ann.name
2319
2341
  if ann_name in generic_bindings:
@@ -2677,6 +2699,29 @@ class Interpreter(Visitor):
2677
2699
  val = self.visit(node.left)
2678
2700
  return self.visit(node.right) if val is None else val
2679
2701
 
2702
+ def visit_WithExpr(self, node) -> Any:
2703
+ """v2.2.0: with obj { .field = val, ... } — clone and modify."""
2704
+ from stdlib_values import InScriptInstance
2705
+ base = self.visit(node.obj)
2706
+ if isinstance(base, InScriptInstance):
2707
+ clone = InScriptInstance(
2708
+ struct_name=base.struct_name,
2709
+ fields=dict(base.fields),
2710
+ )
2711
+ elif isinstance(base, dict):
2712
+ clone = dict(base)
2713
+ else:
2714
+ import copy; clone = copy.copy(base)
2715
+ for field_name, val_expr in node.fields:
2716
+ val = self.visit(val_expr)
2717
+ if isinstance(clone, InScriptInstance):
2718
+ clone.fields[field_name] = val
2719
+ elif isinstance(clone, dict):
2720
+ clone[field_name] = val
2721
+ else:
2722
+ _set_attr(clone, field_name, val, node.line, self)
2723
+ return clone
2724
+
2680
2725
  def visit_PipeExpr(self, node: PipeExpr) -> Any:
2681
2726
  """value |> fn — calls fn(value)."""
2682
2727
  val = self.visit(node.value)
@@ -126,6 +126,7 @@ class TT(Enum):
126
126
  HASH = auto() # # (attribute / annotation)
127
127
  AT = auto() # @ (decorator)
128
128
  NULLISH = auto() # ?? (nullish coalescing)
129
+ NULLISH_EQ = auto() # ??= (null-coalescing assignment) v2.2.0
129
130
  QUESTION_DOT= auto() # ?. (optional chaining)
130
131
  PIPE_GT = auto() # |> (pipe operator)
131
132
  FSTRING = auto() # f"..." (interpolated string — value is raw template)
@@ -574,7 +575,9 @@ class Lexer:
574
575
  if self.match(":"): emit(TT.DOUBLE_COLON, "::")
575
576
  else: emit(TT.COLON, ":")
576
577
  elif ch == "?":
577
- if self.match("?"): emit(TT.NULLISH, "??")
578
+ if self.match("?"):
579
+ if self.match("="): emit(TT.NULLISH_EQ, "??=") # v2.2.0
580
+ else: emit(TT.NULLISH, "??")
578
581
  elif self.match("."): emit(TT.QUESTION_DOT, "?.")
579
582
  else: emit(TT.QUESTION, "?")
580
583
  elif ch == "#":
@@ -402,8 +402,24 @@ class Parser:
402
402
  line, col = self._pos()
403
403
 
404
404
  # Tuple type: (int, float) — used for multiple return values
405
+ # v2.2.0: named return type (min: float, max: float)
405
406
  if self.check(TT.LPAREN):
406
407
  self.advance() # consume '('
408
+ # Detect named tuple: (IDENT COLON ...)
409
+ if (self.current.type == TT.IDENT
410
+ and self.peek.type == TT.COLON):
411
+ named_fields = []
412
+ while True:
413
+ fname = self.expect_ident("Expected field name in named return type")
414
+ self.expect(TT.COLON, "Expected ':' after field name")
415
+ ftype = self.parse_type_annotation()
416
+ named_fields.append((fname, ftype))
417
+ if not self.match(TT.COMMA):
418
+ break
419
+ self.expect(TT.RPAREN, "Expected ')' to close named return type")
420
+ ann = TypeAnnotation(name="NamedTuple", line=line, col=col)
421
+ ann.named_fields = named_fields
422
+ return ann
407
423
  # Just consume the tuple type contents and treat it as "Tuple"
408
424
  depth = 1
409
425
  while not self.is_at_end() and depth > 0:
@@ -416,12 +432,16 @@ class Parser:
416
432
  self.advance() # consume final ')'
417
433
  return TypeAnnotation(name="Tuple", line=line, col=col)
418
434
 
419
- # Array type: [int]
435
+ # Array type: [int] or bare [] (any-element array)
420
436
  if self.match(TT.LBRACKET):
421
- inner = self.parse_type_annotation()
422
- self.expect(TT.RBRACKET, "Expected ']' to close array type")
423
- ann = TypeAnnotation(name="Array", is_array=True,
424
- generics=[inner], line=line, col=col)
437
+ if self.check(TT.RBRACKET):
438
+ self.advance() # consume ']' bare [] means Array<any>
439
+ ann = TypeAnnotation(name="Array", is_array=True, line=line, col=col)
440
+ else:
441
+ inner = self.parse_type_annotation()
442
+ self.expect(TT.RBRACKET, "Expected ']' to close array type")
443
+ ann = TypeAnnotation(name="Array", is_array=True,
444
+ generics=[inner], line=line, col=col)
425
445
  # Dict type: {string: int}
426
446
  elif self.match(TT.LBRACE):
427
447
  key = self.parse_type_annotation()
@@ -647,6 +667,47 @@ class Parser:
647
667
  elif self.check(TT.ELLIPSIS):
648
668
  self.advance() # consume '...' (alias for variadic)
649
669
  is_variadic = True
670
+
671
+ # v2.2.0: dict destructuring param — fn f({x, y}: Point)
672
+ if self.check(TT.LBRACE):
673
+ self.advance() # consume '{'
674
+ names = []
675
+ rest = None
676
+ while not self.check(TT.RBRACE) and not self.check(TT.EOF):
677
+ if self.check(TT.ELLIPSIS):
678
+ self.advance()
679
+ rest = self.expect_ident("Expected rest name after '...'")
680
+ break
681
+ names.append(self.expect_ident("Expected field name"))
682
+ self.match(TT.COMMA)
683
+ self.expect(TT.RBRACE, "Expected '}' to close destructuring param")
684
+ type_ann = None
685
+ if self.match(TT.COLON):
686
+ type_ann = self.parse_type_annotation()
687
+ from ast_nodes import DestructParam
688
+ return DestructParam(kind='dict', names=names, rest=rest,
689
+ type_ann=type_ann, line=line, col=col)
690
+
691
+ # v2.2.0: array destructuring param — fn f([head, ...tail]: [])
692
+ if self.check(TT.LBRACKET):
693
+ self.advance() # consume '['
694
+ names = []
695
+ rest = None
696
+ while not self.check(TT.RBRACKET) and not self.check(TT.EOF):
697
+ if self.check(TT.ELLIPSIS):
698
+ self.advance()
699
+ rest = self.expect_ident("Expected rest name after '...'")
700
+ break
701
+ names.append(self.expect_ident("Expected element binding"))
702
+ self.match(TT.COMMA)
703
+ self.expect(TT.RBRACKET, "Expected ']' to close destructuring param")
704
+ type_ann = None
705
+ if self.match(TT.COLON):
706
+ type_ann = self.parse_type_annotation()
707
+ from ast_nodes import DestructParam
708
+ return DestructParam(kind='array', names=names, rest=rest,
709
+ type_ann=type_ann, line=line, col=col)
710
+
650
711
  name = self.expect_ident("Expected parameter name")
651
712
  type_ann = None
652
713
  if self.match(TT.COLON):
@@ -1418,6 +1479,7 @@ class Parser:
1418
1479
  TT.CARET_EQ: "^=",
1419
1480
  TT.LSHIFT_EQ: "<<=",
1420
1481
  TT.RSHIFT_EQ: ">>=",
1482
+ TT.NULLISH_EQ: "??=", # v2.2.0
1421
1483
  }
1422
1484
 
1423
1485
  if self.current.type in ASSIGN_OPS:
@@ -1427,7 +1489,8 @@ class Parser:
1427
1489
 
1428
1490
  # Extract the binary operator from compound ops (strip trailing =)
1429
1491
  # e.g. "+=" → "+", "**=" → "**"
1430
- binop = op.rstrip("=") if op != "=" else None
1492
+ # "??=" is special — not a binary op, handled by interpreter directly
1493
+ binop = op.rstrip("=") if op not in ("=", "??=") else None
1431
1494
 
1432
1495
  # Determine which assignment node to create
1433
1496
  if isinstance(expr, IdentExpr):
@@ -1594,8 +1657,32 @@ class Parser:
1594
1657
  while self.check(TT.LT, TT.GT, TT.LTE, TT.GTE):
1595
1658
  op = self.advance().value
1596
1659
  right = self.parse_shift()
1597
- left = BinaryExpr(left=left, op=op, right=right,
1598
- line=line, col=col)
1660
+ node = BinaryExpr(left=left, op=op, right=right,
1661
+ line=line, col=col)
1662
+ # v2.2.0: chained comparisons — 0 < x < 10 → (0<x) and (x<10)
1663
+ # If another comparison operator follows, desugar into `and`
1664
+ if self.check(TT.LT, TT.GT, TT.LTE, TT.GTE):
1665
+ left = node # left side of `and` is current comparison
1666
+ # `right` becomes the new LHS for the next comparison
1667
+ # We need to avoid evaluating `right` twice, but in a simple
1668
+ # interpreter it's safe to reuse the same AST node (no side effects
1669
+ # on simple expressions). Build: node AND (right op next)
1670
+ op2 = self.advance().value
1671
+ right2 = self.parse_shift()
1672
+ rhs = BinaryExpr(left=right, op=op2, right=right2,
1673
+ line=line, col=col)
1674
+ left = BinaryExpr(left=node, op="&&", right=rhs,
1675
+ line=line, col=col)
1676
+ while self.check(TT.LT, TT.GT, TT.LTE, TT.GTE):
1677
+ op3 = self.advance().value
1678
+ right3 = self.parse_shift()
1679
+ rhs3 = BinaryExpr(left=right2, op=op3, right=right3,
1680
+ line=line, col=col)
1681
+ left = BinaryExpr(left=left, op="&&", right=rhs3,
1682
+ line=line, col=col)
1683
+ right2 = right3
1684
+ return left
1685
+ left = node
1599
1686
  return left
1600
1687
 
1601
1688
  def parse_shift(self) -> Node:
@@ -1879,6 +1966,25 @@ class Parser:
1879
1966
  # Grouped expression: (expr) OR Tuple: (expr, expr, ...)
1880
1967
  if tok.type == TT.LPAREN:
1881
1968
  self.advance() # consume '('
1969
+ # v2.2.0: named return tuple literal — (name: expr, name: expr, ...)
1970
+ # Detect: IDENT COLON pattern at the start
1971
+ if (self.current.type == TT.IDENT
1972
+ and self.peek.type == TT.COLON):
1973
+ pairs = []
1974
+ while True:
1975
+ key = self.expect_ident("Expected field name in named tuple")
1976
+ self.expect(TT.COLON, "Expected ':' after field name")
1977
+ val = self.parse_expr()
1978
+ pairs.append((key, val))
1979
+ if not self.match(TT.COMMA):
1980
+ break
1981
+ self.expect(TT.RPAREN, "Expected ')' to close named tuple")
1982
+ # Return as a DictLiteralExpr with string-key pairs
1983
+ return DictLiteralExpr(
1984
+ pairs=[(StringLiteralExpr(value=k, line=line, col=col), v)
1985
+ for k, v in pairs],
1986
+ line=line, col=col
1987
+ )
1882
1988
  first = self.parse_expr()
1883
1989
  if self.check(TT.COMMA):
1884
1990
  # It's a tuple literal
@@ -1948,6 +2054,23 @@ class Parser:
1948
2054
  self.advance() # consume 'comptime'
1949
2055
  body = self.parse_block()
1950
2056
  return ComptimeExpr(body=body, line=line, col=col)
2057
+ # v2.2.0: with expr { .field = val, ... } — clone-and-modify
2058
+ if tok.value == "with":
2059
+ self.advance() # consume 'with'
2060
+ obj = self.parse_postfix()
2061
+ self.expect(TT.LBRACE, "Expected '{' after 'with <expr>'")
2062
+ fields = []
2063
+ while not self.check(TT.RBRACE) and not self.check(TT.EOF):
2064
+ self.expect(TT.DOT, "Expected '.field = val' inside with block")
2065
+ field_name = self.expect_ident("Expected field name after '.'")
2066
+ self.expect(TT.ASSIGN, "Expected '=' after field name in with block")
2067
+ val = self.parse_expr()
2068
+ fields.append((field_name, val))
2069
+ self.match(TT.COMMA)
2070
+ self.match(TT.NEWLINE) if hasattr(TT, 'NEWLINE') else None
2071
+ self.expect(TT.RBRACE, "Expected '}' to close with block")
2072
+ from ast_nodes import WithExpr
2073
+ return WithExpr(obj=obj, fields=fields, line=line, col=col)
1951
2074
  return self.parse_ident_or_struct_init()
1952
2075
 
1953
2076
  # Type keywords used as constructors: Vec2(…) → these lex as IDENT anyway
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inscript-lang"
7
- version = "2.1.1"
7
+ version = "2.2.0"
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.1.1"
43
+ VERSION = "2.2.0"
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