zexus 1.8.0 → 1.8.2

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 (51) hide show
  1. package/README.md +34 -6
  2. package/bin/zexus +12 -2
  3. package/bin/zpics +12 -2
  4. package/bin/zpm +12 -2
  5. package/bin/zx +12 -2
  6. package/bin/zx-deploy +12 -2
  7. package/bin/zx-dev +12 -2
  8. package/bin/zx-run +12 -2
  9. package/package.json +2 -1
  10. package/rust_core/Cargo.lock +603 -0
  11. package/rust_core/Cargo.toml +26 -0
  12. package/rust_core/README.md +15 -0
  13. package/rust_core/pyproject.toml +25 -0
  14. package/rust_core/src/binary_bytecode.rs +543 -0
  15. package/rust_core/src/contract_vm.rs +643 -0
  16. package/rust_core/src/executor.rs +847 -0
  17. package/rust_core/src/hasher.rs +90 -0
  18. package/rust_core/src/lib.rs +71 -0
  19. package/rust_core/src/merkle.rs +128 -0
  20. package/rust_core/src/rust_vm.rs +2313 -0
  21. package/rust_core/src/signature.rs +79 -0
  22. package/rust_core/src/state_adapter.rs +281 -0
  23. package/rust_core/src/validator.rs +116 -0
  24. package/scripts/postinstall.js +204 -21
  25. package/src/zexus/__init__.py +1 -1
  26. package/src/zexus/cli/main.py +1 -1
  27. package/src/zexus/cli/zpm.py +1 -1
  28. package/src/zexus/evaluator/bytecode_compiler.py +150 -52
  29. package/src/zexus/evaluator/core.py +151 -809
  30. package/src/zexus/evaluator/expressions.py +27 -22
  31. package/src/zexus/evaluator/functions.py +171 -126
  32. package/src/zexus/evaluator/statements.py +55 -112
  33. package/src/zexus/module_cache.py +20 -9
  34. package/src/zexus/object.py +330 -38
  35. package/src/zexus/parser/parser.py +103 -23
  36. package/src/zexus/parser/strategy_context.py +318 -6
  37. package/src/zexus/parser/strategy_structural.py +2 -2
  38. package/src/zexus/persistence.py +46 -17
  39. package/src/zexus/security.py +140 -234
  40. package/src/zexus/type_checker.py +44 -5
  41. package/src/zexus/vm/binary_bytecode.py +7 -3
  42. package/src/zexus/vm/bytecode.py +6 -0
  43. package/src/zexus/vm/cache.py +24 -46
  44. package/src/zexus/vm/compiler.py +549 -68
  45. package/src/zexus/vm/memory_pool.py +21 -9
  46. package/src/zexus/vm/vm.py +609 -95
  47. package/src/zexus/zpm/package_manager.py +1 -1
  48. package/src/zexus.egg-info/PKG-INFO +56 -12
  49. package/src/zexus.egg-info/SOURCES.txt +14 -0
  50. package/src/zexus.egg-info/entry_points.txt +5 -1
  51. package/src/zexus.egg-info/requires.txt +26 -0
@@ -31,6 +31,7 @@ from ..object import (
31
31
  Map as ZMap,
32
32
  Null as ZNull,
33
33
  EvaluationError as ZEvaluationError,
34
+ DateTime as ZDateTime,
34
35
  )
35
36
 
36
37
  # ==================== Backend / Optional Imports ====================
@@ -436,6 +437,9 @@ class VM:
436
437
  self._in_execution = 0
437
438
  self._native_jit_auto_enabled = False
438
439
  self._native_jit_auto_threshold = 700
440
+ # SECURITY (H9): Call-depth tracking to prevent unbounded recursion
441
+ self._call_depth = 0
442
+ self._MAX_CALL_DEPTH = 256
439
443
 
440
444
  # --- Rust VM Adaptive Routing (Phase 3 + Phase 6) ---
441
445
  self._rust_vm_available = _RUST_VM_AVAILABLE
@@ -667,6 +671,22 @@ class VM:
667
671
  """Return a child VM to the pool for reuse."""
668
672
  if hasattr(self, "_vm_pool") and self._vm_pool is not None:
669
673
  if len(self._vm_pool) < 1000:
674
+ # MEDIUM (M14): Scrub references that could leak state across pooled VMs.
675
+ try:
676
+ vm.env = None
677
+ except Exception:
678
+ pass
679
+ for attr in ("_closure_cells", "_name_cache", "_method_cache", "_events", "_tasks"):
680
+ try:
681
+ val = getattr(vm, attr, None)
682
+ if isinstance(val, dict):
683
+ val.clear()
684
+ except Exception:
685
+ pass
686
+ try:
687
+ vm._parent_env = None
688
+ except Exception:
689
+ pass
670
690
  self._vm_pool.append(vm)
671
691
 
672
692
  @classmethod
@@ -763,6 +783,9 @@ class VM:
763
783
  vm.enable_fast_loop = getattr(parent_vm, "enable_fast_loop", False)
764
784
  vm.fast_loop_threshold = getattr(parent_vm, "fast_loop_threshold", 512)
765
785
  vm._fast_loop_stats = {"used": False, "reason": ""}
786
+ # SECURITY (H9): Call-depth tracking for child VMs
787
+ vm._call_depth = 0
788
+ vm._MAX_CALL_DEPTH = getattr(parent_vm, "_MAX_CALL_DEPTH", 256)
766
789
 
767
790
  # Settings
768
791
  vm.worker_count = parent_vm.worker_count
@@ -902,6 +925,82 @@ class VM:
902
925
  self.builtins["__vm_use_module__"] = self._vm_use_module
903
926
  if "__vm_from_module__" not in self.builtins:
904
927
  self.builtins["__vm_from_module__"] = self._vm_from_module
