zexus 1.6.8 → 1.7.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 (177) hide show
  1. package/README.md +12 -5
  2. package/package.json +1 -1
  3. package/src/__init__.py +7 -0
  4. package/src/zexus/__init__.py +1 -1
  5. package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
  7. package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
  8. package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
  9. package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
  10. package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
  11. package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
  12. package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
  13. package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
  14. package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
  15. package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
  16. package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
  17. package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
  18. package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  19. package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
  20. package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
  22. package/src/zexus/advanced_types.py +17 -2
  23. package/src/zexus/blockchain/__init__.py +411 -0
  24. package/src/zexus/blockchain/accelerator.py +1160 -0
  25. package/src/zexus/blockchain/chain.py +660 -0
  26. package/src/zexus/blockchain/consensus.py +821 -0
  27. package/src/zexus/blockchain/contract_vm.py +1019 -0
  28. package/src/zexus/blockchain/crypto.py +79 -14
  29. package/src/zexus/blockchain/events.py +526 -0
  30. package/src/zexus/blockchain/loadtest.py +721 -0
  31. package/src/zexus/blockchain/monitoring.py +350 -0
  32. package/src/zexus/blockchain/mpt.py +716 -0
  33. package/src/zexus/blockchain/multichain.py +951 -0
  34. package/src/zexus/blockchain/multiprocess_executor.py +338 -0
  35. package/src/zexus/blockchain/network.py +886 -0
  36. package/src/zexus/blockchain/node.py +666 -0
  37. package/src/zexus/blockchain/rpc.py +1203 -0
  38. package/src/zexus/blockchain/rust_bridge.py +421 -0
  39. package/src/zexus/blockchain/storage.py +423 -0
  40. package/src/zexus/blockchain/tokens.py +750 -0
  41. package/src/zexus/blockchain/upgradeable.py +1004 -0
  42. package/src/zexus/blockchain/verification.py +1602 -0
  43. package/src/zexus/blockchain/wallet.py +621 -0
  44. package/src/zexus/capability_system.py +184 -9
  45. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  46. package/src/zexus/cli/main.py +383 -34
  47. package/src/zexus/cli/zpm.py +1 -1
  48. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  53. package/src/zexus/compiler/bytecode.py +124 -7
  54. package/src/zexus/compiler/compat_runtime.py +6 -2
  55. package/src/zexus/compiler/lexer.py +16 -5
  56. package/src/zexus/compiler/parser.py +108 -7
  57. package/src/zexus/compiler/semantic.py +18 -19
  58. package/src/zexus/compiler/zexus_ast.py +26 -1
  59. package/src/zexus/concurrency_system.py +79 -0
  60. package/src/zexus/config.py +54 -0
  61. package/src/zexus/crypto_bridge.py +244 -8
  62. package/src/zexus/dap/__init__.py +10 -0
  63. package/src/zexus/dap/__main__.py +4 -0
  64. package/src/zexus/dap/dap_server.py +391 -0
  65. package/src/zexus/dap/debug_engine.py +298 -0
  66. package/src/zexus/environment.py +112 -9
  67. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  70. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  71. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  72. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  73. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  74. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  75. package/src/zexus/evaluator/bytecode_compiler.py +457 -37
  76. package/src/zexus/evaluator/core.py +644 -50
  77. package/src/zexus/evaluator/expressions.py +358 -62
  78. package/src/zexus/evaluator/functions.py +458 -20
  79. package/src/zexus/evaluator/resource_limiter.py +4 -4
  80. package/src/zexus/evaluator/statements.py +774 -122
  81. package/src/zexus/evaluator/unified_execution.py +573 -72
  82. package/src/zexus/evaluator/utils.py +14 -2
  83. package/src/zexus/evaluator_original.py +1 -1
  84. package/src/zexus/event_loop.py +186 -0
  85. package/src/zexus/lexer.py +742 -458
  86. package/src/zexus/lsp/__init__.py +1 -1
  87. package/src/zexus/lsp/definition_provider.py +163 -9
  88. package/src/zexus/lsp/server.py +22 -8
  89. package/src/zexus/lsp/symbol_provider.py +182 -9
  90. package/src/zexus/module_cache.py +239 -9
  91. package/src/zexus/module_manager.py +129 -1
  92. package/src/zexus/object.py +76 -6
  93. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  94. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  95. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  96. package/src/zexus/parser/parser.py +1349 -408
  97. package/src/zexus/parser/strategy_context.py +755 -58
  98. package/src/zexus/parser/strategy_structural.py +121 -21
  99. package/src/zexus/persistence.py +15 -1
  100. package/src/zexus/renderer/__init__.py +61 -0
  101. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  103. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  104. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  105. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  106. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  107. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  108. package/src/zexus/renderer/backend.py +261 -0
  109. package/src/zexus/renderer/canvas.py +78 -0
  110. package/src/zexus/renderer/color_system.py +201 -0
  111. package/src/zexus/renderer/graphics.py +31 -0
  112. package/src/zexus/renderer/layout.py +222 -0
  113. package/src/zexus/renderer/main_renderer.py +66 -0
  114. package/src/zexus/renderer/painter.py +30 -0
  115. package/src/zexus/renderer/tk_backend.py +208 -0
  116. package/src/zexus/renderer/web_backend.py +260 -0
  117. package/src/zexus/runtime/__init__.py +10 -2
  118. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  119. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  120. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  121. package/src/zexus/runtime/file_flags.py +137 -0
  122. package/src/zexus/runtime/load_manager.py +368 -0
  123. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  124. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  125. package/src/zexus/security.py +424 -34
  126. package/src/zexus/stdlib/fs.py +23 -18
  127. package/src/zexus/stdlib/http.py +289 -186
  128. package/src/zexus/stdlib/sockets.py +207 -163
  129. package/src/zexus/stdlib/websockets.py +282 -0
  130. package/src/zexus/stdlib_integration.py +369 -2
  131. package/src/zexus/strategy_recovery.py +6 -3
  132. package/src/zexus/type_checker.py +423 -0
  133. package/src/zexus/virtual_filesystem.py +189 -2
  134. package/src/zexus/vm/__init__.py +113 -3
  135. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  136. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  137. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  138. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  139. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  140. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  141. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  142. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  143. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  144. package/src/zexus/vm/async_optimizer.py +80 -6
  145. package/src/zexus/vm/binary_bytecode.py +659 -0
  146. package/src/zexus/vm/bytecode.py +59 -11
  147. package/src/zexus/vm/bytecode_converter.py +26 -12
  148. package/src/zexus/vm/cabi.c +1985 -0
  149. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  150. package/src/zexus/vm/cabi.h +127 -0
  151. package/src/zexus/vm/cache.py +561 -17
  152. package/src/zexus/vm/compiler.py +818 -51
  153. package/src/zexus/vm/fastops.c +15743 -0
  154. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  155. package/src/zexus/vm/fastops.pyx +288 -0
  156. package/src/zexus/vm/gas_metering.py +50 -9
  157. package/src/zexus/vm/jit.py +364 -20
  158. package/src/zexus/vm/native_jit_backend.py +1816 -0
  159. package/src/zexus/vm/native_runtime.cpp +1388 -0
  160. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  161. package/src/zexus/vm/optimizer.py +161 -11
  162. package/src/zexus/vm/parallel_vm.py +140 -45
  163. package/src/zexus/vm/peephole_optimizer.py +82 -4
  164. package/src/zexus/vm/profiler.py +38 -18
  165. package/src/zexus/vm/register_allocator.py +16 -5
  166. package/src/zexus/vm/register_vm.py +8 -5
  167. package/src/zexus/vm/vm.py +3581 -531
  168. package/src/zexus/vm/wasm_compiler.py +658 -0
  169. package/src/zexus/zexus_ast.py +137 -11
  170. package/src/zexus/zexus_token.py +16 -5
  171. package/src/zexus/zpm/installer.py +55 -15
  172. package/src/zexus/zpm/package_manager.py +1 -1
  173. package/src/zexus/zpm/registry.py +257 -28
  174. package/src/zexus.egg-info/PKG-INFO +16 -6
  175. package/src/zexus.egg-info/SOURCES.txt +129 -17
  176. package/src/zexus.egg-info/entry_points.txt +1 -0
  177. package/src/zexus.egg-info/requires.txt +4 -0
