zexus 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -6
- package/bin/zexus +12 -2
- package/bin/zpics +12 -2
- package/bin/zpm +12 -2
- package/bin/zx +12 -2
- package/bin/zx-deploy +12 -2
- package/bin/zx-dev +12 -2
- package/bin/zx-run +12 -2
- package/package.json +2 -1
- package/rust_core/Cargo.lock +603 -0
- package/rust_core/Cargo.toml +26 -0
- package/rust_core/README.md +15 -0
- package/rust_core/pyproject.toml +25 -0
- package/rust_core/src/binary_bytecode.rs +543 -0
- package/rust_core/src/contract_vm.rs +643 -0
- package/rust_core/src/executor.rs +847 -0
- package/rust_core/src/hasher.rs +90 -0
- package/rust_core/src/lib.rs +71 -0
- package/rust_core/src/merkle.rs +128 -0
- package/rust_core/src/rust_vm.rs +2313 -0
- package/rust_core/src/signature.rs +79 -0
- package/rust_core/src/state_adapter.rs +281 -0
- package/rust_core/src/validator.rs +116 -0
- package/scripts/postinstall.js +204 -21
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/evaluator/bytecode_compiler.py +150 -52
- package/src/zexus/evaluator/core.py +151 -809
- package/src/zexus/evaluator/expressions.py +27 -22
- package/src/zexus/evaluator/functions.py +171 -126
- package/src/zexus/evaluator/statements.py +55 -112
- package/src/zexus/module_cache.py +20 -9
- package/src/zexus/object.py +330 -38
- package/src/zexus/parser/parser.py +103 -23
- package/src/zexus/parser/strategy_context.py +318 -6
- package/src/zexus/parser/strategy_structural.py +2 -2
- package/src/zexus/persistence.py +46 -17
- package/src/zexus/security.py +140 -234
- package/src/zexus/type_checker.py +44 -5
- package/src/zexus/vm/binary_bytecode.py +7 -3
- package/src/zexus/vm/bytecode.py +6 -0
- package/src/zexus/vm/cache.py +24 -46
- package/src/zexus/vm/compiler.py +549 -68
- package/src/zexus/vm/memory_pool.py +21 -9
- package/src/zexus/vm/vm.py +609 -95
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +56 -12
- package/src/zexus.egg-info/SOURCES.txt +14 -0
- package/src/zexus.egg-info/entry_points.txt +5 -1
- package/src/zexus.egg-info/requires.txt +26 -0
package/src/zexus/object.py
CHANGED
|
@@ -3,9 +3,69 @@ import time
|
|
|
3
3
|
import random
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import atexit
|
|
7
10
|
from threading import Lock
|
|
8
11
|
|
|
12
|
+
|
|
13
|
+
# ===============================================
|
|
14
|
+
# SECURITY: Path & Identifier Sanitization (v1.8.1)
|
|
15
|
+
# ===============================================
|
|
16
|
+
|
|
17
|
+
def _safe_resolve_path(path: str, sandbox: str = None) -> str:
|
|
18
|
+
"""Resolve *path* and ensure it stays within *sandbox* (default: cwd).
|
|
19
|
+
|
|
20
|
+
Raises ``PermissionError`` if the resolved path escapes the sandbox.
|
|
21
|
+
Rejects paths that contain null bytes.
|
|
22
|
+
"""
|
|
23
|
+
if "\x00" in path:
|
|
24
|
+
raise PermissionError("Path contains null bytes")
|
|
25
|
+
if sandbox is None:
|
|
26
|
+
sandbox = os.getcwd()
|
|
27
|
+
sandbox = os.path.realpath(sandbox)
|
|
28
|
+
resolved = os.path.realpath(os.path.join(sandbox, path))
|
|
29
|
+
# Ensure the resolved path is still within the sandbox
|
|
30
|
+
if not (resolved == sandbox or resolved.startswith(sandbox + os.sep)):
|
|
31
|
+
raise PermissionError(
|
|
32
|
+
f"Path traversal denied: '{path}' resolves outside the project directory"
|
|
33
|
+
)
|
|
34
|
+
return resolved
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Regex: only alphanumeric, underscore, hyphen, dot allowed in identifiers
|
|
38
|
+
_SAFE_ID_RE = re.compile(r'^[A-Za-z0-9_.\-]+$')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sanitize_identifier(name: str, label: str = "identifier") -> str:
|
|
42
|
+
"""Ensure *name* is a safe filesystem identifier (no path separators, no traversal).
|
|
43
|
+
|
|
44
|
+
Raises ``ValueError`` on invalid input.
|
|
45
|
+
"""
|
|
46
|
+
if not name or not isinstance(name, str):
|
|
47
|
+
raise ValueError(f"Invalid {label}: must be a non-empty string")
|
|
48
|
+
if not _SAFE_ID_RE.match(name):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Invalid {label} '{name}': only alphanumeric, underscore, hyphen, "
|
|
51
|
+
f"and dot characters are allowed"
|
|
52
|
+
)
|
|
53
|
+
# Extra guard: reject hidden-file tricks and double-dot
|
|
54
|
+
if name.startswith('.') or '..' in name:
|
|
55
|
+
raise ValueError(f"Invalid {label} '{name}': leading dot or '..' not allowed")
|
|
56
|
+
return name
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Env vars that must never be modified by Zexus programs
|
|
60
|
+
BLOCKED_ENV_VARS = frozenset({
|
|
61
|
+
'PATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'DYLD_INSERT_LIBRARIES',
|
|
62
|
+
'DYLD_LIBRARY_PATH', 'PYTHONPATH', 'PYTHONSTARTUP', 'PYTHONHOME',
|
|
63
|
+
'NODE_PATH', 'NODE_OPTIONS', 'PERL5LIB', 'RUBYLIB',
|
|
64
|
+
'CLASSPATH', 'HOME', 'USER', 'SHELL', 'LOGNAME',
|
|
65
|
+
'SSH_AUTH_SOCK', 'GPG_AGENT_INFO',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
|
|
9
69
|
class Object:
|
|
10
70
|
def inspect(self):
|
|
11
71
|
raise NotImplementedError("Subclasses must implement this method")
|
|
@@ -489,10 +549,43 @@ class Coroutine(Object):
|
|
|
489
549
|
|
|
490
550
|
# === ENTITY OBJECTS ===
|
|
491
551
|
|
|
552
|
+
# Thread-local evaluator cache for entity/contract method execution.
|
|
553
|
+
# Kept lazy to avoid circular imports at module import time.
|
|
554
|
+
_entity_evaluator_cache = threading.local()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _get_cached_evaluator():
|
|
558
|
+
evaluator = getattr(_entity_evaluator_cache, "evaluator", None)
|
|
559
|
+
if evaluator is None:
|
|
560
|
+
from .evaluator.core import Evaluator
|
|
561
|
+
evaluator = Evaluator()
|
|
562
|
+
_entity_evaluator_cache.evaluator = evaluator
|
|
563
|
+
return evaluator
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _clear_cached_evaluator():
|
|
567
|
+
if hasattr(_entity_evaluator_cache, "evaluator"):
|
|
568
|
+
del _entity_evaluator_cache.evaluator
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
# Best-effort cleanup for the current thread at interpreter shutdown.
|
|
572
|
+
atexit.register(_clear_cached_evaluator)
|
|
573
|
+
|
|
492
574
|
class EntityDefinition(Object):
|
|
493
|
-
|
|
575
|
+
"""Entity definition.
|
|
576
|
+
|
|
577
|
+
MEDIUM (M5): This is the canonical EntityDefinition implementation.
|
|
578
|
+
It supports:
|
|
579
|
+
- properties as either a dict (new format) or list (legacy format)
|
|
580
|
+
- optional methods (Action objects)
|
|
581
|
+
- optional parent entity (inheritance)
|
|
582
|
+
- optional dependency injection for marked properties
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
def __init__(self, name, properties, methods=None, parent=None):
|
|
494
586
|
self.name = name
|
|
495
|
-
self.properties = properties #
|
|
587
|
+
self.properties = properties # dict or list
|
|
588
|
+
self.methods = methods or {}
|
|
496
589
|
self.parent = parent # Optional parent entity for inheritance
|
|
497
590
|
|
|
498
591
|
def type(self):
|
|
@@ -506,33 +599,169 @@ class EntityDefinition(Object):
|
|
|
506
599
|
else:
|
|
507
600
|
props_str = ", ".join([f"{prop['name']}: {prop['type']}" for prop in self.properties])
|
|
508
601
|
return f"entity {self.name} {{ {props_str} }}"
|
|
602
|
+
|
|
603
|
+
def get_all_properties(self):
|
|
604
|
+
"""Get all properties including inherited ones (parent first)."""
|
|
605
|
+
props = {}
|
|
606
|
+
if self.parent:
|
|
607
|
+
try:
|
|
608
|
+
props.update(self.parent.get_all_properties())
|
|
609
|
+
except Exception:
|
|
610
|
+
pass
|
|
611
|
+
if isinstance(self.properties, dict):
|
|
612
|
+
props.update(self.properties)
|
|
613
|
+
else:
|
|
614
|
+
# Legacy list format: [{name,type,default_value?}, ...]
|
|
615
|
+
for prop in self.properties:
|
|
616
|
+
try:
|
|
617
|
+
props[prop['name']] = prop
|
|
618
|
+
except Exception:
|
|
619
|
+
continue
|
|
620
|
+
return props
|
|
509
621
|
|
|
510
622
|
def create_instance(self, initial_values=None):
|
|
511
|
-
"""Create an instance of this entity with optional initial values"""
|
|
512
|
-
|
|
623
|
+
"""Create an instance of this entity with optional initial values."""
|
|
624
|
+
injected_values = dict(initial_values or {})
|
|
625
|
+
|
|
626
|
+
# Dependency injection support: fill missing injected deps with container values.
|
|
627
|
+
injected_deps = getattr(self, 'injected_deps', None)
|
|
628
|
+
if injected_deps:
|
|
629
|
+
try:
|
|
630
|
+
from .dependency_injection import get_di_registry
|
|
631
|
+
registry = get_di_registry()
|
|
632
|
+
container = registry.get_container("__main__")
|
|
633
|
+
except Exception:
|
|
634
|
+
container = None
|
|
635
|
+
|
|
636
|
+
for dep_name in injected_deps:
|
|
637
|
+
if dep_name in injected_values:
|
|
638
|
+
continue
|
|
639
|
+
if container is None:
|
|
640
|
+
injected_values[dep_name] = NULL
|
|
641
|
+
continue
|
|
642
|
+
try:
|
|
643
|
+
injected_values[dep_name] = container.get(dep_name)
|
|
644
|
+
except BaseException:
|
|
645
|
+
injected_values[dep_name] = NULL
|
|
646
|
+
|
|
647
|
+
return EntityInstance(self, injected_values)
|
|
513
648
|
|
|
514
649
|
class EntityInstance(Object):
|
|
515
650
|
def __init__(self, entity_def, values):
|
|
516
651
|
self.entity_def = entity_def
|
|
517
|
-
self.
|
|
652
|
+
self.data = values or {}
|
|
653
|
+
# Backward compatibility alias
|
|
654
|
+
self.values = self.data
|
|
655
|
+
self._validate_properties()
|
|
518
656
|
|
|
519
657
|
def type(self):
|
|
520
658
|
return "ENTITY_INSTANCE"
|
|
521
659
|
|
|
522
660
|
def inspect(self):
|
|
523
|
-
values_str = ", ".join([
|
|
661
|
+
values_str = ", ".join([
|
|
662
|
+
f"{k}: {v.inspect() if hasattr(v, 'inspect') else v}" for k, v in self.data.items()
|
|
663
|
+
])
|
|
524
664
|
return f"{self.entity_def.name} {{ {values_str} }}"
|
|
665
|
+
|
|
666
|
+
def __str__(self):
|
|
667
|
+
return self.inspect()
|
|
668
|
+
|
|
669
|
+
def __repr__(self):
|
|
670
|
+
return self.__str__()
|
|
671
|
+
|
|
672
|
+
def _validate_properties(self):
|
|
673
|
+
"""Populate missing injected/default values for declared properties."""
|
|
674
|
+
try:
|
|
675
|
+
all_props = self.entity_def.get_all_properties()
|
|
676
|
+
except Exception:
|
|
677
|
+
all_props = {}
|
|
678
|
+
|
|
679
|
+
for prop_name, prop_config in all_props.items():
|
|
680
|
+
if prop_name in self.data:
|
|
681
|
+
continue
|
|
682
|
+
|
|
683
|
+
# New-format dict config
|
|
684
|
+
if isinstance(prop_config, dict):
|
|
685
|
+
if prop_config.get("injected", False):
|
|
686
|
+
try:
|
|
687
|
+
from .dependency_injection import get_di_registry
|
|
688
|
+
registry = get_di_registry()
|
|
689
|
+
container = registry.get_container("__main__")
|
|
690
|
+
if container is not None:
|
|
691
|
+
self.data[prop_name] = container.get(prop_name)
|
|
692
|
+
else:
|
|
693
|
+
self.data[prop_name] = NULL
|
|
694
|
+
except Exception:
|
|
695
|
+
self.data[prop_name] = NULL
|
|
696
|
+
continue
|
|
697
|
+
|
|
698
|
+
if "default_value" in prop_config:
|
|
699
|
+
self.data[prop_name] = prop_config.get("default_value")
|
|
700
|
+
continue
|
|
701
|
+
|
|
702
|
+
# Legacy list config may include default_value
|
|
703
|
+
if isinstance(prop_config, dict) and "default_value" in prop_config:
|
|
704
|
+
self.data[prop_name] = prop_config.get("default_value")
|
|
525
705
|
|
|
526
706
|
def get(self, property_name):
|
|
527
|
-
return self.
|
|
707
|
+
return self.data.get(property_name, NULL)
|
|
528
708
|
|
|
529
709
|
def set(self, property_name, value):
|
|
530
|
-
# Check if property exists in entity definition
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
710
|
+
# Check if property exists in entity definition (including inherited props)
|
|
711
|
+
try:
|
|
712
|
+
props = self.entity_def.get_all_properties()
|
|
713
|
+
if property_name not in props:
|
|
714
|
+
return FALSE
|
|
715
|
+
except Exception:
|
|
716
|
+
# Legacy list-only entity definitions
|
|
717
|
+
try:
|
|
718
|
+
prop_def = next(
|
|
719
|
+
(prop for prop in self.entity_def.properties if prop.get('name') == property_name),
|
|
720
|
+
None,
|
|
721
|
+
)
|
|
722
|
+
if not prop_def:
|
|
723
|
+
return FALSE
|
|
724
|
+
except Exception:
|
|
725
|
+
return FALSE
|
|
726
|
+
|
|
727
|
+
existing = self.data.get(property_name)
|
|
728
|
+
if existing is not None and existing.__class__.__name__ == 'SealedObject':
|
|
729
|
+
return EvaluationError(f"Cannot modify sealed property: {property_name}")
|
|
730
|
+
|
|
731
|
+
self.data[property_name] = value
|
|
732
|
+
return TRUE
|
|
733
|
+
|
|
734
|
+
def call_method(self, method_name, args):
|
|
735
|
+
"""Call an entity method (Action) defined on the entity definition."""
|
|
736
|
+
if not hasattr(self.entity_def, 'methods') or method_name not in self.entity_def.methods:
|
|
737
|
+
return EvaluationError(f"Method '{method_name}' not supported for ENTITY_INSTANCE")
|
|
738
|
+
|
|
739
|
+
method = self.entity_def.methods[method_name]
|
|
740
|
+
|
|
741
|
+
# Create a new environment for method execution
|
|
742
|
+
from .environment import Environment as ExecEnvironment
|
|
743
|
+
method_env = ExecEnvironment(outer=method.env if hasattr(method, 'env') else None)
|
|
744
|
+
method_env.set('this', self)
|
|
745
|
+
|
|
746
|
+
# Bind method parameters
|
|
747
|
+
if hasattr(method, 'parameters'):
|
|
748
|
+
for i, param in enumerate(method.parameters):
|
|
749
|
+
if i >= len(args):
|
|
750
|
+
break
|
|
751
|
+
if hasattr(param, 'name'):
|
|
752
|
+
param_name = param.name.value if hasattr(param.name, 'value') else str(param.name)
|
|
753
|
+
elif hasattr(param, 'value'):
|
|
754
|
+
param_name = param.value
|
|
755
|
+
else:
|
|
756
|
+
param_name = str(param)
|
|
757
|
+
method_env.set(param_name, args[i])
|
|
758
|
+
|
|
759
|
+
evaluator = _get_cached_evaluator()
|
|
760
|
+
|
|
761
|
+
result = evaluator.eval_node(method.body, method_env, stack_trace=[])
|
|
762
|
+
if isinstance(result, ReturnValue):
|
|
763
|
+
return result.value
|
|
764
|
+
return result
|
|
536
765
|
|
|
537
766
|
# === UTILITY CLASSES ===
|
|
538
767
|
|
|
@@ -600,9 +829,12 @@ class File(Object):
|
|
|
600
829
|
try:
|
|
601
830
|
if isinstance(path, String):
|
|
602
831
|
path = path.value
|
|
603
|
-
|
|
832
|
+
resolved = _safe_resolve_path(path)
|
|
833
|
+
with open(resolved, 'r', encoding='utf-8') as f:
|
|
604
834
|
# Files are external data sources - return untrusted strings
|
|
605
835
|
return String(f.read(), is_trusted=False)
|
|
836
|
+
except PermissionError as e:
|
|
837
|
+
return EvaluationError(f"File read denied: {str(e)}")
|
|
606
838
|
except Exception as e:
|
|
607
839
|
return EvaluationError(f"File read error: {str(e)}")
|
|
608
840
|
|
|
@@ -613,9 +845,12 @@ class File(Object):
|
|
|
613
845
|
path = path.value
|
|
614
846
|
if isinstance(content, String):
|
|
615
847
|
content = content.value
|
|
616
|
-
|
|
848
|
+
resolved = _safe_resolve_path(path)
|
|
849
|
+
with open(resolved, 'w', encoding='utf-8') as f:
|
|
617
850
|
f.write(content)
|
|
618
851
|
return Boolean(True)
|
|
852
|
+
except PermissionError as e:
|
|
853
|
+
return EvaluationError(f"File write denied: {str(e)}")
|
|
619
854
|
except Exception as e:
|
|
620
855
|
return EvaluationError(f"File write error: {str(e)}")
|
|
621
856
|
|
|
@@ -623,7 +858,11 @@ class File(Object):
|
|
|
623
858
|
def exists(path):
|
|
624
859
|
if isinstance(path, String):
|
|
625
860
|
path = path.value
|
|
626
|
-
|
|
861
|
+
try:
|
|
862
|
+
resolved = _safe_resolve_path(path)
|
|
863
|
+
return Boolean(os.path.exists(resolved))
|
|
864
|
+
except PermissionError:
|
|
865
|
+
return Boolean(False)
|
|
627
866
|
|
|
628
867
|
# === MEDIUM TIER ===
|
|
629
868
|
@staticmethod
|
|
@@ -729,18 +968,65 @@ class File(Object):
|
|
|
729
968
|
_lock = Lock()
|
|
730
969
|
|
|
731
970
|
@staticmethod
|
|
732
|
-
def lock_file(path):
|
|
733
|
-
"""Lock file for exclusive access
|
|
971
|
+
def lock_file(path, timeout=None):
|
|
972
|
+
"""Lock file for exclusive access.
|
|
973
|
+
|
|
974
|
+
SECURITY (M9): Previously this could block forever and hang the interpreter.
|
|
975
|
+
A default timeout is applied (configurable via env var).
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
path: File path (string or String)
|
|
979
|
+
timeout: Optional seconds (Integer/Float/str/number). If omitted, uses
|
|
980
|
+
$ZEXUS_FILE_LOCK_TIMEOUT_SECONDS (default: 30).
|
|
981
|
+
"""
|
|
734
982
|
try:
|
|
735
983
|
if isinstance(path, String):
|
|
736
984
|
path = path.value
|
|
737
985
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
986
|
+
# Normalize key to reduce accidental duplicate lock objects
|
|
987
|
+
try:
|
|
988
|
+
lock_key = os.path.realpath(str(path))
|
|
989
|
+
except Exception:
|
|
990
|
+
lock_key = str(path)
|
|
991
|
+
|
|
992
|
+
timeout_seconds = None
|
|
993
|
+
if timeout is None:
|
|
994
|
+
env_timeout = os.environ.get("ZEXUS_FILE_LOCK_TIMEOUT_SECONDS", "30")
|
|
995
|
+
try:
|
|
996
|
+
timeout_seconds = float(env_timeout)
|
|
997
|
+
except Exception:
|
|
998
|
+
timeout_seconds = 30.0
|
|
999
|
+
else:
|
|
1000
|
+
if isinstance(timeout, Integer):
|
|
1001
|
+
timeout_seconds = float(timeout.value)
|
|
1002
|
+
elif isinstance(timeout, Float):
|
|
1003
|
+
timeout_seconds = float(timeout.value)
|
|
1004
|
+
elif isinstance(timeout, String):
|
|
1005
|
+
timeout_seconds = float(timeout.value)
|
|
1006
|
+
else:
|
|
1007
|
+
timeout_seconds = float(timeout)
|
|
1008
|
+
|
|
1009
|
+
if timeout_seconds is not None and timeout_seconds < 0:
|
|
1010
|
+
timeout_seconds = 0.0
|
|
741
1011
|
|
|
742
|
-
|
|
1012
|
+
with File._lock:
|
|
1013
|
+
lock_obj = File._file_locks.get(lock_key)
|
|
1014
|
+
if lock_obj is None:
|
|
1015
|
+
lock_obj = Lock()
|
|
1016
|
+
File._file_locks[lock_key] = lock_obj
|
|
1017
|
+
|
|
1018
|
+
# Acquire without holding the global map lock
|
|
1019
|
+
acquired = False
|
|
1020
|
+
if timeout_seconds is None:
|
|
1021
|
+
# Shouldn't happen, but keep backward-compatible behavior
|
|
1022
|
+
lock_obj.acquire()
|
|
1023
|
+
acquired = True
|
|
1024
|
+
else:
|
|
1025
|
+
acquired = lock_obj.acquire(timeout=timeout_seconds)
|
|
1026
|
+
|
|
1027
|
+
if acquired:
|
|
743
1028
|
return Boolean(True)
|
|
1029
|
+
return EvaluationError(f"File lock timeout after {timeout_seconds:.3f}s")
|
|
744
1030
|
except Exception as e:
|
|
745
1031
|
return EvaluationError(f"File lock error: {str(e)}")
|
|
746
1032
|
|
|
@@ -751,10 +1037,22 @@ class File(Object):
|
|
|
751
1037
|
if isinstance(path, String):
|
|
752
1038
|
path = path.value
|
|
753
1039
|
|
|
1040
|
+
try:
|
|
1041
|
+
lock_key = os.path.realpath(str(path))
|
|
1042
|
+
except Exception:
|
|
1043
|
+
lock_key = str(path)
|
|
1044
|
+
|
|
754
1045
|
with File._lock:
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1046
|
+
lock_obj = File._file_locks.get(lock_key)
|
|
1047
|
+
if lock_obj is not None:
|
|
1048
|
+
try:
|
|
1049
|
+
if hasattr(lock_obj, "locked") and not lock_obj.locked():
|
|
1050
|
+
return Boolean(False)
|
|
1051
|
+
lock_obj.release()
|
|
1052
|
+
return Boolean(True)
|
|
1053
|
+
except RuntimeError:
|
|
1054
|
+
# Release on an unlocked lock
|
|
1055
|
+
return Boolean(False)
|
|
758
1056
|
return Boolean(False)
|
|
759
1057
|
except Exception as e:
|
|
760
1058
|
return EvaluationError(f"File unlock error: {str(e)}")
|
|
@@ -996,13 +1294,10 @@ class Environment:
|
|
|
996
1294
|
"""Initialize persistence system if scope is provided"""
|
|
997
1295
|
if self.persistence_scope:
|
|
998
1296
|
try:
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
self._persistent_storage = PersistentStorage(self.persistence_scope)
|
|
1004
|
-
self._memory_tracker = MemoryTracker()
|
|
1005
|
-
self._memory_tracker.start_tracking()
|
|
1297
|
+
from .persistence import PersistentStorage, MemoryTracker
|
|
1298
|
+
self._persistent_storage = PersistentStorage(self.persistence_scope)
|
|
1299
|
+
self._memory_tracker = MemoryTracker()
|
|
1300
|
+
self._memory_tracker.start_tracking()
|
|
1006
1301
|
except (ImportError, Exception):
|
|
1007
1302
|
# Persistence module not available or error - continue without it
|
|
1008
1303
|
pass
|
|
@@ -1042,12 +1337,9 @@ class Environment:
|
|
|
1042
1337
|
"""Enable memory leak detection"""
|
|
1043
1338
|
if not self._memory_tracker:
|
|
1044
1339
|
try:
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
from .persistence import MemoryTracker
|
|
1049
|
-
self._memory_tracker = MemoryTracker()
|
|
1050
|
-
self._memory_tracker.start_tracking()
|
|
1340
|
+
from .persistence import MemoryTracker
|
|
1341
|
+
self._memory_tracker = MemoryTracker()
|
|
1342
|
+
self._memory_tracker.start_tracking()
|
|
1051
1343
|
except (ImportError, Exception):
|
|
1052
1344
|
pass
|
|
1053
1345
|
|
|
@@ -140,6 +140,7 @@ class UltimateParser:
|
|
|
140
140
|
REQUIRE: self.parse_require_statement,
|
|
141
141
|
REVERT: self.parse_revert_statement,
|
|
142
142
|
LIMIT: self.parse_limit_statement,
|
|
143
|
+
PROTOCOL: self.parse_protocol_statement,
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
# Traditional parser setup (fallback)
|
|
@@ -361,6 +362,15 @@ class UltimateParser:
|
|
|
361
362
|
if match_brace_depth == 0:
|
|
362
363
|
in_match_brace = False
|
|
363
364
|
elif t.type == LAMBDA and getattr(t, 'literal', None) == '=>':
|
|
365
|
+
# Exclude watch => patterns — those are reactive watchers, not lambdas
|
|
366
|
+
if idx > 0 and all_tokens[idx - 1].type == IDENT:
|
|
367
|
+
# Check if this is a watch statement: watch <expr> =>
|
|
368
|
+
# Walk back past identifiers and dots (e.g. watch order.status =>)
|
|
369
|
+
watch_idx = idx - 2
|
|
370
|
+
while watch_idx >= 0 and all_tokens[watch_idx].type in (IDENT, DOT):
|
|
371
|
+
watch_idx -= 1
|
|
372
|
+
if watch_idx >= 0 and getattr(all_tokens[watch_idx], 'type', None) == WATCH:
|
|
373
|
+
continue # Skip watch arrows
|
|
364
374
|
has_non_match_arrow = True
|
|
365
375
|
break
|
|
366
376
|
if has_non_match_arrow:
|
|
@@ -2817,24 +2827,30 @@ class UltimateParser:
|
|
|
2817
2827
|
self.errors.append(f"Line {token.line}:{token.column} - Expected expression after 'watch'")
|
|
2818
2828
|
return None
|
|
2819
2829
|
|
|
2820
|
-
#
|
|
2821
|
-
if
|
|
2822
|
-
self.
|
|
2823
|
-
return None
|
|
2830
|
+
# Check for '=>' (LAMBDA token) — optional; allow watch expr { ... } form
|
|
2831
|
+
if self.peek_token_is(LAMBDA):
|
|
2832
|
+
self.next_token() # consume '=>'
|
|
2824
2833
|
|
|
2825
|
-
|
|
2826
|
-
|
|
2834
|
+
# Parse reaction (block or expression)
|
|
2835
|
+
if self.peek_token_is(LBRACE):
|
|
2836
|
+
self.next_token()
|
|
2837
|
+
reaction = self.parse_block("watch")
|
|
2838
|
+
else:
|
|
2839
|
+
self.next_token()
|
|
2840
|
+
reaction_block = BlockStatement()
|
|
2841
|
+
stmt = self.parse_statement()
|
|
2842
|
+
if stmt is None:
|
|
2843
|
+
self.errors.append(f"Line {self.cur_token.line}:{self.cur_token.column} - Expected reaction after '=>'")
|
|
2844
|
+
return None
|
|
2845
|
+
reaction_block.statements.append(stmt)
|
|
2846
|
+
reaction = reaction_block
|
|
2847
|
+
elif self.peek_token_is(LBRACE):
|
|
2848
|
+
# Form: watch expr { ... }
|
|
2827
2849
|
self.next_token()
|
|
2828
2850
|
reaction = self.parse_block("watch")
|
|
2829
2851
|
else:
|
|
2830
|
-
self.
|
|
2831
|
-
|
|
2832
|
-
stmt = self.parse_statement()
|
|
2833
|
-
if stmt is None:
|
|
2834
|
-
self.errors.append(f"Line {self.cur_token.line}:{self.cur_token.column} - Expected reaction after '=>'")
|
|
2835
|
-
return None
|
|
2836
|
-
reaction_block.statements.append(stmt)
|
|
2837
|
-
reaction = reaction_block
|
|
2852
|
+
self.errors.append(f"Line {self.cur_token.line}:{self.cur_token.column} - Expected '=>' or '{{' in watch statement")
|
|
2853
|
+
return None
|
|
2838
2854
|
|
|
2839
2855
|
if reaction is None:
|
|
2840
2856
|
self.errors.append(f"Line {self.cur_token.line}:{self.cur_token.column} - Expected reaction after '=>'")
|
|
@@ -4451,6 +4467,54 @@ class UltimateParser:
|
|
|
4451
4467
|
|
|
4452
4468
|
return InterfaceStatement(name=interface_name, methods=methods, properties=properties)
|
|
4453
4469
|
|
|
4470
|
+
def parse_protocol_statement(self):
|
|
4471
|
+
"""Parse protocol definition statement
|
|
4472
|
+
|
|
4473
|
+
protocol Greetable {
|
|
4474
|
+
action greet() -> string
|
|
4475
|
+
action farewell()
|
|
4476
|
+
}
|
|
4477
|
+
"""
|
|
4478
|
+
token = self.cur_token
|
|
4479
|
+
self.next_token()
|
|
4480
|
+
|
|
4481
|
+
# protocol Name { ... }
|
|
4482
|
+
if not self.cur_token_is(IDENT):
|
|
4483
|
+
self.errors.append(f"Line {token.line}:{token.column} - Expected protocol name")
|
|
4484
|
+
return None
|
|
4485
|
+
|
|
4486
|
+
protocol_name = Identifier(self.cur_token.literal)
|
|
4487
|
+
self.next_token()
|
|
4488
|
+
|
|
4489
|
+
methods = []
|
|
4490
|
+
|
|
4491
|
+
if self.cur_token_is(LBRACE):
|
|
4492
|
+
self.next_token()
|
|
4493
|
+
while not self.cur_token_is(RBRACE) and self.cur_token.type != EOF:
|
|
4494
|
+
if self.cur_token_is(ACTION) or self.cur_token_is(FUNCTION):
|
|
4495
|
+
self.next_token()
|
|
4496
|
+
if self.cur_token_is(IDENT):
|
|
4497
|
+
methods.append(self.cur_token.literal)
|
|
4498
|
+
self.next_token()
|
|
4499
|
+
# Skip past params and return type annotation
|
|
4500
|
+
while not self.cur_token_is(SEMICOLON) and not self.cur_token_is(RBRACE) and self.cur_token.type != EOF:
|
|
4501
|
+
self.next_token()
|
|
4502
|
+
if self.cur_token_is(SEMICOLON):
|
|
4503
|
+
self.next_token()
|
|
4504
|
+
else:
|
|
4505
|
+
self.next_token()
|
|
4506
|
+
elif self.cur_token_is(IDENT):
|
|
4507
|
+
methods.append(self.cur_token.literal)
|
|
4508
|
+
self.next_token()
|
|
4509
|
+
while not self.cur_token_is(SEMICOLON) and not self.cur_token_is(RBRACE) and self.cur_token.type != EOF:
|
|
4510
|
+
self.next_token()
|
|
4511
|
+
if self.cur_token_is(SEMICOLON):
|
|
4512
|
+
self.next_token()
|
|
4513
|
+
else:
|
|
4514
|
+
self.next_token()
|
|
4515
|
+
|
|
4516
|
+
return ProtocolStatement(name=protocol_name, methods=methods)
|
|
4517
|
+
|
|
4454
4518
|
def parse_type_alias_statement(self):
|
|
4455
4519
|
"""Parse type alias statement"""
|
|
4456
4520
|
token = self.cur_token
|
|
@@ -4551,8 +4615,16 @@ class UltimateParser:
|
|
|
4551
4615
|
|
|
4552
4616
|
return UsingStatement(resource_name=resource_name, resource_expr=resource_expr, body=body)
|
|
4553
4617
|
|
|
4618
|
+
def parse_type_expression(self):
|
|
4619
|
+
"""Parse a simple type expression (identifier)."""
|
|
4620
|
+
if self.cur_token_is(IDENT):
|
|
4621
|
+
type_node = Identifier(self.cur_token.literal)
|
|
4622
|
+
self.next_token()
|
|
4623
|
+
return type_node
|
|
4624
|
+
return None
|
|
4625
|
+
|
|
4554
4626
|
def parse_channel_statement(self):
|
|
4555
|
-
"""Parse channel declaration: channel<type> name
|
|
4627
|
+
"""Parse channel declaration: channel<type>[capacity] name"""
|
|
4556
4628
|
token = self.cur_token
|
|
4557
4629
|
self.next_token() # consume CHANNEL
|
|
4558
4630
|
|
|
@@ -4570,25 +4642,33 @@ class UltimateParser:
|
|
|
4570
4642
|
|
|
4571
4643
|
self.next_token()
|
|
4572
4644
|
|
|
4645
|
+
# Optional capacity in brackets: [10]
|
|
4646
|
+
capacity = None
|
|
4647
|
+
if self.cur_token_is(LBRACKET):
|
|
4648
|
+
self.next_token()
|
|
4649
|
+
capacity = self.parse_expression(LOWEST)
|
|
4650
|
+
if not self.cur_token_is(RBRACKET):
|
|
4651
|
+
self.errors.append(f"Line {token.line}:{token.column} - Expected ']' after channel capacity")
|
|
4652
|
+
return None
|
|
4653
|
+
self.next_token()
|
|
4654
|
+
|
|
4573
4655
|
# Parse channel name
|
|
4574
4656
|
if not self.cur_token_is(IDENT):
|
|
4575
4657
|
self.errors.append(f"Line {token.line}:{token.column} - Expected channel name")
|
|
4576
4658
|
return None
|
|
4577
4659
|
|
|
4578
|
-
name = self.cur_token.literal
|
|
4660
|
+
name = Identifier(self.cur_token.literal)
|
|
4579
4661
|
self.next_token()
|
|
4580
4662
|
|
|
4581
|
-
# Optional capacity
|
|
4582
|
-
capacity
|
|
4583
|
-
if self.cur_token_is(ASSIGN):
|
|
4663
|
+
# Optional capacity via assignment: = expr
|
|
4664
|
+
if capacity is None and self.cur_token_is(ASSIGN):
|
|
4584
4665
|
self.next_token()
|
|
4585
4666
|
capacity = self.parse_expression(LOWEST)
|
|
4586
4667
|
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4668
|
+
# Semicolons are optional in modern Zexus
|
|
4669
|
+
if self.cur_token_is(SEMICOLON):
|
|
4670
|
+
self.next_token()
|
|
4590
4671
|
|
|
4591
|
-
self.next_token()
|
|
4592
4672
|
return ChannelStatement(name=name, element_type=element_type, capacity=capacity)
|
|
4593
4673
|
|
|
4594
4674
|
def parse_send_statement(self):
|