928
+ # Channel builtins for VM concurrency support
929
+ if "__create_channel__" not in self.builtins:
930
+ self.builtins["__create_channel__"] = self._vm_create_channel
931
+ if "send" not in self.builtins:
932
+ self.builtins["send"] = self._vm_send
933
+ if "receive" not in self.builtins:
934
+ self.builtins["receive"] = self._vm_receive
935
+ if "close_channel" not in self.builtins:
936
+ self.builtins["close_channel"] = self._vm_close_channel
937
+
938
+ # --- Channel builtins for VM ---
939
+ def _vm_create_channel(self, name, element_type=None, capacity=0):
940
+ """Create a channel and store it in the VM environment."""
941
+ try:
942
+ from ..concurrency_system import Channel
943
+ except ImportError:
944
+ # Fallback: use a simple queue-based channel
945
+ import queue as _queue_mod
946
+
947
+ class _SimpleChannel:
948
+ def __init__(self, name, capacity=0):
949
+ self.name = name
950
+ self._queue = _queue_mod.Queue(maxsize=capacity if capacity else 0)
951
+ self._closed = False
952
+
953
+ def send(self, value, timeout=5.0):
954
+ if self._closed:
955
+ raise RuntimeError(f"Channel '{self.name}' is closed")
956
+ self._queue.put(value, timeout=timeout)
957
+
958
+ def receive(self, timeout=5.0):
959
+ if self._closed and self._queue.empty():
960
+ return None
961
+ try:
962
+ return self._queue.get(timeout=timeout)
963
+ except _queue_mod.Empty:
964
+ return None
965
+
966
+ def close(self):
967
+ self._closed = True
968
+
969
+ Channel = _SimpleChannel
970
+
971
+ cap = int(capacity) if capacity else 0
972
+ ch = Channel(name=name, capacity=cap) if element_type is None else Channel(name=name, element_type=element_type, capacity=cap)
973
+ self.env[name] = ch
974
+ return ch
975
+
976
+ def _vm_send(self, channel, value):
977
+ """Send value to channel."""
978
+ if channel is None or not hasattr(channel, 'send'):
979
+ return None
980
+ try:
981
+ channel.send(value, timeout=5.0)
982
+ except Exception:
983
+ pass
984
+ return None
985
+
986
+ def _vm_receive(self, channel):
987
+ """Receive value from channel."""
988
+ if channel is None or not hasattr(channel, 'receive'):
989
+ return None
990
+ try:
991
+ return channel.receive(timeout=5.0)
992
+ except Exception:
993
+ return None
994
+
995
+ def _vm_close_channel(self, channel):
996
+ """Close a channel."""
997
+ if channel is None or not hasattr(channel, 'close'):
998
+ return None
999
+ try:
1000
+ channel.close()
1001
+ except Exception:
1002
+ pass
1003
+ return None
905
1004
 
906
1005
  def _vm_use_module(self, spec):
907
1006
  if spec is None:
@@ -1147,17 +1246,29 @@ class VM:
1147
1246
  self._bump_env_version(key, value)
1148
1247
  return module_env
1149
1248
 
1150
- try:
1151
- mod = importlib.import_module(module_path)
1152
- key = alias or module_path
1153
- self.env[key] = mod
1154
- self._bump_env_version(key, mod)
1155
- return mod
1156
- except Exception:
1157
- key = alias or module_path
1158
- self.env[key] = None
1159
- self._bump_env_version(key, None)
1160
- return None
1249
+ # SECURITY (H6): Only allow safe Python stdlib modules via importlib.
1250
+ # Dangerous modules (os, subprocess, socket, ctypes, etc.) are blocked.
1251
+ _SAFE_PYTHON_MODULES = frozenset({
1252
+ 'math', 'json', 'hashlib', 'base64', 'collections', 'datetime',
1253
+ 'decimal', 'fractions', 'itertools', 'functools', 'operator',
1254
+ 'string', 'textwrap', 'unicodedata', 'enum', 'dataclasses',
1255
+ 'copy', 'pprint', 'reprlib', 'numbers', 'cmath', 'statistics',
1256
+ 'random', 'secrets', 'uuid', 're', 'struct', 'binascii',
1257
+ 'html', 'urllib.parse', 'ipaddress', 'typing', 'abc',
1258
+ })
1259
+ if module_path in _SAFE_PYTHON_MODULES:
1260
+ try:
1261
+ mod = importlib.import_module(module_path)
1262
+ key = alias or module_path
1263
+ self.env[key] = mod
1264
+ self._bump_env_version(key, mod)
1265
+ return mod
1266
+ except Exception:
1267
+ pass
1268
+ key = alias or module_path
1269
+ self.env[key] = None
1270
+ self._bump_env_version(key, None)
1271
+ return None
1161
1272
 
1162
1273
  def _get_cached_method(self, target: Any, method_name: str):
1163
1274
  if target is None:
@@ -1255,7 +1366,23 @@ class VM:
1255
1366
  pct = (count / total_ops * 100) if total_ops else 0.0
1256
1367
  print(f"[VM PROFILE] {op_name} count={count} pct={pct:.2f}%")
1257
1368
  return result
1258
-
1369
+ except Exception:
1370
+ # LOGIC (L4): If an exception escapes a TX block, revert state to snapshot.
1371
+ if self.env.get("_in_transaction", False):
1372
+ try:
1373
+ self.env["_blockchain_state"] = dict(self.env.get("_tx_snapshot", {}))
1374
+ self.env["_tx_pending_state"] = {}
1375
+ self.env.pop("_tx_snapshot", None)
1376
+ if self.use_memory_manager and "_tx_memory_snapshot" in self.env:
1377
+ self._managed_objects = dict(self.env["_tx_memory_snapshot"])
1378
+ del self.env["_tx_memory_snapshot"]
1379
+ tx_stack = self.env.get("_tx_stack", [])
1380
+ if tx_stack:
1381
+ tx_stack.pop()
1382
+ self.env["_in_transaction"] = bool(tx_stack)
1383
+ except Exception:
1384
+ pass
1385
+ raise
1259
1386
  finally:
1260
1387
  self._in_execution = max(0, getattr(self, "_in_execution", 1) - 1)
1261
1388
  self._total_execution_time += (time.perf_counter() - start_time)
@@ -1535,12 +1662,9 @@ class VM:
1535
1662
  return {'jit_enabled': False}
1536
1663
 
1537
1664
  def _ensure_recursion_headroom(self, minimum: int = 5000):
1538
- try:
1539
- current = sys.getrecursionlimit()
1540
- if current < minimum:
1541
- sys.setrecursionlimit(minimum)
1542
- except Exception:
1543
- pass
1665
+ # SECURITY (H13): No longer increases sys.setrecursionlimit.
1666
+ # VM-level call depth is enforced by _call_depth / _MAX_CALL_DEPTH instead.
1667
+ pass
1544
1668
 
1545
1669
  def clear_jit_cache(self):
1546
1670
  if self.use_jit and self.jit_compiler:
@@ -1598,25 +1722,13 @@ class VM:
1598
1722
  del self._managed_objects[name]
1599
1723
  return {'collected': collected, 'gc_time': gc_time}
