inscript-lang 2.1.2__tar.gz → 2.3.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.2 → inscript_lang-2.3.0}/PKG-INFO +1 -1
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/ast_nodes.py +21 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript.py +1 -1
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/interpreter.py +185 -19
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/lexer.py +4 -1
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/parser.py +164 -8
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/pyproject.toml +1 -1
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/repl.py +1 -1
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/stdlib.py +71 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/stdlib_values.py +148 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/README.md +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/analyzer.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/compiler.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/environment.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/errors.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript_fmt.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/inscript_test.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/pygame_backend.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/setup.cfg +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/setup.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/stdlib_extended.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/stdlib_extended_2.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/stdlib_game.py +0 -0
- {inscript_lang-2.1.2 → inscript_lang-2.3.0}/vm.py +0 -0
|
@@ -735,3 +735,24 @@ 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
|
|
754
|
+
|
|
755
|
+
@dataclass
|
|
756
|
+
class TaskSpawnExpr(Node):
|
|
757
|
+
"""v2.3.0: spawn <expr> — run expression concurrently, return InScriptTask."""
|
|
758
|
+
expr: object
|
|
@@ -421,13 +421,16 @@ class Interpreter(Visitor):
|
|
|
421
421
|
"is_bool": lambda v: isinstance(v, bool),
|
|
422
422
|
"is_array": lambda v: isinstance(v, list),
|
|
423
423
|
"is_dict": lambda v: isinstance(v, dict),
|
|
424
|
-
# ── Concurrency
|
|
425
|
-
"thread":
|
|
426
|
-
"channel": lambda cap=0:
|
|
427
|
-
"make_channel": lambda cap=0:
|
|
428
|
-
"chan_send":
|
|
429
|
-
"chan_recv":
|
|
430
|
-
"
|
|
424
|
+
# ── Concurrency v2.3.0 ───────────────────────────────────────────
|
|
425
|
+
"thread": lambda fn, *args: self._spawn_thread(fn, list(args)),
|
|
426
|
+
"channel": lambda cap=0: __import__('stdlib_values').InScriptChannel(int(cap) if cap else 0),
|
|
427
|
+
"make_channel": lambda cap=0: __import__('stdlib_values').InScriptChannel(int(cap) if cap else 0),
|
|
428
|
+
"chan_send": lambda ch, v: ch.send(v) if hasattr(ch, 'send') else (ch["_channel"].put(v) if isinstance(ch, dict) and "_channel" in ch else None),
|
|
429
|
+
"chan_recv": lambda ch: ch.recv() if hasattr(ch, 'recv') else (ch["_channel"].get(timeout=5) if isinstance(ch, dict) and "_channel" in ch else None),
|
|
430
|
+
"chan_try_recv": lambda ch: ch.try_recv() if hasattr(ch, 'try_recv') else None,
|
|
431
|
+
"mutex": lambda v=None: __import__('stdlib_values').InScriptMutex(v),
|
|
432
|
+
"rwlock": lambda v=None: __import__('stdlib_values').InScriptRWLock(v),
|
|
433
|
+
"sleep": lambda s: __import__('time').sleep(s),
|
|
431
434
|
# ── JSON ──────────────────────────────────────────────────────────
|
|
432
435
|
"json_encode": lambda v: __import__('json').dumps(v),
|
|
433
436
|
"json_decode": lambda s: __import__('json').loads(s),
|
|
@@ -1080,6 +1083,30 @@ class Interpreter(Visitor):
|
|
|
1080
1083
|
if isinstance(iterable, dict) and iterable.get("_enum_definition"):
|
|
1081
1084
|
variants = [v for k, v in iterable.items() if not k.startswith("_")]
|
|
1082
1085
|
iterable = variants
|
|
1086
|
+
# v2.3.0: async for var in channel { } — drain channel until closed
|
|
1087
|
+
from stdlib_values import InScriptChannel as _ISCh
|
|
1088
|
+
if isinstance(iterable, _ISCh) and getattr(node, '_async_iter', False):
|
|
1089
|
+
while not (iterable.closed and iterable._q.empty()):
|
|
1090
|
+
val = iterable.try_recv()
|
|
1091
|
+
if val is None:
|
|
1092
|
+
if iterable.closed:
|
|
1093
|
+
break
|
|
1094
|
+
import time as _t; _t.sleep(0.001)
|
|
1095
|
+
continue
|
|
1096
|
+
self._push("for")
|
|
1097
|
+
self._env.define(node.var_name, val)
|
|
1098
|
+
try:
|
|
1099
|
+
self._exec_block_no_scope(node.body)
|
|
1100
|
+
except BreakSignal as sig:
|
|
1101
|
+
self._pop()
|
|
1102
|
+
if sig.label: raise
|
|
1103
|
+
break
|
|
1104
|
+
except ContinueSignal as sig:
|
|
1105
|
+
self._pop()
|
|
1106
|
+
if sig.label: raise
|
|
1107
|
+
continue
|
|
1108
|
+
self._pop()
|
|
1109
|
+
return
|
|
1083
1110
|
# Support list, range, InScriptRange, string, dict, generator
|
|
1084
1111
|
if isinstance(iterable, InScriptRange):
|
|
1085
1112
|
it = iter(iterable)
|
|
@@ -1950,7 +1977,13 @@ class Interpreter(Visitor):
|
|
|
1950
1977
|
op = node.op
|
|
1951
1978
|
target = node.target
|
|
1952
1979
|
|
|
1953
|
-
if op
|
|
1980
|
+
if op == "??=":
|
|
1981
|
+
# v2.2.0: null-coalescing assignment — only assign if current value is nil
|
|
1982
|
+
cur = self.visit(target)
|
|
1983
|
+
if cur is not None:
|
|
1984
|
+
return cur # already non-nil, do nothing
|
|
1985
|
+
# fall through to write val
|
|
1986
|
+
elif op != "=":
|
|
1954
1987
|
# Compound assignment: strip '=' to get operator ("+=" → "+", "**=" → "**")
|
|
1955
1988
|
cur = self.visit(target)
|
|
1956
1989
|
sym_op = op.rstrip("=")
|
|
@@ -2314,6 +2347,22 @@ class Interpreter(Visitor):
|
|
|
2314
2347
|
if getattr(param, 'is_variadic', False):
|
|
2315
2348
|
env.define(param.name, avs[i:]); break
|
|
2316
2349
|
val = avs[i] if i < len(avs) else (self.visit(param.default) if param.default else None)
|
|
2350
|
+
# v2.2.0: DestructParam — dict or array destructuring
|
|
2351
|
+
from ast_nodes import DestructParam as _DP
|
|
2352
|
+
if isinstance(param, _DP):
|
|
2353
|
+
if param.kind == 'dict':
|
|
2354
|
+
src = val if isinstance(val, dict) else (val.__dict__ if hasattr(val, '__dict__') else {})
|
|
2355
|
+
for fname in param.names:
|
|
2356
|
+
env.define(fname, src.get(fname))
|
|
2357
|
+
if param.rest:
|
|
2358
|
+
env.define(param.rest, {k: v for k, v in src.items() if k not in param.names})
|
|
2359
|
+
else: # array
|
|
2360
|
+
src = list(val) if val is not None else []
|
|
2361
|
+
for j, fname in enumerate(param.names):
|
|
2362
|
+
env.define(fname, src[j] if j < len(src) else None)
|
|
2363
|
+
if param.rest:
|
|
2364
|
+
env.define(param.rest, src[len(param.names):])
|
|
2365
|
+
continue
|
|
2317
2366
|
if param.type_ann and val is not None:
|
|
2318
2367
|
ann_name = param.type_ann.name
|
|
2319
2368
|
if ann_name in generic_bindings:
|
|
@@ -2502,11 +2551,10 @@ class Interpreter(Visitor):
|
|
|
2502
2551
|
|
|
2503
2552
|
def visit_AwaitExpr(self, node: AwaitExpr) -> Any:
|
|
2504
2553
|
"""
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
so they are catchable by InScript try/catch blocks.
|
|
2554
|
+
v2.3.0: await <expr>
|
|
2555
|
+
InScript async fns return InScriptCoroutine. We drive them to completion
|
|
2556
|
+
using send(None) — the coroutine body runs synchronously and raises
|
|
2557
|
+
StopIteration(result) immediately (InScript has no real IO yields).
|
|
2510
2558
|
"""
|
|
2511
2559
|
val = self.visit(node.expr)
|
|
2512
2560
|
if isinstance(val, InScriptCoroutine):
|
|
@@ -2516,17 +2564,77 @@ class Interpreter(Visitor):
|
|
|
2516
2564
|
except StopIteration as e:
|
|
2517
2565
|
return e.value
|
|
2518
2566
|
except InScriptRuntimeError:
|
|
2519
|
-
raise
|
|
2567
|
+
raise
|
|
2520
2568
|
except Exception as e:
|
|
2521
|
-
# Convert Python exceptions to InScriptRuntimeError so
|
|
2522
|
-
# they are catchable by InScript try/catch blocks.
|
|
2523
2569
|
err = InScriptRuntimeError(str(e), getattr(node, 'line', 0))
|
|
2524
2570
|
err.thrown_value = str(e)
|
|
2525
2571
|
raise err
|
|
2526
|
-
return val # plain value
|
|
2572
|
+
return val # plain value passthrough
|
|
2527
2573
|
|
|
2528
|
-
def visit_SpawnExpr(self, node
|
|
2529
|
-
|
|
2574
|
+
def visit_SpawnExpr(self, node) -> Any:
|
|
2575
|
+
"""ECS spawn — handled by game backend; stub in core interpreter."""
|
|
2576
|
+
return None
|
|
2577
|
+
|
|
2578
|
+
def visit_TaskSpawnExpr(self, node) -> Any:
|
|
2579
|
+
"""v2.3.0: spawn <expr> — run in background thread, return InScriptTask."""
|
|
2580
|
+
from stdlib_values import InScriptTask
|
|
2581
|
+
expr_val = self.visit(node.expr)
|
|
2582
|
+
if isinstance(expr_val, InScriptCoroutine):
|
|
2583
|
+
coro = expr_val.coro
|
|
2584
|
+
def _run_coro():
|
|
2585
|
+
try:
|
|
2586
|
+
coro.send(None)
|
|
2587
|
+
except StopIteration as e:
|
|
2588
|
+
return e.value
|
|
2589
|
+
return None
|
|
2590
|
+
task = InScriptTask(_run_coro, None)
|
|
2591
|
+
elif callable(expr_val):
|
|
2592
|
+
fn = expr_val
|
|
2593
|
+
def _run_fn():
|
|
2594
|
+
return self._call_fn(fn, [])
|
|
2595
|
+
task = InScriptTask(_run_fn, None)
|
|
2596
|
+
else:
|
|
2597
|
+
val = expr_val
|
|
2598
|
+
task = InScriptTask(lambda: val, None)
|
|
2599
|
+
return task
|
|
2600
|
+
|
|
2601
|
+
def visit_SelectStmt(self, node) -> Any:
|
|
2602
|
+
"""v2.3.0: select { case x = ch.recv() { } case ch.send(v) { } case timeout(t) { } }"""
|
|
2603
|
+
from stdlib_values import InScriptChannel
|
|
2604
|
+
from ast_nodes import CallExpr, GetAttrExpr
|
|
2605
|
+
for clause in node.clauses:
|
|
2606
|
+
kind = clause.get("kind")
|
|
2607
|
+
if kind == "recv":
|
|
2608
|
+
# clause["channel"] is CallExpr(callee=GetAttr(ch, "recv"), args=[])
|
|
2609
|
+
call_expr = clause.get("channel")
|
|
2610
|
+
if call_expr and isinstance(call_expr, CallExpr):
|
|
2611
|
+
ch = self.visit(call_expr.callee.obj)
|
|
2612
|
+
else:
|
|
2613
|
+
ch = None
|
|
2614
|
+
if isinstance(ch, InScriptChannel):
|
|
2615
|
+
val = ch.try_recv()
|
|
2616
|
+
if val is not None:
|
|
2617
|
+
var = clause.get("var")
|
|
2618
|
+
if var:
|
|
2619
|
+
self._env.define(var, val)
|
|
2620
|
+
self.visit(clause["body"])
|
|
2621
|
+
return
|
|
2622
|
+
elif kind == "send":
|
|
2623
|
+
# clause["channel"] is CallExpr(callee=GetAttr(ch, "send"), args=[val])
|
|
2624
|
+
call_expr = clause.get("channel")
|
|
2625
|
+
if call_expr and isinstance(call_expr, CallExpr):
|
|
2626
|
+
ch = self.visit(call_expr.callee.obj)
|
|
2627
|
+
val = self.visit(call_expr.args[0].value) if call_expr.args else None
|
|
2628
|
+
else:
|
|
2629
|
+
ch = val = None
|
|
2630
|
+
if isinstance(ch, InScriptChannel) and not ch._q.full():
|
|
2631
|
+
ch.send(val)
|
|
2632
|
+
self.visit(clause["body"])
|
|
2633
|
+
return
|
|
2634
|
+
elif kind == "timeout":
|
|
2635
|
+
# Timeout clause fires as fallback — always last resort
|
|
2636
|
+
self.visit(clause["body"])
|
|
2637
|
+
return
|
|
2530
2638
|
|
|
2531
2639
|
def visit_MatchArm(self, node: MatchArm) -> Any:
|
|
2532
2640
|
return self.visit(node.body)
|
|
@@ -2677,6 +2785,29 @@ class Interpreter(Visitor):
|
|
|
2677
2785
|
val = self.visit(node.left)
|
|
2678
2786
|
return self.visit(node.right) if val is None else val
|
|
2679
2787
|
|
|
2788
|
+
def visit_WithExpr(self, node) -> Any:
|
|
2789
|
+
"""v2.2.0: with obj { .field = val, ... } — clone and modify."""
|
|
2790
|
+
from stdlib_values import InScriptInstance
|
|
2791
|
+
base = self.visit(node.obj)
|
|
2792
|
+
if isinstance(base, InScriptInstance):
|
|
2793
|
+
clone = InScriptInstance(
|
|
2794
|
+
struct_name=base.struct_name,
|
|
2795
|
+
fields=dict(base.fields),
|
|
2796
|
+
)
|
|
2797
|
+
elif isinstance(base, dict):
|
|
2798
|
+
clone = dict(base)
|
|
2799
|
+
else:
|
|
2800
|
+
import copy; clone = copy.copy(base)
|
|
2801
|
+
for field_name, val_expr in node.fields:
|
|
2802
|
+
val = self.visit(val_expr)
|
|
2803
|
+
if isinstance(clone, InScriptInstance):
|
|
2804
|
+
clone.fields[field_name] = val
|
|
2805
|
+
elif isinstance(clone, dict):
|
|
2806
|
+
clone[field_name] = val
|
|
2807
|
+
else:
|
|
2808
|
+
_set_attr(clone, field_name, val, node.line, self)
|
|
2809
|
+
return clone
|
|
2810
|
+
|
|
2680
2811
|
def visit_PipeExpr(self, node: PipeExpr) -> Any:
|
|
2681
2812
|
"""value |> fn — calls fn(value)."""
|
|
2682
2813
|
val = self.visit(node.value)
|
|
@@ -2824,6 +2955,41 @@ def _arr_count(lst, fn_or_val, interp):
|
|
|
2824
2955
|
|
|
2825
2956
|
|
|
2826
2957
|
def _get_attr(obj: Any, name: str, line: int, interp: Interpreter) -> Any:
|
|
2958
|
+
# v2.3.0: concurrency primitive attribute dispatch
|
|
2959
|
+
from stdlib_values import InScriptTask, InScriptChannel, InScriptMutex, InScriptRWLock, InScriptTimerHandle
|
|
2960
|
+
|
|
2961
|
+
if isinstance(obj, InScriptTask):
|
|
2962
|
+
if name == "join": return lambda timeout=None: obj.join(timeout)
|
|
2963
|
+
if name == "done": return obj.done
|
|
2964
|
+
if name == "result": return obj._result
|
|
2965
|
+
interp._error(f"Task has no member '{name}'. Available: join, done, result", line)
|
|
2966
|
+
|
|
2967
|
+
if isinstance(obj, InScriptChannel):
|
|
2968
|
+
if name == "send": return lambda v: obj.send(v)
|
|
2969
|
+
if name == "recv": return lambda: obj.recv()
|
|
2970
|
+
if name == "try_recv": return lambda: obj.try_recv()
|
|
2971
|
+
if name == "close": return lambda: obj.close()
|
|
2972
|
+
if name == "closed": return obj.closed
|
|
2973
|
+
if name == "size": return obj._q.qsize()
|
|
2974
|
+
if name == "is_empty": return obj._q.empty()
|
|
2975
|
+
interp._error(f"Channel has no member '{name}'. Available: send, recv, try_recv, close, closed, size", line)
|
|
2976
|
+
|
|
2977
|
+
if isinstance(obj, InScriptMutex):
|
|
2978
|
+
if name == "lock": return lambda fn: obj.lock(lambda args: interp._call_fn(fn, args))
|
|
2979
|
+
if name == "set": return lambda v: obj.set(v)
|
|
2980
|
+
if name == "value": return obj.value
|
|
2981
|
+
interp._error(f"Mutex has no member '{name}'. Available: lock, set, value", line)
|
|
2982
|
+
|
|
2983
|
+
if isinstance(obj, InScriptRWLock):
|
|
2984
|
+
if name == "read": return lambda fn: obj.read(lambda args: interp._call_fn(fn, args))
|
|
2985
|
+
if name == "write": return lambda fn: obj.write(lambda args: interp._call_fn(fn, args))
|
|
2986
|
+
if name == "value": return obj.value
|
|
2987
|
+
interp._error(f"RWLock has no member '{name}'. Available: read, write, value", line)
|
|
2988
|
+
|
|
2989
|
+
if isinstance(obj, InScriptTimerHandle):
|
|
2990
|
+
if name == "cancel": return lambda: obj.cancel()
|
|
2991
|
+
interp._error(f"TimerHandle has no member '{name}'. Available: cancel", line)
|
|
2992
|
+
|
|
2827
2993
|
# StructDecl used as namespace for static method access: M.sq(5)
|
|
2828
2994
|
if isinstance(obj, StructDecl):
|
|
2829
2995
|
if hasattr(obj, '_static_ns') and name in obj._static_ns:
|
|
@@ -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 == "#":
|
|
@@ -147,6 +147,12 @@ class Parser:
|
|
|
147
147
|
|
|
148
148
|
# --- Async/Await ---
|
|
149
149
|
if tok.type == TT.ASYNC:
|
|
150
|
+
# v2.3.0: async for var in channel { } — iterate over async iterable
|
|
151
|
+
if self.peek.type == TT.FOR:
|
|
152
|
+
self.advance() # consume 'async'
|
|
153
|
+
node = self.parse_for_in()
|
|
154
|
+
node._async_iter = True # flag for interpreter
|
|
155
|
+
return node
|
|
150
156
|
return self.parse_async_fn()
|
|
151
157
|
|
|
152
158
|
# --- Await expr ---
|
|
@@ -402,8 +408,24 @@ class Parser:
|
|
|
402
408
|
line, col = self._pos()
|
|
403
409
|
|
|
404
410
|
# Tuple type: (int, float) — used for multiple return values
|
|
411
|
+
# v2.2.0: named return type (min: float, max: float)
|
|
405
412
|
if self.check(TT.LPAREN):
|
|
406
413
|
self.advance() # consume '('
|
|
414
|
+
# Detect named tuple: (IDENT COLON ...)
|
|
415
|
+
if (self.current.type == TT.IDENT
|
|
416
|
+
and self.peek.type == TT.COLON):
|
|
417
|
+
named_fields = []
|
|
418
|
+
while True:
|
|
419
|
+
fname = self.expect_ident("Expected field name in named return type")
|
|
420
|
+
self.expect(TT.COLON, "Expected ':' after field name")
|
|
421
|
+
ftype = self.parse_type_annotation()
|
|
422
|
+
named_fields.append((fname, ftype))
|
|
423
|
+
if not self.match(TT.COMMA):
|
|
424
|
+
break
|
|
425
|
+
self.expect(TT.RPAREN, "Expected ')' to close named return type")
|
|
426
|
+
ann = TypeAnnotation(name="NamedTuple", line=line, col=col)
|
|
427
|
+
ann.named_fields = named_fields
|
|
428
|
+
return ann
|
|
407
429
|
# Just consume the tuple type contents and treat it as "Tuple"
|
|
408
430
|
depth = 1
|
|
409
431
|
while not self.is_at_end() and depth > 0:
|
|
@@ -416,12 +438,16 @@ class Parser:
|
|
|
416
438
|
self.advance() # consume final ')'
|
|
417
439
|
return TypeAnnotation(name="Tuple", line=line, col=col)
|
|
418
440
|
|
|
419
|
-
# Array type: [int]
|
|
441
|
+
# Array type: [int] or bare [] (any-element array)
|
|
420
442
|
if self.match(TT.LBRACKET):
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
443
|
+
if self.check(TT.RBRACKET):
|
|
444
|
+
self.advance() # consume ']' — bare [] means Array<any>
|
|
445
|
+
ann = TypeAnnotation(name="Array", is_array=True, line=line, col=col)
|
|
446
|
+
else:
|
|
447
|
+
inner = self.parse_type_annotation()
|
|
448
|
+
self.expect(TT.RBRACKET, "Expected ']' to close array type")
|
|
449
|
+
ann = TypeAnnotation(name="Array", is_array=True,
|
|
450
|
+
generics=[inner], line=line, col=col)
|
|
425
451
|
# Dict type: {string: int}
|
|
426
452
|
elif self.match(TT.LBRACE):
|
|
427
453
|
key = self.parse_type_annotation()
|
|
@@ -647,6 +673,47 @@ class Parser:
|
|
|
647
673
|
elif self.check(TT.ELLIPSIS):
|
|
648
674
|
self.advance() # consume '...' (alias for variadic)
|
|
649
675
|
is_variadic = True
|
|
676
|
+
|
|
677
|
+
# v2.2.0: dict destructuring param — fn f({x, y}: Point)
|
|
678
|
+
if self.check(TT.LBRACE):
|
|
679
|
+
self.advance() # consume '{'
|
|
680
|
+
names = []
|
|
681
|
+
rest = None
|
|
682
|
+
while not self.check(TT.RBRACE) and not self.check(TT.EOF):
|
|
683
|
+
if self.check(TT.ELLIPSIS):
|
|
684
|
+
self.advance()
|
|
685
|
+
rest = self.expect_ident("Expected rest name after '...'")
|
|
686
|
+
break
|
|
687
|
+
names.append(self.expect_ident("Expected field name"))
|
|
688
|
+
self.match(TT.COMMA)
|
|
689
|
+
self.expect(TT.RBRACE, "Expected '}' to close destructuring param")
|
|
690
|
+
type_ann = None
|
|
691
|
+
if self.match(TT.COLON):
|
|
692
|
+
type_ann = self.parse_type_annotation()
|
|
693
|
+
from ast_nodes import DestructParam
|
|
694
|
+
return DestructParam(kind='dict', names=names, rest=rest,
|
|
695
|
+
type_ann=type_ann, line=line, col=col)
|
|
696
|
+
|
|
697
|
+
# v2.2.0: array destructuring param — fn f([head, ...tail]: [])
|
|
698
|
+
if self.check(TT.LBRACKET):
|
|
699
|
+
self.advance() # consume '['
|
|
700
|
+
names = []
|
|
701
|
+
rest = None
|
|
702
|
+
while not self.check(TT.RBRACKET) and not self.check(TT.EOF):
|
|
703
|
+
if self.check(TT.ELLIPSIS):
|
|
704
|
+
self.advance()
|
|
705
|
+
rest = self.expect_ident("Expected rest name after '...'")
|
|
706
|
+
break
|
|
707
|
+
names.append(self.expect_ident("Expected element binding"))
|
|
708
|
+
self.match(TT.COMMA)
|
|
709
|
+
self.expect(TT.RBRACKET, "Expected ']' to close destructuring param")
|
|
710
|
+
type_ann = None
|
|
711
|
+
if self.match(TT.COLON):
|
|
712
|
+
type_ann = self.parse_type_annotation()
|
|
713
|
+
from ast_nodes import DestructParam
|
|
714
|
+
return DestructParam(kind='array', names=names, rest=rest,
|
|
715
|
+
type_ann=type_ann, line=line, col=col)
|
|
716
|
+
|
|
650
717
|
name = self.expect_ident("Expected parameter name")
|
|
651
718
|
type_ann = None
|
|
652
719
|
if self.match(TT.COLON):
|
|
@@ -1418,6 +1485,7 @@ class Parser:
|
|
|
1418
1485
|
TT.CARET_EQ: "^=",
|
|
1419
1486
|
TT.LSHIFT_EQ: "<<=",
|
|
1420
1487
|
TT.RSHIFT_EQ: ">>=",
|
|
1488
|
+
TT.NULLISH_EQ: "??=", # v2.2.0
|
|
1421
1489
|
}
|
|
1422
1490
|
|
|
1423
1491
|
if self.current.type in ASSIGN_OPS:
|
|
@@ -1427,7 +1495,8 @@ class Parser:
|
|
|
1427
1495
|
|
|
1428
1496
|
# Extract the binary operator from compound ops (strip trailing =)
|
|
1429
1497
|
# e.g. "+=" → "+", "**=" → "**"
|
|
1430
|
-
|
|
1498
|
+
# "??=" is special — not a binary op, handled by interpreter directly
|
|
1499
|
+
binop = op.rstrip("=") if op not in ("=", "??=") else None
|
|
1431
1500
|
|
|
1432
1501
|
# Determine which assignment node to create
|
|
1433
1502
|
if isinstance(expr, IdentExpr):
|
|
@@ -1594,8 +1663,32 @@ class Parser:
|
|
|
1594
1663
|
while self.check(TT.LT, TT.GT, TT.LTE, TT.GTE):
|
|
1595
1664
|
op = self.advance().value
|
|
1596
1665
|
right = self.parse_shift()
|
|
1597
|
-
|
|
1598
|
-
|
|
1666
|
+
node = BinaryExpr(left=left, op=op, right=right,
|
|
1667
|
+
line=line, col=col)
|
|
1668
|
+
# v2.2.0: chained comparisons — 0 < x < 10 → (0<x) and (x<10)
|
|
1669
|
+
# If another comparison operator follows, desugar into `and`
|
|
1670
|
+
if self.check(TT.LT, TT.GT, TT.LTE, TT.GTE):
|
|
1671
|
+
left = node # left side of `and` is current comparison
|
|
1672
|
+
# `right` becomes the new LHS for the next comparison
|
|
1673
|
+
# We need to avoid evaluating `right` twice, but in a simple
|
|
1674
|
+
# interpreter it's safe to reuse the same AST node (no side effects
|
|
1675
|
+
# on simple expressions). Build: node AND (right op next)
|
|
1676
|
+
op2 = self.advance().value
|
|
1677
|
+
right2 = self.parse_shift()
|
|
1678
|
+
rhs = BinaryExpr(left=right, op=op2, right=right2,
|
|
1679
|
+
line=line, col=col)
|
|
1680
|
+
left = BinaryExpr(left=node, op="&&", right=rhs,
|
|
1681
|
+
line=line, col=col)
|
|
1682
|
+
while self.check(TT.LT, TT.GT, TT.LTE, TT.GTE):
|
|
1683
|
+
op3 = self.advance().value
|
|
1684
|
+
right3 = self.parse_shift()
|
|
1685
|
+
rhs3 = BinaryExpr(left=right2, op=op3, right=right3,
|
|
1686
|
+
line=line, col=col)
|
|
1687
|
+
left = BinaryExpr(left=left, op="&&", right=rhs3,
|
|
1688
|
+
line=line, col=col)
|
|
1689
|
+
right2 = right3
|
|
1690
|
+
return left
|
|
1691
|
+
left = node
|
|
1599
1692
|
return left
|
|
1600
1693
|
|
|
1601
1694
|
def parse_shift(self) -> Node:
|
|
@@ -1730,6 +1823,26 @@ class Parser:
|
|
|
1730
1823
|
args = self.parse_arg_list()
|
|
1731
1824
|
expr = CallExpr(callee=expr, args=args, line=line, col=col)
|
|
1732
1825
|
|
|
1826
|
+
# v2.3.0: generic builtin call: channel<T>(cap) / Stack<T>() etc.
|
|
1827
|
+
# Pattern: IDENT < TYPE_OR_IDENT > (
|
|
1828
|
+
elif (self.check(TT.LT)
|
|
1829
|
+
and isinstance(expr, IdentExpr)
|
|
1830
|
+
and self.peek.type in (TT.INT_TYPE, TT.FLOAT_TYPE, TT.STRING_TYPE,
|
|
1831
|
+
TT.BOOL_TYPE, TT.IDENT, TT.VOID_TYPE)):
|
|
1832
|
+
saved = self.pos
|
|
1833
|
+
self.advance() # consume '<'
|
|
1834
|
+
self.advance() # consume type name
|
|
1835
|
+
if self.check(TT.GT):
|
|
1836
|
+
self.advance() # consume '>'
|
|
1837
|
+
if self.check(TT.LPAREN):
|
|
1838
|
+
# It's a generic call — type param is stripped (runtime ignores it)
|
|
1839
|
+
args = self.parse_arg_list()
|
|
1840
|
+
expr = CallExpr(callee=expr, args=args, line=line, col=col)
|
|
1841
|
+
continue
|
|
1842
|
+
# Not a generic call — backtrack and stop postfix chain
|
|
1843
|
+
self.pos = saved
|
|
1844
|
+
break
|
|
1845
|
+
|
|
1733
1846
|
# Optional chaining: expr?.member or expr?.method(args)
|
|
1734
1847
|
elif self.check(TT.QUESTION_DOT):
|
|
1735
1848
|
self.advance() # consume '?.'
|
|
@@ -1879,6 +1992,25 @@ class Parser:
|
|
|
1879
1992
|
# Grouped expression: (expr) OR Tuple: (expr, expr, ...)
|
|
1880
1993
|
if tok.type == TT.LPAREN:
|
|
1881
1994
|
self.advance() # consume '('
|
|
1995
|
+
# v2.2.0: named return tuple literal — (name: expr, name: expr, ...)
|
|
1996
|
+
# Detect: IDENT COLON pattern at the start
|
|
1997
|
+
if (self.current.type == TT.IDENT
|
|
1998
|
+
and self.peek.type == TT.COLON):
|
|
1999
|
+
pairs = []
|
|
2000
|
+
while True:
|
|
2001
|
+
key = self.expect_ident("Expected field name in named tuple")
|
|
2002
|
+
self.expect(TT.COLON, "Expected ':' after field name")
|
|
2003
|
+
val = self.parse_expr()
|
|
2004
|
+
pairs.append((key, val))
|
|
2005
|
+
if not self.match(TT.COMMA):
|
|
2006
|
+
break
|
|
2007
|
+
self.expect(TT.RPAREN, "Expected ')' to close named tuple")
|
|
2008
|
+
# Return as a DictLiteralExpr with string-key pairs
|
|
2009
|
+
return DictLiteralExpr(
|
|
2010
|
+
pairs=[(StringLiteralExpr(value=k, line=line, col=col), v)
|
|
2011
|
+
for k, v in pairs],
|
|
2012
|
+
line=line, col=col
|
|
2013
|
+
)
|
|
1882
2014
|
first = self.parse_expr()
|
|
1883
2015
|
if self.check(TT.COMMA):
|
|
1884
2016
|
# It's a tuple literal
|
|
@@ -1933,6 +2065,13 @@ class Parser:
|
|
|
1933
2065
|
expr = self.parse_unary()
|
|
1934
2066
|
return AwaitExpr(expr=expr, line=line, col=col)
|
|
1935
2067
|
|
|
2068
|
+
# v2.3.0: spawn <expr> — concurrency task
|
|
2069
|
+
if tok.type == TT.SPAWN:
|
|
2070
|
+
self.advance() # consume 'spawn'
|
|
2071
|
+
expr = self.parse_unary()
|
|
2072
|
+
from ast_nodes import TaskSpawnExpr
|
|
2073
|
+
return TaskSpawnExpr(expr=expr, line=line, col=col)
|
|
2074
|
+
|
|
1936
2075
|
# try { expr } catch e { expr } — try as expression (returns value)
|
|
1937
2076
|
if tok.type == TT.TRY:
|
|
1938
2077
|
return self._parse_try_expr()
|
|
@@ -1948,6 +2087,23 @@ class Parser:
|
|
|
1948
2087
|
self.advance() # consume 'comptime'
|
|
1949
2088
|
body = self.parse_block()
|
|
1950
2089
|
return ComptimeExpr(body=body, line=line, col=col)
|
|
2090
|
+
# v2.2.0: with expr { .field = val, ... } — clone-and-modify
|
|
2091
|
+
if tok.value == "with":
|
|
2092
|
+
self.advance() # consume 'with'
|
|
2093
|
+
obj = self.parse_postfix()
|
|
2094
|
+
self.expect(TT.LBRACE, "Expected '{' after 'with <expr>'")
|
|
2095
|
+
fields = []
|
|
2096
|
+
while not self.check(TT.RBRACE) and not self.check(TT.EOF):
|
|
2097
|
+
self.expect(TT.DOT, "Expected '.field = val' inside with block")
|
|
2098
|
+
field_name = self.expect_ident("Expected field name after '.'")
|
|
2099
|
+
self.expect(TT.ASSIGN, "Expected '=' after field name in with block")
|
|
2100
|
+
val = self.parse_expr()
|
|
2101
|
+
fields.append((field_name, val))
|
|
2102
|
+
self.match(TT.COMMA)
|
|
2103
|
+
self.match(TT.NEWLINE) if hasattr(TT, 'NEWLINE') else None
|
|
2104
|
+
self.expect(TT.RBRACE, "Expected '}' to close with block")
|
|
2105
|
+
from ast_nodes import WithExpr
|
|
2106
|
+
return WithExpr(obj=obj, fields=fields, line=line, col=col)
|
|
1951
2107
|
return self.parse_ident_or_struct_init()
|
|
1952
2108
|
|
|
1953
2109
|
# 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.3.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.3.0"
|
|
44
44
|
|
|
45
45
|
# ── ANSI colours ──────────────────────────────────────────────────────────────
|
|
46
46
|
def _c(code, text):
|
|
@@ -1063,8 +1063,79 @@ except KeyError:
|
|
|
1063
1063
|
"sleep_sync": _timer_sleep_sync, # synchronous fallback
|
|
1064
1064
|
"now": lambda: int(_time_mod.time() * 1000), # ms since epoch
|
|
1065
1065
|
"now_sec": lambda: _time_mod.time(), # seconds since epoch
|
|
1066
|
+
# v2.3.0: after / every / cancel
|
|
1067
|
+
"after": None, # patched below
|
|
1068
|
+
"every": None, # patched below
|
|
1069
|
+
"cancel": None, # patched below
|
|
1066
1070
|
})
|
|
1067
1071
|
|
|
1072
|
+
# v2.3.0: patch timer.after / timer.every / timer.cancel
|
|
1073
|
+
try:
|
|
1074
|
+
import threading as _thr_mod
|
|
1075
|
+
from stdlib_values import InScriptTimerHandle as _THndl
|
|
1076
|
+
|
|
1077
|
+
def _timer_after(ms, fn):
|
|
1078
|
+
"""timer.after(ms, fn) — call fn() once after ms milliseconds."""
|
|
1079
|
+
def _fire():
|
|
1080
|
+
try: fn([])
|
|
1081
|
+
except Exception: pass
|
|
1082
|
+
t = _thr_mod.Timer(int(ms) / 1000.0, _fire)
|
|
1083
|
+
t.daemon = True
|
|
1084
|
+
t.start()
|
|
1085
|
+
return _THndl(t)
|
|
1086
|
+
|
|
1087
|
+
def _timer_every(ms, fn):
|
|
1088
|
+
"""timer.every(ms, fn) — call fn() every ms milliseconds until cancelled."""
|
|
1089
|
+
handle = [None]
|
|
1090
|
+
def _tick():
|
|
1091
|
+
try: fn([])
|
|
1092
|
+
except Exception: pass
|
|
1093
|
+
t = _thr_mod.Timer(int(ms) / 1000.0, _tick)
|
|
1094
|
+
t.daemon = True
|
|
1095
|
+
handle[0] = t
|
|
1096
|
+
t.start()
|
|
1097
|
+
t0 = _thr_mod.Timer(int(ms) / 1000.0, _tick)
|
|
1098
|
+
t0.daemon = True
|
|
1099
|
+
handle[0] = t0
|
|
1100
|
+
t0.start()
|
|
1101
|
+
class _EveryHandle:
|
|
1102
|
+
def cancel(self): handle[0] and handle[0].cancel()
|
|
1103
|
+
def __repr__(self): return "<TimerHandle periodic>"
|
|
1104
|
+
return _EveryHandle()
|
|
1105
|
+
|
|
1106
|
+
def _timer_cancel(handle):
|
|
1107
|
+
if hasattr(handle, 'cancel'): handle.cancel()
|
|
1108
|
+
|
|
1109
|
+
_MODULES["timer"]["after"] = _timer_after
|
|
1110
|
+
_MODULES["timer"]["every"] = _timer_every
|
|
1111
|
+
_MODULES["timer"]["cancel"] = _timer_cancel
|
|
1112
|
+
except Exception as _te:
|
|
1113
|
+
pass
|
|
1114
|
+
|
|
1115
|
+
# v2.3.0: channel, mutex, rwlock as global builtins
|
|
1116
|
+
try:
|
|
1117
|
+
from stdlib_values import InScriptChannel as _ISChannel
|
|
1118
|
+
from stdlib_values import InScriptMutex as _ISMutex
|
|
1119
|
+
from stdlib_values import InScriptRWLock as _ISRWLock
|
|
1120
|
+
|
|
1121
|
+
def _make_channel(capacity=0):
|
|
1122
|
+
cap = capacity[0] if isinstance(capacity, list) and capacity else (capacity or 0)
|
|
1123
|
+
return _ISChannel(int(cap))
|
|
1124
|
+
|
|
1125
|
+
def _make_mutex(value=None):
|
|
1126
|
+
v = value[0] if isinstance(value, list) and value else value
|
|
1127
|
+
return _ISMutex(v)
|
|
1128
|
+
|
|
1129
|
+
def _make_rwlock(value=None):
|
|
1130
|
+
v = value[0] if isinstance(value, list) and value else value
|
|
1131
|
+
return _ISRWLock(v)
|
|
1132
|
+
|
|
1133
|
+
# Register as global builtins (not modules)
|
|
1134
|
+
# channel/mutex/rwlock are registered directly in the interpreter globals (interpreter.py)
|
|
1135
|
+
pass
|
|
1136
|
+
except Exception as _ce:
|
|
1137
|
+
pass
|
|
1138
|
+
|
|
1068
1139
|
|
|
1069
1140
|
|
|
1070
1141
|
# ── Extended stdlib (Part 1): collections, datetime, fs, process, log, test, compress
|
|
@@ -326,3 +326,151 @@ class InScriptGenerator:
|
|
|
326
326
|
except StopIteration:
|
|
327
327
|
self._done = True
|
|
328
328
|
return None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# ── v2.3.0: Concurrency primitives ───────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
import threading as _threading
|
|
334
|
+
import queue as _queue
|
|
335
|
+
|
|
336
|
+
class InScriptTask:
|
|
337
|
+
"""
|
|
338
|
+
v2.3.0: Handle returned by `spawn expr`.
|
|
339
|
+
Runs a callable in a background thread.
|
|
340
|
+
"""
|
|
341
|
+
def __init__(self, fn, args):
|
|
342
|
+
self._result = None
|
|
343
|
+
self._error = None
|
|
344
|
+
self._done = _threading.Event()
|
|
345
|
+
self._thread = _threading.Thread(target=self._run, args=(fn, args), daemon=True)
|
|
346
|
+
self._thread.start()
|
|
347
|
+
|
|
348
|
+
def _run(self, fn, args):
|
|
349
|
+
try:
|
|
350
|
+
self._result = fn()
|
|
351
|
+
except Exception as e:
|
|
352
|
+
self._error = e
|
|
353
|
+
finally:
|
|
354
|
+
self._done.set()
|
|
355
|
+
|
|
356
|
+
def join(self, timeout=None):
|
|
357
|
+
"""Block until task completes. Returns result or raises."""
|
|
358
|
+
self._done.wait(timeout)
|
|
359
|
+
if self._error:
|
|
360
|
+
raise self._error
|
|
361
|
+
return self._result
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def done(self):
|
|
365
|
+
return self._done.is_set()
|
|
366
|
+
|
|
367
|
+
def __repr__(self):
|
|
368
|
+
return f"<Task {'done' if self.done else 'running'}>"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class InScriptChannel:
|
|
372
|
+
"""
|
|
373
|
+
v2.3.0: `channel<T>(capacity)` — typed message-passing queue.
|
|
374
|
+
.send(v) — put value (blocks if full, capacity=0 means unbounded)
|
|
375
|
+
.recv() — get value (blocks until available)
|
|
376
|
+
.try_recv() — get value or nil (non-blocking)
|
|
377
|
+
.close() — mark channel closed
|
|
378
|
+
.closed — bool
|
|
379
|
+
"""
|
|
380
|
+
def __init__(self, capacity: int = 0):
|
|
381
|
+
self._q = _queue.Queue(maxsize=capacity)
|
|
382
|
+
self._closed = False
|
|
383
|
+
|
|
384
|
+
def send(self, value):
|
|
385
|
+
if self._closed:
|
|
386
|
+
raise RuntimeError("send on closed channel")
|
|
387
|
+
self._q.put(value)
|
|
388
|
+
|
|
389
|
+
def recv(self):
|
|
390
|
+
return self._q.get()
|
|
391
|
+
|
|
392
|
+
def try_recv(self):
|
|
393
|
+
try:
|
|
394
|
+
return self._q.get_nowait()
|
|
395
|
+
except _queue.Empty:
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
def close(self):
|
|
399
|
+
self._closed = True
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def closed(self):
|
|
403
|
+
return self._closed
|
|
404
|
+
|
|
405
|
+
def __repr__(self):
|
|
406
|
+
return f"<Channel size={self._q.qsize()} closed={self._closed}>"
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class InScriptMutex:
|
|
410
|
+
"""
|
|
411
|
+
v2.3.0: `mutex(value)` — thread-safe value wrapper.
|
|
412
|
+
.lock(fn) — call fn(value) under lock, returns fn's result
|
|
413
|
+
.value — read current value (unsafe, prefer lock())
|
|
414
|
+
.set(v) — set value under lock
|
|
415
|
+
"""
|
|
416
|
+
def __init__(self, value):
|
|
417
|
+
self._value = value
|
|
418
|
+
self._lock = _threading.Lock()
|
|
419
|
+
|
|
420
|
+
def lock(self, fn):
|
|
421
|
+
with self._lock:
|
|
422
|
+
result = fn([self._value]) if callable(fn) else None
|
|
423
|
+
return result
|
|
424
|
+
|
|
425
|
+
def set(self, value):
|
|
426
|
+
with self._lock:
|
|
427
|
+
self._value = value
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def value(self):
|
|
431
|
+
return self._value
|
|
432
|
+
|
|
433
|
+
def __repr__(self):
|
|
434
|
+
return f"<Mutex value={self._value!r}>"
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class InScriptRWLock:
|
|
438
|
+
"""
|
|
439
|
+
v2.3.0: `rwlock(value)` — reader/writer lock.
|
|
440
|
+
.read(fn) — call fn(value) under read lock
|
|
441
|
+
.write(fn) — call fn(value) under write lock, fn may return new value
|
|
442
|
+
"""
|
|
443
|
+
def __init__(self, value):
|
|
444
|
+
self._value = value
|
|
445
|
+
self._lock = _threading.RLock() # simplified: use reentrant lock
|
|
446
|
+
|
|
447
|
+
def read(self, fn):
|
|
448
|
+
with self._lock:
|
|
449
|
+
return fn([self._value]) if callable(fn) else self._value
|
|
450
|
+
|
|
451
|
+
def write(self, fn):
|
|
452
|
+
with self._lock:
|
|
453
|
+
result = fn([self._value]) if callable(fn) else None
|
|
454
|
+
if result is not None:
|
|
455
|
+
self._value = result
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def value(self):
|
|
460
|
+
return self._value
|
|
461
|
+
|
|
462
|
+
def __repr__(self):
|
|
463
|
+
return f"<RWLock value={self._value!r}>"
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class InScriptTimerHandle:
|
|
467
|
+
"""Handle returned by timer.after / timer.every for cancellation."""
|
|
468
|
+
def __init__(self, timer_obj):
|
|
469
|
+
self._timer = timer_obj # threading.Timer or periodic wrapper
|
|
470
|
+
|
|
471
|
+
def cancel(self):
|
|
472
|
+
if hasattr(self._timer, 'cancel'):
|
|
473
|
+
self._timer.cancel()
|
|
474
|
+
|
|
475
|
+
def __repr__(self):
|
|
476
|
+
return "<TimerHandle>"
|
|
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
|