zexus 1.8.2 → 1.8.3
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 +89 -64
- package/package.json +1 -1
- package/rust_core/Cargo.lock +1 -1
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/builtin_modules.py +50 -13
- package/src/zexus/cli/main.py +46 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/evaluator/bytecode_compiler.py +11 -2
- package/src/zexus/evaluator/core.py +4 -1
- package/src/zexus/evaluator/expressions.py +11 -2
- package/src/zexus/evaluator/functions.py +72 -0
- package/src/zexus/evaluator/resource_limiter.py +1 -1
- package/src/zexus/evaluator/statements.py +44 -4
- package/src/zexus/kernel/__init__.py +34 -0
- package/src/zexus/kernel/hooks.py +276 -0
- package/src/zexus/kernel/registry.py +203 -0
- package/src/zexus/kernel/zir/__init__.py +145 -0
- package/src/zexus/lexer.py +7 -0
- package/src/zexus/object.py +28 -5
- package/src/zexus/parser/parser.py +53 -11
- package/src/zexus/parser/strategy_context.py +179 -10
- package/src/zexus/security.py +26 -2
- package/src/zexus/stdlib/blockchain.py +84 -0
- package/src/zexus/stdlib/http_server.py +2 -2
- package/src/zexus/stdlib/math.py +25 -17
- package/src/zexus/stdlib_integration.py +119 -2
- package/src/zexus/type_checker.py +17 -12
- package/src/zexus/vm/compiler.py +57 -6
- package/src/zexus/vm/fastops.c +4704 -1263
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +81 -3
- package/src/zexus/vm/optimizer.py +65 -27
- package/src/zexus/vm/vm.py +871 -98
- package/src/zexus/zexus_ast.py +4 -1
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +90 -65
- package/src/zexus.egg-info/SOURCES.txt +51 -0
|
@@ -1228,11 +1228,45 @@ class StatementEvaluatorMixin:
|
|
|
1228
1228
|
if is_error(iterable):
|
|
1229
1229
|
return iterable
|
|
1230
1230
|
|
|
1231
|
+
# R-007 fix: Support Map iteration in for-each loops
|
|
1232
|
+
if isinstance(iterable, Map):
|
|
1233
|
+
result = NULL
|
|
1234
|
+
for key, val in iterable.pairs.items():
|
|
1235
|
+
try:
|
|
1236
|
+
self.resource_limiter.check_iterations()
|
|
1237
|
+
except Exception as e:
|
|
1238
|
+
from ..evaluator.resource_limiter import ResourceError, TimeoutError
|
|
1239
|
+
if isinstance(e, (ResourceError, TimeoutError)):
|
|
1240
|
+
return EvaluationError(str(e))
|
|
1241
|
+
raise
|
|
1242
|
+
|
|
1243
|
+
if node.index:
|
|
1244
|
+
# for each key, val in map
|
|
1245
|
+
key_obj = String(key) if isinstance(key, str) else key
|
|
1246
|
+
env.set(node.index.value, key_obj)
|
|
1247
|
+
env.set(node.item.value, val)
|
|
1248
|
+
else:
|
|
1249
|
+
# for each key in map (single variable gets the key)
|
|
1250
|
+
key_obj = String(key) if isinstance(key, str) else key
|
|
1251
|
+
env.set(node.item.value, key_obj)
|
|
1252
|
+
|
|
1253
|
+
result = self.eval_node(node.body, env, stack_trace)
|
|
1254
|
+
if isinstance(result, ReturnValue):
|
|
1255
|
+
return result
|
|
1256
|
+
if isinstance(result, BreakException):
|
|
1257
|
+
return NULL
|
|
1258
|
+
if isinstance(result, ContinueException):
|
|
1259
|
+
result = NULL
|
|
1260
|
+
continue
|
|
1261
|
+
if isinstance(result, EvaluationError):
|
|
1262
|
+
return result
|
|
1263
|
+
return result
|
|
1264
|
+
|
|
1231
1265
|
if not isinstance(iterable, List):
|
|
1232
|
-
return EvaluationError("ForEach expects List")
|
|
1266
|
+
return EvaluationError("ForEach expects List or Map")
|
|
1233
1267
|
|
|
1234
1268
|
result = NULL
|
|
1235
|
-
for item in iterable.elements:
|
|
1269
|
+
for idx, item in enumerate(iterable.elements):
|
|
1236
1270
|
# Resource limit check (Security Fix #7)
|
|
1237
1271
|
try:
|
|
1238
1272
|
self.resource_limiter.check_iterations()
|
|
@@ -1243,6 +1277,9 @@ class StatementEvaluatorMixin:
|
|
|
1243
1277
|
return EvaluationError(str(e))
|
|
1244
1278
|
raise # Re-raise if not a resource error
|
|
1245
1279
|
|
|
1280
|
+
# R-006 fix: Support indexed for-each (for each i, item in list)
|
|
1281
|
+
if node.index:
|
|
1282
|
+
env.set(node.index.value, Integer(idx))
|
|
1246
1283
|
env.set(node.item.value, item)
|
|
1247
1284
|
result = self.eval_node(node.body, env, stack_trace)
|
|
1248
1285
|
if isinstance(result, ReturnValue):
|
|
@@ -1881,7 +1918,10 @@ class StatementEvaluatorMixin:
|
|
|
1881
1918
|
|
|
1882
1919
|
for nm in names:
|
|
1883
1920
|
val = env.get(nm)
|
|
1884
|
-
|
|
1921
|
+
# R-011 fix: Use ``is None`` instead of ``not val`` so that valid
|
|
1922
|
+
# objects which might be falsy (empty lists, NULL placeholders from
|
|
1923
|
+
# pre-registration, etc.) are not incorrectly rejected.
|
|
1924
|
+
if val is None:
|
|
1885
1925
|
return EvaluationError(f"Cannot export undefined: {nm}")
|
|
1886
1926
|
|
|
1887
1927
|
# If inside a module, add to module's exports list
|
|
@@ -2302,7 +2342,7 @@ class StatementEvaluatorMixin:
|
|
|
2302
2342
|
|
|
2303
2343
|
if isinstance(prop, dict):
|
|
2304
2344
|
# For dict format, default_value is in the dict
|
|
2305
|
-
if 'default_value' in prop:
|
|
2345
|
+
if 'default_value' in prop and prop['default_value'] is not None:
|
|
2306
2346
|
def_val = self.eval_node(prop['default_value'], env, stack_trace)
|
|
2307
2347
|
if is_error(def_val):
|
|
2308
2348
|
return def_val
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zexus Kernel — Extension layer for the interpreter and VM.
|
|
3
|
+
|
|
4
|
+
The kernel sits *alongside* the existing evaluator and VM. It does NOT
|
|
5
|
+
replace them. Instead it provides:
|
|
6
|
+
|
|
7
|
+
* **DomainRegistry** — a place where feature domains (blockchain, web,
|
|
8
|
+
system, …) register their capabilities so the interpreter can discover
|
|
9
|
+
them at runtime.
|
|
10
|
+
* **ZIR** — a formal opcode catalogue that documents (and validates) the
|
|
11
|
+
bytecode the VM already uses, plus domain-specific opcode ranges.
|
|
12
|
+
* **Hooks** — optional integration points the evaluator/VM can call into
|
|
13
|
+
(e.g. ``kernel.resolve_opcode()``, ``kernel.check_security()``).
|
|
14
|
+
|
|
15
|
+
The kernel is entirely opt-in. Everything works without it — but with
|
|
16
|
+
it, third-party domains can plug into the Zexus runtime cleanly.
|
|
17
|
+
|
|
18
|
+
Quick start
|
|
19
|
+
-----------
|
|
20
|
+
>>> from zexus.kernel import get_kernel
|
|
21
|
+
>>> k = get_kernel()
|
|
22
|
+
>>> k.registry.list_domains() # see what's loaded
|
|
23
|
+
>>> k.registry.get_domain("blockchain") # query a specific domain
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .registry import DomainRegistry, get_registry
|
|
27
|
+
from .hooks import Kernel, get_kernel
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DomainRegistry",
|
|
31
|
+
"get_registry",
|
|
32
|
+
"Kernel",
|
|
33
|
+
"get_kernel",
|
|
34
|
+
]
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kernel Hooks — Integration points between the kernel and the interpreter.
|
|
3
|
+
|
|
4
|
+
The :class:`Kernel` object is the single entry-point that the evaluator,
|
|
5
|
+
VM, or CLI can *optionally* use to tap into the domain/extension system.
|
|
6
|
+
|
|
7
|
+
It does NOT wrap or replace any existing class. It provides *additional*
|
|
8
|
+
services that sit alongside the evaluator and VM:
|
|
9
|
+
|
|
10
|
+
* Resolve domain-specific opcodes to handler functions.
|
|
11
|
+
* Compose security policies across multiple domains.
|
|
12
|
+
* Broadcast lifecycle events (program start, module load, …).
|
|
13
|
+
* Provide a unified introspection API for tooling (LSP, debugger).
|
|
14
|
+
|
|
15
|
+
Example — wiring the kernel into an existing run:
|
|
16
|
+
|
|
17
|
+
from zexus.evaluator import Evaluator
|
|
18
|
+
from zexus.kernel import get_kernel
|
|
19
|
+
|
|
20
|
+
kernel = get_kernel()
|
|
21
|
+
kernel.boot() # auto-discovers built-in domains
|
|
22
|
+
|
|
23
|
+
evaluator = Evaluator()
|
|
24
|
+
# The evaluator still works exactly as before.
|
|
25
|
+
# But now you can also ask the kernel:
|
|
26
|
+
info = kernel.registry.get_domain("blockchain")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import threading
|
|
32
|
+
import time
|
|
33
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
34
|
+
|
|
35
|
+
from .registry import DomainRegistry, DomainDescriptor, get_registry
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Lifecycle events
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
class KernelEvent:
|
|
43
|
+
"""Simple event descriptor."""
|
|
44
|
+
|
|
45
|
+
__slots__ = ("name", "timestamp", "data")
|
|
46
|
+
|
|
47
|
+
def __init__(self, name: str, data: Optional[Dict[str, Any]] = None) -> None:
|
|
48
|
+
self.name = name
|
|
49
|
+
self.timestamp = time.time()
|
|
50
|
+
self.data = data or {}
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return f"KernelEvent({self.name!r})"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Kernel
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
class Kernel:
|
|
61
|
+
"""Extension layer that sits alongside the interpreter and VM.
|
|
62
|
+
|
|
63
|
+
The kernel is **purely additive** — the interpreter works with or
|
|
64
|
+
without it. When present it provides domain discovery, opcode
|
|
65
|
+
resolution, security composition, and lifecycle events.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, registry: Optional[DomainRegistry] = None) -> None:
|
|
69
|
+
self.registry: DomainRegistry = registry or get_registry()
|
|
70
|
+
self._booted = False
|
|
71
|
+
self._event_listeners: Dict[str, List[Callable[[KernelEvent], None]]] = {}
|
|
72
|
+
self._opcode_handlers: Dict[int, Callable] = {}
|
|
73
|
+
self._middleware: List[Callable] = []
|
|
74
|
+
|
|
75
|
+
# -- Boot ---------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def boot(self) -> "Kernel":
|
|
78
|
+
"""Discover and register all built-in domains.
|
|
79
|
+
|
|
80
|
+
Safe to call multiple times — subsequent calls are no-ops.
|
|
81
|
+
Returns *self* for chaining.
|
|
82
|
+
"""
|
|
83
|
+
if self._booted:
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
self._register_builtin_domains()
|
|
87
|
+
self._booted = True
|
|
88
|
+
self._emit("kernel.booted", {"domains": list(self.registry.domain_names)})
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def is_booted(self) -> bool:
|
|
93
|
+
return self._booted
|
|
94
|
+
|
|
95
|
+
# -- Opcode resolution --------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def register_opcode_handler(self, opcode: int, handler: Callable) -> None:
|
|
98
|
+
"""Map a domain opcode to a Python callable.
|
|
99
|
+
|
|
100
|
+
This lets domain authors provide executable implementations for
|
|
101
|
+
their custom opcodes. The VM can query these at dispatch time.
|
|
102
|
+
"""
|
|
103
|
+
self._opcode_handlers[opcode] = handler
|
|
104
|
+
|
|
105
|
+
def get_opcode_handler(self, opcode: int) -> Optional[Callable]:
|
|
106
|
+
"""Return the handler for *opcode*, or ``None``."""
|
|
107
|
+
return self._opcode_handlers.get(opcode)
|
|
108
|
+
|
|
109
|
+
def resolve_opcode_domain(self, opcode: int) -> Optional[str]:
|
|
110
|
+
"""Return the domain name that owns *opcode*."""
|
|
111
|
+
return self.registry.resolve_opcode(opcode)
|
|
112
|
+
|
|
113
|
+
# -- Security composition -----------------------------------------------
|
|
114
|
+
|
|
115
|
+
def check_security(self, operation: str, context: Optional[Dict[str, Any]] = None) -> bool:
|
|
116
|
+
"""Ask every registered domain's security policy to approve *operation*.
|
|
117
|
+
|
|
118
|
+
Returns ``True`` only if **all** domains approve (or have no policy).
|
|
119
|
+
"""
|
|
120
|
+
context = context or {}
|
|
121
|
+
for desc in self.registry.list_domains():
|
|
122
|
+
policy = desc.security_policy
|
|
123
|
+
if policy is not None and hasattr(policy, "check"):
|
|
124
|
+
try:
|
|
125
|
+
if not policy.check(operation, context):
|
|
126
|
+
return False
|
|
127
|
+
except Exception:
|
|
128
|
+
return False
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
# -- Middleware ----------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def use(self, middleware_fn: Callable) -> None:
|
|
134
|
+
"""Register a middleware function.
|
|
135
|
+
|
|
136
|
+
Middleware is called in order for certain pipeline stages
|
|
137
|
+
(compile, execute) and can inspect or transform the data.
|
|
138
|
+
"""
|
|
139
|
+
self._middleware.append(middleware_fn)
|
|
140
|
+
|
|
141
|
+
def run_middleware(self, stage: str, data: Any) -> Any:
|
|
142
|
+
"""Run all middleware for *stage*, threading *data* through."""
|
|
143
|
+
for mw in self._middleware:
|
|
144
|
+
try:
|
|
145
|
+
result = mw(stage, data)
|
|
146
|
+
if result is not None:
|
|
147
|
+
data = result
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
# -- Events -------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
def on(self, event_name: str, callback: Callable[[KernelEvent], None]) -> None:
|
|
155
|
+
"""Subscribe to a kernel lifecycle event."""
|
|
156
|
+
self._event_listeners.setdefault(event_name, []).append(callback)
|
|
157
|
+
|
|
158
|
+
def _emit(self, event_name: str, data: Optional[Dict[str, Any]] = None) -> None:
|
|
159
|
+
event = KernelEvent(event_name, data)
|
|
160
|
+
for cb in self._event_listeners.get(event_name, []):
|
|
161
|
+
try:
|
|
162
|
+
cb(event)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
# Also fire wildcard listeners
|
|
166
|
+
for cb in self._event_listeners.get("*", []):
|
|
167
|
+
try:
|
|
168
|
+
cb(event)
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
# -- Introspection ------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def status(self) -> Dict[str, Any]:
|
|
175
|
+
"""Return a snapshot of the kernel state (useful for tooling/debug)."""
|
|
176
|
+
domains = self.registry.list_domains()
|
|
177
|
+
return {
|
|
178
|
+
"booted": self._booted,
|
|
179
|
+
"domain_count": len(domains),
|
|
180
|
+
"domains": {d.name: d.version for d in domains},
|
|
181
|
+
"opcode_handlers": len(self._opcode_handlers),
|
|
182
|
+
"middleware": len(self._middleware),
|
|
183
|
+
"event_subscriptions": {
|
|
184
|
+
k: len(v) for k, v in self._event_listeners.items()
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# -- Built-in domain registration (private) -----------------------------
|
|
189
|
+
|
|
190
|
+
def _register_builtin_domains(self) -> None:
|
|
191
|
+
"""Register the built-in domains that ship with the interpreter.
|
|
192
|
+
|
|
193
|
+
Each registration is idempotent — already-registered domains are
|
|
194
|
+
silently skipped.
|
|
195
|
+
"""
|
|
196
|
+
builtins = [
|
|
197
|
+
{
|
|
198
|
+
"name": "blockchain",
|
|
199
|
+
"version": "1.8.3",
|
|
200
|
+
"description": "Ledger, smart-contract execution, crypto, gas metering",
|
|
201
|
+
"opcodes": {
|
|
202
|
+
0x1000: "HASH_BLOCK",
|
|
203
|
+
0x1001: "VERIFY_SIGNATURE",
|
|
204
|
+
0x1002: "STATE_READ",
|
|
205
|
+
0x1003: "STATE_WRITE",
|
|
206
|
+
0x1004: "GAS_CHARGE",
|
|
207
|
+
0x1005: "MERKLE_ROOT",
|
|
208
|
+
0x1006: "CREATE_TX",
|
|
209
|
+
0x1007: "EMIT_EVENT",
|
|
210
|
+
0x1008: "REQUIRE",
|
|
211
|
+
0x1009: "BALANCE_OF",
|
|
212
|
+
0x100A: "CREATE_CHAIN",
|
|
213
|
+
0x100B: "ADD_BLOCK",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"name": "web",
|
|
218
|
+
"version": "1.8.3",
|
|
219
|
+
"description": "HTTP server/client, WebSocket, middleware",
|
|
220
|
+
"opcodes": {
|
|
221
|
+
0x1100: "HTTP_ROUTE",
|
|
222
|
+
0x1101: "HTTP_METHOD",
|
|
223
|
+
0x1102: "HTTP_RESPONSE",
|
|
224
|
+
0x1103: "WS_CONNECT",
|
|
225
|
+
0x1104: "WS_SEND",
|
|
226
|
+
0x1105: "WS_RECEIVE",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "system",
|
|
231
|
+
"version": "1.8.3",
|
|
232
|
+
"description": "File I/O, process management, environment variables",
|
|
233
|
+
"opcodes": {
|
|
234
|
+
0x1200: "FS_READ",
|
|
235
|
+
0x1201: "FS_WRITE",
|
|
236
|
+
0x1202: "FS_DELETE",
|
|
237
|
+
0x1203: "FS_LIST_DIR",
|
|
238
|
+
0x1204: "PROCESS_SPAWN",
|
|
239
|
+
0x1205: "ENV_GET",
|
|
240
|
+
0x1206: "ENV_SET",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "ui",
|
|
245
|
+
"version": "1.8.3",
|
|
246
|
+
"description": "Terminal graphics, web rendering, native widgets",
|
|
247
|
+
"opcodes": {},
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
for domain in builtins:
|
|
252
|
+
if self.registry.get_domain(domain["name"]) is None:
|
|
253
|
+
self.registry.register_domain(**domain)
|
|
254
|
+
|
|
255
|
+
def __repr__(self) -> str:
|
|
256
|
+
state = "booted" if self._booted else "idle"
|
|
257
|
+
n = len(self.registry.list_domains())
|
|
258
|
+
return f"Kernel({state}, {n} domains)"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Singleton
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
_global_kernel: Optional[Kernel] = None
|
|
266
|
+
_kernel_lock = threading.Lock()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_kernel() -> Kernel:
|
|
270
|
+
"""Return the process-wide :class:`Kernel` singleton."""
|
|
271
|
+
global _global_kernel
|
|
272
|
+
if _global_kernel is None:
|
|
273
|
+
with _kernel_lock:
|
|
274
|
+
if _global_kernel is None:
|
|
275
|
+
_global_kernel = Kernel()
|
|
276
|
+
return _global_kernel
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Domain Registry — Central registration point for Zexus domains.
|
|
3
|
+
|
|
4
|
+
Each domain (blockchain, web, ui, system, …) registers itself here at
|
|
5
|
+
import-time. The kernel uses the registry to resolve domain-specific
|
|
6
|
+
opcodes, security policies, and runtime services without directly
|
|
7
|
+
importing any domain package.
|
|
8
|
+
|
|
9
|
+
Usage
|
|
10
|
+
-----
|
|
11
|
+
>>> from zexus.kernel import get_registry
|
|
12
|
+
>>> reg = get_registry()
|
|
13
|
+
>>> reg.register_domain(
|
|
14
|
+
... name="blockchain",
|
|
15
|
+
... version="1.0.0",
|
|
16
|
+
... opcodes={0x1000: "HASH_BLOCK", 0x1001: "VERIFY_SIG"},
|
|
17
|
+
... )
|
|
18
|
+
>>> reg.get_domain("blockchain").version
|
|
19
|
+
'1.0.0'
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import threading
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Domain descriptor
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DomainDescriptor:
|
|
35
|
+
"""Immutable description of a registered domain."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
version: str = "0.0.0"
|
|
39
|
+
opcodes: Dict[int, str] = field(default_factory=dict)
|
|
40
|
+
security_policy: Optional[Any] = None
|
|
41
|
+
runtime: Optional[Any] = None
|
|
42
|
+
validate_zir: Optional[Callable] = None
|
|
43
|
+
# Additional metadata
|
|
44
|
+
description: str = ""
|
|
45
|
+
dependencies: List[str] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return f"DomainDescriptor(name={self.name!r}, version={self.version!r}, opcodes={len(self.opcodes)})"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Domain Registry
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
class DomainRegistry:
|
|
56
|
+
"""Thread-safe singleton registry for Zexus domains.
|
|
57
|
+
|
|
58
|
+
The registry enforces two key constraints:
|
|
59
|
+
1. Domain names are unique.
|
|
60
|
+
2. Opcode ranges must not overlap between domains.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
self._lock = threading.Lock()
|
|
65
|
+
self._domains: Dict[str, DomainDescriptor] = {}
|
|
66
|
+
self._opcode_owners: Dict[int, str] = {} # opcode → domain name
|
|
67
|
+
self._listeners: List[Callable[[DomainDescriptor], None]] = []
|
|
68
|
+
|
|
69
|
+
# -- Registration -------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def register_domain(
|
|
72
|
+
self,
|
|
73
|
+
name: str,
|
|
74
|
+
*,
|
|
75
|
+
version: str = "0.0.0",
|
|
76
|
+
opcodes: Optional[Dict[int, str]] = None,
|
|
77
|
+
security_policy: Optional[Any] = None,
|
|
78
|
+
runtime: Optional[Any] = None,
|
|
79
|
+
validate_zir: Optional[Callable] = None,
|
|
80
|
+
description: str = "",
|
|
81
|
+
dependencies: Optional[List[str]] = None,
|
|
82
|
+
) -> DomainDescriptor:
|
|
83
|
+
"""Register a new domain.
|
|
84
|
+
|
|
85
|
+
Raises
|
|
86
|
+
------
|
|
87
|
+
ValueError
|
|
88
|
+
If *name* is already registered or any opcode collides with
|
|
89
|
+
another domain's opcode range.
|
|
90
|
+
"""
|
|
91
|
+
opcodes = opcodes or {}
|
|
92
|
+
dependencies = dependencies or []
|
|
93
|
+
|
|
94
|
+
with self._lock:
|
|
95
|
+
if name in self._domains:
|
|
96
|
+
raise ValueError(f"Domain {name!r} is already registered")
|
|
97
|
+
|
|
98
|
+
# Check for opcode collisions
|
|
99
|
+
for op, op_name in opcodes.items():
|
|
100
|
+
if op in self._opcode_owners:
|
|
101
|
+
owner = self._opcode_owners[op]
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Opcode 0x{op:04X} ({op_name}) collides with "
|
|
104
|
+
f"domain {owner!r}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
desc = DomainDescriptor(
|
|
108
|
+
name=name,
|
|
109
|
+
version=version,
|
|
110
|
+
opcodes=opcodes,
|
|
111
|
+
security_policy=security_policy,
|
|
112
|
+
runtime=runtime,
|
|
113
|
+
validate_zir=validate_zir,
|
|
114
|
+
description=description,
|
|
115
|
+
dependencies=dependencies,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self._domains[name] = desc
|
|
119
|
+
for op in opcodes:
|
|
120
|
+
self._opcode_owners[op] = name
|
|
121
|
+
|
|
122
|
+
# Notify listeners
|
|
123
|
+
for listener in self._listeners:
|
|
124
|
+
try:
|
|
125
|
+
listener(desc)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
return desc
|
|
130
|
+
|
|
131
|
+
def unregister_domain(self, name: str) -> None:
|
|
132
|
+
"""Remove a previously registered domain."""
|
|
133
|
+
with self._lock:
|
|
134
|
+
desc = self._domains.pop(name, None)
|
|
135
|
+
if desc:
|
|
136
|
+
for op in desc.opcodes:
|
|
137
|
+
self._opcode_owners.pop(op, None)
|
|
138
|
+
|
|
139
|
+
# -- Query --------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def get_domain(self, name: str) -> Optional[DomainDescriptor]:
|
|
142
|
+
"""Return the descriptor for *name*, or ``None``."""
|
|
143
|
+
return self._domains.get(name)
|
|
144
|
+
|
|
145
|
+
def list_domains(self) -> List[DomainDescriptor]:
|
|
146
|
+
"""Return all registered domains (snapshot)."""
|
|
147
|
+
with self._lock:
|
|
148
|
+
return list(self._domains.values())
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def domain_names(self) -> Set[str]:
|
|
152
|
+
"""Set of registered domain names."""
|
|
153
|
+
return set(self._domains.keys())
|
|
154
|
+
|
|
155
|
+
def resolve_opcode(self, opcode: int) -> Optional[str]:
|
|
156
|
+
"""Return the domain name that owns *opcode*, or ``None``."""
|
|
157
|
+
return self._opcode_owners.get(opcode)
|
|
158
|
+
|
|
159
|
+
# -- Listeners ----------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def on_domain_registered(self, callback: Callable[[DomainDescriptor], None]) -> None:
|
|
162
|
+
"""Register a callback that fires whenever a new domain is registered."""
|
|
163
|
+
self._listeners.append(callback)
|
|
164
|
+
|
|
165
|
+
# -- Utilities ----------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def check_dependencies(self, name: str) -> List[str]:
|
|
168
|
+
"""Return list of missing dependencies for domain *name*.
|
|
169
|
+
|
|
170
|
+
Returns an empty list if all dependencies are satisfied.
|
|
171
|
+
"""
|
|
172
|
+
desc = self._domains.get(name)
|
|
173
|
+
if desc is None:
|
|
174
|
+
return [name]
|
|
175
|
+
return [dep for dep in desc.dependencies if dep not in self._domains]
|
|
176
|
+
|
|
177
|
+
def reset(self) -> None:
|
|
178
|
+
"""Remove all registered domains (useful for testing)."""
|
|
179
|
+
with self._lock:
|
|
180
|
+
self._domains.clear()
|
|
181
|
+
self._opcode_owners.clear()
|
|
182
|
+
|
|
183
|
+
def __repr__(self) -> str:
|
|
184
|
+
names = ", ".join(sorted(self._domains.keys()))
|
|
185
|
+
return f"DomainRegistry(domains=[{names}])"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Singleton
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
_global_registry: Optional[DomainRegistry] = None
|
|
193
|
+
_registry_lock = threading.Lock()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_registry() -> DomainRegistry:
|
|
197
|
+
"""Return the process-wide :class:`DomainRegistry` singleton."""
|
|
198
|
+
global _global_registry
|
|
199
|
+
if _global_registry is None:
|
|
200
|
+
with _registry_lock:
|
|
201
|
+
if _global_registry is None:
|
|
202
|
+
_global_registry = DomainRegistry()
|
|
203
|
+
return _global_registry
|