1600
1724
 
1601
- # Fallback: Manual environment cleanup for non-managed memory
1602
- # Clear variables that are no longer referenced
1725
+ # Fallback: Without a memory manager, do NOT delete user variables.
1726
+ # Deleting non-underscore env keys breaks program state and can silently
1727
+ # corrupt execution. Keep this as a no-op to preserve semantics.
1603
1728
  if force:
1604
- initial_count = len(self.env)
1605
- # Keep only builtins and parent env references
1606
- keys_to_remove = []
1607
- for key in list(self.env.keys()):
1608
- # Don't remove special keys or builtins
1609
- if not key.startswith('_') and key not in self.builtins:
1610
- keys_to_remove.append(key)
1611
-
1612
- # Remove temporary variables
1613
- for key in keys_to_remove:
1614
- del self.env[key]
1615
-
1616
- cleared = initial_count - len(self.env)
1617
- return {'collected': cleared, 'message': 'Environment variables cleared'}
1618
-
1619
- return {'collected': 0, 'message': 'Memory manager disabled or not forced'}
1729
+ return {'collected': 0, 'message': 'No-op: memory manager disabled (force ignored)'}
1730
+
1731
+ return {'collected': 0, 'message': 'Memory manager disabled'}
1620
1732
 
1621
1733
 
1622
1734
  def _allocate_managed(self, value: Any, name: str = None, root: bool = False) -> int:
@@ -1904,8 +2016,10 @@ class VM:
1904
2016
  stack_append = stack.append
1905
2017
  # stack_pop = stack.pop
1906
2018
  def stack_pop():
2019
+ # MEDIUM (M15): Match the main VM stack behaviour; stack underflow
2020
+ # should not silently return None.
1907
2021
  if not stack:
1908
- return None
2022
+ raise IndexError("pop from empty stack")
1909
2023
  return stack.pop()
1910
2024
 
1911
2025
  ip = 0
@@ -1993,15 +2107,32 @@ class VM:
1993
2107
  a = stack_pop() if stack else 0
1994
2108
  if hasattr(a, 'value'): a = a.value
1995
2109
  if hasattr(b, 'value'): b = b.value
1996
- stack_append(a + b)
2110
+ # Type coercion: if either operand is a string, convert both to string
2111
+ if isinstance(a, str) or isinstance(b, str):
2112
+ stack_append(str(a if a is not None else '') + str(b if b is not None else ''))
2113
+ else:
2114
+ if a is None: a = 0
2115
+ if b is None: b = 0
2116
+ stack_append(a + b)
1997
2117
  elif op_name == "SUB":
1998
- b = stack_pop() if stack else 0
1999
- a = stack_pop() if stack else 0
2000
- if hasattr(a, 'value'): a = a.value
2001
- if hasattr(b, 'value'): b = b.value
2002
- if a is None: a = 0
2003
- if b is None: b = 0
2004
- stack_append(a - b)
2118
+ b_raw = stack_pop() if stack else 0
2119
+ a_raw = stack_pop() if stack else 0
2120
+ # DateTime arithmetic
2121
+ if isinstance(a_raw, ZDateTime) and isinstance(b_raw, ZDateTime):
2122
+ stack_append(a_raw.timestamp - b_raw.timestamp)
2123
+ elif isinstance(a_raw, ZDateTime):
2124
+ b = b_raw.value if hasattr(b_raw, 'value') else b_raw
2125
+ if b is None: b = 0
2126
+ stack_append(ZDateTime(a_raw.timestamp - float(b)))
2127
+ else:
2128
+ a = a_raw.value if hasattr(a_raw, 'value') else a_raw
2129
+ b = b_raw.value if hasattr(b_raw, 'value') else b_raw
2130
+ if a is None: a = 0
2131
+ if b is None: b = 0
2132
+ try:
2133
+ stack_append(a - b)
2134
+ except TypeError:
2135
+ stack_append(0)
2005
2136
  elif op_name == "MUL":
2006
2137
  b = stack_pop() if stack else 0
2007
2138
  a = stack_pop() if stack else 0
@@ -2017,7 +2148,19 @@ class VM:
2017
2148
  elif op_name == "MOD":
2018
2149
  b = stack_pop() if stack else 1
2019
2150
  a = stack_pop() if stack else 0
2151
+ if hasattr(a, 'value'): a = a.value
2152
+ if hasattr(b, 'value'): b = b.value
2153
+ if a is None: a = 0
2154
+ if b is None: b = 1
2020
2155
  stack_append(a % b if b != 0 else 0)
2156
+ elif op_name == "POW":
2157
+ b = stack_pop() if stack else 1
2158
+ a = stack_pop() if stack else 0
2159
+ if hasattr(a, 'value'): a = a.value
2160
+ if hasattr(b, 'value'): b = b.value
2161
+ if a is None: a = 0
2162
+ if b is None: b = 0
2163
+ stack_append(_safe_pow(a, b))
2021
2164
  elif op_name == "EQ":
2022
2165
  b = stack_pop() if stack else None
2023
2166
  a = stack_pop() if stack else None
@@ -2049,6 +2192,18 @@ class VM:
2049
2192
  elif op_name == "NOT":
2050
2193
  a = stack_pop() if stack else False
2051
2194
  stack_append(not a)
2195
+ elif op_name == "AND":
2196
+ b = stack_pop() if stack else False
2197
+ a = stack_pop() if stack else False
2198
+ if hasattr(a, 'value'): a = a.value
2199
+ if hasattr(b, 'value'): b = b.value
2200
+ stack_append(bool(a) and bool(b))
2201
+ elif op_name == "OR":
2202
+ b = stack_pop() if stack else False
2203
+ a = stack_pop() if stack else False
2204
+ if hasattr(a, 'value'): a = a.value
2205
+ if hasattr(b, 'value'): b = b.value
2206
+ stack_append(bool(a) or bool(b))
2052
2207
  elif op_name == "NEG":
2053
2208
  a = stack_pop() if stack else 0
2054
2209
  stack_append(-a)
@@ -2058,6 +2213,52 @@ class VM:
2058
2213
  cond = stack_pop() if stack else None
2059
2214
  if not cond:
2060
2215
  ip = operand
