zexus 1.7.2 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +57 -6
  2. package/package.json +2 -1
  3. package/rust_core/Cargo.lock +603 -0
  4. package/rust_core/Cargo.toml +26 -0
  5. package/rust_core/README.md +15 -0
  6. package/rust_core/pyproject.toml +25 -0
  7. package/rust_core/src/binary_bytecode.rs +543 -0
  8. package/rust_core/src/contract_vm.rs +643 -0
  9. package/rust_core/src/executor.rs +847 -0
  10. package/rust_core/src/hasher.rs +90 -0
  11. package/rust_core/src/lib.rs +71 -0
  12. package/rust_core/src/merkle.rs +128 -0
  13. package/rust_core/src/rust_vm.rs +2313 -0
  14. package/rust_core/src/signature.rs +79 -0
  15. package/rust_core/src/state_adapter.rs +281 -0
  16. package/rust_core/src/validator.rs +116 -0
  17. package/scripts/postinstall.js +34 -2
  18. package/src/zexus/__init__.py +1 -1
  19. package/src/zexus/blockchain/accelerator.py +27 -0
  20. package/src/zexus/blockchain/contract_vm.py +409 -3
  21. package/src/zexus/blockchain/rust_bridge.py +64 -0
  22. package/src/zexus/cli/main.py +1 -1
  23. package/src/zexus/cli/zpm.py +1 -1
  24. package/src/zexus/evaluator/bytecode_compiler.py +150 -52
  25. package/src/zexus/evaluator/core.py +151 -809
  26. package/src/zexus/evaluator/expressions.py +27 -22
  27. package/src/zexus/evaluator/functions.py +171 -126
  28. package/src/zexus/evaluator/statements.py +55 -112
  29. package/src/zexus/module_cache.py +20 -9
  30. package/src/zexus/object.py +330 -38
  31. package/src/zexus/parser/parser.py +69 -14
  32. package/src/zexus/parser/strategy_context.py +228 -5
  33. package/src/zexus/parser/strategy_structural.py +2 -2
  34. package/src/zexus/persistence.py +46 -17
  35. package/src/zexus/security.py +140 -234
  36. package/src/zexus/type_checker.py +44 -5
  37. package/src/zexus/vm/binary_bytecode.py +7 -3
  38. package/src/zexus/vm/bytecode.py +6 -0
  39. package/src/zexus/vm/cache.py +24 -46
  40. package/src/zexus/vm/compiler.py +80 -20
  41. package/src/zexus/vm/fastops.c +1093 -2975
  42. package/src/zexus/vm/gas_metering.py +2 -2
  43. package/src/zexus/vm/memory_pool.py +21 -9
  44. package/src/zexus/vm/vm.py +527 -67
  45. package/src/zexus/zpm/package_manager.py +1 -1
  46. package/src/zexus.egg-info/PKG-INFO +79 -12
  47. package/src/zexus.egg-info/SOURCES.txt +23 -1
  48. package/src/zexus.egg-info/requires.txt +26 -0
  49. package/src/zexus.egg-info/entry_points.txt +0 -4
@@ -14,8 +14,11 @@ import sqlite3
14
14
  import time
15
15
  import hashlib
16
16
  import threading
17
+ import logging
17
18
  from zexus.config import config as zexus_config
18
19
 
20
+ logger = logging.getLogger(__name__)
21
+
19
22
  # Try importing advanced database drivers
20
23
  try:
21
24
  import plyvel # For LevelDB
@@ -31,7 +34,12 @@ except ImportError:
31
34
 
32
35
  from zexus.object import (
33
36
  Environment, Map, String, Integer, Float, Boolean as BooleanObj,
34
- Builtin, List, Null, EvaluationError as ObjectEvaluationError, NULL
37
+ Builtin, List, Null, EvaluationError as ObjectEvaluationError, NULL,
38
+ _sanitize_identifier,
39
+ EntityDefinition,
40
+ EntityInstance,
41
+ _get_cached_evaluator,
42
+ _clear_cached_evaluator,
35
43
  )
36
44
 
37
45
  try:
@@ -39,13 +47,6 @@ try:
39
47
  except ImportError: # Fallback if optional type missing
40
48
  ContractReference = None
41
49
 
