zexus 1.7.1 → 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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +300 -20
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/lexer.py +10 -5
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +10 -1
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +441 -37
- package/src/zexus/evaluator/core.py +560 -49
- package/src/zexus/evaluator/expressions.py +122 -49
- package/src/zexus/evaluator/functions.py +417 -16
- package/src/zexus/evaluator/statements.py +521 -118
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -486
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +237 -9
- package/src/zexus/object.py +64 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +786 -285
- package/src/zexus/parser/strategy_context.py +407 -66
- package/src/zexus/parser/strategy_structural.py +117 -19
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +15 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +14 -1
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +28 -1
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +557 -17
- package/src/zexus/vm/compiler.py +703 -5
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +83 -2
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +118 -42
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3411 -573
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +63 -11
- package/src/zexus/zexus_token.py +13 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +7 -4
- package/src/zexus.egg-info/SOURCES.txt +116 -9
- package/src/zexus.egg-info/entry_points.txt +1 -0
- 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')
|
|
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
|
|
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,
|
|
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."""
|