2216
+ elif op_name == "JUMP_IF_TRUE":
2217
+ cond = stack_pop() if stack else None
2218
+ if hasattr(cond, 'value'):
2219
+ cond = cond.value
2220
+ if cond:
2221
+ ip = operand
2222
+ elif op_name == "IMPORT":
2223
+ # Mirror async IMPORT behavior (minimal)
2224
+ try:
2225
+ if isinstance(operand, (list, tuple)) and operand:
2226
+ mod_name = const(operand[0])
2227
+ alias = const(operand[1]) if len(operand) > 1 else ""
2228
+ names = const(operand[2]) if len(operand) > 2 else []
2229
+ is_named = const(operand[3]) if len(operand) > 3 else False
2230
+ else:
2231
+ mod_name = const(operand)
2232
+ alias, names, is_named = "", [], False
2233
+ self._execute_import(mod_name, alias=alias or "", names=names, is_named=bool(is_named))
2234
+ except Exception:
2235
+ pass
2236
+ elif op_name == "EXPORT":
2237
+ # vm/compiler.py emits: LOAD_NAME <name_idx>; EXPORT <name_idx>
2238
+ try:
2239
+ if isinstance(operand, int):
2240
+ name = const(operand)
2241
+ value = stack_pop() if stack else None
2242
+ elif isinstance(operand, (list, tuple)) and operand:
2243
+ name = const(operand[0])
2244
+ value = stack_pop() if stack else None
2245
+ if len(operand) > 1:
2246
+ value = const(operand[1])
2247
+ else:
2248
+ name = stack_pop() if stack else None
2249
+ value = stack_pop() if stack else None
2250
+
2251
+ export_fn = getattr(self.env, "export", None)
2252
+ if callable(export_fn):
2253
+ try:
2254
+ export_fn(name, value)
2255
+ except Exception:
2256
+ pass
2257
+ else:
2258
+ self.env[name] = value
2259
+ self._bump_env_version(name, value)
2260
+ except Exception:
2261
+ pass
2061
2262
  elif op_name == "RETURN":
2062
2263
  return stack_pop() if stack else None
2063
2264
  elif op_name == "BUILD_LIST":
@@ -2203,7 +2404,17 @@ class VM:
2203
2404
  result = attr(*args)
2204
2405
  elif isinstance(target, dict) and method_name in target:
2205
2406
  candidate = target[method_name]
2206
- result = candidate(*args) if callable(candidate) else candidate
2407
+ # Handle Builtin objects (have .fn), ZAction/ZLambda (have .call),
2408
+ # and regular callables
2409
+ if callable(candidate):
2410
+ result = candidate(*args)
2411
+ elif hasattr(candidate, 'fn') and callable(candidate.fn):
2412
+ # Builtin from evaluator — call inner fn
2413
+ result = candidate.fn(*args)
2414
+ elif hasattr(candidate, 'call') and callable(candidate.call):
2415
+ result = candidate.call(*args)
2416
+ else:
2417
+ result = candidate
2207
2418
  else:
2208
2419
  result = attr
2209
2420
  except Exception:
@@ -2394,6 +2605,7 @@ class VM:
2394
2605
  elif op_name == "LEDGER_APPEND":
2395
2606
  entry = stack_pop()
2396
2607
  ledger = env.setdefault("_ledger", [])
2608
+ # MEDIUM (M12): Keep ledger bounded to prevent memory exhaustion.
2397
2609
  if len(ledger) < 10000: # Size limit
2398
2610
  if isinstance(entry, dict) and "timestamp" not in entry:
2399
2611
  entry["timestamp"] = time.time()
@@ -2429,8 +2641,16 @@ class VM:
2429
2641
  data = stack_pop(); action = stack_pop()
2430
2642
  if hasattr(action, 'value'): action = action.value
2431
2643
  if hasattr(data, 'value'): data = data.value
2432
- env.setdefault("_audit_log", []).append(
2433
- {"timestamp": ts, "action": action, "data": data})
2644
+ # MEDIUM (M13): Cap audit log growth to prevent memory exhaustion.
2645
+ audit = env.setdefault("_audit_log", [])
2646
+ entry = {"timestamp": ts, "action": action, "data": data}
2647
+ if len(audit) >= 10000:
2648
+ # Drop oldest entries; keep most recent.
2649
+ try:
2650
+ del audit[:1000]
2651
+ except Exception:
2652
+ audit[:] = audit[-9000:]
2653
+ audit.append(entry)
2434
2654
 
2435
2655
  elif op_name == "RESTRICT_ACCESS":
2436
2656
  restriction = stack_pop(); prop = stack_pop(); obj = stack_pop()
@@ -2455,6 +2675,48 @@ class VM:
2455
2675
  if caller and caller not in allowed:
2456
2676
  raise ZEvaluationError(f"Access denied: '{r_key}' restricted to allowed addresses")
2457
2677
 
2678
+ elif op_name == "SPAWN":
2679
+ # Sync SPAWN: schedule the function to run in a background thread
2680
+ import threading
2681
+ val = stack_pop() if stack else None
2682
+ if callable(val):
2683
+ # val is a callable — run it in a daemon thread
2684
+ t = threading.Thread(target=val, daemon=True)
2685
+ t.start()
2686
+ stack_append(t)
2687
+ elif isinstance(val, dict) and "bytecode" in val:
2688
+ # val is a function descriptor — execute its bytecode in a thread
2689
+ func_desc = val
2690
+ def _run_func_desc(fd=func_desc):
2691
+ try:
2692
+ child_vm = VM.create_child(parent_vm=self, env=dict(env))
2693
+ child_vm._run_stack_bytecode_sync(fd["bytecode"], debug=False)
2694
+ self._return_vm_to_pool(child_vm)
2695
+ except Exception:
2696
+ pass
2697
+ t = threading.Thread(target=_run_func_desc, daemon=True)
2698
+ t.start()
2699
+ stack_append(t)
2700
+ else:
2701
+ # Already a result (not spawnable) — just push it back
2702
+ stack_append(val)
2703
+
2704
+ elif op_name == "AWAIT":
2705
+ import threading
2706
+ val = stack_pop() if stack else None
2707
+ if isinstance(val, threading.Thread):
2708
+ val.join(timeout=30)
2709
+ stack_append(None)
2710
+ elif isinstance(val, str) and val in self._tasks:
2711
+ # asyncio task — run in event loop
2712
+ try:
2713
+ result = self._run_coroutine_sync(self._tasks[val])
2714
+ stack_append(result)
2715
+ except Exception:
2716
+ stack_append(None)
2717
+ else:
2718
+ stack_append(val)
2719
+
2458
2720
  else:
2459
2721
  # Truly unknown op — fallback to async path