42
- # =============================================
43
- # Shared Evaluator Cache for Performance
44
- # =============================================
45
- # Creating a new Evaluator() for every contract method call is expensive
46
- # This cache provides thread-local evaluators that can be reused
47
-
48
- _evaluator_cache = threading.local()
49
50
  _vm_action_context = threading.local()
50
51
 
51
52
  def _get_vm_action_context() -> bool:
@@ -54,33 +55,36 @@ def _get_vm_action_context() -> bool:
54
55
  def _set_vm_action_context(flag: bool) -> None:
55
56
  _vm_action_context.disable_vm = bool(flag)
56
57
 
57
- def _get_cached_evaluator():
58
- """Get a cached evaluator instance for the current thread.
59
-
60
- This significantly improves performance for contract method calls
61
- by avoiding repeated Evaluator() initialization overhead.
62
- """
63
- if not hasattr(_evaluator_cache, 'evaluator'):
64
- from zexus.evaluator.core import Evaluator
65
- _evaluator_cache.evaluator = Evaluator()
66
- return _evaluator_cache.evaluator
58
+ __all__ = [
59
+ # Re-export entity classes for backwards compatibility
60
+ "EntityDefinition",
61
+ "EntityInstance",
62
+ ]
63
+
64
+ # Storage directory roots (created lazily)
65
+ STORAGE_DIR = "chain_data"
66
+ AUDIT_DIR = os.path.join(STORAGE_DIR, "audit_logs")
67
67
 
68
- def _clear_evaluator_cache():
69
- """Clear the evaluator cache (useful for testing)."""
70
- if hasattr(_evaluator_cache, 'evaluator'):
71
- del _evaluator_cache.evaluator
72
68
 
73
- # =============================================
69
+ def _ensure_storage_dirs() -> None:
70
+ """Ensure storage directories exist.
74
71
 
75
- # Ensure storage directory exists
76
- STORAGE_DIR = "chain_data"
77
- if not os.path.exists(STORAGE_DIR):
78
- os.makedirs(STORAGE_DIR)
72
+ MEDIUM (M3): Avoid filesystem writes at import time; create directories only
73
+ when storage/audit features are actually used.
74
+ """
75
+ os.makedirs(STORAGE_DIR, exist_ok=True)
76
+ os.makedirs(AUDIT_DIR, exist_ok=True)
79
77
 
80
- # Audit logging directory
81
- AUDIT_DIR = os.path.join(STORAGE_DIR, "audit_logs")
82
- if not os.path.exists(AUDIT_DIR):
83
- os.makedirs(AUDIT_DIR)
78
+
79
+ def _ensure_parent_dir(path: str) -> None:
80
+ """Ensure the parent directory for a path exists.
81
+
82
+ MEDIUM (M3): Used to lazily create storage directories only when a feature
83
+ actually persists data.
84
+ """
85
+ parent = os.path.dirname(path)
86
+ if parent:
87
+ os.makedirs(parent, exist_ok=True)
84
88
 
85
89
 
86
90
  class AuditLog:
@@ -94,6 +98,8 @@ class AuditLog:
94
98
  self.entries = [] # In-memory audit log
95
99
  self.max_entries = max_entries
96
100
  self.persist_to_file = persist_to_file
101
+ if self.persist_to_file:
102
+ _ensure_storage_dirs()
97
103
  self.audit_file = os.path.join(AUDIT_DIR, f"audit_{uuid.uuid4().hex[:8]}.jsonl")
98
104
 
99
105
  def log(self, data_name, action, data_type, timestamp=None, additional_context=None):
@@ -368,7 +374,6 @@ class SecurityContext:
368
374
  - For matching trails, record a derived audit entry and also print to stdout.