@@ -54,6 +54,8 @@ class ErrorRecoveryEngine:
54
54
  return 'missing_parenthesis'
55
55
  elif "missing }" in error_msg or "brace" in error_msg:
56
56
  return 'missing_brace'
57
+ elif "finally" in error_msg:
58
+ return 'expected_catch'
57
59
  else:
58
60
  return 'syntax_error'
59
61
 
@@ -160,8 +162,8 @@ class ErrorRecoveryEngine:
160
162
 
161
163
  # Look for catch blocks in current block or parent blocks
162
164
  for block_id, block in blocks.items():
163
- if block.get('subtype') == 'try_catch':
164
- if block.get('catch_section'):
165
+ if block.get('subtype') in ('try_catch', 'try_catch_statement'):
166
+ if block.get('catch_section') or block.get('finally_section'):
165
167
  return block
166
168
  # Check nested blocks
167
169
  for nested in block.get('nested_blocks', []):
@@ -195,7 +197,8 @@ class ErrorRecoveryEngine:
195
197
  return TryCatchStatement(
196
198
  try_block=BlockStatement(),
197
199
  error_variable=Identifier("error"),
198
- catch_block=BlockStatement()
200
+ catch_block=BlockStatement(),
201
+ finally_block=None
199
202
  )
200
203
 