2460
2722
  return self._run_coroutine_sync(self._run_stack_bytecode(bytecode, debug))
@@ -2569,6 +2831,22 @@ class VM:
2569
2831
  """Synchronous callable invocation for fast dispatch."""
2570
2832
  if fn is None:
2571
2833
  return None
2834
+ # SECURITY (H9): Enforce call-depth limit
2835
+ call_depth = getattr(self, '_call_depth', 0)
2836
+ max_depth = getattr(self, '_MAX_CALL_DEPTH', 256)
2837
+ if call_depth >= max_depth:
2838
+ raise RecursionError(
2839
+ f"VM call depth exceeded maximum ({max_depth}). "
2840
+ "Possible infinite recursion."
2841
+ )
2842
+ self._call_depth = call_depth + 1
2843
+ try:
2844
+ return self._invoke_callable_sync_inner(fn, args)
2845
+ finally:
2846
+ self._call_depth = getattr(self, '_call_depth', 1) - 1
2847
+
2848
+ def _invoke_callable_sync_inner(self, fn, args):
2849
+ """Inner implementation of callable invocation."""
2572
2850
  real_fn = fn.fn if hasattr(fn, "fn") else fn
2573
2851
  ZAction, ZLambda = _get_action_types()
2574
2852
  if ZAction is not None and isinstance(real_fn, (ZAction, ZLambda)):
@@ -2773,14 +3051,20 @@ class VM:
2773
3051
  _cached_ZAction, _cached_ZLambda = _get_action_types()
2774
3052
 
2775
3053
  class _EvalStack:
2776
- __slots__ = ("data", "sp")
3054
+ __slots__ = ("data", "sp", "_max_depth")
3055
+ _MAX_STACK_DEPTH = 100_000 # SECURITY (H8): prevent unbounded stack growth
2777
3056
 
2778
3057
  def __init__(self, capacity: int):
2779
3058
  base = max(32, capacity)
2780
3059
  self.data = [None] * base
2781
3060
  self.sp = 0
3061
+ self._max_depth = _EvalStack._MAX_STACK_DEPTH
2782
3062
 
2783
3063
  def _ensure_capacity(self):
3064
+ if self.sp >= self._max_depth:
3065
+ raise OverflowError(
3066
+ f"VM stack overflow: depth {self.sp} exceeds maximum {self._max_depth}"
3067
+ )
2784
3068
  if self.sp >= len(self.data):
2785
3069
  self.data.extend([None] * len(self.data))
2786
3070
 
@@ -2931,22 +3215,57 @@ class VM:
2931
3215
 
2932
3216
  def _binary_op(func):
2933
3217
  def wrapper(_):
2934
- b = _unwrap(stack.pop() if stack else 0)
2935
- a = _unwrap(stack.pop() if stack else 0)
2936
- if a is None: a = 0
2937
- if b is None: b = 0
3218
+ b_raw = stack.pop() if stack else 0
3219
+ a_raw = stack.pop() if stack else 0
3220
+ # Keep raw objects for DateTime-aware arithmetic
3221
+ a = _unwrap(a_raw)
3222
+ b = _unwrap(b_raw)
2938
3223
  if isinstance(a, ZEvaluationError):
2939
3224
  stack.append(a)
2940
3225
  return
2941
3226
  if isinstance(b, ZEvaluationError):
2942
3227
  stack.append(b)
2943
3228
  return
3229
+ # DateTime arithmetic
3230
+ if isinstance(a_raw, ZDateTime) or isinstance(b_raw, ZDateTime):
3231
+ a_dt = a_raw if isinstance(a_raw, ZDateTime) else None
3232
+ b_dt = b_raw if isinstance(b_raw, ZDateTime) else None
3233
+ if a_dt and b_dt:
3234
+ # DateTime - DateTime = seconds difference
3235
+ stack.append(a_dt.timestamp - b_dt.timestamp)
3236
+ elif a_dt:
3237
+ # DateTime +/- number
3238
+ val = float(b) if b is not None else 0.0
3239
+ try:
3240
+ result = func(a_dt.timestamp, val)
3241
+ stack.append(ZDateTime(result))
3242
+ except Exception:
3243
+ stack.append(0)
3244
+ else:
3245
+ val = float(a) if a is not None else 0.0
3246
+ try:
3247
+ result = func(val, b_dt.timestamp)
3248
+ stack.append(result)
3249
+ except Exception:
3250
+ stack.append(0)
3251
+ return
3252
+ if a is None: a = 0
3253
+ if b is None: b = 0
3254
+ # Type coercion for string concatenation in ADD
3255
+ if isinstance(a, str) or isinstance(b, str):
3256
+ try:
3257
+ stack.append(func(a, b))
3258
+ except TypeError:
3259
+ stack.append(str(a) + str(b))
3260
+ return
2944
3261
  try:
2945
3262
  stack.append(func(a, b))
2946
- except Exception as exc:
2947
- if os.environ.get("ZEXUS_VM_PROFILE_OPS"):
2948
- print(f"[VM DEBUG] binary op error func={func} a={a!r} b={b!r} exc={exc}")
2949
- raise
3263
+ except TypeError:
3264
+ # Graceful fallback — coerce to strings
3265
+ try:
3266
+ stack.append(str(a) + str(b))
3267
+ except Exception:
3268
+ stack.append(0)
2950
3269
  return wrapper
2951
3270
 
2952
3271
  def _binary_bool_op(func):
@@ -3166,7 +3485,16 @@ class VM:
3166
3485
  result = attr(*args)
3167
3486
  elif isinstance(target, dict) and method_name in target:
3168
3487
  candidate = target[method_name]
3169
- result = candidate(*args) if callable(candidate) else candidate
3488
+ # Handle Builtin objects (have .fn), ZAction/ZLambda (have .call),
3489
+ # and regular callables
3490
+ if callable(candidate):
3491
+ result = candidate(*args)
3492
+ elif hasattr(candidate, 'fn') and callable(candidate.fn):
3493
+ result = candidate.fn(*args)
3494
+ elif hasattr(candidate, 'call') and callable(candidate.call):
3495
+ result = candidate.call(*args)
3496
+ else:
3497
+ result = candidate
3170
3498
  else:
3171
3499
  result = attr
3172
3500
  if _verbose_active and self._call_method_trace_count <= 25:
@@ -3242,19 +3570,41 @@ class VM:
3242
3570
  stack.append(-a)
3243
3571
 
3244
3572
  def _op_add(_):