369
375
  """
370
376
  try:
371
- # simple stringify payload for filtering
372
377
  payload_str = json.dumps(payload) if not isinstance(payload, str) else payload
373
378
  except Exception:
374
379
  try:
@@ -390,8 +395,12 @@ class SecurityContext:
390
395
  if isinstance(flt, str) and flt.startswith('re:'):
391
396
  import re
392
397
  pattern = flt[3:]
393
- if re.search(pattern, payload_str):
394
- matched = True
398
+ # SECURITY (H2): Guard against ReDoS — timeout regex match
399
+ try:
400
+ if re.search(pattern, payload_str, flags=0):
401
+ matched = True
402
+ except re.error:
403
+ logger.debug("Invalid trail regex filter; skipping", exc_info=True)
395
404
  elif isinstance(flt, str) and ':' in flt:
396
405
  # key:value pattern
397
406
  k, v = flt.split(':', 1)
@@ -407,6 +416,7 @@ class SecurityContext:
407
416
  if flt in payload_str:
408
417
  matched = True
409
418
  except Exception:
419
+ logger.debug("Trail filter evaluation failed; skipping trail", exc_info=True)
410
420
  matched = False
411
421
 
412
422
  if not matched:
@@ -424,7 +434,7 @@ class SecurityContext:
424
434
  try:
425
435
  self.audit_log.entries.append(entry)
426
436
  except Exception:
427
- pass
437
+ logger.debug("Failed to append trail entry to audit_log.entries", exc_info=True)
428
438
 
429
439
  # Deliver to configured sinks
430
440
  for sink in list(self.trail_sinks):
@@ -433,16 +443,15 @@ class SecurityContext:
433
443
  if stype == 'stdout':
434
444
  print(f"[TRAIL:{tid}] {event_type} -> {payload_str}")
435
445
  elif stype == 'file':
446
+ _ensure_storage_dirs()
436
447
  path = sink.get('path') or os.path.join(AUDIT_DIR, 'trails.jsonl')
437
- try:
438
- with open(path, 'a', encoding='utf-8') as sf:
439
- sf.write(json.dumps(entry) + '\n')
440
- except Exception:
441
- pass
448
+ with open(path, 'a', encoding='utf-8') as sf:
449
+ sf.write(json.dumps(entry) + '\n')
442
450
  elif stype == 'sqlite':
451
+ _ensure_storage_dirs()
443
452
  db_path = sink.get('db_path') or os.path.join(STORAGE_DIR, 'trails.db')
453
+ conn = sqlite3.connect(db_path, check_same_thread=False)
444
454
  try:
445
- conn = sqlite3.connect(db_path, check_same_thread=False)
446
455
  cur = conn.cursor()
447
456
  cur.execute('''CREATE TABLE IF NOT EXISTS trails (
448
457
  id TEXT PRIMARY KEY,
@@ -455,18 +464,14 @@ class SecurityContext:
455
464
  entry['id'], entry['trail_id'], entry['event_type'], entry['payload'], entry['timestamp']
456
465
  ))
457
466
  conn.commit()
467
+ finally:
458
468
  conn.close()
459
- except Exception:
460
- pass
461
469
  elif stype == 'callback':
462
470
  cb = sink.get('callback')
463
- try:
464
- if callable(cb):
465
- cb(entry)
466
- except Exception:
467
- pass
471
+ if callable(cb):
472
+ cb(entry)
468
473
  except Exception:
469
- pass
474
+ logger.debug("Trail sink delivery failed", exc_info=True)
470
475
 
471
476
 
472
477
  def register_verify_check(self, name, check_func):
@@ -524,13 +529,39 @@ def get_security_context():
524
529
 
525
530
 
526
531
  def _is_ip_in_list(ip, ip_list):
527
- """Check if IP matches CIDR or exact match in list"""
532
+ """Check if IP matches CIDR or exact match in list.
533
+
534
+ SECURITY (H1): Uses proper bit-masking for CIDR checks instead of string prefix.
535
+ """
536
+ import struct
537
+ import socket
538
+
539
+ def _ip_to_int(ip_str):
540
+ """Convert dotted-quad IP to 32-bit integer."""
541
+ try:
542
+ return struct.unpack('!I', socket.inet_aton(ip_str))[0]
543
+ except (socket.error, struct.error, OSError):
544
+ return None
545
+
546
+ ip_int = _ip_to_int(ip)
547
+ if ip_int is None:
548
+ return False
549
+
528
550
  for pattern in ip_list:
529
551
  if "/" in pattern: # CIDR notation
530
- # Simplified CIDR check (would need proper IP math for production)
531
- network_part = pattern.split("/")[0]
532
- if ip.startswith(network_part.rsplit(".", 1)[0]):
533
- return True
552
+ try:
553
+ network_str, prefix_len_str = pattern.split("/", 1)
554
+ prefix_len = int(prefix_len_str)
555
+ if prefix_len < 0 or prefix_len > 32:
556
+ continue
557
+ network_int = _ip_to_int(network_str)
558
+ if network_int is None:
559
+ continue
560
+ mask = (0xFFFFFFFF << (32 - prefix_len)) & 0xFFFFFFFF
561
+ if (ip_int & mask) == (network_int & mask):
562
+ return True
563
+ except (ValueError, TypeError):
564
+ continue
534
565
  elif ip == pattern: # Exact match
535
566
  return True
536
567
  return False
@@ -540,177 +571,7 @@ def _is_ip_in_list(ip, ip_list):
540
571
  # ENTITY SYSTEM - Object-Oriented Data Structures
541
572
  # ===============================================
542
573
 
543
- class EntityDefinition:
544
- """Represents an entity definition with properties and methods"""
545
-
546
- def __init__(self, name, properties, methods=None, parent=None):
547
- self.name = name
548
- self.properties = properties # {prop_name: {type, default_value}}
549
- self.methods = methods or {} # {method_name: Action}
550
- self.parent = parent # Parent entity (inheritance)
551
-
552
- def create_instance(self, values=None):
553
- """Create an instance of this entity with dependency injection support"""
554
- # Perform dependency injection for marked properties
555
- injected_values = values or {}
556
-
557
- # Check if this entity has injected dependencies
558
- if hasattr(self, 'injected_deps') and self.injected_deps:
559
- from zexus.dependency_injection import get_di_registry
560
-
561
- registry = get_di_registry()
562
- # Use __main__ as default module context
563
- container = registry.get_container("__main__")
564
-
565
- for dep_name in self.injected_deps:
566
- if dep_name not in injected_values:
567
- # Try to inject from DI container
568
- try:
569
- injected_value = container.get(dep_name)
570
- injected_values[dep_name] = injected_value
571
- except BaseException as e:
572
- # Dependency not available - use NULL placeholder
573
- from zexus.object import NULL
574
- injected_values[dep_name] = NULL
575
-
576
- instance = EntityInstance(self, injected_values)
577
- return instance
578
-
579
- def get_all_properties(self):
580
- """Get all properties including inherited ones, in correct order (parent first, then child)"""
581
- props = {}
582
- # First add parent properties
583
- if self.parent:
584
- parent_props = self.parent.get_all_properties()
585
- props.update(parent_props)
586
- # Then add/override with child properties
587
- props.update(self.properties)
588
- return props
589
-
590
-
591
- class EntityInstance:
592
- """Represents an instance of an entity"""
593
-
594
- def __init__(self, entity_def, values):
595
- self.entity_def = entity_def
596
- self.data = values or {}
597
- self._validate_properties()
598
-
599
- def _validate_properties(self):
600
- """Validate that all required properties are present and inject dependencies"""
601
- all_props = self.entity_def.get_all_properties()
602
- for prop_name, prop_config in all_props.items():
603
- if prop_name not in self.data:
604
- # Check if this is an injected dependency
605
- if prop_config.get("injected", False):
606
- # Try to inject from DI registry
607
- try:
608
- from zexus.dependency_injection import get_di_registry
609
- from zexus.object import NULL
610
- registry = get_di_registry()
611
- container = registry.get_container("__main__")
612
- if container:
613
- injected_value = container.get(prop_name)
614
- self.data[prop_name] = injected_value
615
- else:
616
- # No container, set to NULL
617
- self.data[prop_name] = NULL
618
- except Exception:
619
- # If injection fails, set to NULL
620
- from zexus.object import NULL
621
- self.data[prop_name] = NULL
622
- elif "default_value" in prop_config:
623
- self.data[prop_name] = prop_config["default_value"]
624
-
625
- def get(self, property_name):
626
- """Get property value"""
627
- return self.data.get(property_name)
628
-
629
- def set(self, property_name, value):
630
- """Set property value (prevent modification if property is sealed)"""
631
- if property_name not in self.entity_def.get_all_properties():
632
- raise ValueError(f"Unknown property: {property_name}")
633
- existing = self.data.get(property_name)
634
- # Avoid importing SealedObject here to prevent circular imports; use name-based check
635
- if existing is not None and existing.__class__.__name__ == 'SealedObject':
636
- raise ValueError(f"Cannot modify sealed property: {property_name}")
637
- self.data[property_name] = value
638
-
639
- def to_dict(self):
640
- """Convert to dictionary"""
641
- return self.data
642
-
643
- def __str__(self):
644
- """String representation of entity instance"""
645
- entity_name = self.entity_def.name if hasattr(self.entity_def, 'name') else 'Entity'
646
- # Format properties nicely
647
- props = []
648
- for key, value in self.data.items():
649
- # Convert value to a readable string
650
- if hasattr(value, 'value'):
651
- # Object wrapper with value attribute (Integer, String, etc.)
652
- props.append(f"{key}={value.value}")
653
- elif hasattr(value, '__class__') and hasattr(value.__class__, '__name__'):
654
- if value.__class__.__name__ in ['EntityInstance', 'SealedObject']:
655
- props.append(f"{key}=<{value.__class__.__name__}>")
656
- else:
657
- try:
658
- props.append(f"{key}={value}")
659
- except:
660
- props.append(f"{key}=<object>")
661
- else:
662
- props.append(f"{key}={value}")
663
- props_str = ", ".join(props)
664
- return f"{entity_name}({props_str})"
665
-
666
- def __repr__(self):
667
- """Python representation"""
668
- return self.__str__()
669
-
670
- def call_method(self, method_name, args):
671
- """Call a method on this entity instance"""
672
- if method_name not in self.entity_def.methods:
673
- from zexus.object import EvaluationError
674
- return EvaluationError(f"Method '{method_name}' not supported for ENTITY_INSTANCE")
675
-
676
- # Get the method (Action or Function)
677
- method = self.entity_def.methods[method_name]
678
-
679
- # Create a new environment for the method execution
680
- from zexus.environment import Environment
681
- method_env = Environment(outer=method.env if hasattr(method, 'env') else None)
682
-
683
- # Bind 'this' to the current instance in the method environment
684
- method_env.set('this', self)
685
-
686
- # Bind method parameters to arguments
687
- if hasattr(method, 'parameters'):
688
- for i, param in enumerate(method.parameters):
689
- if i < len(args):
690
- # Handle both Identifier objects and ParameterNode objects
691
- if hasattr(param, 'name'):
692
- # It's a ParameterNode with name and type
693
- param_name = param.name.value if hasattr(param.name, 'value') else str(param.name)
694
- elif hasattr(param, 'value'):
695
- # It's an Identifier
696
- param_name = param.value
697
- else:
698
- # Fallback to string representation
699
- param_name = str(param)
700
- method_env.set(param_name, args[i])
701
-
702
- # Use cached evaluator for performance (avoids repeated Evaluator() initialization)
703
- evaluator = _get_cached_evaluator()
704
-
705
- # Execute the method body with stack trace
706
- result = evaluator.eval_node(method.body, method_env, stack_trace=[])
707
-
708
- # Unwrap return values
709
- from zexus.object import ReturnValue
710
- if isinstance(result, ReturnValue):
711
- return result.value
712
-
713
- return result
574
+ # MEDIUM (M5): EntityDefinition/EntityInstance are provided by `zexus.object`.
714
575
 
715
576
 
716
577
  # ===============================================
@@ -787,6 +648,7 @@ class InMemoryBackend(StorageBackend):
787
648
  class SQLiteBackend(StorageBackend):
788
649
  def __init__(self, db_path):
789
650
  import sqlite3
651
+ _ensure_parent_dir(db_path)
790
652
  self.conn = sqlite3.connect(db_path, check_same_thread=False)
791
653
  self.cursor = self.conn.cursor()
792
654
 
@@ -863,6 +725,7 @@ class SQLiteBackend(StorageBackend):
863
725
  class LevelDBBackend(StorageBackend):
864
726
  def __init__(self, db_path):
865
727
  if not _LEVELDB_AVAILABLE: raise ImportError("plyvel not installed")
728
+ _ensure_parent_dir(db_path)
866
729
  self.db = plyvel.DB(db_path, create_if_missing=True)
867
730
 
868
731
  def set(self, key, value):
@@ -887,6 +750,7 @@ class LevelDBBackend(StorageBackend):
887
750
  class RocksDBBackend(StorageBackend):
888
751
  def __init__(self, db_path):
889
752
  if not _ROCKSDB_AVAILABLE: raise ImportError("rocksdb not installed")
753
+ _ensure_parent_dir(db_path)
890
754
  self.db = rocksdb.DB(db_path, rocksdb.Options(create_if_missing=True))
891
755
 
892
756
  def set(self, key, value):
@@ -1030,6 +894,9 @@ class ContractStorage:
1030
894
  def __init__(self, contract_id, db_type=None):
1031
895
  self.transaction_log = []
1032
896
 
897
+ # SECURITY (C6): Sanitize contract_id to prevent path traversal
898
+ contract_id = _sanitize_identifier(str(contract_id), "contract_id")
899
+
1033
900
  # Determine storage type: explicit > env var > default (memory)
1034
901
  if db_type is None:
1035
902
  db_type = os.environ.get("ZEXUS_STORAGE_ENGINE", "memory")
@@ -1043,11 +910,16 @@ class ContractStorage:
1043
910
  if db_type == "memory":
1044
911
  self.backend = InMemoryBackend()
1045
912
  elif db_type == "leveldb" and _LEVELDB_AVAILABLE:
913
+ _ensure_parent_dir(base_path)
1046
914
  self.backend = LevelDBBackend(base_path)
1047
915
  elif db_type == "rocksdb" and _ROCKSDB_AVAILABLE:
1048
- self.backend = RocksDBBackend(f"{base_path}.rdb")
916
+ db_path = f"{base_path}.rdb"
917
+ _ensure_parent_dir(db_path)
918
+ self.backend = RocksDBBackend(db_path)
1049
919
  elif db_type == "sqlite":
1050
- self.backend = SQLiteBackend(f"{base_path}.sqlite")
920
+ db_path = f"{base_path}.sqlite"
921
+ _ensure_parent_dir(db_path)
922
+ self.backend = SQLiteBackend(db_path)
1051
923
  else:
1052
924
  # Unknown type, fall back to memory
1053
925
  self.backend = InMemoryBackend()
@@ -1538,12 +1410,21 @@ class SmartContract:
1538
1410
  register_contract(self)
1539
1411
 
1540
1412
  def __del__(self):
1541
- """Ensure storage is committed on cleanup"""
1413
+ """Best-effort commit on cleanup.
1414
+
1415
+ Destructors must never raise; however, silently swallowing exceptions
1416
+ can hide storage corruption or shutdown bugs. We log in debug mode.
1417
+ """
1542
1418
  try:
1543
- if hasattr(self, 'storage'):
1544
- self.storage.commit_batch(force=True)
1545
- except:
1546
- pass # Ignore errors during cleanup
1419
+ storage = getattr(self, 'storage', None)
1420
+ if storage is not None:
1421
+ storage.commit_batch(force=True)
1422
+ except Exception as exc:
1423
+ try:
1424
+ if zexus_config.should_log('debug'):
1425
+ print(f"⚠️ SmartContract.__del__ commit failed: {exc}")
1426
+ except Exception:
1427
+ pass
1547
1428
 
1548
1429
  def instantiate(self, args=None):
1549
1430
  """Create a new instance of this contract when called like ZiverWallet()."""
@@ -2077,8 +1958,33 @@ class AuthConfig:
2077
1958
 
2078
1959
  def validate_token(self, token):
2079
1960
  """Validate a token"""
2080
- # In production, this would validate with OAuth provider
2081
- return True
1961
+ # LI10: Don't unconditionally accept tokens.
1962
+ # This is still a lightweight heuristic (no provider integration), but it
1963
+ # rejects obviously invalid/empty values.
1964
+ if token is None:
1965
+ return False
1966
+
1967
+ try:
1968
+ token_str = token.value if hasattr(token, 'value') else str(token)
1969
+ except Exception:
1970
+ return False
1971
+
1972
+ token_str = token_str.strip()
1973
+ if not token_str:
1974
+ return False
1975
+
1976
+ if token_str.lower().startswith("bearer "):
1977
+ token_str = token_str[7:].strip()
1978
+ if not token_str:
1979
+ return False
1980
+
1981
+ # Accept JWT-like tokens (three base64url-ish segments) or opaque tokens
1982
+ # with a reasonable minimum length.
1983
+ parts = token_str.split('.')
1984
+ if len(parts) == 3 and all(parts):
1985
+ return True
1986
+
1987
+ return len(token_str) >= 16
2082
1988
 
2083
1989
  def is_token_expired(self, token_data):
2084
1990
  """Check if token is expired"""
@@ -290,6 +290,31 @@ class StaticTypeChecker:
290
290
  self._check_block(stmt.body, ret_ts)
291
291
  self._pop_scope()
292
292
 
293
+ def _check_EntityStatement(self, stmt):
294
+ """Register entity as a callable constructor.
295
+
296
+ Entity constructors can be called two ways:
297
+ - Positional: Point(3, 4) — N args matching N properties
298
+ - Map style: Point{x: 3, y: 4} — parsed as 1 MapLiteral arg
299
+
300
+ We register with _is_entity_constructor marker so _check_call
301
+ can skip strict arity validation for the single-map-arg case.
302
+ """
303
+ name = stmt.name.value if hasattr(stmt.name, 'value') else str(stmt.name)
304
+ param_types: List[Tuple[str, Optional[TypeSpec]]] = []
305
+ for prop in (stmt.properties or []):
306
+ prop_name = prop.get('name', '') if isinstance(prop, dict) else getattr(prop, 'name', '')
307
+ prop_type = prop.get('type', None) if isinstance(prop, dict) else getattr(prop, 'type', None)
308
+ ts = _resolve_annotation(prop_type)
309
+ param_types.append((prop_name, ts))
310
+ if name:
311
+ self._scope.define_callable(name, param_types, None)
312
+ self._scope.define(name, TypeSpec(BaseType.ANY))
313
+ # Mark this name as an entity constructor for flexible arity
314
+ if not hasattr(self._scope, '_entity_names'):
315
+ self._scope._entity_names = set()
316
+ self._scope._entity_names.add(name)
317
+
293
318
  def _check_FunctionStatement(self, stmt: ast.FunctionStatement):
294
319
  # Reuse action logic
295
320
  name = stmt.name if isinstance(stmt.name, str) else getattr(stmt.name, "value", None)
@@ -382,14 +407,28 @@ class StaticTypeChecker:
382
407
  params, _ = sig
383
408
  args = call.arguments or []
384
409
 
385
- # Arity check
410
+ # Check if this is an entity constructor — they accept flexible arity
411
+ # (either N positional args or 1 MapLiteral arg for {field: value} syntax)
412
+ is_entity = False
413
+ scope = self._scope
414
+ while scope:
415
+ if hasattr(scope, '_entity_names') and fn_name in scope._entity_names:
416
+ is_entity = True
417
+ break
418
+ scope = scope.parent
419
+
420
+ # Arity check (skip for entity constructors with single map arg)
386
421
  expected = len(params)
387
422
  got = len(args)
388
423
  if got != expected:
389
- self._error(
390
- f"'{fn_name}' expects {expected} argument(s) but got {got}",
391
- call,
392
- )
424
+ if is_entity and got == 1:
425
+ # Entity{field: value} syntax single MapLiteral arg is valid
426
+ pass
427
+ else:
428
+ self._error(
429
+ f"'{fn_name}' expects {expected} argument(s) but got {got}",
430
+ call,
431
+ )
393
432
 
394
433
  # Per-argument type check
395
434
  for i, (pname, pts) in enumerate(params):
@@ -391,9 +391,13 @@ def _deserialize_constant(r: _Reader) -> Any:
391
391
  result[key] = val
392
392
  return result
393
393
  elif tag == ConstTag.OPAQUE:
394
- import pickle
395
- data = r.raw_bytes()
396
- return pickle.loads(data)
394
+ # SECURITY (C10): pickle.loads() removed — arbitrary code execution risk.
395
+ # Skip the raw bytes but refuse to deserialize.
396
+ _data = r.raw_bytes()
397
+ raise ValueError(
398
+ "OPAQUE constant tag is disabled for security (pickle deserialization). "
399
+ "Re-compile the .zxc file with a safe serialization format."
400
+ )
397
401
  else:
398
402
  raise ValueError(f"Unknown constant tag: 0x{tag:02x}")
399
403
 
@@ -342,6 +342,12 @@ class BytecodeBuilder:
342
342
  idx = self.emit("JUMP_IF_FALSE", None)
343
343
  self._forward_refs.setdefault(label, []).append(idx)
344
344
  return idx
345
+
346
+ def emit_jump_if_true(self, label: str) -> int:
347
+ """Emit a conditional jump (true) to a label"""
348
+ idx = self.emit("JUMP_IF_TRUE", None)
349
+ self._forward_refs.setdefault(label, []).append(idx)
350
+ return idx
345
351
 
346
352
  def resolve_labels(self):
347
353
  """Resolve all forward label references"""