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.
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/PKG-INFO +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/ast_nodes.py +5 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript.py +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/interpreter.py +139 -18
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/parser.py +33 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/pyproject.toml +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/repl.py +1 -1
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib.py +71 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib_values.py +148 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/README.md +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/analyzer.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/compiler.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/environment.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/errors.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_fmt.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/SOURCES.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_lang.egg-info/top_level.txt +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/inscript_test.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/lexer.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/pygame_backend.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/setup.cfg +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/setup.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib_extended.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib_extended_2.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/stdlib_game.py +0 -0
- {inscript_lang-2.2.0 → inscript_lang-2.3.0}/vm.py +0 -0
|
@@ -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
|
|
@@ -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)
|
|
@@ -2524,11 +2551,10 @@ class Interpreter(Visitor):
|
|
|
2524
2551
|
|
|
2525
2552
|
def visit_AwaitExpr(self, node: AwaitExpr) -> Any:
|
|
2526
2553
|
"""
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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
|
|
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
|
|
2572
|
+
return val # plain value passthrough
|
|
2549
2573
|
|
|
2550
|
-
def visit_SpawnExpr(self, node
|
|
2551
|
-
|
|
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.
|
|
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
|
|
File without changes
|