3245
- b = _unwrap(stack_pop() if stack else 0)
3246
- a = _unwrap(stack_pop() if stack else 0)
3247
- if a is None:
3248
- a = 0
3249
- if b is None:
3250
- b = 0
3573
+ b_raw = stack_pop() if stack else 0
3574
+ a_raw = stack_pop() if stack else 0
3575
+ # DateTime arithmetic
3576
+ if isinstance(a_raw, ZDateTime) or isinstance(b_raw, ZDateTime):
3577
+ a_dt = a_raw if isinstance(a_raw, ZDateTime) else None
3578
+ b_dt = b_raw if isinstance(b_raw, ZDateTime) else None
3579
+ if a_dt and b_dt:
3580
+ stack_append(a_dt.timestamp + b_dt.timestamp)
3581
+ elif a_dt:
3582
+ b = _unwrap(b_raw)
3583
+ if b is None: b = 0
3584
+ stack_append(ZDateTime(a_dt.timestamp + float(b)))
3585
+ else:
3586
+ a = _unwrap(a_raw)
3587
+ if a is None: a = 0
3588
+ stack_append(ZDateTime(b_raw.timestamp + float(a)))
3589
+ return
3590
+ b = _unwrap(b_raw)
3591
+ a = _unwrap(a_raw)
3251
3592
  if isinstance(a, ZEvaluationError):
3252
3593
  stack_append(a)
3253
3594
  return
3254
3595
  if isinstance(b, ZEvaluationError):
3255
3596
  stack_append(b)
3256
3597
  return
3257
- stack_append(a + b)
3598
+ # Type coercion: if either operand is a string, convert both to string
3599
+ if isinstance(a, str) or isinstance(b, str):
3600
+ stack_append(str(a if a is not None else '') + str(b if b is not None else ''))
3601
+ else:
3602
+ if a is None: a = 0
3603
+ if b is None: b = 0
3604
+ try:
3605
+ stack_append(a + b)
3606
+ except TypeError:
3607
+ stack_append(str(a) + str(b))
3258
3608
 
3259
3609
  def _op_not(_):
3260
3610
  a = stack.pop() if stack else False
@@ -3366,13 +3716,33 @@ class VM:
3366
3716
  def _op_read(_):
3367
3717
  path = stack.pop() if stack else None
3368
3718
  try:
3369
- import os
3370
- if path and os.path.exists(path):
3371
- with open(path, 'r') as f:
3372
- stack.append(f.read())
3373
- else:
3374
- stack.append(None)
3375
- except:
3719
+ import os as _os
3720
+ if path:
3721
+ # SECURITY (C11): Sandbox file reads to the module root (env __DIR__) when present.
3722
+ # This keeps sandboxing while allowing tests/runtime to set the project root without
3723
+ # relying on global process CWD.
3724
+ sandbox = _os.getcwd()
3725
+ try:
3726
+ env_dir = env_get("__DIR__", None)
3727
+ if env_dir:
3728
+ sandbox = _os.path.realpath(str(env_dir))
3729
+ except Exception:
3730
+ pass
3731
+
3732
+ if isinstance(path, str) and '\x00' not in path:
3733
+ candidate = path
3734
+ if _os.path.isabs(candidate):
3735
+ resolved = _os.path.realpath(candidate)
3736
+ else:
3737
+ resolved = _os.path.realpath(_os.path.join(sandbox, candidate))
3738
+
3739
+ if resolved == sandbox or resolved.startswith(sandbox + _os.sep):
3740
+ if _os.path.exists(resolved):
3741
+ with open(resolved, 'r') as f:
3742
+ stack.append(f.read())
3743
+ return
3744
+ stack.append(None)
3745
+ except (IOError, OSError, PermissionError):
3376
3746
  stack.append(None)
3377
3747
 
3378
3748
  def _op_store_func(operand):
@@ -3399,7 +3769,7 @@ class VM:
3399
3769
  "MUL": _binary_op(lambda a, b: a * b),
3400
3770
  "DIV": _binary_op(lambda a, b: a / b if b != 0 else 0),
3401
3771
  "MOD": _binary_op(lambda a, b: a % b if b != 0 else 0),
3402
- "POW": _binary_op(lambda a, b: a ** b),
3772
+ "POW": _binary_op(lambda a, b: _safe_pow(a, b)),
3403
3773
  "NEG": _op_neg,
3404
3774
  "EQ": _binary_bool_op(lambda a, b: a == b),
3405
3775
  "NEQ": _binary_bool_op(lambda a, b: a != b),
@@ -3456,6 +3826,7 @@ class VM:
3456
3826
  (self.enable_fast_loop or auto_fast_loop)
3457
3827
  and not profile_ops
3458
3828
  and not gas_enabled # Never skip gas metering (including gas_light)
3829
+ and not gas_light # SECURITY (C13): also block fast loop under gas_light
3459
3830
  and not trace_interval
3460
3831
  and trace_ip_range is None
3461
3832
  and not trace_loads_active
@@ -3472,6 +3843,8 @@ class VM:
3472
3843
  reasons.append("opcode_profile")
3473
3844
  if gas_enabled and not gas_light:
3474
3845
  reasons.append("gas")
3846
+ if gas_light:
3847
+ reasons.append("gas_light")
3475
3848
  if trace_interval:
3476
3849
  reasons.append("trace_interval")
3477
3850
  if trace_ip_range is not None:
@@ -3665,7 +4038,7 @@ class VM:
3665
4038
  self._jit_registers = {}
3666
4039
  self._jit_registers[reg] = value
3667
4040
 
3668
- elif op_name == "LOAD_VAR_REG":
4041
+ res = _safe_pow(v1, v2)
3669
4042
  reg, name_idx = operand
3670
4043
  name = const(name_idx)
3671
4044
  value = _resolve(name)
@@ -3960,12 +4333,31 @@ class VM:
3960
4333
  # Auto-unwrap evaluator objects
3961
4334
  if hasattr(a, 'value'): a = a.value
3962
4335
  if hasattr(b, 'value'): b = b.value
3963
- stack.append(a + b)
4336
+ # Type coercion: if either operand is a string, convert both to string
4337
+ if isinstance(a, str) or isinstance(b, str):
4338
+ stack.append(str(a if a is not None else '') + str(b if b is not None else ''))
4339
+ else:
4340
+ if a is None: a = 0
4341
+ if b is None: b = 0
4342
+ stack.append(a + b)
3964
4343
  elif op_name == "SUB":