201
204
  def _create_synthetic_catch_block(self):
@@ -0,0 +1,423 @@
1
+ """
2
+ Static Type Checker for Zexus
3
+ =============================
4
+
5
+ Performs a single AST-walking pass **before** evaluation to detect type
6
+ errors at authoring time. The checker intentionally operates on
7
+ *annotations only* — it will never execute user code.
8
+
9
+ Usage::
10
+
11
+ from zexus.type_checker import StaticTypeChecker
12
+ checker = StaticTypeChecker()
13
+ diagnostics = checker.check(program_ast)
14
+ for d in diagnostics:
15
+ print(d)
16
+
17
+ Each diagnostic is a ``TypeDiagnostic`` named-tuple with ``level``
18
+ (``"error"`` or ``"warning"``), ``message``, and an optional ``node``
19
+ reference.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from typing import Any, Dict, List, Optional, Tuple
26
+
27
+ from .type_system import BaseType, TypeSpec, STANDARD_TYPES, parse_type_annotation
28
+ from . import zexus_ast as ast
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Diagnostic container
33
+ # ---------------------------------------------------------------------------
34
+
35
+ @dataclass
36
+ class TypeDiagnostic:
37
+ level: str # "error" | "warning"
38
+ message: str
39
+ node: Optional[ast.Node] = None
40
+
41
+ def __str__(self) -> str:
42
+ prefix = "TypeError" if self.level == "error" else "TypeWarning"
43
+ return f"[{prefix}] {self.message}"
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Scope (symbol table for one lexical scope)
48
+ # ---------------------------------------------------------------------------
49
+
50
+ @dataclass
51
+ class _Scope:
52
+ """Tracks declared names -> TypeSpec in a single lexical scope."""
53
+ parent: Optional["_Scope"] = None
54
+ symbols: Dict[str, TypeSpec] = field(default_factory=dict)
55
+ # Actions/functions: name -> (param_types, return_type)
56
+ callables: Dict[str, Tuple[List[Tuple[str, Optional[TypeSpec]]], Optional[TypeSpec]]] = field(
57
+ default_factory=dict
58
+ )
59
+
60
+ def define(self, name: str, ts: Optional[TypeSpec]):
61
+ self.symbols[name] = ts # type: ignore[assignment]
62
+
63
+ def lookup(self, name: str) -> Optional[TypeSpec]:
64
+ if name in self.symbols:
65
+ return self.symbols[name]
66
+ if self.parent:
67
+ return self.parent.lookup(name)
68
+ return None
69
+
70
+ def define_callable(
71
+ self,
72
+ name: str,
73
+ params: List[Tuple[str, Optional[TypeSpec]]],
74
+ return_type: Optional[TypeSpec],
75
+ ):
76
+ self.callables[name] = (params, return_type)
77
+
78
+ def lookup_callable(
79
+ self, name: str
80
+ ) -> Optional[Tuple[List[Tuple[str, Optional[TypeSpec]]], Optional[TypeSpec]]]:
81
+ if name in self.callables:
82
+ return self.callables[name]
83
+ if self.parent:
84
+ return self.parent.lookup_callable(name)
85
+ return None
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Type inference helpers
90
+ # ---------------------------------------------------------------------------
91
+
92
+ _LITERAL_TYPE_MAP = {
93
+ ast.IntegerLiteral: BaseType.INT,
94
+ ast.FloatLiteral: BaseType.FLOAT,
95
+ ast.StringLiteral: BaseType.STRING,
96
+ ast.Boolean: BaseType.BOOL,
97
+ }
98
+
99
+
100
+ def _resolve_annotation(annotation) -> Optional[TypeSpec]:
101
+ """Convert a string or ``Identifier`` annotation to ``TypeSpec``,
102
+ returning *None* when the annotation is absent or un-parseable."""
103
+ if annotation is None:
104
+ return None
105
+ # Handle both raw strings and Identifier AST nodes
106
+ if isinstance(annotation, ast.Identifier):
107
+ annotation = annotation.value
108
+ if not isinstance(annotation, str):
109
+ annotation = str(annotation)
110
+ return parse_type_annotation(annotation)
111
+
112
+
113
+ def _infer_expr_type(node: ast.Expression, scope: _Scope) -> Optional[TypeSpec]:
114
+ """Best-effort inference of the static type of *node*.
115
+
116
+ Returns ``None`` when the type cannot be determined statically.
117
+ """
118
+ node_cls = type(node)
119
+
120
+ # Literals
121
+ base = _LITERAL_TYPE_MAP.get(node_cls)
122
+ if base is not None:
123
+ return TypeSpec(base)
124
+
125
+ # Array/List literal
126
+ if isinstance(node, ast.ListLiteral):
127
+ if node.elements:
128
+ elem_type = _infer_expr_type(node.elements[0], scope)
129
+ return TypeSpec(BaseType.ARRAY, array_of=elem_type)
130
+ return TypeSpec(BaseType.ARRAY, array_of=TypeSpec(BaseType.ANY))
131
+
132
+ # Map literal
133
+ if isinstance(node, ast.MapLiteral):
134
+ return TypeSpec(BaseType.OBJECT)
135
+
136
+ # Identifier — look up in scope
137
+ if isinstance(node, ast.Identifier):
138
+ return scope.lookup(node.value)
139
+
140
+ # Infix — numeric ops yield numeric types
141
+ if isinstance(node, ast.InfixExpression):
142
+ if node.operator in ("+", "-", "*", "/", "%", "**"):
143
+ left = _infer_expr_type(node.left, scope)
144
+ right = _infer_expr_type(node.right, scope)
145
+ if left and right:
146
+ if left.base_type == BaseType.FLOAT or right.base_type == BaseType.FLOAT:
147
+ return TypeSpec(BaseType.FLOAT)
148
+ if left.base_type == BaseType.INT and right.base_type == BaseType.INT:
149
+ if node.operator == "/":
150
+ return TypeSpec(BaseType.FLOAT)
151
+ return TypeSpec(BaseType.INT)
152
+ # String concatenation
153
+ if node.operator == "+" and left.base_type == BaseType.STRING:
154
+ return TypeSpec(BaseType.STRING)
155
+ return None
156
+ if node.operator in ("==", "!=", "<", ">", "<=", ">=", "&&", "||", "and", "or"):
157
+ return TypeSpec(BaseType.BOOL)
158
+
159
+ # Prefix — `!` => bool, `-` => numeric
160
+ if isinstance(node, ast.PrefixExpression):
161
+ if node.operator == "!":
162
+ return TypeSpec(BaseType.BOOL)
163
+ if node.operator == "-":
164
+ return _infer_expr_type(node.right, scope)
165
+
166
+ # Call expression — look up return type
167
+ if isinstance(node, ast.CallExpression):
168
+ fn_name = None
169
+ if isinstance(node.function, ast.Identifier):
170
+ fn_name = node.function.value
171
+ if fn_name:
172
+ sig = scope.lookup_callable(fn_name)
173
+ if sig:
174
+ _, ret = sig
175
+ return ret
176
+
177
+ return None
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Compatibility check
182
+ # ---------------------------------------------------------------------------
183
+
184
+ def _is_compatible(declared: TypeSpec, actual: TypeSpec) -> bool:
185
+ """Check whether *actual* can be assigned to a slot of type *declared*."""
186
+ if declared.base_type == BaseType.ANY or actual.base_type == BaseType.ANY:
187
+ return True
188
+ # number promotion: int -> float
189
+ if declared.base_type == BaseType.FLOAT and actual.base_type == BaseType.INT:
190
+ return True
191
+ if declared.base_type != actual.base_type:
192
+ return False
193
+ # Array element types
194
+ if declared.array_of and actual.array_of:
195
+ return _is_compatible(declared.array_of, actual.array_of)
196
+ return True
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Main checker
201
+ # ---------------------------------------------------------------------------
202
+
203
+ class StaticTypeChecker:
204
+ """Walk an AST ``Program`` and collect type diagnostics."""
205
+
206
+ def __init__(self) -> None:
207
+ self.diagnostics: List[TypeDiagnostic] = []
208
+ self._scope: _Scope = _Scope() # global scope
209
+
210
+ # -- public API ---------------------------------------------------------
211
+
212
+ def check(self, program: ast.Program) -> List[TypeDiagnostic]:
213
+ """Run the checker on *program* and return all diagnostics."""
214
+ self.diagnostics = []
215
+ self._scope = _Scope()
216
+ for stmt in program.statements:
217
+ self._check_statement(stmt)
218
+ return list(self.diagnostics)
219
+
220
+ # -- helpers ------------------------------------------------------------
221
+
222
+ def _error(self, msg: str, node: Optional[ast.Node] = None):
223
+ self.diagnostics.append(TypeDiagnostic("error", msg, node))
224
+
225
+ def _warning(self, msg: str, node: Optional[ast.Node] = None):
226
+ self.diagnostics.append(TypeDiagnostic("warning", msg, node))
227
+
228
+ def _push_scope(self) -> _Scope:
229
+ self._scope = _Scope(parent=self._scope)
230
+ return self._scope
231
+
232
+ def _pop_scope(self):
233
+ if self._scope.parent:
234
+ self._scope = self._scope.parent
235
+
236
+ # -- statement visitors -------------------------------------------------
237
+
238
+ def _check_statement(self, stmt: ast.Statement):
239
+ handler = getattr(self, f"_check_{type(stmt).__name__}", None)
240
+ if handler:
241
+ handler(stmt)
242
+
243
+ def _check_LetStatement(self, stmt: ast.LetStatement):
244
+ ann = _resolve_annotation(getattr(stmt, "type_annotation", None))
245
+ if stmt.value is not None:
246
+ self._check_expression(stmt.value)
247
+ actual = _infer_expr_type(stmt.value, self._scope)
248
+ if ann and actual and not _is_compatible(ann, actual):
249
+ self._error(
250
+ f"Cannot assign {actual.base_type.value} to "
251
+ f"'{stmt.name}' declared as {ann.base_type.value}",
252
+ stmt,
253
+ )
254
+ name = stmt.name.value if isinstance(stmt.name, ast.Identifier) else None
255
+ if name:
256
+ self._scope.define(name, ann)
257
+
258
+ def _check_ConstStatement(self, stmt: ast.ConstStatement):
259
+ ann = _resolve_annotation(getattr(stmt, "type_annotation", None))
260
+ if stmt.value is not None:
261
+ self._check_expression(stmt.value)
262
+ actual = _infer_expr_type(stmt.value, self._scope)
263
+ if ann and actual and not _is_compatible(ann, actual):
264
+ self._error(
265
+ f"Cannot assign {actual.base_type.value} to "
266
+ f"const '{stmt.name}' declared as {ann.base_type.value}",
267
+ stmt,
268
+ )
269
+ name = stmt.name.value if isinstance(stmt.name, ast.Identifier) else None
270
+ if name:
271
+ self._scope.define(name, ann)
272
+
273
+ def _check_ActionStatement(self, stmt: ast.ActionStatement):
274
+ name = stmt.name if isinstance(stmt.name, str) else getattr(stmt.name, "value", None)
275
+ param_types: List[Tuple[str, Optional[TypeSpec]]] = []
276
+ for p in stmt.parameters or []:
277
+ ann_str = getattr(p, "type_annotation", None)
278
+ ts = _resolve_annotation(ann_str)
279
+ param_types.append((p.value, ts))
280
+ ret_ts = _resolve_annotation(stmt.return_type)
281
+ if name:
282
+ self._scope.define_callable(name, param_types, ret_ts)
283
+ self._scope.define(name, TypeSpec(BaseType.ACTION))
284
+
285
+ # Check body in a nested scope
286
+ self._push_scope()
287
+ for pname, pts in param_types:
288
+ self._scope.define(pname, pts)
289
+ if stmt.body:
290
+ self._check_block(stmt.body, ret_ts)
291
+ self._pop_scope()
292
+
293
+ def _check_FunctionStatement(self, stmt: ast.FunctionStatement):
294
+ # Reuse action logic
295
+ name = stmt.name if isinstance(stmt.name, str) else getattr(stmt.name, "value", None)
296
+ param_types: List[Tuple[str, Optional[TypeSpec]]] = []
297
+ for p in stmt.parameters or []:
298
+ ann_str = getattr(p, "type_annotation", None)
299
+ ts = _resolve_annotation(ann_str)
300
+ param_types.append((p.value, ts))
301
+ ret_ts = _resolve_annotation(getattr(stmt, "return_type", None))
302
+ if name:
303
+ self._scope.define_callable(name, param_types, ret_ts)
304
+ self._scope.define(name, TypeSpec(BaseType.ACTION))
305
+
306
+ self._push_scope()
307
+ for pname, pts in param_types:
308
+ self._scope.define(pname, pts)
309
+ if stmt.body:
310
+ self._check_block(stmt.body, ret_ts)
311
+ self._pop_scope()
312
+
313
+ def _check_ReturnStatement(self, stmt: ast.ReturnStatement):
314
+ # Return type validation is handled during block check
315
+ pass
316
+
317
+ def _check_ExpressionStatement(self, stmt: ast.ExpressionStatement):
318
+ if stmt.expression:
319
+ self._check_expression(stmt.expression)
320
+
321
+ def _check_IfStatement(self, stmt):
322
+ if hasattr(stmt, "condition") and stmt.condition:
323
+ self._check_expression(stmt.condition)
324
+ if hasattr(stmt, "consequence") and stmt.consequence:
325
+ self._check_block(stmt.consequence)
326
+ if hasattr(stmt, "alternative") and stmt.alternative:
327
+ self._check_block(stmt.alternative)
328
+
329
+ def _check_WhileStatement(self, stmt):
330
+ if hasattr(stmt, "condition") and stmt.condition:
331
+ self._check_expression(stmt.condition)
332
+ if hasattr(stmt, "body") and stmt.body:
333
+ self._check_block(stmt.body)
334
+
335
+ def _check_ForStatement(self, stmt):
336
+ if hasattr(stmt, "body") and stmt.body:
337
+ self._check_block(stmt.body)
338
+
339
+ # -- block / body visitor -----------------------------------------------
340
+
341
+ def _check_block(self, block, expected_return: Optional[TypeSpec] = None):
342
+ stmts = getattr(block, "statements", None)
343
+ if not stmts:
344
+ return
345
+ for s in stmts:
346
+ self._check_statement(s)
347
+ # Check return type
348
+ if expected_return and isinstance(s, ast.ReturnStatement):
349
+ if s.return_value:
350
+ actual = _infer_expr_type(s.return_value, self._scope)
351
+ if actual and not _is_compatible(expected_return, actual):
352
+ self._error(
353
+ f"Return type {actual.base_type.value} is incompatible "
354
+ f"with declared return type {expected_return.base_type.value}",
355
+ s,
356
+ )
357
+
358
+ # -- expression visitors ------------------------------------------------
359
+
360
+ def _check_expression(self, expr: ast.Expression):
361
+ if isinstance(expr, ast.CallExpression):
362
+ self._check_call(expr)
363
+ elif isinstance(expr, ast.InfixExpression):
364
+ if expr.left:
365
+ self._check_expression(expr.left)
366
+ if expr.right:
367
+ self._check_expression(expr.right)
368
+ self._check_infix_types(expr)
369
+ elif isinstance(expr, ast.PrefixExpression):
370
+ if expr.right:
371
+ self._check_expression(expr.right)
372
+
373
+ def _check_call(self, call: ast.CallExpression):
374
+ fn_name = None
375
+ if isinstance(call.function, ast.Identifier):
376
+ fn_name = call.function.value
377
+ if not fn_name:
378
+ return
379
+ sig = self._scope.lookup_callable(fn_name)
380
+ if sig is None:
381
+ return # unknown function — skip
382
+ params, _ = sig
383
+ args = call.arguments or []
384
+
385
+ # Arity check
386
+ expected = len(params)
387
+ got = len(args)
388
+ if got != expected:
389
+ self._error(
390
+ f"'{fn_name}' expects {expected} argument(s) but got {got}",
391
+ call,
392
+ )
393
+
394
+ # Per-argument type check
395
+ for i, (pname, pts) in enumerate(params):
396
+ if i >= len(args):
397
+ break
398
+ if pts is None:
399
+ continue
400
+ actual = _infer_expr_type(args[i], self._scope)
401
+ if actual and not _is_compatible(pts, actual):
402
+ self._error(
403
+ f"Argument '{pname}' of '{fn_name}' expects "
404
+ f"{pts.base_type.value} but got {actual.base_type.value}",
405
+ call,
406
+ )
407
+
408
+ def _check_infix_types(self, expr: ast.InfixExpression):
409
+ """Warn on obviously wrong infix operations."""
410
+ if expr.operator in ("+", "-", "*", "/", "%", "**"):
411
+ left = _infer_expr_type(expr.left, self._scope)
412
+ right = _infer_expr_type(expr.right, self._scope)
413
+ if left and right:
414
+ # String + Number is fine (concatenation). But Number - String isn't.
415
+ if expr.operator != "+" and (
416
+ (left.base_type == BaseType.STRING and right.base_type in (BaseType.INT, BaseType.FLOAT))
417
+ or (right.base_type == BaseType.STRING and left.base_type in (BaseType.INT, BaseType.FLOAT))
418
+ ):
419
+ self._warning(
420
+ f"Suspicious operation: {left.base_type.value} "
421
+ f"{expr.operator} {right.base_type.value}",
422
+ expr,
423
+ )
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Virtual filesystem and memory layer for sandboxed execution.
3
3
 
4
- Provides isolated file access and memory quotas for plugins.
4
+ Provides isolated file access, memory quotas, and a read cache for plugins.
5
5
  Each plugin operates in a restricted filesystem namespace.
6
6
  """
