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.
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/PKG-INFO +1 -1
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/ast_nodes.py +16 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript.py +1 -1
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/interpreter.py +46 -1
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/lexer.py +4 -1
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/parser.py +131 -8
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/pyproject.toml +1 -1
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/repl.py +1 -1
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/README.md +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/analyzer.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/compiler.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/environment.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/errors.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_fmt.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/inscript_test.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/pygame_backend.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/setup.cfg +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/setup.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib_extended.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib_extended_2.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib_game.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/stdlib_values.py +0 -0
- {inscript_lang-2.1.1 → inscript_lang-2.2.0}/vm.py +0 -0
|
@@ -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
|
|
@@ -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("?"):
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1598
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|