3965
- b = stack.pop() if stack else 0; a = stack.pop() if stack else 0
3966
- if hasattr(a, 'value'): a = a.value
3967
- if hasattr(b, 'value'): b = b.value
3968
- stack.append(a - b)
4344
+ b_raw = stack.pop() if stack else 0; a_raw = stack.pop() if stack else 0
4345
+ # DateTime arithmetic
4346
+ if isinstance(a_raw, ZDateTime) and isinstance(b_raw, ZDateTime):
4347
+ stack.append(a_raw.timestamp - b_raw.timestamp)
4348
+ elif isinstance(a_raw, ZDateTime):
4349
+ b = b_raw.value if hasattr(b_raw, 'value') else b_raw
4350
+ if b is None: b = 0
4351
+ stack.append(ZDateTime(a_raw.timestamp - float(b)))
4352
+ else:
4353
+ a = a_raw.value if hasattr(a_raw, 'value') else a_raw
4354
+ b = b_raw.value if hasattr(b_raw, 'value') else b_raw
4355
+ if a is None: a = 0
4356
+ if b is None: b = 0
4357
+ try:
4358
+ stack.append(a - b)
4359
+ except TypeError:
4360
+ stack.append(0)
3969
4361
  elif op_name == "MUL":
3970
4362
  b = stack.pop() if stack else 0; a = stack.pop() if stack else 0
3971
4363
  if hasattr(a, 'value'): a = a.value
@@ -3977,11 +4369,17 @@ class VM:
3977
4369
  if hasattr(b, 'value'): b = b.value
3978
4370
  stack.append(a / b if b != 0 else 0)
3979
4371
  elif op_name == "MOD":
3980
- b = stack.pop() if stack else 1; a = stack.pop() if stack else 0
4372
+ b = _unwrap(stack.pop() if stack else 1)
4373
+ a = _unwrap(stack.pop() if stack else 0)
4374
+ if a is None: a = 0
4375
+ if b is None: b = 1
3981
4376
  stack.append(a % b if b != 0 else 0)
3982
4377
  elif op_name == "POW":
3983
- b = stack.pop() if stack else 1; a = stack.pop() if stack else 0
3984
- stack.append(a ** b)
4378
+ b = _unwrap(stack.pop() if stack else 0)
4379
+ a = _unwrap(stack.pop() if stack else 0)
4380
+ if a is None: a = 0
4381
+ if b is None: b = 0
4382
+ stack.append(_safe_pow(a, b))
3985
4383
  elif op_name == "NEG":
3986
4384
  a = stack.pop() if stack else 0
3987
4385
  stack.append(-a)
@@ -4006,6 +4404,14 @@ class VM:
4006
4404
  elif op_name == "NOT":
4007
4405
  a = stack.pop() if stack else False
4008
4406
  stack.append(not a)
4407
+ elif op_name == "AND":
4408
+ b = _unwrap(stack.pop() if stack else False)
4409
+ a = _unwrap(stack.pop() if stack else False)
4410
+ stack.append(bool(a) and bool(b))
4411
+ elif op_name == "OR":
4412
+ b = _unwrap(stack.pop() if stack else False)
4413
+ a = _unwrap(stack.pop() if stack else False)
4414
+ stack.append(bool(a) or bool(b))
4009
4415
 
4010
4416
  # --- Control Flow ---
4011
4417
  elif op_name == "JUMP":
@@ -4013,6 +4419,9 @@ class VM:
4013
4419
  elif op_name == "JUMP_IF_FALSE":
4014
4420
  cond = stack.pop() if stack else None
4015
4421
  if not cond: ip = operand
4422
+ elif op_name == "JUMP_IF_TRUE":
4423
+ cond = _unwrap(stack.pop() if stack else None)
4424
+ if cond: ip = operand
4016
4425
  elif op_name == "RETURN":
4017
4426
  return stack.pop() if stack else None
4018
4427
 
@@ -4208,15 +4617,20 @@ class VM:
4208
4617
  self._execute_import(mod_name, alias=alias or "", names=names, is_named=bool(is_named))
4209
4618
 
4210
4619
  elif op_name == "EXPORT":
4620
+ # vm/compiler.py emits: LOAD_NAME <name_idx>; EXPORT <name_idx>
4621
+ # Support both legacy tuple operand and int operand.
4211
4622
  name = None
4212
4623
  value = None
4213
- if isinstance(operand, (list, tuple)) and operand:
4624
+ if isinstance(operand, int):
4625
+ name = const(operand)
4626
+ value = stack.pop() if stack else None
4627
+ elif isinstance(operand, (list, tuple)) and operand:
4214
4628
  name = const(operand[0])
4629
+ value = stack.pop() if stack else None
4215
4630
  if len(operand) > 1:
4216
4631
  value = const(operand[1])
4217
- if name is None:
4632
+ else:
4218
4633
  name = stack.pop() if stack else None
4219
- if value is None:
4220
4634
  value = stack.pop() if stack else None
4221
4635
 
4222
4636
  export_fn = getattr(self.env, "export", None)
@@ -4234,12 +4648,34 @@ class VM:
4234
4648
  path = stack.pop() if stack else None
4235
4649
  try:
4236
4650
  if path is not None:
4237
- with open(path, "w") as f:
4238
- if isinstance(payload, bytes):
4239
- f.write(payload.decode("utf-8"))
4240
- else:
4241
- f.write(str(payload) if payload is not None else "")
4242
- stack.append(True)
4651
+ # SECURITY (C12): Sandbox file writes to the module root (env __DIR__) when present.
4652
+ import os as _os_w
4653
+ sandbox = _os_w.getcwd()
4654
+ try:
4655
+ env_dir = self.env.get("__DIR__", None)
4656
+ if env_dir:
4657
+ sandbox = _os_w.path.realpath(str(env_dir))
4658
+ except Exception:
4659
+ pass
4660
+
4661
+ path_str = str(path)
4662
+ if "\x00" in path_str:
4663
+ stack.append(False)
4664
+ continue
4665
+
4666
+ if _os_w.path.isabs(path_str):
4667
+ resolved = _os_w.path.realpath(path_str)
4668
+ else:
4669
+ resolved = _os_w.path.realpath(_os_w.path.join(sandbox, path_str))
4670
+ if not (resolved == sandbox or resolved.startswith(sandbox + _os_w.sep)):
4671
+ stack.append(False) # path traversal denied
4672
+ else:
4673
+ with open(resolved, "w") as f:
4674
+ if isinstance(payload, bytes):
4675
+ f.write(payload.decode("utf-8"))
4676
+ else:
4677
+ f.write(str(payload) if payload is not None else "")
4678
+ stack.append(True)
4243
4679
  else:
4244
4680
  stack.append(False)
4245
4681
  except Exception:
@@ -4561,7 +4997,13 @@ class VM:
4561
4997
  data = data.value if hasattr(data, 'value') else data
4562
4998
 
4563
4999
  entry = {"timestamp": ts, "action": action, "data": data}
4564
- self.env.setdefault("_audit_log", []).append(entry)
5000
+ audit = self.env.setdefault("_audit_log", [])
5001
+ if len(audit) >= 10000:
5002
+ try:
5003
+ del audit[:1000]
5004
+ except Exception:
5005
+ audit[:] = audit[-9000:]
5006
+ audit.append(entry)
4565
5007
  if self.debug: print(f"[AUDIT] {entry}")
4566
5008
 
4567
5009
  elif op_name == "RESTRICT_ACCESS":
@@ -4606,7 +5048,9 @@ class VM:
4606
5048
  entry = stack.pop() if stack else None
4607
5049
  if isinstance(entry, dict) and "timestamp" not in entry:
4608
5050
  entry["timestamp"] = time.time()
4609
- self.env.setdefault("_ledger", []).append(entry)
5051
+ ledger = self.env.setdefault("_ledger", [])
5052
+ if len(ledger) < 10000:
5053
+ ledger.append(entry)
4610
5054
 
4611
5055
  elif op_name in ("PARALLEL_START", "PARALLEL_END"):
4612
5056
  # Marker ops for parallel execution - no-op in stack VM
@@ -4625,6 +5069,22 @@ class VM:
4625
5069
  if debug: print(f"[VM ERROR RECOVERY] {e}")
4626
5070
  self.env.setdefault("_errors", []).append(str(e))
4627
5071
  else:
5072
+ # LOGIC (L4): If an exception escapes a TX block, revert state to snapshot.
5073
+ if self.env.get("_in_transaction", False):
5074
+ try:
5075
+ self.env["_blockchain_state"] = dict(self.env.get("_tx_snapshot", {}))
5076
+ self.env["_tx_pending_state"] = {}
5077
+ self.env.pop("_tx_snapshot", None)
5078
+ if self.use_memory_manager and "_tx_memory_snapshot" in self.env:
5079
+ self._managed_objects = dict(self.env["_tx_memory_snapshot"])
5080
+ del self.env["_tx_memory_snapshot"]
5081
+ tx_stack = self.env.get("_tx_stack", [])
5082
+ if tx_stack:
5083
+ tx_stack.pop()
5084
+ self.env["_in_transaction"] = bool(tx_stack)
5085
+ except Exception:
5086
+ # Best-effort revert; never swallow the original exception.
5087
+ pass
4628
5088
  raise
4629
5089
 
4630
5090
  if profile_ops and opcode_counts is not None:
@@ -4731,7 +5191,11 @@ class VM:
4731
5191
  print(f"[VM TRACE] action error: {result.message}")
4732
5192
  return self._unwrap_after_builtin(result)
4733
5193
  except Exception:
4734
- pass
5194
+ # ZAction/ZLambda handling failed — but don't silently swallow
5195
+ # If this was a ZAction, we should NOT fall through to the callable check
5196
+ # because ZAction objects are not directly callable and would be returned raw
5197
+ if ZAction is not None and isinstance(real_fn, (ZAction, ZLambda)):
5198
+ return None
4735
5199
 
4736
5200
  if not callable(real_fn): return real_fn
4737
5201
 
@@ -4756,7 +5220,7 @@ class VM:
4756
5220
  res = await res
4757
5221
  return self._unwrap_after_builtin(res)
4758
5222
  except Exception as e:
4759
- return e
5223
+ return ZEvaluationError(f"{type(e).__name__}: {e}")
4760
5224
 
4761
5225
  def _to_coro(self, fn, args):
4762
5226
  if asyncio.iscoroutinefunction(fn):
@@ -4999,6 +5463,56 @@ class VM:
4999
5463
  def create_vm(mode: str = "auto", use_jit: bool = True, **kwargs) -> VM:
5000
5464
  return VM(mode=VMMode(mode.lower()), use_jit=use_jit, **kwargs)
5001
5465
 
5466
+
5467
+ def _safe_pow(a, b, *, max_exp: int = 10000, max_bits: int = 8192):
5468
+ """Bound exponentiation to avoid exponent DoS.
5469
+
5470
+ LI11: Prevent pathological cases like `10 ** 10_000_000`.
5471
+ The guard is intentionally conservative; it applies mainly to integer
5472
+ exponentiation.
5473
+ """
5474
+ try:
5475
+ # Normalize wrapped evaluator objects
5476
+ if hasattr(a, 'value'):
5477
+ a = a.value
5478
+ if hasattr(b, 'value'):
5479
+ b = b.value
5480
+
5481
+ # Only guard integer-ish exponentiation; float pow is already bounded by
5482
+ # IEEE semantics but can still be expensive for huge integer exponents.
5483
+ if isinstance(b, bool):
5484
+ b_int = int(b)
5485
+ elif isinstance(b, int):
5486
+ b_int = b
5487
+ else:
5488
+ # If exponent isn't an int, fall back to Python pow
5489
+ return a ** b
5490
+
5491
+ if abs(b_int) > max_exp:
5492
+ raise OverflowError(f"POW exponent too large: {b_int}")
5493
+
5494
+ # Rough bit-growth guard for integer base
5495
+ if isinstance(a, bool):
5496
+ a_int = int(a)
5497
+ elif isinstance(a, int):
5498
+ a_int = a
5499
+ else:
5500
+ a_int = None
5501
+
5502
+ if a_int is not None and b_int >= 0:
5503
+ # If base is -1, 0, 1 then pow is cheap
5504
+ if a_int not in (-1, 0, 1):
5505
+ est_bits = max(1, abs(a_int).bit_length()) * b_int
5506
+ if est_bits > max_bits:
5507
+ raise OverflowError(
5508
+ f"POW result too large (estimated {est_bits} bits > {max_bits})"
5509
+ )
5510
+
5511
+ return a ** b_int
5512
+ except Exception:
5513
+ # Let callers decide how to surface the error
5514
+ raise
5515
+
5002
5516
  def create_high_performance_vm() -> VM:
5003
5517
  return create_vm(
5004
5518
  mode="auto",