7
7
 
@@ -11,6 +11,8 @@ from pathlib import Path
11
11
  from enum import Enum
12
12
  import os
13
13
  import sys
14
+ import threading
15
+ import time
14
16
 
15
17
 
16
18
  class FileAccessMode(Enum):
@@ -166,12 +168,174 @@ class SandboxFileSystem:
166
168
  """Get filesystem access log."""
167
169
  return self.access_log.copy()
168
170
 
171
+ # ------------------------------------------------------------------
172
+ # Actual I/O operations (sandboxed)
173
+ # ------------------------------------------------------------------
174
+
175
+ def read_file(self, virtual_path: str) -> str:
176
+ """Read file contents through VFS access control.
177
+
178
+ Raises PermissionError or FileNotFoundError on failure.
179
+ """
180
+ result = self.resolve_path(virtual_path)
181
+ if result is None:
182
+ self.log_access("read", virtual_path, False, "path not mounted")
183
+ raise PermissionError(f"Path not accessible: {virtual_path}")
184
+ real_path, mode = result
185
+ if mode not in (FileAccessMode.READ, FileAccessMode.READ_WRITE):
186
+ self.log_access("read", virtual_path, False, "no read permission")
187
+ raise PermissionError(f"Read access denied: {virtual_path}")
188
+ self.log_access("read", virtual_path, True)
189
+ with open(real_path, "r") as f:
190
+ return f.read()
191
+
192
+ def write_file(self, virtual_path: str, content: str) -> None:
193
+ """Write file contents through VFS access control."""
194
+ result = self.resolve_path(virtual_path)
195
+ if result is None:
196
+ self.log_access("write", virtual_path, False, "path not mounted")
197
+ raise PermissionError(f"Path not accessible: {virtual_path}")
198
+ real_path, mode = result
199
+ if mode not in (FileAccessMode.WRITE, FileAccessMode.READ_WRITE):
200
+ self.log_access("write", virtual_path, False, "no write permission")
201
+ raise PermissionError(f"Write access denied: {virtual_path}")
202
+ self.log_access("write", virtual_path, True)
203
+ os.makedirs(os.path.dirname(real_path), exist_ok=True)
204
+ with open(real_path, "w") as f:
205
+ f.write(content)
206
+
207
+ def append_file(self, virtual_path: str, content: str) -> None:
208
+ """Append to a file through VFS access control."""
209
+ result = self.resolve_path(virtual_path)
210
+ if result is None:
211
+ self.log_access("append", virtual_path, False, "path not mounted")
212
+ raise PermissionError(f"Path not accessible: {virtual_path}")
213
+ real_path, mode = result
214
+ if mode not in (FileAccessMode.WRITE, FileAccessMode.READ_WRITE):
215
+ self.log_access("append", virtual_path, False, "no write permission")
216
+ raise PermissionError(f"Append access denied: {virtual_path}")
217
+ self.log_access("append", virtual_path, True)
218
+ with open(real_path, "a") as f:
219
+ f.write(content)
220
+
221
+ def file_exists(self, virtual_path: str) -> bool:
222
+ """Check if a virtual path references an existing file."""
223
+ result = self.resolve_path(virtual_path)
224
+ if result is None:
225
+ return False
226
+ return os.path.exists(result[0])
227
+
228
+ def list_dir(self, virtual_path: str) -> List[str]:
229
+ """List directory contents through VFS."""
230
+ result = self.resolve_path(virtual_path)
231
+ if result is None:
232
+ raise PermissionError(f"Path not accessible: {virtual_path}")
233
+ real_path, mode = result
234
+ if mode not in (FileAccessMode.READ, FileAccessMode.READ_WRITE):
235
+ raise PermissionError(f"Read access denied for directory: {virtual_path}")
236
+ return os.listdir(real_path)
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # File content cache — avoids repeated disk reads for hot paths
241
+ # ---------------------------------------------------------------------------
242
+
243
+ class FileContentCache:
244
+ """Thread-safe LRU-ish read cache for file contents.
245
+
246
+ Speeds up repeated reads to the same path (e.g. ``use "utils.zx"``
247
+ imported by many modules). Cached entries are invalidated when the
248
+ file's mtime changes.
249
+ """
250
+
251
+ def __init__(self, max_entries: int = 256, max_bytes: int = 32 * 1024 * 1024):
252
+ self._lock = threading.Lock()
253
+ self._max_entries = max_entries
254
+ self._max_bytes = max_bytes
255
+ self._current_bytes = 0
256
+ # path -> (content, mtime, size)
257
+ self._cache: Dict[str, Tuple[str, float, int]] = {}
258
+ self._hits = 0
259
+ self._misses = 0
260
+
261
+ def get(self, real_path: str) -> Optional[str]:
262
+ """Return cached content if still valid, else None."""
263
+ with self._lock:
264
+ entry = self._cache.get(real_path)
265
+ if entry is None:
266
+ self._misses += 1
267
+ return None
268
+ content, cached_mtime, size = entry
269
+ try:
270
+ current_mtime = os.path.getmtime(real_path)
271
+ except OSError:
272
+ # File gone — evict
273
+ self._evict(real_path)
274
+ self._misses += 1
275
+ return None
276
+ if current_mtime != cached_mtime:
277
+ self._evict(real_path)
278
+ self._misses += 1
279
+ return None
280
+ self._hits += 1
281
+ return content
282
+
283
+ def put(self, real_path: str, content: str) -> None:
284
+ """Store file content in cache."""
285
+ size = len(content.encode("utf-8", errors="replace"))
286
+ if size > self._max_bytes // 2:
287
+ return # Don't cache files larger than half the budget
288
+ with self._lock:
289
+ # Evict old entry if present
290
+ self._evict(real_path)
291
+ # Evict LRU entries if over budget
292
+ while (len(self._cache) >= self._max_entries
293
+ or self._current_bytes + size > self._max_bytes) and self._cache:
294
+ oldest_key = next(iter(self._cache))
295
+ self._evict(oldest_key)
296
+ try:
297
+ mtime = os.path.getmtime(real_path)
298
+ except OSError:
299
+ return
300
+ self._cache[real_path] = (content, mtime, size)
301
+ self._current_bytes += size
302
+
303
+ def invalidate(self, real_path: str) -> None:
304
+ """Remove a specific path from cache (e.g. after write)."""
305
+ with self._lock:
306
+ self._evict(real_path)
307
+
308
+ def clear(self) -> None:
309
+ """Flush the entire cache."""
310
+ with self._lock:
311
+ self._cache.clear()
312
+ self._current_bytes = 0
313
+
314
+ def stats(self) -> Dict[str, Any]:
315
+ """Return cache statistics."""
316
+ with self._lock:
317
+ total = self._hits + self._misses
318
+ return {
319
+ "entries": len(self._cache),
320
+ "bytes": self._current_bytes,
321
+ "hits": self._hits,
322
+ "misses": self._misses,
323
+ "hit_rate": self._hits / total if total else 0.0,
324
+ }
325
+
326
+ # internal
327
+ def _evict(self, key: str):
328
+ entry = self._cache.pop(key, None)
329
+ if entry:
330
+ self._current_bytes -= entry[2]
331
+
169
332
 
