inscript-lang 2.2.0__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.
Files changed (30) hide show
  1. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/PKG-INFO +1 -1
  2. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/ast_nodes.py +5 -0
  3. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript.py +1 -1
  4. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/PKG-INFO +1 -1
  5. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/interpreter.py +139 -18
  6. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/parser.py +33 -0
  7. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/pyproject.toml +1 -1
  8. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/repl.py +1 -1
  9. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib.py +71 -0
  10. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib_values.py +148 -0
  11. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/README.md +0 -0
  12. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/analyzer.py +0 -0
  13. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/compiler.py +0 -0
  14. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/environment.py +0 -0
  15. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/errors.py +0 -0
  16. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_fmt.py +0 -0
  17. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/SOURCES.txt +0 -0
  18. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
  19. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/entry_points.txt +0 -0
  20. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/requires.txt +0 -0
  21. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/top_level.txt +0 -0
  22. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_test.py +0 -0
  23. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/lexer.py +0 -0
  24. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/pygame_backend.py +0 -0
  25. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/setup.cfg +0 -0
  26. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/setup.py +0 -0
  27. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib_extended.py +0 -0
  28. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib_extended_2.py +0 -0
  29. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib_game.py +0 -0
  30. {inscript_lang-2.2.0 → inscript_lang-2.3.0}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.2.0
3
+ Version: 2.3.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
@@ -751,3 +751,8 @@ class DestructParam(Node):
751
751
  names: list # list of str (field/element names to bind)
752
752
  rest: object # str name for rest element (...tail), or None
753
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
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
24
24
  SemanticError, InScriptRuntimeError,
25
25
  MultiError, InScriptWarning)
26
26
 
27
- VERSION = "2.2.0"
27
+ VERSION = "2.3.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.2.0
3
+ Version: 2.3.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
@@ -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 (lightweight stubs, full impl via async/await) ─────
425
- "thread": lambda fn, *args: self._spawn_thread(fn, list(args)),
426
- "channel": lambda cap=0: {"_type":"channel","_channel":__import__("queue").SimpleQueue(),"_cap":int(cap)},
427
- "make_channel": lambda cap=0: {"_type":"channel","_channel":__import__("queue").SimpleQueue(),"_cap":int(cap)},
428
- "chan_send": lambda ch, v: ch["_channel"].put(v) if isinstance(ch,dict) and "_channel" in ch else None,
429
- "chan_recv": lambda ch: ch["_channel"].get(timeout=5) if isinstance(ch,dict) and "_channel" in ch else None,
430
- "sleep": lambda s: __import__('time').sleep(s),
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)
@@ -2524,11 +2551,10 @@ class Interpreter(Visitor):
2524
2551
 
2525
2552
  def visit_AwaitExpr(self, node: AwaitExpr) -> Any:
2526
2553
  """
2527
- v1.9.7: True async/await.
2528
- - InScriptCoroutine drive via Python coroutine protocol (sync driver).
2529
- - Plain value passthrough (backwards compatible).
2530
- - Exceptions from inside the coroutine are converted to InScriptRuntimeError
2531
- 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).
2532
2558
  """
2533
2559
  val = self.visit(node.expr)
2534
2560
  if isinstance(val, InScriptCoroutine):
@@ -2538,17 +2564,77 @@ class Interpreter(Visitor):
2538
2564
  except StopIteration as e:
2539
2565
  return e.value
2540
2566
  except InScriptRuntimeError:
2541
- raise # already an InScript error, let it propagate
2567
+ raise
2542
2568
  except Exception as e:
2543
- # Convert Python exceptions to InScriptRuntimeError so
2544
- # they are catchable by InScript try/catch blocks.
2545
2569
  err = InScriptRuntimeError(str(e), getattr(node, 'line', 0))
2546
2570
  err.thrown_value = str(e)
2547
2571
  raise err
2548
- return val # plain value: passthrough
2572
+ return val # plain value passthrough
2549
2573
 
2550
- def visit_SpawnExpr(self, node: SpawnExpr) -> Any:
2551
- return None # Phase 6 (ECS)
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
2552
2638
 
2553
2639
  def visit_MatchArm(self, node: MatchArm) -> Any:
2554
2640
  return self.visit(node.body)
@@ -2869,6 +2955,41 @@ def _arr_count(lst, fn_or_val, interp):
2869
2955
 
2870
2956
 
2871
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
+
2872
2993
  # StructDecl used as namespace for static method access: M.sq(5)
2873
2994
  if isinstance(obj, StructDecl):
2874
2995
  if hasattr(obj, '_static_ns') and name in obj._static_ns:
@@ -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 ---
@@ -1817,6 +1823,26 @@ class Parser:
1817
1823
  args = self.parse_arg_list()
1818
1824
  expr = CallExpr(callee=expr, args=args, line=line, col=col)
1819
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
+
1820
1846
  # Optional chaining: expr?.member or expr?.method(args)
1821
1847
  elif self.check(TT.QUESTION_DOT):
1822
1848
  self.advance() # consume '?.'
@@ -2039,6 +2065,13 @@ class Parser:
2039
2065
  expr = self.parse_unary()
2040
2066
  return AwaitExpr(expr=expr, line=line, col=col)
2041
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
+
2042
2075
  # try { expr } catch e { expr } — try as expression (returns value)
2043
2076
  if tok.type == TT.TRY:
2044
2077
  return self._parse_try_expr()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inscript-lang"
7
- version = "2.2.0"
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.2.0"
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