170
333
  class VirtualFileSystemManager:
171
334
  """
172
335
  Manages virtual filesystems for multiple sandboxes.
173
336
 
174
- Coordinates sandbox creation, isolation, and resource cleanup.
337
+ Coordinates sandbox creation, isolation, resource cleanup, and
338
+ provides a global file content cache shared across sandboxes.
175
339
  """
176
340
 
177
341
  def __init__(self):
@@ -179,6 +343,7 @@ class VirtualFileSystemManager:
179
343
  self.sandboxes: Dict[str, SandboxFileSystem] = {}
180
344
  self.memory_quotas: Dict[str, MemoryQuota] = {}
181
345
  self.default_memory_quota = 1024 * 1024 * 100 # 100MB default
346
+ self.file_cache = FileContentCache()
182
347
 
183
348
  def create_sandbox(self, sandbox_id: str, memory_quota_mb: int = 100) -> SandboxFileSystem:
184
349
  """
@@ -245,6 +410,28 @@ class VirtualFileSystemManager:
245
410
  """List all active sandboxes."""
246
411
  return list(self.sandboxes.keys())
247
412
 
413
+ def get_sandbox_filesystem(self, context: str = "default") -> Optional[SandboxFileSystem]:
414
+ """Get sandbox filesystem by context name (alias for get_sandbox)."""
415
+ return self.sandboxes.get(context)
416
+
417
+ # ------------------------------------------------------------------
418
+ # Cached file I/O helpers (used by builtins when no sandbox is active)
419
+ # ------------------------------------------------------------------
420
+
421
+ def cached_read(self, real_path: str) -> str:
422
+ """Read a file, using the content cache when possible."""
423
+ content = self.file_cache.get(real_path)
424
+ if content is not None:
425
+ return content
426
+ with open(real_path, "r") as f:
427
+ content = f.read()
428
+ self.file_cache.put(real_path, content)
429
+ return content
430
+
431
+ def invalidate_cache(self, real_path: str):
432
+ """Invalidate cache entry after a write."""
433
+ self.file_cache.invalidate(real_path)
434
+
248
435
 
249
436
  class StandardMounts:
250
437
  """Standard filesystem mount configurations."""