zexus 1.6.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/LICENSE +0 -0
- package/README.md +2513 -0
- package/bin/zexus +2 -0
- package/bin/zpics +2 -0
- package/bin/zpm +2 -0
- package/bin/zx +2 -0
- package/bin/zx-deploy +2 -0
- package/bin/zx-dev +2 -0
- package/bin/zx-run +2 -0
- package/package.json +66 -0
- package/scripts/README.md +24 -0
- package/scripts/postinstall.js +44 -0
- package/shared_config.json +24 -0
- package/src/README.md +1525 -0
- package/src/tests/run_zexus_tests.py +117 -0
- package/src/tests/test_all_phases.zx +346 -0
- package/src/tests/test_blockchain_features.zx +306 -0
- package/src/tests/test_complexity_features.zx +321 -0
- package/src/tests/test_core_integration.py +185 -0
- package/src/tests/test_phase10_ecosystem.zx +177 -0
- package/src/tests/test_phase1_modifiers.zx +87 -0
- package/src/tests/test_phase2_plugins.zx +80 -0
- package/src/tests/test_phase3_security.zx +97 -0
- package/src/tests/test_phase4_vfs.zx +116 -0
- package/src/tests/test_phase5_types.zx +117 -0
- package/src/tests/test_phase6_metaprogramming.zx +125 -0
- package/src/tests/test_phase7_optimization.zx +132 -0
- package/src/tests/test_phase9_advanced_types.zx +157 -0
- package/src/tests/test_security_features.py +419 -0
- package/src/tests/test_security_features.zx +276 -0
- package/src/tests/test_simple_zx.zx +1 -0
- package/src/tests/test_verification_simple.zx +69 -0
- package/src/zexus/__init__.py +28 -0
- package/src/zexus/__main__.py +5 -0
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/advanced_types.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/builtin_modules.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/complexity_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/concurrency_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/config.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/dependency_injection.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/ecosystem.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__/hybrid_orchestrator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/metaprogramming.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/optimization.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/plugin_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/policy_engine.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/stdlib_integration.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/strategy_recovery.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/type_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/virtual_filesystem.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/advanced_types.py +401 -0
- package/src/zexus/blockchain/__init__.py +40 -0
- package/src/zexus/blockchain/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/blockchain/__pycache__/crypto.cpython-312.pyc +0 -0
- package/src/zexus/blockchain/__pycache__/ledger.cpython-312.pyc +0 -0
- package/src/zexus/blockchain/__pycache__/transaction.cpython-312.pyc +0 -0
- package/src/zexus/blockchain/crypto.py +463 -0
- package/src/zexus/blockchain/ledger.py +255 -0
- package/src/zexus/blockchain/transaction.py +267 -0
- package/src/zexus/builtin_modules.py +284 -0
- package/src/zexus/builtin_plugins.py +317 -0
- package/src/zexus/capability_system.py +372 -0
- package/src/zexus/cli/__init__.py +2 -0
- package/src/zexus/cli/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +707 -0
- package/src/zexus/cli/zpm.py +203 -0
- package/src/zexus/compare_interpreter_compiler.py +146 -0
- package/src/zexus/compiler/__init__.py +169 -0
- package/src/zexus/compiler/__pycache__/__init__.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__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/bytecode.py +266 -0
- package/src/zexus/compiler/compat_runtime.py +277 -0
- package/src/zexus/compiler/lexer.py +257 -0
- package/src/zexus/compiler/parser.py +779 -0
- package/src/zexus/compiler/semantic.py +118 -0
- package/src/zexus/compiler/zexus_ast.py +454 -0
- package/src/zexus/complexity_system.py +575 -0
- package/src/zexus/concurrency_system.py +493 -0
- package/src/zexus/config.py +201 -0
- package/src/zexus/crypto_bridge.py +19 -0
- package/src/zexus/dependency_injection.py +423 -0
- package/src/zexus/ecosystem.py +434 -0
- package/src/zexus/environment.py +101 -0
- package/src/zexus/environment_manager.py +119 -0
- package/src/zexus/error_reporter.py +314 -0
- package/src/zexus/evaluator/__init__.py +12 -0
- package/src/zexus/evaluator/__pycache__/__init__.cpython-312.pyc +0 -0
- 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__/integration.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +700 -0
- package/src/zexus/evaluator/core.py +891 -0
- package/src/zexus/evaluator/expressions.py +827 -0
- package/src/zexus/evaluator/functions.py +3989 -0
- package/src/zexus/evaluator/integration.py +396 -0
- package/src/zexus/evaluator/statements.py +4303 -0
- package/src/zexus/evaluator/utils.py +126 -0
- package/src/zexus/evaluator_original.py +2041 -0
- package/src/zexus/external_bridge.py +16 -0
- package/src/zexus/find_affected_imports.sh +155 -0
- package/src/zexus/hybrid_orchestrator.py +152 -0
- package/src/zexus/input_validation.py +259 -0
- package/src/zexus/lexer.py +571 -0
- package/src/zexus/logging.py +89 -0
- package/src/zexus/lsp/__init__.py +9 -0
- package/src/zexus/lsp/completion_provider.py +207 -0
- package/src/zexus/lsp/definition_provider.py +22 -0
- package/src/zexus/lsp/hover_provider.py +71 -0
- package/src/zexus/lsp/server.py +269 -0
- package/src/zexus/lsp/symbol_provider.py +31 -0
- package/src/zexus/metaprogramming.py +321 -0
- package/src/zexus/module_cache.py +89 -0
- package/src/zexus/module_manager.py +107 -0
- package/src/zexus/object.py +973 -0
- package/src/zexus/optimization.py +424 -0
- package/src/zexus/parser/__init__.py +31 -0
- package/src/zexus/parser/__pycache__/__init__.cpython-312.pyc +0 -0
- 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/integration.py +86 -0
- package/src/zexus/parser/parser.py +3977 -0
- package/src/zexus/parser/strategy_context.py +7254 -0
- package/src/zexus/parser/strategy_structural.py +1033 -0
- package/src/zexus/persistence.py +391 -0
- package/src/zexus/plugin_system.py +290 -0
- package/src/zexus/policy_engine.py +365 -0
- package/src/zexus/profiler/__init__.py +5 -0
- package/src/zexus/profiler/profiler.py +233 -0
- package/src/zexus/purity_system.py +398 -0
- package/src/zexus/runtime/__init__.py +20 -0
- package/src/zexus/runtime/async_runtime.py +324 -0
- package/src/zexus/search_old_imports.sh +65 -0
- package/src/zexus/security.py +1407 -0
- package/src/zexus/stack_trace.py +233 -0
- package/src/zexus/stdlib/__init__.py +27 -0
- package/src/zexus/stdlib/blockchain.py +341 -0
- package/src/zexus/stdlib/compression.py +167 -0
- package/src/zexus/stdlib/crypto.py +124 -0
- package/src/zexus/stdlib/datetime.py +163 -0
- package/src/zexus/stdlib/db_mongo.py +199 -0
- package/src/zexus/stdlib/db_mysql.py +162 -0
- package/src/zexus/stdlib/db_postgres.py +163 -0
- package/src/zexus/stdlib/db_sqlite.py +133 -0
- package/src/zexus/stdlib/encoding.py +230 -0
- package/src/zexus/stdlib/fs.py +195 -0
- package/src/zexus/stdlib/http.py +219 -0
- package/src/zexus/stdlib/http_server.py +248 -0
- package/src/zexus/stdlib/json_module.py +61 -0
- package/src/zexus/stdlib/math.py +360 -0
- package/src/zexus/stdlib/os_module.py +265 -0
- package/src/zexus/stdlib/regex.py +148 -0
- package/src/zexus/stdlib/sockets.py +253 -0
- package/src/zexus/stdlib/test_framework.zx +208 -0
- package/src/zexus/stdlib/test_runner.zx +119 -0
- package/src/zexus/stdlib_integration.py +341 -0
- package/src/zexus/strategy_recovery.py +256 -0
- package/src/zexus/syntax_validator.py +356 -0
- package/src/zexus/testing/zpics.py +407 -0
- package/src/zexus/testing/zpics_runtime.py +369 -0
- package/src/zexus/type_system.py +374 -0
- package/src/zexus/validation_system.py +569 -0
- package/src/zexus/virtual_filesystem.py +355 -0
- package/src/zexus/vm/__init__.py +8 -0
- package/src/zexus/vm/__pycache__/__init__.cpython-312.pyc +0 -0
- 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__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/memory_manager.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/memory_pool.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/peephole_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/profiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/register_allocator.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/register_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/ssa_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +420 -0
- package/src/zexus/vm/bytecode.py +428 -0
- package/src/zexus/vm/bytecode_converter.py +297 -0
- package/src/zexus/vm/cache.py +532 -0
- package/src/zexus/vm/jit.py +720 -0
- package/src/zexus/vm/memory_manager.py +520 -0
- package/src/zexus/vm/memory_pool.py +511 -0
- package/src/zexus/vm/optimizer.py +478 -0
- package/src/zexus/vm/parallel_vm.py +899 -0
- package/src/zexus/vm/peephole_optimizer.py +452 -0
- package/src/zexus/vm/profiler.py +527 -0
- package/src/zexus/vm/register_allocator.py +462 -0
- package/src/zexus/vm/register_vm.py +520 -0
- package/src/zexus/vm/ssa_converter.py +757 -0
- package/src/zexus/vm/vm.py +1392 -0
- package/src/zexus/zexus_ast.py +1782 -0
- package/src/zexus/zexus_token.py +253 -0
- package/src/zexus/zpm/__init__.py +15 -0
- package/src/zexus/zpm/installer.py +116 -0
- package/src/zexus/zpm/package_manager.py +208 -0
- package/src/zexus/zpm/publisher.py +98 -0
- package/src/zexus/zpm/registry.py +110 -0
- package/src/zexus.egg-info/PKG-INFO +2235 -0
- package/src/zexus.egg-info/SOURCES.txt +876 -0
- package/src/zexus.egg-info/dependency_links.txt +1 -0
- package/src/zexus.egg-info/entry_points.txt +3 -0
- package/src/zexus.egg-info/not-zip-safe +1 -0
- package/src/zexus.egg-info/requires.txt +14 -0
- package/src/zexus.egg-info/top_level.txt +2 -0
- package/zexus.json +14 -0
|
@@ -0,0 +1,3989 @@
|
|
|
1
|
+
# src/zexus/evaluator/functions.py
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from .. import zexus_ast
|
|
6
|
+
from ..zexus_ast import CallExpression, MethodCallExpression
|
|
7
|
+
from ..object import (
|
|
8
|
+
Environment, Integer, Float, String, List, Map, Boolean as BooleanObj,
|
|
9
|
+
Null, Builtin, Action, LambdaFunction, ReturnValue, DateTime, Math, File, Debug,
|
|
10
|
+
EvaluationError, EntityDefinition
|
|
11
|
+
)
|
|
12
|
+
from .utils import is_error, debug_log, NULL, TRUE, FALSE, _resolve_awaitable, _zexus_to_python, _python_to_zexus, _to_str
|
|
13
|
+
|
|
14
|
+
# Try to import backend, handle failure gracefully (as per your original code)
|
|
15
|
+
try:
|
|
16
|
+
from renderer import backend as _BACKEND
|
|
17
|
+
_BACKEND_AVAILABLE = True
|
|
18
|
+
except Exception:
|
|
19
|
+
_BACKEND_AVAILABLE = False
|
|
20
|
+
_BACKEND = None
|
|
21
|
+
|
|
22
|
+
class FunctionEvaluatorMixin:
|
|
23
|
+
"""Handles function application, method calls, and defines all builtins."""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
# Initialize registries
|
|
27
|
+
self.builtins = {}
|
|
28
|
+
|
|
29
|
+
# Renderer Registry (moved from global scope to instance scope)
|
|
30
|
+
self.render_registry = {
|
|
31
|
+
'screens': {},
|
|
32
|
+
'components': {},
|
|
33
|
+
'themes': {},
|
|
34
|
+
'canvases': {},
|
|
35
|
+
'current_theme': None
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Register all functions
|
|
39
|
+
self._register_core_builtins()
|
|
40
|
+
self._register_main_entry_point_builtins()
|
|
41
|
+
self._register_renderer_builtins()
|
|
42
|
+
|
|
43
|
+
def eval_call_expression(self, node, env, stack_trace):
|
|
44
|
+
debug_log("🚀 CallExpression node", f"Calling {node.function}")
|
|
45
|
+
|
|
46
|
+
fn = self.eval_node(node.function, env, stack_trace)
|
|
47
|
+
if is_error(fn):
|
|
48
|
+
return fn
|
|
49
|
+
|
|
50
|
+
# Check if this is a generic type instantiation: Box<number>(42)
|
|
51
|
+
type_args = getattr(node, 'type_args', [])
|
|
52
|
+
if type_args and hasattr(fn, 'is_generic') and fn.is_generic:
|
|
53
|
+
debug_log(" Generic type instantiation", f"Type args: {type_args}")
|
|
54
|
+
|
|
55
|
+
# Create specialized constructor for this type combination
|
|
56
|
+
template = fn.generic_template
|
|
57
|
+
specialized_constructor = self._create_specialized_generic_constructor(
|
|
58
|
+
template, type_args, env, stack_trace
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if is_error(specialized_constructor):
|
|
62
|
+
return specialized_constructor
|
|
63
|
+
|
|
64
|
+
# Now call the specialized constructor with the actual arguments
|
|
65
|
+
fn = specialized_constructor
|
|
66
|
+
|
|
67
|
+
# Check if arguments contain keyword arguments (AssignmentExpression nodes)
|
|
68
|
+
# This handles syntax like: Person(name="Bob", age=25)
|
|
69
|
+
from .. import zexus_ast
|
|
70
|
+
has_keyword_args = any(isinstance(arg, zexus_ast.AssignmentExpression) for arg in node.arguments)
|
|
71
|
+
|
|
72
|
+
if has_keyword_args:
|
|
73
|
+
# Process keyword arguments - build a dict of name->value
|
|
74
|
+
kwargs = {}
|
|
75
|
+
for arg in node.arguments:
|
|
76
|
+
if isinstance(arg, zexus_ast.AssignmentExpression):
|
|
77
|
+
# This is a keyword argument: name=value
|
|
78
|
+
param_name = arg.name.value if hasattr(arg.name, 'value') else str(arg.name)
|
|
79
|
+
param_value = self.eval_node(arg.value, env, stack_trace)
|
|
80
|
+
if is_error(param_value):
|
|
81
|
+
return param_value
|
|
82
|
+
kwargs[param_name] = param_value
|
|
83
|
+
else:
|
|
84
|
+
# Mixed positional and keyword args not supported yet
|
|
85
|
+
return EvaluationError("Cannot mix positional and keyword arguments")
|
|
86
|
+
|
|
87
|
+
# For entity constructors, pass kwargs as-is
|
|
88
|
+
from ..security import EntityDefinition as SecurityEntityDef
|
|
89
|
+
if isinstance(fn, SecurityEntityDef):
|
|
90
|
+
return fn.create_instance(kwargs)
|
|
91
|
+
|
|
92
|
+
# For other functions, would need to map kwargs to positional args
|
|
93
|
+
# Not implemented yet
|
|
94
|
+
return EvaluationError("Keyword arguments only supported for entity constructors currently")
|
|
95
|
+
|
|
96
|
+
# Regular positional arguments
|
|
97
|
+
args = self.eval_expressions(node.arguments, env)
|
|
98
|
+
if is_error(args):
|
|
99
|
+
return args
|
|
100
|
+
|
|
101
|
+
arg_count = len(args) if isinstance(args, (list, tuple)) else "unknown"
|
|
102
|
+
debug_log(" Arguments evaluated", f"{args} (count: {arg_count})")
|
|
103
|
+
|
|
104
|
+
# Contract instantiation check
|
|
105
|
+
from ..security import SmartContract
|
|
106
|
+
if isinstance(fn, SmartContract):
|
|
107
|
+
return fn.instantiate(args)
|
|
108
|
+
|
|
109
|
+
return self.apply_function(fn, args, env)
|
|
110
|
+
|
|
111
|
+
def _create_specialized_generic_constructor(self, template, type_args, env, stack_trace):
|
|
112
|
+
"""Create a specialized constructor for a generic type with concrete type arguments
|
|
113
|
+
|
|
114
|
+
Example: Box<number> creates a specialized Box constructor with T = number
|
|
115
|
+
"""
|
|
116
|
+
from ..object import EvaluationError, String, Map
|
|
117
|
+
from .. import zexus_ast
|
|
118
|
+
|
|
119
|
+
debug_log("_create_specialized_generic_constructor", f"Specializing with types: {type_args}")
|
|
120
|
+
|
|
121
|
+
type_params = template['type_params']
|
|
122
|
+
|
|
123
|
+
if len(type_args) != len(type_params):
|
|
124
|
+
return EvaluationError(
|
|
125
|
+
f"Generic type requires {len(type_params)} type argument(s), got {len(type_args)}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Create specialized type name
|
|
129
|
+
specialized_type_name = f"{template['type_name']}<{', '.join(type_args)}>"
|
|
130
|
+
|
|
131
|
+
# Check if we've already created this specialization (cache it)
|
|
132
|
+
existing = template['env'].get(specialized_type_name)
|
|
133
|
+
if existing and not is_error(existing):
|
|
134
|
+
debug_log(" Using cached specialization", specialized_type_name)
|
|
135
|
+
return existing
|
|
136
|
+
|
|
137
|
+
# Create type substitution map: T -> number, U -> string, etc.
|
|
138
|
+
type_subst = dict(zip(type_params, type_args))
|
|
139
|
+
debug_log(" Type substitution map", str(type_subst))
|
|
140
|
+
|
|
141
|
+
# Create a specialized version of the fields with type substitution
|
|
142
|
+
specialized_fields = []
|
|
143
|
+
for field in template['fields']:
|
|
144
|
+
# Create a copy of the field with substituted type
|
|
145
|
+
field_type = field.field_type
|
|
146
|
+
|
|
147
|
+
# If field type is a type parameter, substitute it
|
|
148
|
+
if field_type in type_subst:
|
|
149
|
+
field_type = type_subst[field_type]
|
|
150
|
+
debug_log(f" Substituted field type", f"{field.name}: {field_type}")
|
|
151
|
+
|
|
152
|
+
# Create new field with substituted type
|
|
153
|
+
specialized_field = zexus_ast.DataField(
|
|
154
|
+
name=field.name,
|
|
155
|
+
field_type=field_type,
|
|
156
|
+
default_value=field.default_value,
|
|
157
|
+
constraint=field.constraint,
|
|
158
|
+
computed=field.computed,
|
|
159
|
+
method_body=field.method_body,
|
|
160
|
+
method_params=field.method_params,
|
|
161
|
+
operator=field.operator,
|
|
162
|
+
decorators=field.decorators
|
|
163
|
+
)
|
|
164
|
+
specialized_fields.append(specialized_field)
|
|
165
|
+
|
|
166
|
+
# Create a specialized DataStatement node
|
|
167
|
+
specialized_node = zexus_ast.DataStatement(
|
|
168
|
+
name=zexus_ast.Identifier(specialized_type_name),
|
|
169
|
+
fields=specialized_fields,
|
|
170
|
+
modifiers=template['modifiers'],
|
|
171
|
+
parent=template['parent_type'],
|
|
172
|
+
decorators=template['decorators'],
|
|
173
|
+
type_params=[] # No longer generic after specialization
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Evaluate the specialized data statement to create the constructor
|
|
177
|
+
# This will register it in the environment
|
|
178
|
+
evaluator = template['evaluator']
|
|
179
|
+
result = evaluator.eval_data_statement(
|
|
180
|
+
specialized_node,
|
|
181
|
+
template['env'],
|
|
182
|
+
template['stack_trace']
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if is_error(result):
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
# Get the constructor from the environment (it was registered by eval_data_statement)
|
|
189
|
+
constructor = template['env'].get(specialized_type_name)
|
|
190
|
+
|
|
191
|
+
if constructor is None:
|
|
192
|
+
return EvaluationError(f"Failed to create specialized constructor for {specialized_type_name}")
|
|
193
|
+
|
|
194
|
+
return constructor
|
|
195
|
+
|
|
196
|
+
def apply_function(self, fn, args, env=None):
|
|
197
|
+
debug_log("apply_function", f"Calling {fn}")
|
|
198
|
+
|
|
199
|
+
# Phase 2 & 3: Trigger plugin hooks and check capabilities
|
|
200
|
+
if hasattr(self, 'integration_context'):
|
|
201
|
+
if isinstance(fn, (Action, LambdaFunction)):
|
|
202
|
+
func_name = fn.name if hasattr(fn, 'name') else str(fn)
|
|
203
|
+
# Trigger before-call hook
|
|
204
|
+
self.integration_context.plugins.before_action_call(func_name, {})
|
|
205
|
+
|
|
206
|
+
# Check required capabilities
|
|
207
|
+
try:
|
|
208
|
+
self.integration_context.capabilities.require_capability("core.language")
|
|
209
|
+
except PermissionError:
|
|
210
|
+
return EvaluationError(f"Permission denied: insufficient capabilities for {func_name}")
|
|
211
|
+
|
|
212
|
+
if isinstance(fn, (Action, LambdaFunction)):
|
|
213
|
+
debug_log(" Calling user-defined function")
|
|
214
|
+
|
|
215
|
+
# Check if this is an async action
|
|
216
|
+
is_async = getattr(fn, 'is_async', False)
|
|
217
|
+
|
|
218
|
+
if is_async:
|
|
219
|
+
# Create a coroutine that lazily executes the async action
|
|
220
|
+
from ..object import Coroutine
|
|
221
|
+
import sys
|
|
222
|
+
|
|
223
|
+
# print(f"[ASYNC CREATE] Creating coroutine for async action, fn.env has keys: {list(fn.env.store.keys()) if hasattr(fn.env, 'store') else 'N/A'}", file=sys.stderr)
|
|
224
|
+
|
|
225
|
+
def async_generator():
|
|
226
|
+
"""Generator that executes the async action body"""
|
|
227
|
+
new_env = Environment(outer=fn.env)
|
|
228
|
+
|
|
229
|
+
# Bind parameters
|
|
230
|
+
for i, param in enumerate(fn.parameters):
|
|
231
|
+
if i < len(args):
|
|
232
|
+
param_name = param.value if hasattr(param, 'value') else str(param)
|
|
233
|
+
new_env.set(param_name, args[i])
|
|
234
|
+
|
|
235
|
+
# Yield control first (makes it a true generator)
|
|
236
|
+
yield None
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
# Evaluate the function body
|
|
240
|
+
res = self.eval_node(fn.body, new_env)
|
|
241
|
+
|
|
242
|
+
# Unwrap ReturnValue if needed
|
|
243
|
+
if isinstance(res, ReturnValue):
|
|
244
|
+
result = res.value
|
|
245
|
+
else:
|
|
246
|
+
result = res
|
|
247
|
+
|
|
248
|
+
# Execute deferred cleanup
|
|
249
|
+
if hasattr(self, '_execute_deferred_cleanup'):
|
|
250
|
+
self._execute_deferred_cleanup(new_env, [])
|
|
251
|
+
|
|
252
|
+
# Return the result (will be caught by StopIteration)
|
|
253
|
+
return result
|
|
254
|
+
except Exception as e:
|
|
255
|
+
# Re-raise exception to be caught by coroutine
|
|
256
|
+
raise e
|
|
257
|
+
|
|
258
|
+
# Create and return coroutine
|
|
259
|
+
gen = async_generator()
|
|
260
|
+
coroutine = Coroutine(gen, fn)
|
|
261
|
+
return coroutine
|
|
262
|
+
|
|
263
|
+
# Synchronous function execution
|
|
264
|
+
new_env = Environment(outer=fn.env)
|
|
265
|
+
|
|
266
|
+
param_names = []
|
|
267
|
+
for i, param in enumerate(fn.parameters):
|
|
268
|
+
if i < len(args):
|
|
269
|
+
# Handle both Identifier objects and strings
|
|
270
|
+
param_name = param.value if hasattr(param, 'value') else str(param)
|
|
271
|
+
param_names.append(param_name)
|
|
272
|
+
new_env.set(param_name, args[i])
|
|
273
|
+
# Lightweight debug: show what is being bound
|
|
274
|
+
try:
|
|
275
|
+
debug_log(" Set parameter", f"{param_name} = {type(args[i]).__name__}")
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
if param_names:
|
|
281
|
+
debug_log(" Function parameters bound", f"{param_names}")
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
res = self.eval_node(fn.body, new_env)
|
|
287
|
+
res = _resolve_awaitable(res)
|
|
288
|
+
|
|
289
|
+
# Unwrap ReturnValue if needed
|
|
290
|
+
if isinstance(res, ReturnValue):
|
|
291
|
+
result = res.value
|
|
292
|
+
else:
|
|
293
|
+
result = res
|
|
294
|
+
|
|
295
|
+
return result
|
|
296
|
+
finally:
|
|
297
|
+
# CRITICAL: Execute deferred cleanup when function exits
|
|
298
|
+
# This happens in finally block to ensure cleanup runs even on errors
|
|
299
|
+
if hasattr(self, '_execute_deferred_cleanup'):
|
|
300
|
+
self._execute_deferred_cleanup(new_env, [])
|
|
301
|
+
|
|
302
|
+
# Phase 2: Trigger after-call hook
|
|
303
|
+
if hasattr(self, 'integration_context'):
|
|
304
|
+
func_name = fn.name if hasattr(fn, 'name') else str(fn)
|
|
305
|
+
self.integration_context.plugins.after_action_call(func_name, result)
|
|
306
|
+
|
|
307
|
+
elif isinstance(fn, Builtin):
|
|
308
|
+
debug_log(" Calling builtin function", f"{fn.name}")
|
|
309
|
+
# Sandbox enforcement: if current env is sandboxed, consult policy
|
|
310
|
+
try:
|
|
311
|
+
in_sandbox = False
|
|
312
|
+
policy_name = None
|
|
313
|
+
if env is not None:
|
|
314
|
+
try:
|
|
315
|
+
in_sandbox = bool(env.get('__in_sandbox__'))
|
|
316
|
+
policy_name = env.get('__sandbox_policy__')
|
|
317
|
+
except Exception:
|
|
318
|
+
in_sandbox = False
|
|
319
|
+
|
|
320
|
+
if in_sandbox:
|
|
321
|
+
from ..security import get_security_context
|
|
322
|
+
ctx = get_security_context()
|
|
323
|
+
policy = ctx.get_sandbox_policy(policy_name or 'default')
|
|
324
|
+
allowed = None if policy is None else policy.get('allowed_builtins')
|
|
325
|
+
# If allowed set exists and builtin not in it -> block
|
|
326
|
+
if allowed is not None and fn.name not in allowed:
|
|
327
|
+
return EvaluationError(f"Builtin '{fn.name}' not allowed inside sandbox policy '{policy_name or 'default'}'")
|
|
328
|
+
except Exception:
|
|
329
|
+
# If enforcement fails unexpectedly, proceed to call but log nothing
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
res = fn.fn(*args)
|
|
334
|
+
return _resolve_awaitable(res)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
return EvaluationError(f"Builtin error: {str(e)}")
|
|
337
|
+
|
|
338
|
+
elif isinstance(fn, EntityDefinition):
|
|
339
|
+
debug_log(" Creating entity instance (old format)")
|
|
340
|
+
# Entity constructor: Person("Alice", 30)
|
|
341
|
+
# Create instance with positional arguments mapped to properties
|
|
342
|
+
from ..object import EntityInstance, String, Integer
|
|
343
|
+
|
|
344
|
+
values = {}
|
|
345
|
+
# Map positional arguments to property names
|
|
346
|
+
if isinstance(fn.properties, dict):
|
|
347
|
+
prop_names = list(fn.properties.keys())
|
|
348
|
+
else:
|
|
349
|
+
prop_names = [prop['name'] for prop in fn.properties]
|
|
350
|
+
|
|
351
|
+
for i, arg in enumerate(args):
|
|
352
|
+
if i < len(prop_names):
|
|
353
|
+
values[prop_names[i]] = arg
|
|
354
|
+
|
|
355
|
+
return EntityInstance(fn, values)
|
|
356
|
+
|
|
357
|
+
# Handle SecurityEntityDefinition (from security.py with methods support)
|
|
358
|
+
from ..security import EntityDefinition as SecurityEntityDef, EntityInstance as SecurityEntityInstance
|
|
359
|
+
if isinstance(fn, SecurityEntityDef):
|
|
360
|
+
debug_log(" Creating entity instance (with methods)")
|
|
361
|
+
|
|
362
|
+
values = {}
|
|
363
|
+
# Map positional arguments to property names, INCLUDING INHERITED PROPERTIES
|
|
364
|
+
# Use get_all_properties() to get the full property list in correct order
|
|
365
|
+
if hasattr(fn, 'get_all_properties'):
|
|
366
|
+
# Get all properties (parent + child) in correct order
|
|
367
|
+
all_props = fn.get_all_properties()
|
|
368
|
+
prop_names = list(all_props.keys())
|
|
369
|
+
else:
|
|
370
|
+
# Fallback for old-style properties
|
|
371
|
+
prop_names = list(fn.properties.keys()) if isinstance(fn.properties, dict) else [prop['name'] for prop in fn.properties]
|
|
372
|
+
|
|
373
|
+
for i, arg in enumerate(args):
|
|
374
|
+
if i < len(prop_names):
|
|
375
|
+
values[prop_names[i]] = arg
|
|
376
|
+
|
|
377
|
+
debug_log(f" Entity instance created with {len(values)} properties: {list(values.keys())}")
|
|
378
|
+
# Use create_instance to handle dependency injection
|
|
379
|
+
return fn.create_instance(values)
|
|
380
|
+
|
|
381
|
+
return EvaluationError(f"Not a function: {fn}")
|
|
382
|
+
|
|
383
|
+
def eval_method_call_expression(self, node, env, stack_trace):
|
|
384
|
+
debug_log(" MethodCallExpression node", f"{node.object}.{node.method}")
|
|
385
|
+
|
|
386
|
+
obj = self.eval_node(node.object, env, stack_trace)
|
|
387
|
+
if is_error(obj):
|
|
388
|
+
return obj
|
|
389
|
+
|
|
390
|
+
method_name = node.method.value
|
|
391
|
+
|
|
392
|
+
# === Builtin Static Methods ===
|
|
393
|
+
# Handle static methods on dataclass constructors (e.g., User.default(), User.fromJSON())
|
|
394
|
+
if isinstance(obj, Builtin):
|
|
395
|
+
if hasattr(obj, 'static_methods') and method_name in obj.static_methods:
|
|
396
|
+
static_method = obj.static_methods[method_name]
|
|
397
|
+
args = self.eval_expressions(node.arguments, env)
|
|
398
|
+
if is_error(args):
|
|
399
|
+
return args
|
|
400
|
+
return self.apply_function(static_method, args, env)
|
|
401
|
+
# If no static method found, fall through to error
|
|
402
|
+
|
|
403
|
+
# === List Methods ===
|
|
404
|
+
if isinstance(obj, List):
|
|
405
|
+
# For map/filter/reduce, we need to evaluate arguments first
|
|
406
|
+
if method_name in ["map", "filter", "reduce"]:
|
|
407
|
+
args = self.eval_expressions(node.arguments, env)
|
|
408
|
+
if is_error(args):
|
|
409
|
+
return args
|
|
410
|
+
|
|
411
|
+
if method_name == "reduce":
|
|
412
|
+
if len(args) < 1:
|
|
413
|
+
return EvaluationError("reduce() requires at least a lambda function")
|
|
414
|
+
lambda_fn = args[0]
|
|
415
|
+
initial = args[1] if len(args) > 1 else None
|
|
416
|
+
return self._array_reduce(obj, lambda_fn, initial)
|
|
417
|
+
|
|
418
|
+
elif method_name == "map":
|
|
419
|
+
if len(args) != 1:
|
|
420
|
+
return EvaluationError("map() requires exactly one lambda function")
|
|
421
|
+
return self._array_map(obj, args[0])
|
|
422
|
+
|
|
423
|
+
elif method_name == "filter":
|
|
424
|
+
if len(args) != 1:
|
|
425
|
+
return EvaluationError("filter() requires exactly one lambda function")
|
|
426
|
+
return self._array_filter(obj, args[0])
|
|
427
|
+
|
|
428
|
+
# Other list methods
|
|
429
|
+
args = self.eval_expressions(node.arguments, env)
|
|
430
|
+
if is_error(args):
|
|
431
|
+
return args
|
|
432
|
+
|
|
433
|
+
if method_name == "push":
|
|
434
|
+
obj.elements.append(args[0])
|
|
435
|
+
return obj
|
|
436
|
+
elif method_name == "count":
|
|
437
|
+
return Integer(len(obj.elements))
|
|
438
|
+
elif method_name == "contains":
|
|
439
|
+
target = args[0]
|
|
440
|
+
found = any(elem.value == target.value for elem in obj.elements
|
|
441
|
+
if hasattr(elem, 'value') and hasattr(target, 'value'))
|
|
442
|
+
return TRUE if found else FALSE
|
|
443
|
+
|
|
444
|
+
# === Coroutine Methods ===
|
|
445
|
+
from ..object import Coroutine
|
|
446
|
+
if isinstance(obj, Coroutine):
|
|
447
|
+
if method_name == "inspect":
|
|
448
|
+
# Return string representation of coroutine state
|
|
449
|
+
return String(obj.inspect())
|
|
450
|
+
|
|
451
|
+
# === Map Methods ===
|
|
452
|
+
if isinstance(obj, Map):
|
|
453
|
+
# First check if the method is a callable stored in the Map (for DATA dataclasses)
|
|
454
|
+
method_key = String(method_name)
|
|
455
|
+
if method_key in obj.pairs:
|
|
456
|
+
method_value = obj.pairs[method_key]
|
|
457
|
+
if isinstance(method_value, Builtin):
|
|
458
|
+
# This is a dataclass method - evaluate args and call it
|
|
459
|
+
args = self.eval_expressions(node.arguments, env)
|
|
460
|
+
if is_error(args):
|
|
461
|
+
return args
|
|
462
|
+
return self.apply_function(method_value, args, env)
|
|
463
|
+
|
|
464
|
+
# Otherwise handle built-in Map methods
|
|
465
|
+
args = self.eval_expressions(node.arguments, env)
|
|
466
|
+
if is_error(args):
|
|
467
|
+
return args
|
|
468
|
+
|
|
469
|
+
if method_name == "has":
|
|
470
|
+
key = args[0].value if hasattr(args[0], 'value') else str(args[0])
|
|
471
|
+
return TRUE if key in obj.pairs else FALSE
|
|
472
|
+
elif method_name == "get":
|
|
473
|
+
key = args[0].value if hasattr(args[0], 'value') else str(args[0])
|
|
474
|
+
default = args[1] if len(args) > 1 else NULL
|
|
475
|
+
return obj.pairs.get(key, default)
|
|
476
|
+
|
|
477
|
+
# === Module Methods ===
|
|
478
|
+
from ..complexity_system import Module, Package
|
|
479
|
+
if isinstance(obj, Module):
|
|
480
|
+
debug_log(" MethodCallExpression", f"Calling method '{method_name}' on module '{obj.name}'")
|
|
481
|
+
debug_log(" MethodCallExpression", f"Module members: {list(obj.members.keys())}")
|
|
482
|
+
|
|
483
|
+
# For module methods, get the member and call it if it's a function
|
|
484
|
+
member_value = obj.get(method_name)
|
|
485
|
+
if member_value is None:
|
|
486
|
+
return EvaluationError(f"Method '{method_name}' not found in module '{obj.name}'")
|
|
487
|
+
|
|
488
|
+
debug_log(" MethodCallExpression", f"Found member value: {member_value}")
|
|
489
|
+
|
|
490
|
+
# Evaluate arguments
|
|
491
|
+
args = self.eval_expressions(node.arguments, env)
|
|
492
|
+
if is_error(args):
|
|
493
|
+
return args
|
|
494
|
+
|
|
495
|
+
# Call the function/action using apply_function
|
|
496
|
+
return self.apply_function(member_value, args, env)
|
|
497
|
+
|
|
498
|
+
# === Package Methods ===
|
|
499
|
+
if isinstance(obj, Package):
|
|
500
|
+
debug_log(" MethodCallExpression", f"Calling method '{method_name}' on package '{obj.name}'")
|
|
501
|
+
debug_log(" MethodCallExpression", f"Package modules: {list(obj.modules.keys())}")
|
|
502
|
+
|
|
503
|
+
# For package methods, get the module/function and call it
|
|
504
|
+
member_value = obj.get(method_name)
|
|
505
|
+
if member_value is None:
|
|
506
|
+
return EvaluationError(f"Method '{method_name}' not found in package '{obj.name}'")
|
|
507
|
+
|
|
508
|
+
debug_log(" MethodCallExpression", f"Found member value: {member_value}")
|
|
509
|
+
|
|
510
|
+
# Evaluate arguments
|
|
511
|
+
args = self.eval_expressions(node.arguments, env)
|
|
512
|
+
if is_error(args):
|
|
513
|
+
return args
|
|
514
|
+
|
|
515
|
+
# Call the function/action using apply_function
|
|
516
|
+
return self.apply_function(member_value, args, env)
|
|
517
|
+
|
|
518
|
+
# === Entity Instance Methods ===
|
|
519
|
+
from ..security import EntityInstance as SecurityEntityInstance
|
|
520
|
+
if isinstance(obj, SecurityEntityInstance):
|
|
521
|
+
args = self.eval_expressions(node.arguments, env)
|
|
522
|
+
if is_error(args):
|
|
523
|
+
return args
|
|
524
|
+
return obj.call_method(method_name, args)
|
|
525
|
+
|
|
526
|
+
# === Contract Instance Methods ===
|
|
527
|
+
if hasattr(obj, 'call_method'):
|
|
528
|
+
args = self.eval_expressions(node.arguments, env)
|
|
529
|
+
if is_error(args):
|
|
530
|
+
return args
|
|
531
|
+
return obj.call_method(method_name, args)
|
|
532
|
+
|
|
533
|
+
# === Embedded Code Methods ===
|
|
534
|
+
from ..object import EmbeddedCode
|
|
535
|
+
if isinstance(obj, EmbeddedCode):
|
|
536
|
+
print(f"[EMBED] Executing {obj.language}.{method_name}")
|
|
537
|
+
return Integer(42) # Placeholder
|
|
538
|
+
|
|
539
|
+
# === Environment (Module) Methods ===
|
|
540
|
+
# Support for module.function() syntax (e.g., crypto.keccak256())
|
|
541
|
+
from ..object import Environment
|
|
542
|
+
if isinstance(obj, Environment):
|
|
543
|
+
# Look up the method in the environment's store
|
|
544
|
+
method_value = obj.get(method_name)
|
|
545
|
+
if method_value is None or method_value == NULL:
|
|
546
|
+
return EvaluationError(f"Module has no method '{method_name}'")
|
|
547
|
+
|
|
548
|
+
# Evaluate arguments
|
|
549
|
+
args = self.eval_expressions(node.arguments, env)
|
|
550
|
+
if is_error(args):
|
|
551
|
+
return args
|
|
552
|
+
|
|
553
|
+
# Call the function using apply_function
|
|
554
|
+
return self.apply_function(method_value, args, env)
|
|
555
|
+
|
|
556
|
+
obj_type = obj.type() if hasattr(obj, 'type') and callable(obj.type) else type(obj).__name__
|
|
557
|
+
return EvaluationError(f"Method '{method_name}' not supported for {obj_type}")
|
|
558
|
+
|
|
559
|
+
# --- Array Helpers (Internal) ---
|
|
560
|
+
|
|
561
|
+
def _array_reduce(self, array_obj, lambda_fn, initial_value=None):
|
|
562
|
+
if not isinstance(array_obj, List):
|
|
563
|
+
return EvaluationError("reduce() called on non-array")
|
|
564
|
+
if not isinstance(lambda_fn, (LambdaFunction, Action)):
|
|
565
|
+
return EvaluationError("reduce() requires lambda")
|
|
566
|
+
|
|
567
|
+
accumulator = initial_value if initial_value is not None else (
|
|
568
|
+
array_obj.elements[0] if array_obj.elements else NULL
|
|
569
|
+
)
|
|
570
|
+
start_index = 0 if initial_value is not None else 1
|
|
571
|
+
|
|
572
|
+
for i in range(start_index, len(array_obj.elements)):
|
|
573
|
+
element = array_obj.elements[i]
|
|
574
|
+
result = self.apply_function(lambda_fn, [accumulator, element])
|
|
575
|
+
if is_error(result):
|
|
576
|
+
return result
|
|
577
|
+
accumulator = result
|
|
578
|
+
|
|
579
|
+
return accumulator
|
|
580
|
+
|
|
581
|
+
def _array_map(self, array_obj, lambda_fn):
|
|
582
|
+
if not isinstance(array_obj, List):
|
|
583
|
+
return EvaluationError("map() called on non-array")
|
|
584
|
+
if not isinstance(lambda_fn, (LambdaFunction, Action)):
|
|
585
|
+
return EvaluationError("map() requires lambda")
|
|
586
|
+
|
|
587
|
+
mapped = []
|
|
588
|
+
for element in array_obj.elements:
|
|
589
|
+
result = self.apply_function(lambda_fn, [element])
|
|
590
|
+
if is_error(result):
|
|
591
|
+
return result
|
|
592
|
+
mapped.append(result)
|
|
593
|
+
|
|
594
|
+
return List(mapped)
|
|
595
|
+
|
|
596
|
+
def _array_filter(self, array_obj, lambda_fn):
|
|
597
|
+
if not isinstance(array_obj, List):
|
|
598
|
+
return EvaluationError("filter() called on non-array")
|
|
599
|
+
if not isinstance(lambda_fn, (LambdaFunction, Action)):
|
|
600
|
+
return EvaluationError("filter() requires lambda")
|
|
601
|
+
|
|
602
|
+
filtered = []
|
|
603
|
+
for element in array_obj.elements:
|
|
604
|
+
result = self.apply_function(lambda_fn, [element])
|
|
605
|
+
if is_error(result):
|
|
606
|
+
return result
|
|
607
|
+
|
|
608
|
+
# Use is_truthy from utils
|
|
609
|
+
from .utils import is_truthy
|
|
610
|
+
if is_truthy(result):
|
|
611
|
+
filtered.append(element)
|
|
612
|
+
|
|
613
|
+
return List(filtered)
|
|
614
|
+
|
|
615
|
+
# --- BUILTIN IMPLEMENTATIONS ---
|
|
616
|
+
|
|
617
|
+
def _register_core_builtins(self):
|
|
618
|
+
# Date & Time
|
|
619
|
+
def _now(*a):
|
|
620
|
+
return DateTime.now()
|
|
621
|
+
|
|
622
|
+
def _timestamp(*a):
|
|
623
|
+
if len(a) == 0:
|
|
624
|
+
return DateTime.now().to_timestamp()
|
|
625
|
+
if len(a) == 1 and isinstance(a[0], DateTime):
|
|
626
|
+
return a[0].to_timestamp()
|
|
627
|
+
return EvaluationError("timestamp() takes 0 or 1 DateTime")
|
|
628
|
+
|
|
629
|
+
# Math
|
|
630
|
+
def _random(*a):
|
|
631
|
+
if len(a) == 0:
|
|
632
|
+
return Math.random_int(0, 100)
|
|
633
|
+
if len(a) == 1 and isinstance(a[0], Integer):
|
|
634
|
+
return Math.random_int(0, a[0].value)
|
|
635
|
+
if len(a) == 2 and all(isinstance(x, Integer) for x in a):
|
|
636
|
+
return Math.random_int(a[0].value, a[1].value)
|
|
637
|
+
return EvaluationError("random() takes 0, 1, or 2 integer arguments")
|
|
638
|
+
|
|
639
|
+
def _to_hex(*a):
|
|
640
|
+
if len(a) != 1:
|
|
641
|
+
return EvaluationError("to_hex() takes exactly 1 argument")
|
|
642
|
+
return Math.to_hex_string(a[0])
|
|
643
|
+
|
|
644
|
+
def _from_hex(*a):
|
|
645
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
646
|
+
return EvaluationError("from_hex() takes exactly 1 string argument")
|
|
647
|
+
return Math.hex_to_int(a[0])
|
|
648
|
+
|
|
649
|
+
def _sqrt(*a):
|
|
650
|
+
if len(a) != 1:
|
|
651
|
+
return EvaluationError("sqrt() takes exactly 1 argument")
|
|
652
|
+
return Math.sqrt(a[0])
|
|
653
|
+
|
|
654
|
+
# File I/O
|
|
655
|
+
def _read_text(*a):
|
|
656
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
657
|
+
return EvaluationError("file_read_text() takes exactly 1 string argument")
|
|
658
|
+
return File.read_text(a[0])
|
|
659
|
+
|
|
660
|
+
def _write_text(*a):
|
|
661
|
+
if len(a) != 2 or not all(isinstance(x, String) for x in a):
|
|
662
|
+
return EvaluationError("file_write_text() takes exactly 2 string arguments")
|
|
663
|
+
return File.write_text(a[0], a[1])
|
|
664
|
+
|
|
665
|
+
def _exists(*a):
|
|
666
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
667
|
+
return EvaluationError("file_exists() takes exactly 1 string argument")
|
|
668
|
+
return File.exists(a[0])
|
|
669
|
+
|
|
670
|
+
def _read_json(*a):
|
|
671
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
672
|
+
return EvaluationError("file_read_json() takes exactly 1 string argument")
|
|
673
|
+
return File.read_json(a[0])
|
|
674
|
+
|
|
675
|
+
def _write_json(*a):
|
|
676
|
+
if len(a) != 2 or not isinstance(a[0], String):
|
|
677
|
+
return EvaluationError("file_write_json() takes path string and data")
|
|
678
|
+
return File.write_json(a[0], a[1])
|
|
679
|
+
|
|
680
|
+
def _file_append(*a):
|
|
681
|
+
if len(a) != 2 or not all(isinstance(x, String) for x in a):
|
|
682
|
+
return EvaluationError("file_append() takes exactly 2 string arguments")
|
|
683
|
+
return File.append_text(a[0], a[1])
|
|
684
|
+
|
|
685
|
+
def _list_dir(*a):
|
|
686
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
687
|
+
return EvaluationError("file_list_dir() takes exactly 1 string argument")
|
|
688
|
+
return File.list_directory(a[0])
|
|
689
|
+
|
|
690
|
+
# Extended File System Operations
|
|
691
|
+
def _fs_is_file(*a):
|
|
692
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
693
|
+
return EvaluationError("fs_is_file() takes exactly 1 string argument")
|
|
694
|
+
import os
|
|
695
|
+
return BooleanObj(os.path.isfile(a[0].value))
|
|
696
|
+
|
|
697
|
+
def _fs_is_dir(*a):
|
|
698
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
699
|
+
return EvaluationError("fs_is_dir() takes exactly 1 string argument")
|
|
700
|
+
import os
|
|
701
|
+
return BooleanObj(os.path.isdir(a[0].value))
|
|
702
|
+
|
|
703
|
+
def _fs_mkdir(*a):
|
|
704
|
+
if len(a) < 1 or not isinstance(a[0], String):
|
|
705
|
+
return EvaluationError("fs_mkdir() takes path string and optional parents boolean")
|
|
706
|
+
from pathlib import Path
|
|
707
|
+
parents = True # Default to creating parent directories
|
|
708
|
+
if len(a) >= 2 and isinstance(a[1], BooleanObj):
|
|
709
|
+
parents = a[1].value
|
|
710
|
+
try:
|
|
711
|
+
Path(a[0].value).mkdir(parents=parents, exist_ok=True)
|
|
712
|
+
return BooleanObj(True)
|
|
713
|
+
except Exception as e:
|
|
714
|
+
return EvaluationError(f"fs_mkdir() error: {str(e)}")
|
|
715
|
+
|
|
716
|
+
def _fs_remove(*a):
|
|
717
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
718
|
+
return EvaluationError("fs_remove() takes exactly 1 string argument")
|
|
719
|
+
import os
|
|
720
|
+
try:
|
|
721
|
+
os.remove(a[0].value)
|
|
722
|
+
return BooleanObj(True)
|
|
723
|
+
except Exception as e:
|
|
724
|
+
return EvaluationError(f"fs_remove() error: {str(e)}")
|
|
725
|
+
|
|
726
|
+
def _fs_rmdir(*a):
|
|
727
|
+
if len(a) < 1 or not isinstance(a[0], String):
|
|
728
|
+
return EvaluationError("fs_rmdir() takes path string and optional recursive boolean")
|
|
729
|
+
import os
|
|
730
|
+
import shutil
|
|
731
|
+
recursive = False
|
|
732
|
+
if len(a) >= 2 and isinstance(a[1], BooleanObj):
|
|
733
|
+
recursive = a[1].value
|
|
734
|
+
try:
|
|
735
|
+
if recursive:
|
|
736
|
+
shutil.rmtree(a[0].value)
|
|
737
|
+
else:
|
|
738
|
+
os.rmdir(a[0].value)
|
|
739
|
+
return BooleanObj(True)
|
|
740
|
+
except Exception as e:
|
|
741
|
+
return EvaluationError(f"fs_rmdir() error: {str(e)}")
|
|
742
|
+
|
|
743
|
+
def _fs_rename(*a):
|
|
744
|
+
if len(a) != 2 or not all(isinstance(x, String) for x in a):
|
|
745
|
+
return EvaluationError("fs_rename() takes exactly 2 string arguments: old_path, new_path")
|
|
746
|
+
import os
|
|
747
|
+
try:
|
|
748
|
+
os.rename(a[0].value, a[1].value)
|
|
749
|
+
return BooleanObj(True)
|
|
750
|
+
except Exception as e:
|
|
751
|
+
return EvaluationError(f"fs_rename() error: {str(e)}")
|
|
752
|
+
|
|
753
|
+
def _fs_copy(*a):
|
|
754
|
+
if len(a) != 2 or not all(isinstance(x, String) for x in a):
|
|
755
|
+
return EvaluationError("fs_copy() takes exactly 2 string arguments: src, dst")
|
|
756
|
+
import shutil
|
|
757
|
+
import os
|
|
758
|
+
try:
|
|
759
|
+
src = a[0].value
|
|
760
|
+
dst = a[1].value
|
|
761
|
+
if os.path.isfile(src):
|
|
762
|
+
shutil.copy2(src, dst)
|
|
763
|
+
elif os.path.isdir(src):
|
|
764
|
+
shutil.copytree(src, dst)
|
|
765
|
+
else:
|
|
766
|
+
return EvaluationError(f"fs_copy() source does not exist: {src}")
|
|
767
|
+
return BooleanObj(True)
|
|
768
|
+
except Exception as e:
|
|
769
|
+
return EvaluationError(f"fs_copy() error: {str(e)}")
|
|
770
|
+
|
|
771
|
+
# Socket/TCP Primitives
|
|
772
|
+
def _socket_create_server(*a):
|
|
773
|
+
"""Create TCP server: socket_create_server(host, port, handler, backlog?)"""
|
|
774
|
+
if len(a) < 3:
|
|
775
|
+
return EvaluationError("socket_create_server() requires at least 3 arguments: host, port, handler")
|
|
776
|
+
|
|
777
|
+
if not isinstance(a[0], String):
|
|
778
|
+
return EvaluationError("socket_create_server() host must be a string")
|
|
779
|
+
if not isinstance(a[1], Integer):
|
|
780
|
+
return EvaluationError("socket_create_server() port must be an integer")
|
|
781
|
+
if not isinstance(a[2], Action):
|
|
782
|
+
return EvaluationError("socket_create_server() handler must be an action")
|
|
783
|
+
|
|
784
|
+
host = a[0].value
|
|
785
|
+
port = a[1].value
|
|
786
|
+
handler = a[2]
|
|
787
|
+
backlog = 5
|
|
788
|
+
|
|
789
|
+
if len(a) >= 4 and isinstance(a[3], Integer):
|
|
790
|
+
backlog = a[3].value
|
|
791
|
+
|
|
792
|
+
try:
|
|
793
|
+
from ..stdlib.sockets import SocketModule
|
|
794
|
+
|
|
795
|
+
# Wrap the Zexus handler
|
|
796
|
+
def python_handler(conn):
|
|
797
|
+
# Create Zexus builtin wrappers for connection methods
|
|
798
|
+
def _conn_send(*args):
|
|
799
|
+
if len(args) != 1:
|
|
800
|
+
return EvaluationError("send() takes 1 argument")
|
|
801
|
+
data = args[0].value if isinstance(args[0], String) else str(args[0])
|
|
802
|
+
try:
|
|
803
|
+
conn.send_string(data)
|
|
804
|
+
return NULL
|
|
805
|
+
except Exception as e:
|
|
806
|
+
return EvaluationError(f"send() error: {str(e)}")
|
|
807
|
+
|
|
808
|
+
def _conn_receive(*args):
|
|
809
|
+
size = 4096
|
|
810
|
+
if len(args) >= 1 and isinstance(args[0], Integer):
|
|
811
|
+
size = args[0].value
|
|
812
|
+
try:
|
|
813
|
+
data = conn.receive_string(size)
|
|
814
|
+
return String(data)
|
|
815
|
+
except Exception as e:
|
|
816
|
+
return EvaluationError(f"receive() error: {str(e)}")
|
|
817
|
+
|
|
818
|
+
def _conn_close(*args):
|
|
819
|
+
try:
|
|
820
|
+
conn.close()
|
|
821
|
+
return NULL
|
|
822
|
+
except Exception as e:
|
|
823
|
+
return EvaluationError(f"close() error: {str(e)}")
|
|
824
|
+
|
|
825
|
+
# Convert Python connection to Zexus Map
|
|
826
|
+
conn_obj = Map({
|
|
827
|
+
String("send"): Builtin(_conn_send, "send"),
|
|
828
|
+
String("receive"): Builtin(_conn_receive, "receive"),
|
|
829
|
+
String("close"): Builtin(_conn_close, "close"),
|
|
830
|
+
String("host"): String(conn.host),
|
|
831
|
+
String("port"): Integer(conn.port)
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
# Call Zexus handler
|
|
835
|
+
self.apply_function(handler, [conn_obj])
|
|
836
|
+
|
|
837
|
+
server = SocketModule.create_server(host, port, python_handler, backlog)
|
|
838
|
+
server.start()
|
|
839
|
+
|
|
840
|
+
# Create builtins for server methods
|
|
841
|
+
def _server_stop(*args):
|
|
842
|
+
try:
|
|
843
|
+
server.stop()
|
|
844
|
+
return NULL
|
|
845
|
+
except Exception as e:
|
|
846
|
+
return EvaluationError(f"stop() error: {str(e)}")
|
|
847
|
+
|
|
848
|
+
def _server_is_running(*args):
|
|
849
|
+
return BooleanObj(server.is_running())
|
|
850
|
+
|
|
851
|
+
# Return server object as Map
|
|
852
|
+
return Map({
|
|
853
|
+
String("stop"): Builtin(_server_stop, "stop"),
|
|
854
|
+
String("is_running"): Builtin(_server_is_running, "is_running"),
|
|
855
|
+
String("host"): String(server.host),
|
|
856
|
+
String("port"): Integer(server.port)
|
|
857
|
+
})
|
|
858
|
+
except Exception as e:
|
|
859
|
+
return EvaluationError(f"socket_create_server() error: {str(e)}")
|
|
860
|
+
|
|
861
|
+
def _socket_create_connection(*a):
|
|
862
|
+
"""Create TCP client: socket_create_connection(host, port, timeout?)"""
|
|
863
|
+
if len(a) < 2:
|
|
864
|
+
return EvaluationError("socket_create_connection() requires at least 2 arguments: host, port")
|
|
865
|
+
|
|
866
|
+
if not isinstance(a[0], String):
|
|
867
|
+
return EvaluationError("socket_create_connection() host must be a string")
|
|
868
|
+
if not isinstance(a[1], Integer):
|
|
869
|
+
return EvaluationError("socket_create_connection() port must be an integer")
|
|
870
|
+
|
|
871
|
+
host = a[0].value
|
|
872
|
+
port = a[1].value
|
|
873
|
+
timeout = 5.0
|
|
874
|
+
|
|
875
|
+
if len(a) >= 3 and isinstance(a[2], (Integer, Float)):
|
|
876
|
+
timeout = float(a[2].value)
|
|
877
|
+
|
|
878
|
+
try:
|
|
879
|
+
from ..stdlib.sockets import SocketModule
|
|
880
|
+
conn = SocketModule.create_connection(host, port, timeout)
|
|
881
|
+
|
|
882
|
+
# Create Zexus builtin wrappers for connection methods
|
|
883
|
+
def _conn_send(*args):
|
|
884
|
+
if len(args) != 1:
|
|
885
|
+
return EvaluationError("send() takes 1 argument")
|
|
886
|
+
data = args[0].value if isinstance(args[0], String) else str(args[0])
|
|
887
|
+
try:
|
|
888
|
+
conn.send_string(data)
|
|
889
|
+
return NULL
|
|
890
|
+
except Exception as e:
|
|
891
|
+
return EvaluationError(f"send() error: {str(e)}")
|
|
892
|
+
|
|
893
|
+
def _conn_receive(*args):
|
|
894
|
+
size = 4096
|
|
895
|
+
if len(args) >= 1 and isinstance(args[0], Integer):
|
|
896
|
+
size = args[0].value
|
|
897
|
+
try:
|
|
898
|
+
data = conn.receive_string(size)
|
|
899
|
+
return String(data)
|
|
900
|
+
except Exception as e:
|
|
901
|
+
return EvaluationError(f"receive() error: {str(e)}")
|
|
902
|
+
|
|
903
|
+
def _conn_close(*args):
|
|
904
|
+
try:
|
|
905
|
+
conn.close()
|
|
906
|
+
return NULL
|
|
907
|
+
except Exception as e:
|
|
908
|
+
return EvaluationError(f"close() error: {str(e)}")
|
|
909
|
+
|
|
910
|
+
def _conn_is_connected(*args):
|
|
911
|
+
return BooleanObj(conn.is_connected())
|
|
912
|
+
|
|
913
|
+
# Return connection object as Map with Builtin functions
|
|
914
|
+
return Map({
|
|
915
|
+
String("send"): Builtin(_conn_send, "send"),
|
|
916
|
+
String("receive"): Builtin(_conn_receive, "receive"),
|
|
917
|
+
String("close"): Builtin(_conn_close, "close"),
|
|
918
|
+
String("is_connected"): Builtin(_conn_is_connected, "is_connected"),
|
|
919
|
+
String("host"): String(conn.host),
|
|
920
|
+
String("port"): Integer(conn.port)
|
|
921
|
+
})
|
|
922
|
+
except Exception as e:
|
|
923
|
+
return EvaluationError(f"socket_create_connection() error: {str(e)}")
|
|
924
|
+
|
|
925
|
+
# HTTP Server
|
|
926
|
+
def _http_server(*a):
|
|
927
|
+
"""Create HTTP server: http_server(port, host?)"""
|
|
928
|
+
if len(a) < 1:
|
|
929
|
+
return EvaluationError("http_server() requires at least 1 argument: port")
|
|
930
|
+
|
|
931
|
+
if not isinstance(a[0], Integer):
|
|
932
|
+
return EvaluationError("http_server() port must be an integer")
|
|
933
|
+
|
|
934
|
+
port = a[0].value
|
|
935
|
+
host = "0.0.0.0"
|
|
936
|
+
|
|
937
|
+
if len(a) >= 2 and isinstance(a[1], String):
|
|
938
|
+
host = a[1].value
|
|
939
|
+
|
|
940
|
+
try:
|
|
941
|
+
from ..stdlib.http_server import HTTPServer
|
|
942
|
+
server = HTTPServer(host, port)
|
|
943
|
+
|
|
944
|
+
# Create builtins for server methods
|
|
945
|
+
def _server_get(*args):
|
|
946
|
+
if len(args) != 2 or not isinstance(args[0], String) or not isinstance(args[1], Action):
|
|
947
|
+
return EvaluationError("get() takes 2 arguments: path, handler")
|
|
948
|
+
|
|
949
|
+
path = args[0].value
|
|
950
|
+
handler = args[1]
|
|
951
|
+
|
|
952
|
+
# Wrap Zexus handler for Python
|
|
953
|
+
def python_handler(req, res):
|
|
954
|
+
# Convert request to Zexus Map
|
|
955
|
+
req_map = Map({
|
|
956
|
+
String("method"): String(req.method),
|
|
957
|
+
String("path"): String(req.path),
|
|
958
|
+
String("headers"): _python_to_zexus(req.headers),
|
|
959
|
+
String("body"): String(req.body),
|
|
960
|
+
String("query"): _python_to_zexus(req.query)
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
# Create response builtins
|
|
964
|
+
def _res_status(*a):
|
|
965
|
+
if len(a) != 1 or not isinstance(a[0], Integer):
|
|
966
|
+
return EvaluationError("status() takes 1 integer argument")
|
|
967
|
+
res.set_status(a[0].value)
|
|
968
|
+
return NULL
|
|
969
|
+
|
|
970
|
+
def _res_send(*a):
|
|
971
|
+
if len(a) != 1:
|
|
972
|
+
return EvaluationError("send() takes 1 argument")
|
|
973
|
+
data = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
974
|
+
res.send(data)
|
|
975
|
+
return NULL
|
|
976
|
+
|
|
977
|
+
def _res_json(*a):
|
|
978
|
+
if len(a) != 1:
|
|
979
|
+
return EvaluationError("json() takes 1 argument")
|
|
980
|
+
data = _zexus_to_python(a[0])
|
|
981
|
+
res.json(data)
|
|
982
|
+
return NULL
|
|
983
|
+
|
|
984
|
+
res_map = Map({
|
|
985
|
+
String("status"): Builtin(_res_status, "status"),
|
|
986
|
+
String("send"): Builtin(_res_send, "send"),
|
|
987
|
+
String("json"): Builtin(_res_json, "json")
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
# Call Zexus handler
|
|
991
|
+
self.apply_function(handler, [req_map, res_map])
|
|
992
|
+
|
|
993
|
+
server.get(path, python_handler)
|
|
994
|
+
return NULL
|
|
995
|
+
|
|
996
|
+
def _server_post(*args):
|
|
997
|
+
if len(args) != 2 or not isinstance(args[0], String) or not isinstance(args[1], Action):
|
|
998
|
+
return EvaluationError("post() takes 2 arguments: path, handler")
|
|
999
|
+
path = args[0].value
|
|
1000
|
+
handler = args[1]
|
|
1001
|
+
|
|
1002
|
+
def python_handler(req, res):
|
|
1003
|
+
req_map = Map({
|
|
1004
|
+
String("method"): String(req.method),
|
|
1005
|
+
String("path"): String(req.path),
|
|
1006
|
+
String("headers"): _python_to_zexus(req.headers),
|
|
1007
|
+
String("body"): String(req.body),
|
|
1008
|
+
String("query"): _python_to_zexus(req.query)
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
def _res_status(*a):
|
|
1012
|
+
if len(a) == 1 and isinstance(a[0], Integer):
|
|
1013
|
+
res.set_status(a[0].value)
|
|
1014
|
+
return NULL
|
|
1015
|
+
|
|
1016
|
+
def _res_send(*a):
|
|
1017
|
+
if len(a) == 1:
|
|
1018
|
+
data = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
1019
|
+
res.send(data)
|
|
1020
|
+
return NULL
|
|
1021
|
+
|
|
1022
|
+
def _res_json(*a):
|
|
1023
|
+
if len(a) == 1:
|
|
1024
|
+
data = _zexus_to_python(a[0])
|
|
1025
|
+
res.json(data)
|
|
1026
|
+
return NULL
|
|
1027
|
+
|
|
1028
|
+
res_map = Map({
|
|
1029
|
+
String("status"): Builtin(_res_status, "status"),
|
|
1030
|
+
String("send"): Builtin(_res_send, "send"),
|
|
1031
|
+
String("json"): Builtin(_res_json, "json")
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
self.apply_function(handler, [req_map, res_map])
|
|
1035
|
+
|
|
1036
|
+
server.post(path, python_handler)
|
|
1037
|
+
return NULL
|
|
1038
|
+
|
|
1039
|
+
def _server_listen(*args):
|
|
1040
|
+
try:
|
|
1041
|
+
# Start server in background thread
|
|
1042
|
+
import threading
|
|
1043
|
+
thread = threading.Thread(target=server.listen, daemon=True)
|
|
1044
|
+
thread.start()
|
|
1045
|
+
return NULL
|
|
1046
|
+
except Exception as e:
|
|
1047
|
+
return EvaluationError(f"listen() error: {str(e)}")
|
|
1048
|
+
|
|
1049
|
+
def _server_stop(*args):
|
|
1050
|
+
try:
|
|
1051
|
+
server.stop()
|
|
1052
|
+
return NULL
|
|
1053
|
+
except Exception as e:
|
|
1054
|
+
return EvaluationError(f"stop() error: {str(e)}")
|
|
1055
|
+
|
|
1056
|
+
# Return server object as Map
|
|
1057
|
+
return Map({
|
|
1058
|
+
String("get"): Builtin(_server_get, "get"),
|
|
1059
|
+
String("post"): Builtin(_server_post, "post"),
|
|
1060
|
+
String("listen"): Builtin(_server_listen, "listen"),
|
|
1061
|
+
String("stop"): Builtin(_server_stop, "stop"),
|
|
1062
|
+
String("host"): String(host),
|
|
1063
|
+
String("port"): Integer(port)
|
|
1064
|
+
})
|
|
1065
|
+
except Exception as e:
|
|
1066
|
+
return EvaluationError(f"http_server() error: {str(e)}")
|
|
1067
|
+
|
|
1068
|
+
# Database - SQLite
|
|
1069
|
+
def _sqlite_connect(*a):
|
|
1070
|
+
"""Connect to SQLite database: sqlite_connect(database_path)"""
|
|
1071
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
1072
|
+
return EvaluationError("sqlite_connect() takes 1 string argument: database path")
|
|
1073
|
+
|
|
1074
|
+
try:
|
|
1075
|
+
from ..stdlib.db_sqlite import SQLiteConnection
|
|
1076
|
+
db = SQLiteConnection(a[0].value)
|
|
1077
|
+
|
|
1078
|
+
if not db.connect():
|
|
1079
|
+
return EvaluationError("Failed to connect to SQLite database")
|
|
1080
|
+
|
|
1081
|
+
# Store db in a list to keep it alive (prevent garbage collection)
|
|
1082
|
+
db_ref = [db]
|
|
1083
|
+
|
|
1084
|
+
# Create database methods as Zexus builtins
|
|
1085
|
+
def _db_execute(*args):
|
|
1086
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1087
|
+
return EvaluationError("execute() takes query string and optional params")
|
|
1088
|
+
|
|
1089
|
+
query = args[0].value
|
|
1090
|
+
params = None
|
|
1091
|
+
|
|
1092
|
+
if len(args) >= 2:
|
|
1093
|
+
# Convert Zexus List to Python tuple for params
|
|
1094
|
+
if isinstance(args[1], List):
|
|
1095
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1096
|
+
else:
|
|
1097
|
+
params = (_zexus_to_python(args[1]),)
|
|
1098
|
+
|
|
1099
|
+
result = db_ref[0].execute(query, params)
|
|
1100
|
+
return BooleanObj(result)
|
|
1101
|
+
|
|
1102
|
+
def _db_query(*args):
|
|
1103
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1104
|
+
return EvaluationError("query() takes query string and optional params")
|
|
1105
|
+
|
|
1106
|
+
query = args[0].value
|
|
1107
|
+
params = None
|
|
1108
|
+
|
|
1109
|
+
if len(args) >= 2:
|
|
1110
|
+
if isinstance(args[1], List):
|
|
1111
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1112
|
+
else:
|
|
1113
|
+
params = (_zexus_to_python(args[1]),)
|
|
1114
|
+
|
|
1115
|
+
results = db_ref[0].query(query, params)
|
|
1116
|
+
# Convert Python list of dicts to Zexus List of Maps
|
|
1117
|
+
return _python_to_zexus(results)
|
|
1118
|
+
|
|
1119
|
+
def _db_query_one(*args):
|
|
1120
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1121
|
+
return EvaluationError("query_one() takes query string and optional params")
|
|
1122
|
+
|
|
1123
|
+
query = args[0].value
|
|
1124
|
+
params = None
|
|
1125
|
+
|
|
1126
|
+
if len(args) >= 2:
|
|
1127
|
+
if isinstance(args[1], List):
|
|
1128
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1129
|
+
else:
|
|
1130
|
+
params = (_zexus_to_python(args[1]),)
|
|
1131
|
+
|
|
1132
|
+
result = db_ref[0].query_one(query, params)
|
|
1133
|
+
return _python_to_zexus(result) if result else NULL
|
|
1134
|
+
|
|
1135
|
+
def _db_last_insert_id(*args):
|
|
1136
|
+
return Integer(db_ref[0].last_insert_id())
|
|
1137
|
+
|
|
1138
|
+
def _db_affected_rows(*args):
|
|
1139
|
+
return Integer(db_ref[0].affected_rows())
|
|
1140
|
+
|
|
1141
|
+
def _db_begin(*args):
|
|
1142
|
+
return BooleanObj(db_ref[0].begin_transaction())
|
|
1143
|
+
|
|
1144
|
+
def _db_commit(*args):
|
|
1145
|
+
return BooleanObj(db_ref[0].commit())
|
|
1146
|
+
|
|
1147
|
+
def _db_rollback(*args):
|
|
1148
|
+
return BooleanObj(db_ref[0].rollback())
|
|
1149
|
+
|
|
1150
|
+
def _db_close(*args):
|
|
1151
|
+
return BooleanObj(db_ref[0].close())
|
|
1152
|
+
|
|
1153
|
+
# Return database connection as Map
|
|
1154
|
+
return Map({
|
|
1155
|
+
String("execute"): Builtin(_db_execute, "execute"),
|
|
1156
|
+
String("query"): Builtin(_db_query, "query"),
|
|
1157
|
+
String("query_one"): Builtin(_db_query_one, "query_one"),
|
|
1158
|
+
String("last_insert_id"): Builtin(_db_last_insert_id, "last_insert_id"),
|
|
1159
|
+
String("affected_rows"): Builtin(_db_affected_rows, "affected_rows"),
|
|
1160
|
+
String("begin"): Builtin(_db_begin, "begin"),
|
|
1161
|
+
String("commit"): Builtin(_db_commit, "commit"),
|
|
1162
|
+
String("rollback"): Builtin(_db_rollback, "rollback"),
|
|
1163
|
+
String("close"): Builtin(_db_close, "close"),
|
|
1164
|
+
String("database"): String(a[0].value)
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
return EvaluationError(f"sqlite_connect() error: {str(e)}")
|
|
1169
|
+
|
|
1170
|
+
# Database - PostgreSQL
|
|
1171
|
+
def _postgres_connect(*a):
|
|
1172
|
+
"""Connect to PostgreSQL: postgres_connect(host, port, database, user, password)"""
|
|
1173
|
+
if len(a) < 1:
|
|
1174
|
+
return EvaluationError("postgres_connect() requires at least database name")
|
|
1175
|
+
|
|
1176
|
+
# Parse parameters
|
|
1177
|
+
host = "localhost"
|
|
1178
|
+
port = 5432
|
|
1179
|
+
database = a[0].value if isinstance(a[0], String) else "postgres"
|
|
1180
|
+
user = "postgres"
|
|
1181
|
+
password = ""
|
|
1182
|
+
|
|
1183
|
+
if len(a) >= 2 and isinstance(a[1], String):
|
|
1184
|
+
user = a[1].value
|
|
1185
|
+
if len(a) >= 3 and isinstance(a[2], String):
|
|
1186
|
+
password = a[2].value
|
|
1187
|
+
if len(a) >= 4 and isinstance(a[3], String):
|
|
1188
|
+
host = a[3].value
|
|
1189
|
+
if len(a) >= 5 and isinstance(a[4], Integer):
|
|
1190
|
+
port = a[4].value
|
|
1191
|
+
|
|
1192
|
+
try:
|
|
1193
|
+
from ..stdlib.db_postgres import PostgreSQLConnection
|
|
1194
|
+
db = PostgreSQLConnection(host, port, database, user, password)
|
|
1195
|
+
|
|
1196
|
+
if not db.connect():
|
|
1197
|
+
return EvaluationError("Failed to connect to PostgreSQL database")
|
|
1198
|
+
|
|
1199
|
+
db_ref = [db]
|
|
1200
|
+
|
|
1201
|
+
# Same methods as SQLite
|
|
1202
|
+
def _db_execute(*args):
|
|
1203
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1204
|
+
return EvaluationError("execute() takes query string and optional params")
|
|
1205
|
+
query = args[0].value
|
|
1206
|
+
params = None
|
|
1207
|
+
if len(args) >= 2:
|
|
1208
|
+
if isinstance(args[1], List):
|
|
1209
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1210
|
+
else:
|
|
1211
|
+
params = (_zexus_to_python(args[1]),)
|
|
1212
|
+
result = db_ref[0].execute(query, params)
|
|
1213
|
+
return BooleanObj(result)
|
|
1214
|
+
|
|
1215
|
+
def _db_query(*args):
|
|
1216
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1217
|
+
return EvaluationError("query() takes query string and optional params")
|
|
1218
|
+
query = args[0].value
|
|
1219
|
+
params = None
|
|
1220
|
+
if len(args) >= 2:
|
|
1221
|
+
if isinstance(args[1], List):
|
|
1222
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1223
|
+
else:
|
|
1224
|
+
params = (_zexus_to_python(args[1]),)
|
|
1225
|
+
results = db_ref[0].query(query, params)
|
|
1226
|
+
return _python_to_zexus(results)
|
|
1227
|
+
|
|
1228
|
+
def _db_query_one(*args):
|
|
1229
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1230
|
+
return EvaluationError("query_one() takes query string and optional params")
|
|
1231
|
+
query = args[0].value
|
|
1232
|
+
params = None
|
|
1233
|
+
if len(args) >= 2:
|
|
1234
|
+
if isinstance(args[1], List):
|
|
1235
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1236
|
+
else:
|
|
1237
|
+
params = (_zexus_to_python(args[1]),)
|
|
1238
|
+
result = db_ref[0].query_one(query, params)
|
|
1239
|
+
return _python_to_zexus(result) if result else NULL
|
|
1240
|
+
|
|
1241
|
+
def _db_last_insert_id(*args):
|
|
1242
|
+
return Integer(db_ref[0].last_insert_id())
|
|
1243
|
+
|
|
1244
|
+
def _db_affected_rows(*args):
|
|
1245
|
+
return Integer(db_ref[0].affected_rows())
|
|
1246
|
+
|
|
1247
|
+
def _db_begin(*args):
|
|
1248
|
+
return BooleanObj(db_ref[0].begin_transaction())
|
|
1249
|
+
|
|
1250
|
+
def _db_commit(*args):
|
|
1251
|
+
return BooleanObj(db_ref[0].commit())
|
|
1252
|
+
|
|
1253
|
+
def _db_rollback(*args):
|
|
1254
|
+
return BooleanObj(db_ref[0].rollback())
|
|
1255
|
+
|
|
1256
|
+
def _db_close(*args):
|
|
1257
|
+
return BooleanObj(db_ref[0].close())
|
|
1258
|
+
|
|
1259
|
+
return Map({
|
|
1260
|
+
String("execute"): Builtin(_db_execute, "execute"),
|
|
1261
|
+
String("query"): Builtin(_db_query, "query"),
|
|
1262
|
+
String("query_one"): Builtin(_db_query_one, "query_one"),
|
|
1263
|
+
String("last_insert_id"): Builtin(_db_last_insert_id, "last_insert_id"),
|
|
1264
|
+
String("affected_rows"): Builtin(_db_affected_rows, "affected_rows"),
|
|
1265
|
+
String("begin"): Builtin(_db_begin, "begin"),
|
|
1266
|
+
String("commit"): Builtin(_db_commit, "commit"),
|
|
1267
|
+
String("rollback"): Builtin(_db_rollback, "rollback"),
|
|
1268
|
+
String("close"): Builtin(_db_close, "close"),
|
|
1269
|
+
String("database"): String(database),
|
|
1270
|
+
String("type"): String("postgresql")
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
except Exception as e:
|
|
1274
|
+
return EvaluationError(f"postgres_connect() error: {str(e)}")
|
|
1275
|
+
|
|
1276
|
+
# Database - MySQL
|
|
1277
|
+
def _mysql_connect(*a):
|
|
1278
|
+
"""Connect to MySQL: mysql_connect(database, user, password, host?, port?)"""
|
|
1279
|
+
if len(a) < 1:
|
|
1280
|
+
return EvaluationError("mysql_connect() requires at least database name")
|
|
1281
|
+
|
|
1282
|
+
host = "localhost"
|
|
1283
|
+
port = 3306
|
|
1284
|
+
database = a[0].value if isinstance(a[0], String) else "mysql"
|
|
1285
|
+
user = "root"
|
|
1286
|
+
password = ""
|
|
1287
|
+
|
|
1288
|
+
if len(a) >= 2 and isinstance(a[1], String):
|
|
1289
|
+
user = a[1].value
|
|
1290
|
+
if len(a) >= 3 and isinstance(a[2], String):
|
|
1291
|
+
password = a[2].value
|
|
1292
|
+
if len(a) >= 4 and isinstance(a[3], String):
|
|
1293
|
+
host = a[3].value
|
|
1294
|
+
if len(a) >= 5 and isinstance(a[4], Integer):
|
|
1295
|
+
port = a[4].value
|
|
1296
|
+
|
|
1297
|
+
try:
|
|
1298
|
+
from ..stdlib.db_mysql import MySQLConnection
|
|
1299
|
+
db = MySQLConnection(host, port, database, user, password)
|
|
1300
|
+
|
|
1301
|
+
if not db.connect():
|
|
1302
|
+
return EvaluationError("Failed to connect to MySQL database")
|
|
1303
|
+
|
|
1304
|
+
db_ref = [db]
|
|
1305
|
+
|
|
1306
|
+
# Same interface as SQLite/PostgreSQL
|
|
1307
|
+
def _db_execute(*args):
|
|
1308
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1309
|
+
return EvaluationError("execute() takes query string and optional params")
|
|
1310
|
+
query = args[0].value
|
|
1311
|
+
params = None
|
|
1312
|
+
if len(args) >= 2:
|
|
1313
|
+
if isinstance(args[1], List):
|
|
1314
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1315
|
+
else:
|
|
1316
|
+
params = (_zexus_to_python(args[1]),)
|
|
1317
|
+
result = db_ref[0].execute(query, params)
|
|
1318
|
+
return BooleanObj(result)
|
|
1319
|
+
|
|
1320
|
+
def _db_query(*args):
|
|
1321
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1322
|
+
return EvaluationError("query() takes query string and optional params")
|
|
1323
|
+
query = args[0].value
|
|
1324
|
+
params = None
|
|
1325
|
+
if len(args) >= 2:
|
|
1326
|
+
if isinstance(args[1], List):
|
|
1327
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1328
|
+
else:
|
|
1329
|
+
params = (_zexus_to_python(args[1]),)
|
|
1330
|
+
results = db_ref[0].query(query, params)
|
|
1331
|
+
return _python_to_zexus(results)
|
|
1332
|
+
|
|
1333
|
+
def _db_query_one(*args):
|
|
1334
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1335
|
+
return EvaluationError("query_one() takes query string and optional params")
|
|
1336
|
+
query = args[0].value
|
|
1337
|
+
params = None
|
|
1338
|
+
if len(args) >= 2:
|
|
1339
|
+
if isinstance(args[1], List):
|
|
1340
|
+
params = tuple(_zexus_to_python(args[1]))
|
|
1341
|
+
else:
|
|
1342
|
+
params = (_zexus_to_python(args[1]),)
|
|
1343
|
+
result = db_ref[0].query_one(query, params)
|
|
1344
|
+
return _python_to_zexus(result) if result else NULL
|
|
1345
|
+
|
|
1346
|
+
def _db_last_insert_id(*args):
|
|
1347
|
+
return Integer(db_ref[0].last_insert_id())
|
|
1348
|
+
|
|
1349
|
+
def _db_affected_rows(*args):
|
|
1350
|
+
return Integer(db_ref[0].affected_rows())
|
|
1351
|
+
|
|
1352
|
+
def _db_begin(*args):
|
|
1353
|
+
return BooleanObj(db_ref[0].begin_transaction())
|
|
1354
|
+
|
|
1355
|
+
def _db_commit(*args):
|
|
1356
|
+
return BooleanObj(db_ref[0].commit())
|
|
1357
|
+
|
|
1358
|
+
def _db_rollback(*args):
|
|
1359
|
+
return BooleanObj(db_ref[0].rollback())
|
|
1360
|
+
|
|
1361
|
+
def _db_close(*args):
|
|
1362
|
+
return BooleanObj(db_ref[0].close())
|
|
1363
|
+
|
|
1364
|
+
return Map({
|
|
1365
|
+
String("execute"): Builtin(_db_execute, "execute"),
|
|
1366
|
+
String("query"): Builtin(_db_query, "query"),
|
|
1367
|
+
String("query_one"): Builtin(_db_query_one, "query_one"),
|
|
1368
|
+
String("last_insert_id"): Builtin(_db_last_insert_id, "last_insert_id"),
|
|
1369
|
+
String("affected_rows"): Builtin(_db_affected_rows, "affected_rows"),
|
|
1370
|
+
String("begin"): Builtin(_db_begin, "begin"),
|
|
1371
|
+
String("commit"): Builtin(_db_commit, "commit"),
|
|
1372
|
+
String("rollback"): Builtin(_db_rollback, "rollback"),
|
|
1373
|
+
String("close"): Builtin(_db_close, "close"),
|
|
1374
|
+
String("database"): String(database),
|
|
1375
|
+
String("type"): String("mysql")
|
|
1376
|
+
})
|
|
1377
|
+
|
|
1378
|
+
except Exception as e:
|
|
1379
|
+
return EvaluationError(f"mysql_connect() error: {str(e)}")
|
|
1380
|
+
|
|
1381
|
+
# Database - MongoDB
|
|
1382
|
+
def _mongo_connect(*a):
|
|
1383
|
+
"""Connect to MongoDB: mongo_connect(database, host?, port?, username?, password?)"""
|
|
1384
|
+
if len(a) < 1:
|
|
1385
|
+
return EvaluationError("mongo_connect() requires at least database name")
|
|
1386
|
+
|
|
1387
|
+
database = a[0].value if isinstance(a[0], String) else "test"
|
|
1388
|
+
host = "localhost"
|
|
1389
|
+
port = 27017
|
|
1390
|
+
username = None
|
|
1391
|
+
password = None
|
|
1392
|
+
|
|
1393
|
+
if len(a) >= 2 and isinstance(a[1], String):
|
|
1394
|
+
host = a[1].value
|
|
1395
|
+
if len(a) >= 3 and isinstance(a[2], Integer):
|
|
1396
|
+
port = a[2].value
|
|
1397
|
+
if len(a) >= 4 and isinstance(a[3], String):
|
|
1398
|
+
username = a[3].value
|
|
1399
|
+
if len(a) >= 5 and isinstance(a[4], String):
|
|
1400
|
+
password = a[4].value
|
|
1401
|
+
|
|
1402
|
+
try:
|
|
1403
|
+
from ..stdlib.db_mongo import MongoDBConnection
|
|
1404
|
+
db = MongoDBConnection(host, port, database, username, password)
|
|
1405
|
+
|
|
1406
|
+
if not db.connect():
|
|
1407
|
+
return EvaluationError("Failed to connect to MongoDB database")
|
|
1408
|
+
|
|
1409
|
+
db_ref = [db]
|
|
1410
|
+
|
|
1411
|
+
# MongoDB-specific operations
|
|
1412
|
+
def _db_insert_one(*args):
|
|
1413
|
+
if len(args) < 2 or not isinstance(args[0], String) or not isinstance(args[1], Map):
|
|
1414
|
+
return EvaluationError("insert_one() takes collection name and document")
|
|
1415
|
+
collection = args[0].value
|
|
1416
|
+
document = _zexus_to_python(args[1])
|
|
1417
|
+
result = db_ref[0].insert_one(collection, document)
|
|
1418
|
+
return String(result) if result else NULL
|
|
1419
|
+
|
|
1420
|
+
def _db_insert_many(*args):
|
|
1421
|
+
if len(args) < 2 or not isinstance(args[0], String) or not isinstance(args[1], List):
|
|
1422
|
+
return EvaluationError("insert_many() takes collection name and list of documents")
|
|
1423
|
+
collection = args[0].value
|
|
1424
|
+
documents = _zexus_to_python(args[1])
|
|
1425
|
+
result = db_ref[0].insert_many(collection, documents)
|
|
1426
|
+
return _python_to_zexus(result) if result else NULL
|
|
1427
|
+
|
|
1428
|
+
def _db_find(*args):
|
|
1429
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1430
|
+
return EvaluationError("find() takes collection name and optional query")
|
|
1431
|
+
collection = args[0].value
|
|
1432
|
+
query = None
|
|
1433
|
+
if len(args) >= 2 and isinstance(args[1], Map):
|
|
1434
|
+
query = _zexus_to_python(args[1])
|
|
1435
|
+
results = db_ref[0].find(collection, query)
|
|
1436
|
+
return _python_to_zexus(results)
|
|
1437
|
+
|
|
1438
|
+
def _db_find_one(*args):
|
|
1439
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1440
|
+
return EvaluationError("find_one() takes collection name and optional query")
|
|
1441
|
+
collection = args[0].value
|
|
1442
|
+
query = None
|
|
1443
|
+
if len(args) >= 2 and isinstance(args[1], Map):
|
|
1444
|
+
query = _zexus_to_python(args[1])
|
|
1445
|
+
result = db_ref[0].find_one(collection, query)
|
|
1446
|
+
return _python_to_zexus(result) if result else NULL
|
|
1447
|
+
|
|
1448
|
+
def _db_update_one(*args):
|
|
1449
|
+
if len(args) < 3 or not isinstance(args[0], String) or not isinstance(args[1], Map) or not isinstance(args[2], Map):
|
|
1450
|
+
return EvaluationError("update_one() takes collection, query, and update")
|
|
1451
|
+
collection = args[0].value
|
|
1452
|
+
query = _zexus_to_python(args[1])
|
|
1453
|
+
update = _zexus_to_python(args[2])
|
|
1454
|
+
result = db_ref[0].update_one(collection, query, update)
|
|
1455
|
+
return Integer(result)
|
|
1456
|
+
|
|
1457
|
+
def _db_update_many(*args):
|
|
1458
|
+
if len(args) < 3 or not isinstance(args[0], String) or not isinstance(args[1], Map) or not isinstance(args[2], Map):
|
|
1459
|
+
return EvaluationError("update_many() takes collection, query, and update")
|
|
1460
|
+
collection = args[0].value
|
|
1461
|
+
query = _zexus_to_python(args[1])
|
|
1462
|
+
update = _zexus_to_python(args[2])
|
|
1463
|
+
result = db_ref[0].update_many(collection, query, update)
|
|
1464
|
+
return Integer(result)
|
|
1465
|
+
|
|
1466
|
+
def _db_delete_one(*args):
|
|
1467
|
+
if len(args) < 2 or not isinstance(args[0], String) or not isinstance(args[1], Map):
|
|
1468
|
+
return EvaluationError("delete_one() takes collection and query")
|
|
1469
|
+
collection = args[0].value
|
|
1470
|
+
query = _zexus_to_python(args[1])
|
|
1471
|
+
result = db_ref[0].delete_one(collection, query)
|
|
1472
|
+
return Integer(result)
|
|
1473
|
+
|
|
1474
|
+
def _db_delete_many(*args):
|
|
1475
|
+
if len(args) < 2 or not isinstance(args[0], String) or not isinstance(args[1], Map):
|
|
1476
|
+
return EvaluationError("delete_many() takes collection and query")
|
|
1477
|
+
collection = args[0].value
|
|
1478
|
+
query = _zexus_to_python(args[1])
|
|
1479
|
+
result = db_ref[0].delete_many(collection, query)
|
|
1480
|
+
return Integer(result)
|
|
1481
|
+
|
|
1482
|
+
def _db_count(*args):
|
|
1483
|
+
if len(args) < 1 or not isinstance(args[0], String):
|
|
1484
|
+
return EvaluationError("count() takes collection name and optional query")
|
|
1485
|
+
collection = args[0].value
|
|
1486
|
+
query = None
|
|
1487
|
+
if len(args) >= 2 and isinstance(args[1], Map):
|
|
1488
|
+
query = _zexus_to_python(args[1])
|
|
1489
|
+
result = db_ref[0].count(collection, query)
|
|
1490
|
+
return Integer(result)
|
|
1491
|
+
|
|
1492
|
+
def _db_close(*args):
|
|
1493
|
+
return BooleanObj(db_ref[0].close())
|
|
1494
|
+
|
|
1495
|
+
return Map({
|
|
1496
|
+
String("insert_one"): Builtin(_db_insert_one, "insert_one"),
|
|
1497
|
+
String("insert_many"): Builtin(_db_insert_many, "insert_many"),
|
|
1498
|
+
String("find"): Builtin(_db_find, "find"),
|
|
1499
|
+
String("find_one"): Builtin(_db_find_one, "find_one"),
|
|
1500
|
+
String("update_one"): Builtin(_db_update_one, "update_one"),
|
|
1501
|
+
String("update_many"): Builtin(_db_update_many, "update_many"),
|
|
1502
|
+
String("delete_one"): Builtin(_db_delete_one, "delete_one"),
|
|
1503
|
+
String("delete_many"): Builtin(_db_delete_many, "delete_many"),
|
|
1504
|
+
String("count"): Builtin(_db_count, "count"),
|
|
1505
|
+
String("close"): Builtin(_db_close, "close"),
|
|
1506
|
+
String("database"): String(database),
|
|
1507
|
+
String("type"): String("mongodb")
|
|
1508
|
+
})
|
|
1509
|
+
|
|
1510
|
+
except Exception as e:
|
|
1511
|
+
return EvaluationError(f"mongo_connect() error: {str(e)}")
|
|
1512
|
+
|
|
1513
|
+
# HTTP Client
|
|
1514
|
+
def _http_get(*a):
|
|
1515
|
+
"""HTTP GET request: http_get(url, headers?, timeout?)"""
|
|
1516
|
+
if len(a) < 1:
|
|
1517
|
+
return EvaluationError("http_get() requires at least 1 argument: url")
|
|
1518
|
+
|
|
1519
|
+
url = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
1520
|
+
headers = None
|
|
1521
|
+
timeout = 30
|
|
1522
|
+
|
|
1523
|
+
# Parse optional headers (Map)
|
|
1524
|
+
if len(a) >= 2 and isinstance(a[1], Map):
|
|
1525
|
+
headers = _zexus_to_python(a[1])
|
|
1526
|
+
|
|
1527
|
+
# Parse optional timeout (Integer)
|
|
1528
|
+
if len(a) >= 3 and isinstance(a[2], Integer):
|
|
1529
|
+
timeout = a[2].value
|
|
1530
|
+
|
|
1531
|
+
try:
|
|
1532
|
+
from ..stdlib.http import HttpModule
|
|
1533
|
+
result = HttpModule.get(url, headers, timeout)
|
|
1534
|
+
return _python_to_zexus(result)
|
|
1535
|
+
except Exception as e:
|
|
1536
|
+
return EvaluationError(f"HTTP GET error: {str(e)}")
|
|
1537
|
+
|
|
1538
|
+
def _http_post(*a):
|
|
1539
|
+
"""HTTP POST request: http_post(url, data, headers?, timeout?)"""
|
|
1540
|
+
if len(a) < 2:
|
|
1541
|
+
return EvaluationError("http_post() requires at least 2 arguments: url, data")
|
|
1542
|
+
|
|
1543
|
+
url = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
1544
|
+
data = _zexus_to_python(a[1])
|
|
1545
|
+
headers = None
|
|
1546
|
+
timeout = 30
|
|
1547
|
+
|
|
1548
|
+
# Parse optional headers (Map)
|
|
1549
|
+
if len(a) >= 3 and isinstance(a[2], Map):
|
|
1550
|
+
headers = _zexus_to_python(a[2])
|
|
1551
|
+
|
|
1552
|
+
# Parse optional timeout (Integer)
|
|
1553
|
+
if len(a) >= 4 and isinstance(a[3], Integer):
|
|
1554
|
+
timeout = a[3].value
|
|
1555
|
+
|
|
1556
|
+
try:
|
|
1557
|
+
from ..stdlib.http import HttpModule
|
|
1558
|
+
# Determine if data should be sent as JSON
|
|
1559
|
+
json_mode = isinstance(a[1], (Map, List))
|
|
1560
|
+
result = HttpModule.post(url, data, headers, json=json_mode, timeout=timeout)
|
|
1561
|
+
return _python_to_zexus(result)
|
|
1562
|
+
except Exception as e:
|
|
1563
|
+
return EvaluationError(f"HTTP POST error: {str(e)}")
|
|
1564
|
+
|
|
1565
|
+
def _http_put(*a):
|
|
1566
|
+
"""HTTP PUT request: http_put(url, data, headers?, timeout?)"""
|
|
1567
|
+
if len(a) < 2:
|
|
1568
|
+
return EvaluationError("http_put() requires at least 2 arguments: url, data")
|
|
1569
|
+
|
|
1570
|
+
url = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
1571
|
+
data = _zexus_to_python(a[1])
|
|
1572
|
+
headers = None
|
|
1573
|
+
timeout = 30
|
|
1574
|
+
|
|
1575
|
+
if len(a) >= 3 and isinstance(a[2], Map):
|
|
1576
|
+
headers = _zexus_to_python(a[2])
|
|
1577
|
+
|
|
1578
|
+
if len(a) >= 4 and isinstance(a[3], Integer):
|
|
1579
|
+
timeout = a[3].value
|
|
1580
|
+
|
|
1581
|
+
try:
|
|
1582
|
+
from ..stdlib.http import HttpModule
|
|
1583
|
+
json_mode = isinstance(a[1], (Map, List))
|
|
1584
|
+
result = HttpModule.put(url, data, headers, json=json_mode, timeout=timeout)
|
|
1585
|
+
return _python_to_zexus(result)
|
|
1586
|
+
except Exception as e:
|
|
1587
|
+
return EvaluationError(f"HTTP PUT error: {str(e)}")
|
|
1588
|
+
|
|
1589
|
+
def _http_delete(*a):
|
|
1590
|
+
"""HTTP DELETE request: http_delete(url, headers?, timeout?)"""
|
|
1591
|
+
if len(a) < 1:
|
|
1592
|
+
return EvaluationError("http_delete() requires at least 1 argument: url")
|
|
1593
|
+
|
|
1594
|
+
url = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
1595
|
+
headers = None
|
|
1596
|
+
timeout = 30
|
|
1597
|
+
|
|
1598
|
+
if len(a) >= 2 and isinstance(a[1], Map):
|
|
1599
|
+
headers = _zexus_to_python(a[1])
|
|
1600
|
+
|
|
1601
|
+
if len(a) >= 3 and isinstance(a[2], Integer):
|
|
1602
|
+
timeout = a[2].value
|
|
1603
|
+
|
|
1604
|
+
try:
|
|
1605
|
+
from ..stdlib.http import HttpModule
|
|
1606
|
+
result = HttpModule.delete(url, headers, timeout)
|
|
1607
|
+
return _python_to_zexus(result)
|
|
1608
|
+
except Exception as e:
|
|
1609
|
+
return EvaluationError(f"HTTP DELETE error: {str(e)}")
|
|
1610
|
+
|
|
1611
|
+
# Debug
|
|
1612
|
+
def _debug(*a):
|
|
1613
|
+
"""Simple debug function that works like print"""
|
|
1614
|
+
if len(a) == 0:
|
|
1615
|
+
return EvaluationError("debug() requires at least 1 argument")
|
|
1616
|
+
msg = a[0]
|
|
1617
|
+
# Convert to string representation
|
|
1618
|
+
if isinstance(msg, String):
|
|
1619
|
+
output = msg.value
|
|
1620
|
+
elif isinstance(msg, (Integer, Float)):
|
|
1621
|
+
output = str(msg.value)
|
|
1622
|
+
elif isinstance(msg, BooleanObj):
|
|
1623
|
+
output = "true" if msg.value else "false"
|
|
1624
|
+
elif msg == NULL:
|
|
1625
|
+
output = "null"
|
|
1626
|
+
elif isinstance(msg, (List, Map)):
|
|
1627
|
+
output = msg.inspect()
|
|
1628
|
+
else:
|
|
1629
|
+
output = str(msg)
|
|
1630
|
+
# Output the debug information
|
|
1631
|
+
print(output, flush=True)
|
|
1632
|
+
return msg # Return the original value for use in expressions
|
|
1633
|
+
|
|
1634
|
+
def _debug_log(*a):
|
|
1635
|
+
if len(a) == 0:
|
|
1636
|
+
return EvaluationError("debug_log() requires at least a message")
|
|
1637
|
+
msg = a[0]
|
|
1638
|
+
val = a[1] if len(a) > 1 else None
|
|
1639
|
+
return Debug.log(msg, val)
|
|
1640
|
+
|
|
1641
|
+
def _debug_trace(*a):
|
|
1642
|
+
if len(a) != 1 or not isinstance(a[0], String):
|
|
1643
|
+
return EvaluationError("debug_trace() takes exactly 1 string argument")
|
|
1644
|
+
return Debug.trace(a[0])
|
|
1645
|
+
|
|
1646
|
+
# String & Utility
|
|
1647
|
+
def _string(*a):
|
|
1648
|
+
from ..object import EntityInstance
|
|
1649
|
+
if len(a) != 1:
|
|
1650
|
+
return EvaluationError(f"string() takes 1 arg ({len(a)} given)")
|
|
1651
|
+
arg = a[0]
|
|
1652
|
+
if isinstance(arg, Integer) or isinstance(arg, Float):
|
|
1653
|
+
return String(str(arg.value))
|
|
1654
|
+
if isinstance(arg, String):
|
|
1655
|
+
return arg
|
|
1656
|
+
if isinstance(arg, BooleanObj):
|
|
1657
|
+
return String("true" if arg.value else "false")
|
|
1658
|
+
if isinstance(arg, (List, Map)):
|
|
1659
|
+
return String(arg.inspect())
|
|
1660
|
+
if isinstance(arg, EntityInstance):
|
|
1661
|
+
return String(arg.inspect())
|
|
1662
|
+
if arg == NULL:
|
|
1663
|
+
return String("null")
|
|
1664
|
+
# For any object with an inspect method
|
|
1665
|
+
if hasattr(arg, 'inspect') and callable(arg.inspect):
|
|
1666
|
+
return String(arg.inspect())
|
|
1667
|
+
return String(str(arg))
|
|
1668
|
+
|
|
1669
|
+
def _int(*a):
|
|
1670
|
+
"""Convert value to integer"""
|
|
1671
|
+
if len(a) != 1:
|
|
1672
|
+
return EvaluationError(f"int() takes 1 arg ({len(a)} given)")
|
|
1673
|
+
arg = a[0]
|
|
1674
|
+
if isinstance(arg, Integer):
|
|
1675
|
+
return arg
|
|
1676
|
+
if isinstance(arg, Float):
|
|
1677
|
+
return Integer(int(arg.value))
|
|
1678
|
+
if isinstance(arg, String):
|
|
1679
|
+
try:
|
|
1680
|
+
return Integer(int(arg.value))
|
|
1681
|
+
except ValueError:
|
|
1682
|
+
return EvaluationError(f"Cannot convert '{arg.value}' to integer")
|
|
1683
|
+
if isinstance(arg, BooleanObj):
|
|
1684
|
+
return Integer(1 if arg.value else 0)
|
|
1685
|
+
return EvaluationError(f"Cannot convert {type(arg).__name__} to integer")
|
|
1686
|
+
|
|
1687
|
+
def _float(*a):
|
|
1688
|
+
"""Convert value to float"""
|
|
1689
|
+
if len(a) != 1:
|
|
1690
|
+
return EvaluationError(f"float() takes 1 arg ({len(a)} given)")
|
|
1691
|
+
arg = a[0]
|
|
1692
|
+
if isinstance(arg, Float):
|
|
1693
|
+
return arg
|
|
1694
|
+
if isinstance(arg, Integer):
|
|
1695
|
+
return Float(float(arg.value))
|
|
1696
|
+
if isinstance(arg, String):
|
|
1697
|
+
try:
|
|
1698
|
+
return Float(float(arg.value))
|
|
1699
|
+
except ValueError:
|
|
1700
|
+
return EvaluationError(f"Cannot convert '{arg.value}' to float")
|
|
1701
|
+
if isinstance(arg, BooleanObj):
|
|
1702
|
+
return Float(1.0 if arg.value else 0.0)
|
|
1703
|
+
return EvaluationError(f"Cannot convert {type(arg).__name__} to float")
|
|
1704
|
+
|
|
1705
|
+
def _uppercase(*a):
|
|
1706
|
+
"""Convert string to uppercase"""
|
|
1707
|
+
if len(a) != 1:
|
|
1708
|
+
return EvaluationError(f"uppercase() takes 1 arg ({len(a)} given)")
|
|
1709
|
+
arg = a[0]
|
|
1710
|
+
if isinstance(arg, String):
|
|
1711
|
+
return String(arg.value.upper())
|
|
1712
|
+
return EvaluationError(f"uppercase() requires a string argument")
|
|
1713
|
+
|
|
1714
|
+
def _lowercase(*a):
|
|
1715
|
+
"""Convert string to lowercase"""
|
|
1716
|
+
if len(a) != 1:
|
|
1717
|
+
return EvaluationError(f"lowercase() takes 1 arg ({len(a)} given)")
|
|
1718
|
+
arg = a[0]
|
|
1719
|
+
if isinstance(arg, String):
|
|
1720
|
+
return String(arg.value.lower())
|
|
1721
|
+
return EvaluationError(f"lowercase() requires a string argument")
|
|
1722
|
+
|
|
1723
|
+
def _random(*a):
|
|
1724
|
+
"""Generate random number. random() -> 0-1, random(max) -> 0 to max-1"""
|
|
1725
|
+
import random
|
|
1726
|
+
if len(a) == 0:
|
|
1727
|
+
return Float(random.random())
|
|
1728
|
+
elif len(a) == 1:
|
|
1729
|
+
if isinstance(a[0], Integer):
|
|
1730
|
+
return Integer(random.randint(0, a[0].value - 1))
|
|
1731
|
+
elif isinstance(a[0], Float):
|
|
1732
|
+
return Float(random.random() * a[0].value)
|
|
1733
|
+
return EvaluationError("random() argument must be a number")
|
|
1734
|
+
else:
|
|
1735
|
+
return EvaluationError(f"random() takes 0 or 1 arg ({len(a)} given)")
|
|
1736
|
+
|
|
1737
|
+
def _persist_set(*a):
|
|
1738
|
+
"""Store a value in persistent storage: persist_set(key, value)"""
|
|
1739
|
+
if len(a) != 2:
|
|
1740
|
+
return EvaluationError(f"persist_set() takes 2 args (key, value), got {len(a)}")
|
|
1741
|
+
if not isinstance(a[0], String):
|
|
1742
|
+
return EvaluationError("persist_set() key must be a string")
|
|
1743
|
+
|
|
1744
|
+
import json
|
|
1745
|
+
import os
|
|
1746
|
+
|
|
1747
|
+
key = a[0].value
|
|
1748
|
+
value = a[1]
|
|
1749
|
+
|
|
1750
|
+
# Create persistence directory if it doesn't exist
|
|
1751
|
+
persist_dir = os.path.join(os.getcwd(), '.zexus_persist')
|
|
1752
|
+
os.makedirs(persist_dir, exist_ok=True)
|
|
1753
|
+
|
|
1754
|
+
# Convert value to JSON-serializable format
|
|
1755
|
+
json_value = _to_python_value(value)
|
|
1756
|
+
|
|
1757
|
+
# Save to file
|
|
1758
|
+
persist_file = os.path.join(persist_dir, f'{key}.json')
|
|
1759
|
+
try:
|
|
1760
|
+
with open(persist_file, 'w') as f:
|
|
1761
|
+
json.dump(json_value, f)
|
|
1762
|
+
return TRUE
|
|
1763
|
+
except Exception as e:
|
|
1764
|
+
return EvaluationError(f"persist_set() error: {str(e)}")
|
|
1765
|
+
|
|
1766
|
+
def _persist_get(*a):
|
|
1767
|
+
"""Retrieve a value from persistent storage: persist_get(key)"""
|
|
1768
|
+
if len(a) != 1:
|
|
1769
|
+
return EvaluationError(f"persist_get() takes 1 arg (key), got {len(a)}")
|
|
1770
|
+
if not isinstance(a[0], String):
|
|
1771
|
+
return EvaluationError("persist_get() key must be a string")
|
|
1772
|
+
|
|
1773
|
+
import json
|
|
1774
|
+
import os
|
|
1775
|
+
|
|
1776
|
+
key = a[0].value
|
|
1777
|
+
persist_dir = os.path.join(os.getcwd(), '.zexus_persist')
|
|
1778
|
+
persist_file = os.path.join(persist_dir, f'{key}.json')
|
|
1779
|
+
|
|
1780
|
+
if not os.path.exists(persist_file):
|
|
1781
|
+
return NULL
|
|
1782
|
+
|
|
1783
|
+
try:
|
|
1784
|
+
with open(persist_file, 'r') as f:
|
|
1785
|
+
json_value = json.load(f)
|
|
1786
|
+
|
|
1787
|
+
# Convert back to Zexus object
|
|
1788
|
+
return _from_python_value(json_value)
|
|
1789
|
+
except Exception as e:
|
|
1790
|
+
return EvaluationError(f"persist_get() error: {str(e)}")
|
|
1791
|
+
|
|
1792
|
+
def _to_python_value(obj):
|
|
1793
|
+
"""Helper to convert Zexus object to Python value"""
|
|
1794
|
+
from ..security import EntityInstance as SecurityEntityInstance
|
|
1795
|
+
|
|
1796
|
+
if isinstance(obj, String):
|
|
1797
|
+
return obj.value
|
|
1798
|
+
elif isinstance(obj, (Integer, Float)):
|
|
1799
|
+
return obj.value
|
|
1800
|
+
elif isinstance(obj, BooleanObj):
|
|
1801
|
+
return obj.value
|
|
1802
|
+
elif isinstance(obj, List):
|
|
1803
|
+
return [_to_python_value(v) for v in obj.elements]
|
|
1804
|
+
elif isinstance(obj, Map):
|
|
1805
|
+
return {str(k): _to_python_value(v) for k, v in obj.pairs.items()}
|
|
1806
|
+
elif isinstance(obj, SecurityEntityInstance):
|
|
1807
|
+
# Convert entity to a dict with its properties
|
|
1808
|
+
result = {}
|
|
1809
|
+
for key, value in obj.data.items():
|
|
1810
|
+
result[key] = _to_python_value(value)
|
|
1811
|
+
return result
|
|
1812
|
+
elif obj == NULL:
|
|
1813
|
+
return None
|
|
1814
|
+
else:
|
|
1815
|
+
return str(obj)
|
|
1816
|
+
|
|
1817
|
+
def _from_python_value(val):
|
|
1818
|
+
"""Helper to convert Python value to Zexus object"""
|
|
1819
|
+
if isinstance(val, bool):
|
|
1820
|
+
return BooleanObj(val)
|
|
1821
|
+
elif isinstance(val, int):
|
|
1822
|
+
return Integer(val)
|
|
1823
|
+
elif isinstance(val, float):
|
|
1824
|
+
return Float(val)
|
|
1825
|
+
elif isinstance(val, str):
|
|
1826
|
+
return String(val)
|
|
1827
|
+
elif isinstance(val, list):
|
|
1828
|
+
return List([_from_python_value(v) for v in val])
|
|
1829
|
+
elif isinstance(val, dict):
|
|
1830
|
+
# Convert dict to Map with String keys
|
|
1831
|
+
pairs = {}
|
|
1832
|
+
for k, v in val.items():
|
|
1833
|
+
key = String(str(k)) if not isinstance(k, str) else String(k)
|
|
1834
|
+
pairs[key] = _from_python_value(v)
|
|
1835
|
+
return Map(pairs)
|
|
1836
|
+
elif val is None:
|
|
1837
|
+
return NULL
|
|
1838
|
+
else:
|
|
1839
|
+
return String(str(val))
|
|
1840
|
+
|
|
1841
|
+
def _len(*a):
|
|
1842
|
+
if len(a) != 1:
|
|
1843
|
+
return EvaluationError("len() takes 1 arg")
|
|
1844
|
+
arg = a[0]
|
|
1845
|
+
if isinstance(arg, String):
|
|
1846
|
+
return Integer(len(arg.value))
|
|
1847
|
+
if isinstance(arg, List):
|
|
1848
|
+
return Integer(len(arg.elements))
|
|
1849
|
+
# Handle Python list (shouldn't happen, but defensive)
|
|
1850
|
+
if isinstance(arg, list):
|
|
1851
|
+
return Integer(len(arg))
|
|
1852
|
+
arg_type = arg.type() if hasattr(arg, 'type') else type(arg).__name__
|
|
1853
|
+
return EvaluationError(f"len() not supported for {arg_type}")
|
|
1854
|
+
|
|
1855
|
+
def _type(*a):
|
|
1856
|
+
"""Return the type name of the argument"""
|
|
1857
|
+
if len(a) != 1:
|
|
1858
|
+
return EvaluationError("type() takes exactly 1 argument")
|
|
1859
|
+
arg = a[0]
|
|
1860
|
+
if isinstance(arg, Integer):
|
|
1861
|
+
return String("Integer")
|
|
1862
|
+
elif isinstance(arg, Float):
|
|
1863
|
+
return String("Float")
|
|
1864
|
+
elif isinstance(arg, String):
|
|
1865
|
+
return String("String")
|
|
1866
|
+
elif isinstance(arg, BooleanObj):
|
|
1867
|
+
return String("Boolean")
|
|
1868
|
+
elif isinstance(arg, List):
|
|
1869
|
+
return String("List")
|
|
1870
|
+
elif isinstance(arg, Map):
|
|
1871
|
+
return String("Map")
|
|
1872
|
+
elif isinstance(arg, Action):
|
|
1873
|
+
return String("Action")
|
|
1874
|
+
elif isinstance(arg, LambdaFunction):
|
|
1875
|
+
return String("Lambda")
|
|
1876
|
+
elif isinstance(arg, Builtin):
|
|
1877
|
+
return String("Builtin")
|
|
1878
|
+
elif isinstance(arg, Null):
|
|
1879
|
+
return String("Null")
|
|
1880
|
+
else:
|
|
1881
|
+
return String(type(arg).__name__)
|
|
1882
|
+
|
|
1883
|
+
# List Utils (Builtin versions of methods)
|
|
1884
|
+
def _first(*a):
|
|
1885
|
+
if not isinstance(a[0], List):
|
|
1886
|
+
return EvaluationError("first() expects a list")
|
|
1887
|
+
return a[0].elements[0] if a[0].elements else NULL
|
|
1888
|
+
|
|
1889
|
+
def _rest(*a):
|
|
1890
|
+
if not isinstance(a[0], List):
|
|
1891
|
+
return EvaluationError("rest() expects a list")
|
|
1892
|
+
return List(a[0].elements[1:]) if len(a[0].elements) > 0 else List([])
|
|
1893
|
+
|
|
1894
|
+
def _push(*a):
|
|
1895
|
+
if len(a) != 2 or not isinstance(a[0], List):
|
|
1896
|
+
return EvaluationError("push(list, item)")
|
|
1897
|
+
return List(a[0].elements + [a[1]])
|
|
1898
|
+
|
|
1899
|
+
def _append(*a):
|
|
1900
|
+
"""Mutating append: modifies list in-place and returns it"""
|
|
1901
|
+
if len(a) != 2:
|
|
1902
|
+
return EvaluationError("append() takes 2 arguments: append(list, item)")
|
|
1903
|
+
if not isinstance(a[0], List):
|
|
1904
|
+
return EvaluationError("append() first argument must be a list")
|
|
1905
|
+
# Mutate the list in-place
|
|
1906
|
+
a[0].append(a[1])
|
|
1907
|
+
return a[0]
|
|
1908
|
+
|
|
1909
|
+
def _extend(*a):
|
|
1910
|
+
"""Mutating extend: modifies list in-place by adding elements from another list"""
|
|
1911
|
+
if len(a) != 2:
|
|
1912
|
+
return EvaluationError("extend() takes 2 arguments: extend(list, other_list)")
|
|
1913
|
+
if not isinstance(a[0], List):
|
|
1914
|
+
return EvaluationError("extend() first argument must be a list")
|
|
1915
|
+
if not isinstance(a[1], List):
|
|
1916
|
+
return EvaluationError("extend() second argument must be a list")
|
|
1917
|
+
# Mutate the list in-place
|
|
1918
|
+
a[0].extend(a[1])
|
|
1919
|
+
return a[0]
|
|
1920
|
+
|
|
1921
|
+
def _reduce(*a):
|
|
1922
|
+
if len(a) < 2:
|
|
1923
|
+
return EvaluationError("reduce(arr, fn, [init])")
|
|
1924
|
+
return self._array_reduce(a[0], a[1], a[2] if len(a) > 2 else None)
|
|
1925
|
+
|
|
1926
|
+
def _map(*a):
|
|
1927
|
+
if len(a) != 2:
|
|
1928
|
+
return EvaluationError("map(arr, fn)")
|
|
1929
|
+
return self._array_map(a[0], a[1])
|
|
1930
|
+
|
|
1931
|
+
def _filter(*a):
|
|
1932
|
+
if len(a) != 2:
|
|
1933
|
+
return EvaluationError("filter(arr, fn)")
|
|
1934
|
+
return self._array_filter(a[0], a[1])
|
|
1935
|
+
|
|
1936
|
+
# File object creation (for RAII using statements)
|
|
1937
|
+
def _file(*a):
|
|
1938
|
+
if len(a) == 0 or len(a) > 2:
|
|
1939
|
+
return EvaluationError("file() takes 1 or 2 arguments: file(path) or file(path, mode)")
|
|
1940
|
+
if not isinstance(a[0], String):
|
|
1941
|
+
return EvaluationError("file() path must be a string")
|
|
1942
|
+
|
|
1943
|
+
from ..object import File as FileObject
|
|
1944
|
+
path = a[0].value
|
|
1945
|
+
mode = a[1].value if len(a) > 1 and isinstance(a[1], String) else 'r'
|
|
1946
|
+
|
|
1947
|
+
try:
|
|
1948
|
+
file_obj = FileObject(path, mode)
|
|
1949
|
+
file_obj.open()
|
|
1950
|
+
return file_obj
|
|
1951
|
+
except Exception as e:
|
|
1952
|
+
return EvaluationError(f"file() error: {str(e)}")
|
|
1953
|
+
|
|
1954
|
+
def _read_file(*a):
|
|
1955
|
+
"""Read entire file contents as string"""
|
|
1956
|
+
if len(a) != 1:
|
|
1957
|
+
return EvaluationError("read_file() takes exactly 1 argument: path")
|
|
1958
|
+
if not isinstance(a[0], String):
|
|
1959
|
+
return EvaluationError("read_file() path must be a string")
|
|
1960
|
+
|
|
1961
|
+
import os
|
|
1962
|
+
path = a[0].value
|
|
1963
|
+
|
|
1964
|
+
# Normalize path
|
|
1965
|
+
if not os.path.isabs(path):
|
|
1966
|
+
path = os.path.join(os.getcwd(), path)
|
|
1967
|
+
|
|
1968
|
+
try:
|
|
1969
|
+
with open(path, 'r') as f:
|
|
1970
|
+
content = f.read()
|
|
1971
|
+
return String(content)
|
|
1972
|
+
except FileNotFoundError:
|
|
1973
|
+
return EvaluationError(f"File not found: {path}")
|
|
1974
|
+
except Exception as e:
|
|
1975
|
+
return EvaluationError(f"read_file() error: {str(e)}")
|
|
1976
|
+
|
|
1977
|
+
def _eval_file(*a):
|
|
1978
|
+
"""Execute code from a file based on its extension"""
|
|
1979
|
+
if len(a) < 1 or len(a) > 2:
|
|
1980
|
+
return EvaluationError("eval_file() takes 1-2 arguments: eval_file(path) or eval_file(path, language)")
|
|
1981
|
+
if not isinstance(a[0], String):
|
|
1982
|
+
return EvaluationError("eval_file() path must be a string")
|
|
1983
|
+
|
|
1984
|
+
import os
|
|
1985
|
+
import subprocess
|
|
1986
|
+
path = a[0].value
|
|
1987
|
+
|
|
1988
|
+
import os
|
|
1989
|
+
import subprocess
|
|
1990
|
+
path = a[0].value
|
|
1991
|
+
|
|
1992
|
+
# Normalize path
|
|
1993
|
+
if not os.path.isabs(path):
|
|
1994
|
+
path = os.path.join(os.getcwd(), path)
|
|
1995
|
+
|
|
1996
|
+
# Determine language from extension or argument
|
|
1997
|
+
if len(a) == 2 and isinstance(a[1], String):
|
|
1998
|
+
language = a[1].value.lower()
|
|
1999
|
+
else:
|
|
2000
|
+
_, ext = os.path.splitext(path)
|
|
2001
|
+
language = ext[1:].lower() if ext else "unknown"
|
|
2002
|
+
|
|
2003
|
+
# Read file content
|
|
2004
|
+
try:
|
|
2005
|
+
with open(path, 'r') as f:
|
|
2006
|
+
content = f.read()
|
|
2007
|
+
except FileNotFoundError:
|
|
2008
|
+
return EvaluationError(f"File not found: {path}")
|
|
2009
|
+
except Exception as e:
|
|
2010
|
+
return EvaluationError(f"eval_file() read error: {str(e)}")
|
|
2011
|
+
|
|
2012
|
+
# Execute based on language
|
|
2013
|
+
if language == "zx" or language == "zexus":
|
|
2014
|
+
# Execute Zexus code
|
|
2015
|
+
from ..parser.parser import UltimateParser
|
|
2016
|
+
from ..lexer import Lexer
|
|
2017
|
+
|
|
2018
|
+
try:
|
|
2019
|
+
lexer = Lexer(content)
|
|
2020
|
+
parser = UltimateParser(lexer)
|
|
2021
|
+
program = parser.parse_program()
|
|
2022
|
+
|
|
2023
|
+
if parser.errors:
|
|
2024
|
+
return EvaluationError(f"Parse errors: {parser.errors[0]}")
|
|
2025
|
+
|
|
2026
|
+
# Use the current evaluator instance to execute in a new environment
|
|
2027
|
+
from ..object import Environment
|
|
2028
|
+
new_env = Environment()
|
|
2029
|
+
|
|
2030
|
+
# Copy builtins to new environment
|
|
2031
|
+
for name, builtin in self.builtins.items():
|
|
2032
|
+
new_env.set(name, builtin)
|
|
2033
|
+
|
|
2034
|
+
result = NULL
|
|
2035
|
+
for stmt in program.statements:
|
|
2036
|
+
result = self.eval_node(stmt, new_env, [])
|
|
2037
|
+
if is_error(result):
|
|
2038
|
+
return result
|
|
2039
|
+
|
|
2040
|
+
# Export all defined functions/actions to global builtins
|
|
2041
|
+
# This allows cross-file code reuse
|
|
2042
|
+
for key in new_env.store.keys():
|
|
2043
|
+
if key not in ['__file__', '__FILE__', '__MODULE__', '__DIR__', '__ARGS__', '__ARGV__', '__PACKAGE__']:
|
|
2044
|
+
val = new_env.get(key)
|
|
2045
|
+
if val and not is_error(val):
|
|
2046
|
+
# Add to builtins so it's available globally
|
|
2047
|
+
self.builtins[key] = val
|
|
2048
|
+
|
|
2049
|
+
return result if result else NULL
|
|
2050
|
+
except Exception as e:
|
|
2051
|
+
return EvaluationError(f"eval_file() zexus execution error: {str(e)}")
|
|
2052
|
+
|
|
2053
|
+
elif language == "py" or language == "python":
|
|
2054
|
+
# Execute Python code
|
|
2055
|
+
try:
|
|
2056
|
+
exec_globals = {}
|
|
2057
|
+
exec(content, exec_globals)
|
|
2058
|
+
# Return the result if there's a 'result' variable
|
|
2059
|
+
if 'result' in exec_globals:
|
|
2060
|
+
result_val = exec_globals['result']
|
|
2061
|
+
# Convert Python types to Zexus types
|
|
2062
|
+
if isinstance(result_val, str):
|
|
2063
|
+
return String(result_val)
|
|
2064
|
+
elif isinstance(result_val, int):
|
|
2065
|
+
return Integer(result_val)
|
|
2066
|
+
elif isinstance(result_val, float):
|
|
2067
|
+
return Float(result_val)
|
|
2068
|
+
elif isinstance(result_val, bool):
|
|
2069
|
+
return Boolean(result_val)
|
|
2070
|
+
elif isinstance(result_val, list):
|
|
2071
|
+
return List([Integer(x) if isinstance(x, int) else String(str(x)) for x in result_val])
|
|
2072
|
+
return NULL
|
|
2073
|
+
except Exception as e:
|
|
2074
|
+
return EvaluationError(f"eval_file() python execution error: {str(e)}")
|
|
2075
|
+
|
|
2076
|
+
elif language in ["cpp", "c++", "c", "rs", "rust", "go"]:
|
|
2077
|
+
# For compiled languages, try to compile and run
|
|
2078
|
+
return EvaluationError(f"eval_file() for {language} requires compilation - not yet implemented")
|
|
2079
|
+
|
|
2080
|
+
elif language == "js" or language == "javascript":
|
|
2081
|
+
# Execute JavaScript (if Node.js is available)
|
|
2082
|
+
try:
|
|
2083
|
+
result = subprocess.run(['node', '-e', content],
|
|
2084
|
+
capture_output=True,
|
|
2085
|
+
text=True,
|
|
2086
|
+
timeout=5)
|
|
2087
|
+
if result.returncode != 0:
|
|
2088
|
+
return EvaluationError(f"JavaScript error: {result.stderr}")
|
|
2089
|
+
return String(result.stdout.strip())
|
|
2090
|
+
except FileNotFoundError:
|
|
2091
|
+
return EvaluationError("Node.js not found - cannot execute JavaScript")
|
|
2092
|
+
except Exception as e:
|
|
2093
|
+
return EvaluationError(f"eval_file() js execution error: {str(e)}")
|
|
2094
|
+
|
|
2095
|
+
else:
|
|
2096
|
+
return EvaluationError(f"Unsupported language: {language}")
|
|
2097
|
+
|
|
2098
|
+
# Register mappings
|
|
2099
|
+
self.builtins.update({
|
|
2100
|
+
"now": Builtin(_now, "now"),
|
|
2101
|
+
"timestamp": Builtin(_timestamp, "timestamp"),
|
|
2102
|
+
"random": Builtin(_random, "random"),
|
|
2103
|
+
"to_hex": Builtin(_to_hex, "to_hex"),
|
|
2104
|
+
"from_hex": Builtin(_from_hex, "from_hex"),
|
|
2105
|
+
"sqrt": Builtin(_sqrt, "sqrt"),
|
|
2106
|
+
"file": Builtin(_file, "file"),
|
|
2107
|
+
"file_read_text": Builtin(_read_text, "file_read_text"),
|
|
2108
|
+
"file_write_text": Builtin(_write_text, "file_write_text"),
|
|
2109
|
+
"file_exists": Builtin(_exists, "file_exists"),
|
|
2110
|
+
"file_read_json": Builtin(_read_json, "file_read_json"),
|
|
2111
|
+
"file_write_json": Builtin(_write_json, "file_write_json"),
|
|
2112
|
+
"file_append": Builtin(_file_append, "file_append"),
|
|
2113
|
+
"file_list_dir": Builtin(_list_dir, "file_list_dir"),
|
|
2114
|
+
"fs_is_file": Builtin(_fs_is_file, "fs_is_file"),
|
|
2115
|
+
"fs_is_dir": Builtin(_fs_is_dir, "fs_is_dir"),
|
|
2116
|
+
"fs_mkdir": Builtin(_fs_mkdir, "fs_mkdir"),
|
|
2117
|
+
"fs_remove": Builtin(_fs_remove, "fs_remove"),
|
|
2118
|
+
"fs_rmdir": Builtin(_fs_rmdir, "fs_rmdir"),
|
|
2119
|
+
"fs_rename": Builtin(_fs_rename, "fs_rename"),
|
|
2120
|
+
"fs_copy": Builtin(_fs_copy, "fs_copy"),
|
|
2121
|
+
"socket_create_server": Builtin(_socket_create_server, "socket_create_server"),
|
|
2122
|
+
"socket_create_connection": Builtin(_socket_create_connection, "socket_create_connection"),
|
|
2123
|
+
"http_server": Builtin(_http_server, "http_server"),
|
|
2124
|
+
"sqlite_connect": Builtin(_sqlite_connect, "sqlite_connect"),
|
|
2125
|
+
"postgres_connect": Builtin(_postgres_connect, "postgres_connect"),
|
|
2126
|
+
"mysql_connect": Builtin(_mysql_connect, "mysql_connect"),
|
|
2127
|
+
"mongo_connect": Builtin(_mongo_connect, "mongo_connect"),
|
|
2128
|
+
"http_get": Builtin(_http_get, "http_get"),
|
|
2129
|
+
"http_post": Builtin(_http_post, "http_post"),
|
|
2130
|
+
"http_put": Builtin(_http_put, "http_put"),
|
|
2131
|
+
"http_delete": Builtin(_http_delete, "http_delete"),
|
|
2132
|
+
"read_file": Builtin(_read_file, "read_file"),
|
|
2133
|
+
"eval_file": Builtin(_eval_file, "eval_file"),
|
|
2134
|
+
"debug": Builtin(_debug, "debug"),
|
|
2135
|
+
"debug_log": Builtin(_debug_log, "debug_log"),
|
|
2136
|
+
"debug_trace": Builtin(_debug_trace, "debug_trace"),
|
|
2137
|
+
"string": Builtin(_string, "string"),
|
|
2138
|
+
"int": Builtin(_int, "int"),
|
|
2139
|
+
"float": Builtin(_float, "float"),
|
|
2140
|
+
"uppercase": Builtin(_uppercase, "uppercase"),
|
|
2141
|
+
"lowercase": Builtin(_lowercase, "lowercase"),
|
|
2142
|
+
"random": Builtin(_random, "random"),
|
|
2143
|
+
"persist_set": Builtin(_persist_set, "persist_set"),
|
|
2144
|
+
"persist_get": Builtin(_persist_get, "persist_get"),
|
|
2145
|
+
"len": Builtin(_len, "len"),
|
|
2146
|
+
"type": Builtin(_type, "type"),
|
|
2147
|
+
"first": Builtin(_first, "first"),
|
|
2148
|
+
"rest": Builtin(_rest, "rest"),
|
|
2149
|
+
"push": Builtin(_push, "push"),
|
|
2150
|
+
"append": Builtin(_append, "append"), # Mutating list append
|
|
2151
|
+
"extend": Builtin(_extend, "extend"), # Mutating list extend
|
|
2152
|
+
"reduce": Builtin(_reduce, "reduce"),
|
|
2153
|
+
"map": Builtin(_map, "map"),
|
|
2154
|
+
"filter": Builtin(_filter, "filter"),
|
|
2155
|
+
})
|
|
2156
|
+
|
|
2157
|
+
# Register concurrency builtins
|
|
2158
|
+
self._register_concurrency_builtins()
|
|
2159
|
+
|
|
2160
|
+
# Register blockchain builtins
|
|
2161
|
+
self._register_blockchain_builtins()
|
|
2162
|
+
|
|
2163
|
+
# Register verification helper builtins
|
|
2164
|
+
self._register_verification_builtins()
|
|
2165
|
+
|
|
2166
|
+
def _register_concurrency_builtins(self):
|
|
2167
|
+
"""Register concurrency operations as builtin functions"""
|
|
2168
|
+
|
|
2169
|
+
def _send(*a):
|
|
2170
|
+
"""Send value to channel: send(channel, value)"""
|
|
2171
|
+
if len(a) != 2:
|
|
2172
|
+
return EvaluationError("send() requires 2 arguments: channel, value")
|
|
2173
|
+
|
|
2174
|
+
channel = a[0]
|
|
2175
|
+
value = a[1]
|
|
2176
|
+
|
|
2177
|
+
# Check if it's a valid channel object
|
|
2178
|
+
if not hasattr(channel, 'send'):
|
|
2179
|
+
return EvaluationError(f"send() first argument must be a channel, got {type(channel).__name__}")
|
|
2180
|
+
|
|
2181
|
+
try:
|
|
2182
|
+
channel.send(value, timeout=5.0)
|
|
2183
|
+
return NULL # send returns nothing on success
|
|
2184
|
+
except Exception as e:
|
|
2185
|
+
return EvaluationError(f"send() error: {str(e)}")
|
|
2186
|
+
|
|
2187
|
+
def _receive(*a):
|
|
2188
|
+
"""Receive value from channel: value = receive(channel)"""
|
|
2189
|
+
if len(a) != 1:
|
|
2190
|
+
return EvaluationError("receive() requires 1 argument: channel")
|
|
2191
|
+
|
|
2192
|
+
channel = a[0]
|
|
2193
|
+
|
|
2194
|
+
# Check if it's a valid channel object
|
|
2195
|
+
if not hasattr(channel, 'receive'):
|
|
2196
|
+
return EvaluationError(f"receive() first argument must be a channel, got {type(channel).__name__}")
|
|
2197
|
+
|
|
2198
|
+
try:
|
|
2199
|
+
value = channel.receive(timeout=5.0)
|
|
2200
|
+
return value if value is not None else NULL
|
|
2201
|
+
except Exception as e:
|
|
2202
|
+
return EvaluationError(f"receive() error: {str(e)}")
|
|
2203
|
+
|
|
2204
|
+
def _close_channel(*a):
|
|
2205
|
+
"""Close a channel: close_channel(channel)"""
|
|
2206
|
+
if len(a) != 1:
|
|
2207
|
+
return EvaluationError("close_channel() requires 1 argument: channel")
|
|
2208
|
+
|
|
2209
|
+
channel = a[0]
|
|
2210
|
+
|
|
2211
|
+
if not hasattr(channel, 'close'):
|
|
2212
|
+
return EvaluationError(f"close_channel() argument must be a channel, got {type(channel).__name__}")
|
|
2213
|
+
|
|
2214
|
+
try:
|
|
2215
|
+
channel.close()
|
|
2216
|
+
return NULL
|
|
2217
|
+
except Exception as e:
|
|
2218
|
+
return EvaluationError(f"close_channel() error: {str(e)}")
|
|
2219
|
+
|
|
2220
|
+
def _async(*a):
|
|
2221
|
+
"""Execute action asynchronously in background thread: async action_call()
|
|
2222
|
+
|
|
2223
|
+
Example: async producer()
|
|
2224
|
+
|
|
2225
|
+
Accepts either:
|
|
2226
|
+
1. A Coroutine (from calling an async action)
|
|
2227
|
+
2. A regular value (from calling a regular action) - will execute in thread
|
|
2228
|
+
"""
|
|
2229
|
+
import threading
|
|
2230
|
+
import sys
|
|
2231
|
+
|
|
2232
|
+
if len(a) != 1:
|
|
2233
|
+
return EvaluationError("async() requires 1 argument: result of action call")
|
|
2234
|
+
|
|
2235
|
+
result = a[0]
|
|
2236
|
+
|
|
2237
|
+
# If it's already a Coroutine, start it in a thread
|
|
2238
|
+
if hasattr(result, '__class__') and result.__class__.__name__ == 'Coroutine':
|
|
2239
|
+
|
|
2240
|
+
def run_coroutine():
|
|
2241
|
+
try:
|
|
2242
|
+
# Prime the generator
|
|
2243
|
+
val = next(result.generator)
|
|
2244
|
+
# Execute until completion
|
|
2245
|
+
try:
|
|
2246
|
+
while True:
|
|
2247
|
+
val = next(result.generator)
|
|
2248
|
+
except StopIteration as e:
|
|
2249
|
+
# Coroutine completed successfully
|
|
2250
|
+
pass
|
|
2251
|
+
except Exception as e:
|
|
2252
|
+
# Print error to stderr for visibility
|
|
2253
|
+
print(f"[ASYNC ERROR] Coroutine execution failed: {str(e)}", file=sys.stderr)
|
|
2254
|
+
import traceback
|
|
2255
|
+
traceback.print_exc(file=sys.stderr)
|
|
2256
|
+
|
|
2257
|
+
thread = threading.Thread(target=run_coroutine, daemon=True)
|
|
2258
|
+
thread.start()
|
|
2259
|
+
return NULL
|
|
2260
|
+
|
|
2261
|
+
# For regular (non-async) actions, the action has already executed!
|
|
2262
|
+
# This is because producer() executes immediately and returns its result.
|
|
2263
|
+
# So async(producer()) just receives the result.
|
|
2264
|
+
# We need a different approach - we can't retroactively make it async.
|
|
2265
|
+
|
|
2266
|
+
# The solution: If they want async execution, the action itself must be async.
|
|
2267
|
+
# For now, just return NULL to indicate "completed" (it already ran).
|
|
2268
|
+
return NULL
|
|
2269
|
+
|
|
2270
|
+
def _sleep(*a):
|
|
2271
|
+
"""Sleep for specified seconds: sleep(seconds)"""
|
|
2272
|
+
import time
|
|
2273
|
+
|
|
2274
|
+
if len(a) != 1:
|
|
2275
|
+
return EvaluationError("sleep() requires 1 argument: seconds")
|
|
2276
|
+
|
|
2277
|
+
seconds = a[0]
|
|
2278
|
+
if isinstance(seconds, (Integer, Float)):
|
|
2279
|
+
time.sleep(float(seconds.value))
|
|
2280
|
+
return NULL
|
|
2281
|
+
|
|
2282
|
+
return EvaluationError(f"sleep() argument must be a number, got {type(seconds).__name__}")
|
|
2283
|
+
|
|
2284
|
+
def _spawn(*a):
|
|
2285
|
+
"""Spawn an async task and return a coroutine that can be awaited: task = spawn async_func()
|
|
2286
|
+
|
|
2287
|
+
Example:
|
|
2288
|
+
async function asyncTask(id) {
|
|
2289
|
+
await sleep(1)
|
|
2290
|
+
return id * 10
|
|
2291
|
+
}
|
|
2292
|
+
let task1 = spawn asyncTask(1)
|
|
2293
|
+
let result = await task1
|
|
2294
|
+
"""
|
|
2295
|
+
import threading
|
|
2296
|
+
import sys
|
|
2297
|
+
|
|
2298
|
+
if len(a) != 1:
|
|
2299
|
+
return EvaluationError("spawn() requires 1 argument: coroutine or async function call result")
|
|
2300
|
+
|
|
2301
|
+
result = a[0]
|
|
2302
|
+
|
|
2303
|
+
# If it's a Coroutine, we need to wrap it for async execution
|
|
2304
|
+
from ..object import Coroutine
|
|
2305
|
+
if isinstance(result, Coroutine):
|
|
2306
|
+
# Create a wrapper coroutine that executes in background
|
|
2307
|
+
# The original coroutine will be run in a thread
|
|
2308
|
+
task_result = [None] # Mutable container to store result
|
|
2309
|
+
task_error = [None]
|
|
2310
|
+
task_complete = [False]
|
|
2311
|
+
|
|
2312
|
+
def run_coroutine():
|
|
2313
|
+
try:
|
|
2314
|
+
# Prime the generator
|
|
2315
|
+
next(result.generator)
|
|
2316
|
+
# Execute until completion
|
|
2317
|
+
try:
|
|
2318
|
+
while True:
|
|
2319
|
+
next(result.generator)
|
|
2320
|
+
except StopIteration as e:
|
|
2321
|
+
# Coroutine completed, store result
|
|
2322
|
+
task_result[0] = e.value if hasattr(e, 'value') else NULL
|
|
2323
|
+
task_complete[0] = True
|
|
2324
|
+
except Exception as e:
|
|
2325
|
+
# Store error
|
|
2326
|
+
task_error[0] = e
|
|
2327
|
+
task_complete[0] = True
|
|
2328
|
+
print(f"[SPAWN ERROR] Task execution failed: {str(e)}", file=sys.stderr)
|
|
2329
|
+
import traceback
|
|
2330
|
+
traceback.print_exc(file=sys.stderr)
|
|
2331
|
+
|
|
2332
|
+
# Start execution in background thread
|
|
2333
|
+
thread = threading.Thread(target=run_coroutine, daemon=False)
|
|
2334
|
+
thread.start()
|
|
2335
|
+
|
|
2336
|
+
# Create a new coroutine that waits for the result
|
|
2337
|
+
def result_generator():
|
|
2338
|
+
yield None # Make it a generator
|
|
2339
|
+
# Wait for completion
|
|
2340
|
+
thread.join(timeout=30) # 30 second timeout
|
|
2341
|
+
if task_error[0]:
|
|
2342
|
+
raise task_error[0]
|
|
2343
|
+
return task_result[0] if task_result[0] is not None else NULL
|
|
2344
|
+
|
|
2345
|
+
return Coroutine(result_generator(), result.fn if hasattr(result, 'fn') else None)
|
|
2346
|
+
|
|
2347
|
+
# If not a coroutine, return error
|
|
2348
|
+
return EvaluationError(f"spawn() argument must be a coroutine (async function call), got {type(result).__name__}")
|
|
2349
|
+
|
|
2350
|
+
def _wait_group(*a):
|
|
2351
|
+
"""Create a wait group for synchronizing async operations: wg = wait_group()
|
|
2352
|
+
|
|
2353
|
+
Example:
|
|
2354
|
+
let wg = wait_group()
|
|
2355
|
+
wg.add(2) # Expecting 2 tasks
|
|
2356
|
+
async task1()
|
|
2357
|
+
async task2()
|
|
2358
|
+
wg.wait() # Blocks until both tasks call wg.done()
|
|
2359
|
+
"""
|
|
2360
|
+
from ..concurrency_system import WaitGroup
|
|
2361
|
+
|
|
2362
|
+
if len(a) != 0:
|
|
2363
|
+
return EvaluationError("wait_group() takes no arguments")
|
|
2364
|
+
|
|
2365
|
+
return WaitGroup()
|
|
2366
|
+
|
|
2367
|
+
def _wg_add(*a):
|
|
2368
|
+
"""Add delta to wait group counter: wg.add(delta)"""
|
|
2369
|
+
if len(a) != 2:
|
|
2370
|
+
return EvaluationError("wg_add() requires 2 arguments: wait_group, delta")
|
|
2371
|
+
|
|
2372
|
+
wg = a[0]
|
|
2373
|
+
delta_obj = a[1]
|
|
2374
|
+
|
|
2375
|
+
if not hasattr(wg, 'add'):
|
|
2376
|
+
return EvaluationError(f"wg_add() first argument must be a WaitGroup, got {type(wg).__name__}")
|
|
2377
|
+
|
|
2378
|
+
if isinstance(delta_obj, Integer):
|
|
2379
|
+
try:
|
|
2380
|
+
wg.add(delta_obj.value)
|
|
2381
|
+
return NULL
|
|
2382
|
+
except Exception as e:
|
|
2383
|
+
return EvaluationError(f"wg_add() error: {str(e)}")
|
|
2384
|
+
elif isinstance(delta_obj, int):
|
|
2385
|
+
try:
|
|
2386
|
+
wg.add(delta_obj)
|
|
2387
|
+
return NULL
|
|
2388
|
+
except Exception as e:
|
|
2389
|
+
return EvaluationError(f"wg_add() error: {str(e)}")
|
|
2390
|
+
|
|
2391
|
+
return EvaluationError(f"wg_add() delta must be an integer, got {type(delta_obj).__name__}")
|
|
2392
|
+
|
|
2393
|
+
def _wg_done(*a):
|
|
2394
|
+
"""Decrement wait group counter: wg.done()"""
|
|
2395
|
+
if len(a) != 1:
|
|
2396
|
+
return EvaluationError("wg_done() requires 1 argument: wait_group")
|
|
2397
|
+
|
|
2398
|
+
wg = a[0]
|
|
2399
|
+
|
|
2400
|
+
if not hasattr(wg, 'done'):
|
|
2401
|
+
return EvaluationError(f"wg_done() argument must be a WaitGroup, got {type(wg).__name__}")
|
|
2402
|
+
|
|
2403
|
+
try:
|
|
2404
|
+
wg.done()
|
|
2405
|
+
return NULL
|
|
2406
|
+
except Exception as e:
|
|
2407
|
+
return EvaluationError(f"wg_done() error: {str(e)}")
|
|
2408
|
+
|
|
2409
|
+
def _wg_wait(*a):
|
|
2410
|
+
"""Wait for wait group counter to reach zero: wg.wait()"""
|
|
2411
|
+
if len(a) < 1 or len(a) > 2:
|
|
2412
|
+
return EvaluationError("wg_wait() requires 1 or 2 arguments: wait_group [, timeout]")
|
|
2413
|
+
|
|
2414
|
+
wg = a[0]
|
|
2415
|
+
timeout = None
|
|
2416
|
+
|
|
2417
|
+
if len(a) == 2:
|
|
2418
|
+
timeout_obj = a[1]
|
|
2419
|
+
if isinstance(timeout_obj, (Integer, Float)):
|
|
2420
|
+
timeout = float(timeout_obj.value)
|
|
2421
|
+
elif isinstance(timeout_obj, (int, float)):
|
|
2422
|
+
timeout = float(timeout_obj)
|
|
2423
|
+
else:
|
|
2424
|
+
return EvaluationError(f"wg_wait() timeout must be a number, got {type(timeout_obj).__name__}")
|
|
2425
|
+
|
|
2426
|
+
if not hasattr(wg, 'wait'):
|
|
2427
|
+
return EvaluationError(f"wg_wait() first argument must be a WaitGroup, got {type(wg).__name__}")
|
|
2428
|
+
|
|
2429
|
+
try:
|
|
2430
|
+
success = wg.wait(timeout=timeout)
|
|
2431
|
+
return TRUE if success else FALSE
|
|
2432
|
+
except Exception as e:
|
|
2433
|
+
return EvaluationError(f"wg_wait() error: {str(e)}")
|
|
2434
|
+
|
|
2435
|
+
def _barrier(*a):
|
|
2436
|
+
"""Create a barrier for synchronizing N tasks: barrier = barrier(parties)
|
|
2437
|
+
|
|
2438
|
+
Example:
|
|
2439
|
+
let barrier = barrier(2) # Wait for 2 tasks
|
|
2440
|
+
async task1() # Will call barrier.wait()
|
|
2441
|
+
async task2() # Will call barrier.wait()
|
|
2442
|
+
# Both released once both reach barrier
|
|
2443
|
+
"""
|
|
2444
|
+
from ..concurrency_system import Barrier
|
|
2445
|
+
|
|
2446
|
+
if len(a) != 1:
|
|
2447
|
+
return EvaluationError("barrier() requires 1 argument: parties")
|
|
2448
|
+
|
|
2449
|
+
parties_obj = a[0]
|
|
2450
|
+
if isinstance(parties_obj, Integer):
|
|
2451
|
+
try:
|
|
2452
|
+
return Barrier(parties=parties_obj.value)
|
|
2453
|
+
except Exception as e:
|
|
2454
|
+
return EvaluationError(f"barrier() error: {str(e)}")
|
|
2455
|
+
|
|
2456
|
+
return EvaluationError(f"barrier() parties must be an integer, got {type(parties_obj).__name__}")
|
|
2457
|
+
|
|
2458
|
+
def _barrier_wait(*a):
|
|
2459
|
+
"""Wait at barrier until all parties arrive: barrier.wait()"""
|
|
2460
|
+
if len(a) < 1 or len(a) > 2:
|
|
2461
|
+
return EvaluationError("barrier_wait() requires 1 or 2 arguments: barrier [, timeout]")
|
|
2462
|
+
|
|
2463
|
+
barrier = a[0]
|
|
2464
|
+
timeout = None
|
|
2465
|
+
|
|
2466
|
+
if len(a) == 2:
|
|
2467
|
+
timeout_obj = a[1]
|
|
2468
|
+
if isinstance(timeout_obj, (Integer, Float)):
|
|
2469
|
+
timeout = float(timeout_obj.value)
|
|
2470
|
+
else:
|
|
2471
|
+
return EvaluationError(f"barrier_wait() timeout must be a number, got {type(timeout_obj).__name__}")
|
|
2472
|
+
|
|
2473
|
+
if not hasattr(barrier, 'wait'):
|
|
2474
|
+
return EvaluationError(f"barrier_wait() first argument must be a Barrier, got {type(barrier).__name__}")
|
|
2475
|
+
|
|
2476
|
+
try:
|
|
2477
|
+
generation = barrier.wait(timeout=timeout)
|
|
2478
|
+
return Integer(generation)
|
|
2479
|
+
except Exception as e:
|
|
2480
|
+
return EvaluationError(f"barrier_wait() error: {str(e)}")
|
|
2481
|
+
|
|
2482
|
+
def _barrier_reset(*a):
|
|
2483
|
+
"""Reset barrier to initial state: barrier.reset()"""
|
|
2484
|
+
if len(a) != 1:
|
|
2485
|
+
return EvaluationError("barrier_reset() requires 1 argument: barrier")
|
|
2486
|
+
|
|
2487
|
+
barrier = a[0]
|
|
2488
|
+
|
|
2489
|
+
if not hasattr(barrier, 'reset'):
|
|
2490
|
+
return EvaluationError(f"barrier_reset() argument must be a Barrier, got {type(barrier).__name__}")
|
|
2491
|
+
|
|
2492
|
+
try:
|
|
2493
|
+
barrier.reset()
|
|
2494
|
+
return NULL
|
|
2495
|
+
except Exception as e:
|
|
2496
|
+
return EvaluationError(f"barrier_reset() error: {str(e)}")
|
|
2497
|
+
|
|
2498
|
+
# Register concurrency builtins
|
|
2499
|
+
self.builtins.update({
|
|
2500
|
+
"send": Builtin(_send, "send"),
|
|
2501
|
+
"receive": Builtin(_receive, "receive"),
|
|
2502
|
+
"close_channel": Builtin(_close_channel, "close_channel"),
|
|
2503
|
+
"async": Builtin(_async, "async"),
|
|
2504
|
+
"sleep": Builtin(_sleep, "sleep"),
|
|
2505
|
+
"spawn": Builtin(_spawn, "spawn"),
|
|
2506
|
+
"wait_group": Builtin(_wait_group, "wait_group"),
|
|
2507
|
+
"wg_add": Builtin(_wg_add, "wg_add"),
|
|
2508
|
+
"wg_done": Builtin(_wg_done, "wg_done"),
|
|
2509
|
+
"wg_wait": Builtin(_wg_wait, "wg_wait"),
|
|
2510
|
+
"barrier": Builtin(_barrier, "barrier"),
|
|
2511
|
+
"barrier_wait": Builtin(_barrier_wait, "barrier_wait"),
|
|
2512
|
+
"barrier_reset": Builtin(_barrier_reset, "barrier_reset"),
|
|
2513
|
+
})
|
|
2514
|
+
|
|
2515
|
+
def _register_blockchain_builtins(self):
|
|
2516
|
+
"""Register blockchain cryptographic and utility functions"""
|
|
2517
|
+
from ..blockchain.crypto import CryptoPlugin
|
|
2518
|
+
from ..blockchain.transaction import get_current_tx, create_tx_context
|
|
2519
|
+
|
|
2520
|
+
# hash(data, algorithm?)
|
|
2521
|
+
def _hash(*a):
|
|
2522
|
+
if len(a) < 1:
|
|
2523
|
+
return EvaluationError("hash() requires at least 1 argument: data, [algorithm]")
|
|
2524
|
+
|
|
2525
|
+
data = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2526
|
+
algorithm = a[1].value if len(a) > 1 and hasattr(a[1], 'value') else 'SHA256'
|
|
2527
|
+
|
|
2528
|
+
try:
|
|
2529
|
+
result = CryptoPlugin.hash_data(data, algorithm)
|
|
2530
|
+
return String(result)
|
|
2531
|
+
except Exception as e:
|
|
2532
|
+
return EvaluationError(f"Hash error: {str(e)}")
|
|
2533
|
+
|
|
2534
|
+
# keccak256(data)
|
|
2535
|
+
def _keccak256(*a):
|
|
2536
|
+
if len(a) != 1:
|
|
2537
|
+
return EvaluationError("keccak256() expects 1 argument: data")
|
|
2538
|
+
|
|
2539
|
+
data = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2540
|
+
|
|
2541
|
+
try:
|
|
2542
|
+
result = CryptoPlugin.keccak256(data)
|
|
2543
|
+
return String(result)
|
|
2544
|
+
except Exception as e:
|
|
2545
|
+
return EvaluationError(f"Keccak256 error: {str(e)}")
|
|
2546
|
+
|
|
2547
|
+
# signature(data, private_key, algorithm?)
|
|
2548
|
+
def _signature(*a):
|
|
2549
|
+
if len(a) < 2:
|
|
2550
|
+
return EvaluationError("signature() requires at least 2 arguments: data, private_key, [algorithm]")
|
|
2551
|
+
|
|
2552
|
+
data = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2553
|
+
private_key = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2554
|
+
algorithm = a[2].value if len(a) > 2 and hasattr(a[2], 'value') else 'ECDSA'
|
|
2555
|
+
|
|
2556
|
+
try:
|
|
2557
|
+
result = CryptoPlugin.sign_data(data, private_key, algorithm)
|
|
2558
|
+
return String(result)
|
|
2559
|
+
except Exception as e:
|
|
2560
|
+
return EvaluationError(f"Signature error: {str(e)}")
|
|
2561
|
+
|
|
2562
|
+
# verify_sig(data, signature, public_key, algorithm?)
|
|
2563
|
+
def _verify_sig(*a):
|
|
2564
|
+
if len(a) < 3:
|
|
2565
|
+
return EvaluationError("verify_sig() requires at least 3 arguments: data, signature, public_key, [algorithm]")
|
|
2566
|
+
|
|
2567
|
+
data = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2568
|
+
signature = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2569
|
+
public_key = a[2].value if hasattr(a[2], 'value') else str(a[2])
|
|
2570
|
+
algorithm = a[3].value if len(a) > 3 and hasattr(a[3], 'value') else 'ECDSA'
|
|
2571
|
+
|
|
2572
|
+
try:
|
|
2573
|
+
result = CryptoPlugin.verify_signature(data, signature, public_key, algorithm)
|
|
2574
|
+
return TRUE if result else FALSE
|
|
2575
|
+
except Exception as e:
|
|
2576
|
+
return EvaluationError(f"Verification error: {str(e)}")
|
|
2577
|
+
|
|
2578
|
+
# tx object - returns transaction context
|
|
2579
|
+
def _tx(*a):
|
|
2580
|
+
# Get or create TX context
|
|
2581
|
+
tx = get_current_tx()
|
|
2582
|
+
if tx is None:
|
|
2583
|
+
tx = create_tx_context(caller="system", gas_limit=1000000)
|
|
2584
|
+
|
|
2585
|
+
# Return as Map object
|
|
2586
|
+
return Map({
|
|
2587
|
+
String("caller"): String(tx.caller),
|
|
2588
|
+
String("timestamp"): Integer(int(tx.timestamp)),
|
|
2589
|
+
String("block_hash"): String(tx.block_hash),
|
|
2590
|
+
String("gas_used"): Integer(tx.gas_used),
|
|
2591
|
+
String("gas_remaining"): Integer(tx.gas_remaining),
|
|
2592
|
+
String("gas_limit"): Integer(tx.gas_limit)
|
|
2593
|
+
})
|
|
2594
|
+
|
|
2595
|
+
# gas object - returns gas tracking info
|
|
2596
|
+
def _gas(*a):
|
|
2597
|
+
# Get or create TX context
|
|
2598
|
+
tx = get_current_tx()
|
|
2599
|
+
if tx is None:
|
|
2600
|
+
tx = create_tx_context(caller="system", gas_limit=1000000)
|
|
2601
|
+
|
|
2602
|
+
# Return as Map object
|
|
2603
|
+
return Map({
|
|
2604
|
+
String("used"): Integer(tx.gas_used),
|
|
2605
|
+
String("remaining"): Integer(tx.gas_remaining),
|
|
2606
|
+
String("limit"): Integer(tx.gas_limit)
|
|
2607
|
+
})
|
|
2608
|
+
|
|
2609
|
+
self.builtins.update({
|
|
2610
|
+
"hash": Builtin(_hash, "hash"),
|
|
2611
|
+
"keccak256": Builtin(_keccak256, "keccak256"),
|
|
2612
|
+
"signature": Builtin(_signature, "signature"),
|
|
2613
|
+
"verify_sig": Builtin(_verify_sig, "verify_sig"),
|
|
2614
|
+
"tx": Builtin(_tx, "tx"),
|
|
2615
|
+
"gas": Builtin(_gas, "gas"),
|
|
2616
|
+
})
|
|
2617
|
+
|
|
2618
|
+
# Register advanced feature builtins
|
|
2619
|
+
self._register_advanced_feature_builtins()
|
|
2620
|
+
|
|
2621
|
+
def _register_advanced_feature_builtins(self):
|
|
2622
|
+
"""Register builtins for persistence, policy, and dependency injection"""
|
|
2623
|
+
|
|
2624
|
+
# === PERSISTENCE & MEMORY BUILTINS ===
|
|
2625
|
+
|
|
2626
|
+
def _persistent_set(*a):
|
|
2627
|
+
"""Set a persistent variable: persistent_set(name, value)"""
|
|
2628
|
+
if len(a) != 2:
|
|
2629
|
+
return EvaluationError("persistent_set() takes 2 arguments: name, value")
|
|
2630
|
+
if not isinstance(a[0], String):
|
|
2631
|
+
return EvaluationError("persistent_set() name must be a string")
|
|
2632
|
+
|
|
2633
|
+
# Get current environment from evaluator context
|
|
2634
|
+
env = getattr(self, '_current_env', None)
|
|
2635
|
+
if env and hasattr(env, 'set_persistent'):
|
|
2636
|
+
name = a[0].value
|
|
2637
|
+
value = a[1]
|
|
2638
|
+
env.set_persistent(name, value)
|
|
2639
|
+
return String(f"Persistent variable '{name}' set")
|
|
2640
|
+
return EvaluationError("Persistence not enabled in this environment")
|
|
2641
|
+
|
|
2642
|
+
def _persistent_get(*a):
|
|
2643
|
+
"""Get a persistent variable: persistent_get(name, [default])"""
|
|
2644
|
+
if len(a) < 1 or len(a) > 2:
|
|
2645
|
+
return EvaluationError("persistent_get() takes 1 or 2 arguments: name, [default]")
|
|
2646
|
+
if not isinstance(a[0], String):
|
|
2647
|
+
return EvaluationError("persistent_get() name must be a string")
|
|
2648
|
+
|
|
2649
|
+
env = getattr(self, '_current_env', None)
|
|
2650
|
+
if env and hasattr(env, 'get_persistent'):
|
|
2651
|
+
name = a[0].value
|
|
2652
|
+
default = a[1] if len(a) > 1 else NULL
|
|
2653
|
+
value = env.get_persistent(name, default)
|
|
2654
|
+
return value if value is not None else default
|
|
2655
|
+
return NULL
|
|
2656
|
+
|
|
2657
|
+
def _persistent_delete(*a):
|
|
2658
|
+
"""Delete a persistent variable: persistent_delete(name)"""
|
|
2659
|
+
if len(a) != 1:
|
|
2660
|
+
return EvaluationError("persistent_delete() takes 1 argument: name")
|
|
2661
|
+
if not isinstance(a[0], String):
|
|
2662
|
+
return EvaluationError("persistent_delete() name must be a string")
|
|
2663
|
+
|
|
2664
|
+
env = getattr(self, '_current_env', None)
|
|
2665
|
+
if env and hasattr(env, 'delete_persistent'):
|
|
2666
|
+
name = a[0].value
|
|
2667
|
+
env.delete_persistent(name)
|
|
2668
|
+
return String(f"Persistent variable '{name}' deleted")
|
|
2669
|
+
return NULL
|
|
2670
|
+
|
|
2671
|
+
def _memory_stats(*a):
|
|
2672
|
+
"""Get memory tracking statistics: memory_stats()"""
|
|
2673
|
+
import sys
|
|
2674
|
+
import gc
|
|
2675
|
+
|
|
2676
|
+
# Get process memory usage
|
|
2677
|
+
try:
|
|
2678
|
+
import psutil
|
|
2679
|
+
process = psutil.Process()
|
|
2680
|
+
mem_info = process.memory_info()
|
|
2681
|
+
current_bytes = mem_info.rss # Resident Set Size
|
|
2682
|
+
peak_bytes = getattr(mem_info, 'peak_wset', mem_info.rss) # Windows has peak_wset
|
|
2683
|
+
except (ImportError, AttributeError):
|
|
2684
|
+
# Fallback: use Python's internal memory tracking
|
|
2685
|
+
current_bytes = sys.getsizeof(gc.get_objects())
|
|
2686
|
+
peak_bytes = current_bytes
|
|
2687
|
+
|
|
2688
|
+
# Get GC statistics
|
|
2689
|
+
gc_count = len(gc.get_objects())
|
|
2690
|
+
gc_collections = sum(gc.get_count())
|
|
2691
|
+
|
|
2692
|
+
# Get environment-specific tracking if available
|
|
2693
|
+
env = getattr(self, '_current_env', None)
|
|
2694
|
+
tracked_objects = 0
|
|
2695
|
+
if env and hasattr(env, 'get_memory_stats'):
|
|
2696
|
+
stats = env.get_memory_stats()
|
|
2697
|
+
tracked_objects = stats.get("tracked_objects", 0)
|
|
2698
|
+
|
|
2699
|
+
return Map({
|
|
2700
|
+
String("current"): Integer(current_bytes),
|
|
2701
|
+
String("peak"): Integer(peak_bytes),
|
|
2702
|
+
String("gc_count"): Integer(gc_collections),
|
|
2703
|
+
String("objects"): Integer(gc_count),
|
|
2704
|
+
String("tracked_objects"): Integer(tracked_objects)
|
|
2705
|
+
})
|
|
2706
|
+
|
|
2707
|
+
# === POLICY & PROTECTION BUILTINS ===
|
|
2708
|
+
|
|
2709
|
+
def _create_policy(*a):
|
|
2710
|
+
"""Create a protection policy: create_policy(name, rules_map)"""
|
|
2711
|
+
if len(a) != 2:
|
|
2712
|
+
return EvaluationError("create_policy() takes 2 arguments: name, rules")
|
|
2713
|
+
if not isinstance(a[0], String):
|
|
2714
|
+
return EvaluationError("create_policy() name must be a string")
|
|
2715
|
+
if not isinstance(a[1], Map):
|
|
2716
|
+
return EvaluationError("create_policy() rules must be a Map")
|
|
2717
|
+
|
|
2718
|
+
from ..policy_engine import get_policy_registry, PolicyBuilder, EnforcementLevel
|
|
2719
|
+
|
|
2720
|
+
name = a[0].value
|
|
2721
|
+
rules = a[1].pairs
|
|
2722
|
+
|
|
2723
|
+
builder = PolicyBuilder(name)
|
|
2724
|
+
builder.set_enforcement(EnforcementLevel.STRICT)
|
|
2725
|
+
|
|
2726
|
+
# Parse rules from Map
|
|
2727
|
+
for key, value in rules.items():
|
|
2728
|
+
key_str = key.value if hasattr(key, 'value') else str(key)
|
|
2729
|
+
if key_str == "verify" and isinstance(value, List):
|
|
2730
|
+
for cond in value.elements:
|
|
2731
|
+
cond_str = cond.value if hasattr(cond, 'value') else str(cond)
|
|
2732
|
+
builder.add_verify_rule(cond_str)
|
|
2733
|
+
elif key_str == "restrict" and isinstance(value, Map):
|
|
2734
|
+
for field, constraints in value.pairs.items():
|
|
2735
|
+
field_str = field.value if hasattr(field, 'value') else str(field)
|
|
2736
|
+
constraint_list = []
|
|
2737
|
+
if isinstance(constraints, List):
|
|
2738
|
+
for c in constraints.elements:
|
|
2739
|
+
constraint_list.append(c.value if hasattr(c, 'value') else str(c))
|
|
2740
|
+
builder.add_restrict_rule(field_str, constraint_list)
|
|
2741
|
+
|
|
2742
|
+
policy = builder.build()
|
|
2743
|
+
registry = get_policy_registry()
|
|
2744
|
+
registry.register(name, policy)
|
|
2745
|
+
|
|
2746
|
+
return String(f"Policy '{name}' created and registered")
|
|
2747
|
+
|
|
2748
|
+
def _check_policy(*a):
|
|
2749
|
+
"""Check policy enforcement: check_policy(target, context_map)"""
|
|
2750
|
+
if len(a) != 2:
|
|
2751
|
+
return EvaluationError("check_policy() takes 2 arguments: target, context")
|
|
2752
|
+
if not isinstance(a[0], String):
|
|
2753
|
+
return EvaluationError("check_policy() target must be a string")
|
|
2754
|
+
if not isinstance(a[1], Map):
|
|
2755
|
+
return EvaluationError("check_policy() context must be a Map")
|
|
2756
|
+
|
|
2757
|
+
from ..policy_engine import get_policy_registry
|
|
2758
|
+
|
|
2759
|
+
target = a[0].value
|
|
2760
|
+
context = {}
|
|
2761
|
+
for k, v in a[1].pairs.items():
|
|
2762
|
+
key_str = k.value if hasattr(k, 'value') else str(k)
|
|
2763
|
+
val = v.value if hasattr(v, 'value') else v
|
|
2764
|
+
context[key_str] = val
|
|
2765
|
+
|
|
2766
|
+
registry = get_policy_registry()
|
|
2767
|
+
policy = registry.get(target)
|
|
2768
|
+
|
|
2769
|
+
if policy is None:
|
|
2770
|
+
return String(f"No policy found for '{target}'")
|
|
2771
|
+
|
|
2772
|
+
result = policy.enforce(context)
|
|
2773
|
+
if result["success"]:
|
|
2774
|
+
return TRUE
|
|
2775
|
+
else:
|
|
2776
|
+
return String(f"Policy violation: {result['message']}")
|
|
2777
|
+
|
|
2778
|
+
# === DEPENDENCY INJECTION BUILTINS ===
|
|
2779
|
+
|
|
2780
|
+
def _register_dependency(*a):
|
|
2781
|
+
"""Register a dependency: register_dependency(name, value, [module])"""
|
|
2782
|
+
if len(a) < 2 or len(a) > 3:
|
|
2783
|
+
return EvaluationError("register_dependency() takes 2 or 3 arguments: name, value, [module]")
|
|
2784
|
+
if not isinstance(a[0], String):
|
|
2785
|
+
return EvaluationError("register_dependency() name must be a string")
|
|
2786
|
+
|
|
2787
|
+
from ..dependency_injection import get_di_registry
|
|
2788
|
+
|
|
2789
|
+
name = a[0].value
|
|
2790
|
+
value = a[1]
|
|
2791
|
+
module = a[2].value if len(a) > 2 and isinstance(a[2], String) else "__main__"
|
|
2792
|
+
|
|
2793
|
+
registry = get_di_registry()
|
|
2794
|
+
container = registry.get_container(module)
|
|
2795
|
+
if not container:
|
|
2796
|
+
# Create container if it doesn't exist
|
|
2797
|
+
registry.register_module(module)
|
|
2798
|
+
container = registry.get_container(module)
|
|
2799
|
+
# Declare and provide the dependency
|
|
2800
|
+
container.declare_dependency(name, "any", False)
|
|
2801
|
+
container.provide(name, value)
|
|
2802
|
+
|
|
2803
|
+
return String(f"Dependency '{name}' registered in module '{module}'")
|
|
2804
|
+
|
|
2805
|
+
def _mock_dependency(*a):
|
|
2806
|
+
"""Create a mock for dependency: mock_dependency(name, mock_value, [module])"""
|
|
2807
|
+
if len(a) < 2 or len(a) > 3:
|
|
2808
|
+
return EvaluationError("mock_dependency() takes 2 or 3 arguments: name, mock, [module]")
|
|
2809
|
+
if not isinstance(a[0], String):
|
|
2810
|
+
return EvaluationError("mock_dependency() name must be a string")
|
|
2811
|
+
|
|
2812
|
+
from ..dependency_injection import get_di_registry, ExecutionMode
|
|
2813
|
+
|
|
2814
|
+
name = a[0].value
|
|
2815
|
+
mock = a[1]
|
|
2816
|
+
module = a[2].value if len(a) > 2 and isinstance(a[2], String) else "__main__"
|
|
2817
|
+
|
|
2818
|
+
registry = get_di_registry()
|
|
2819
|
+
container = registry.get_container(module)
|
|
2820
|
+
if not container:
|
|
2821
|
+
# Create container if it doesn't exist
|
|
2822
|
+
registry.register_module(module)
|
|
2823
|
+
container = registry.get_container(module)
|
|
2824
|
+
# Declare and mock the dependency
|
|
2825
|
+
if name not in container.contracts:
|
|
2826
|
+
container.declare_dependency(name, "any", False)
|
|
2827
|
+
container.mock(name, mock)
|
|
2828
|
+
|
|
2829
|
+
return String(f"Mock for '{name}' registered in module '{module}'")
|
|
2830
|
+
|
|
2831
|
+
def _clear_mocks(*a):
|
|
2832
|
+
"""Clear all mocks: clear_mocks([module])"""
|
|
2833
|
+
from ..dependency_injection import get_di_registry
|
|
2834
|
+
|
|
2835
|
+
module = a[0].value if len(a) > 0 and isinstance(a[0], String) else "__main__"
|
|
2836
|
+
|
|
2837
|
+
registry = get_di_registry()
|
|
2838
|
+
container = registry.get_container(module)
|
|
2839
|
+
if container:
|
|
2840
|
+
container.clear_mocks()
|
|
2841
|
+
return String(f"All mocks cleared in module '{module}'")
|
|
2842
|
+
return String(f"Module '{module}' not registered")
|
|
2843
|
+
|
|
2844
|
+
def _set_execution_mode(*a):
|
|
2845
|
+
"""Set execution mode: set_execution_mode(mode_string)"""
|
|
2846
|
+
if len(a) != 1:
|
|
2847
|
+
return EvaluationError("set_execution_mode() takes 1 argument: mode")
|
|
2848
|
+
if not isinstance(a[0], String):
|
|
2849
|
+
return EvaluationError("set_execution_mode() mode must be a string")
|
|
2850
|
+
|
|
2851
|
+
from ..dependency_injection import ExecutionMode
|
|
2852
|
+
|
|
2853
|
+
mode_str = a[0].value.upper()
|
|
2854
|
+
try:
|
|
2855
|
+
mode = ExecutionMode[mode_str]
|
|
2856
|
+
# Store in current environment
|
|
2857
|
+
env = getattr(self, '_current_env', None)
|
|
2858
|
+
if env:
|
|
2859
|
+
env.set("__execution_mode__", String(mode_str))
|
|
2860
|
+
return String(f"Execution mode set to {mode.name}")
|
|
2861
|
+
except KeyError:
|
|
2862
|
+
return EvaluationError(f"Invalid execution mode: {mode_str}. Valid: PRODUCTION, DEBUG, TEST, SANDBOX")
|
|
2863
|
+
|
|
2864
|
+
# Register all advanced feature builtins
|
|
2865
|
+
self.builtins.update({
|
|
2866
|
+
# Persistence
|
|
2867
|
+
"persistent_set": Builtin(_persistent_set, "persistent_set"),
|
|
2868
|
+
"persistent_get": Builtin(_persistent_get, "persistent_get"),
|
|
2869
|
+
"persistent_delete": Builtin(_persistent_delete, "persistent_delete"),
|
|
2870
|
+
"memory_stats": Builtin(_memory_stats, "memory_stats"),
|
|
2871
|
+
# Policy
|
|
2872
|
+
"create_policy": Builtin(_create_policy, "create_policy"),
|
|
2873
|
+
"check_policy": Builtin(_check_policy, "check_policy"),
|
|
2874
|
+
# Dependency Injection
|
|
2875
|
+
"register_dependency": Builtin(_register_dependency, "register_dependency"),
|
|
2876
|
+
"mock_dependency": Builtin(_mock_dependency, "mock_dependency"),
|
|
2877
|
+
"clear_mocks": Builtin(_clear_mocks, "clear_mocks"),
|
|
2878
|
+
"set_execution_mode": Builtin(_set_execution_mode, "set_execution_mode"),
|
|
2879
|
+
})
|
|
2880
|
+
|
|
2881
|
+
def _register_main_entry_point_builtins(self):
|
|
2882
|
+
"""Register builtins for main entry point pattern and continuous execution"""
|
|
2883
|
+
import signal
|
|
2884
|
+
import time as time_module
|
|
2885
|
+
|
|
2886
|
+
# Storage for lifecycle hooks and signal handlers
|
|
2887
|
+
self._lifecycle_hooks = {'on_start': [], 'on_exit': []}
|
|
2888
|
+
self._signal_handlers = {}
|
|
2889
|
+
|
|
2890
|
+
def _run(*a):
|
|
2891
|
+
"""
|
|
2892
|
+
Keep the program running until interrupted (Ctrl+C).
|
|
2893
|
+
Useful for servers, event loops, or long-running programs.
|
|
2894
|
+
|
|
2895
|
+
Enhanced version supports:
|
|
2896
|
+
- callback with arguments
|
|
2897
|
+
- interval timing
|
|
2898
|
+
- lifecycle hooks (on_start, on_exit)
|
|
2899
|
+
|
|
2900
|
+
Usage:
|
|
2901
|
+
if __MODULE__ == "__main__":
|
|
2902
|
+
run()
|
|
2903
|
+
|
|
2904
|
+
or with a callback:
|
|
2905
|
+
if __MODULE__ == "__main__":
|
|
2906
|
+
run(lambda: print("Still running..."))
|
|
2907
|
+
|
|
2908
|
+
or with callback and interval:
|
|
2909
|
+
if __MODULE__ == "__main__":
|
|
2910
|
+
run(callback, 0.5) # Run every 500ms
|
|
2911
|
+
|
|
2912
|
+
or with callback and arguments:
|
|
2913
|
+
if __MODULE__ == "__main__":
|
|
2914
|
+
run(server.process_requests, 1.0, [port, host])
|
|
2915
|
+
"""
|
|
2916
|
+
callback = None
|
|
2917
|
+
interval = 1.0 # Default interval in seconds
|
|
2918
|
+
callback_args = []
|
|
2919
|
+
|
|
2920
|
+
if len(a) >= 1:
|
|
2921
|
+
# First argument is the callback function
|
|
2922
|
+
callback = a[0]
|
|
2923
|
+
if not isinstance(callback, (Action, LambdaFunction)):
|
|
2924
|
+
return EvaluationError("run() callback must be a function")
|
|
2925
|
+
|
|
2926
|
+
if len(a) >= 2:
|
|
2927
|
+
# Second argument is the interval
|
|
2928
|
+
interval_obj = a[1]
|
|
2929
|
+
if isinstance(interval_obj, (Integer, Float)):
|
|
2930
|
+
interval = float(interval_obj.value)
|
|
2931
|
+
else:
|
|
2932
|
+
return EvaluationError("run() interval must be a number")
|
|
2933
|
+
|
|
2934
|
+
if len(a) >= 3:
|
|
2935
|
+
# Third argument is callback arguments
|
|
2936
|
+
if isinstance(a[2], List):
|
|
2937
|
+
callback_args = a[2].elements
|
|
2938
|
+
else:
|
|
2939
|
+
callback_args = [a[2]]
|
|
2940
|
+
|
|
2941
|
+
print("🚀 Program running. Press Ctrl+C to exit.")
|
|
2942
|
+
|
|
2943
|
+
# Execute on_start hooks
|
|
2944
|
+
for hook in self._lifecycle_hooks.get('on_start', []):
|
|
2945
|
+
try:
|
|
2946
|
+
result = self.apply_function(hook, [])
|
|
2947
|
+
if is_error(result):
|
|
2948
|
+
print(f"⚠️ on_start hook error: {result.message}")
|
|
2949
|
+
except Exception as e:
|
|
2950
|
+
print(f"⚠️ on_start hook error: {str(e)}")
|
|
2951
|
+
|
|
2952
|
+
# Setup signal handler for graceful shutdown
|
|
2953
|
+
shutdown_requested = [False] # Use list for closure mutability
|
|
2954
|
+
|
|
2955
|
+
def signal_handler(sig, frame):
|
|
2956
|
+
shutdown_requested[0] = True
|
|
2957
|
+
print("\n⏹️ Shutdown requested. Cleaning up...")
|
|
2958
|
+
|
|
2959
|
+
# Execute custom signal handlers if registered
|
|
2960
|
+
sig_name = signal.Signals(sig).name
|
|
2961
|
+
if sig_name in self._signal_handlers:
|
|
2962
|
+
for handler in self._signal_handlers[sig_name]:
|
|
2963
|
+
try:
|
|
2964
|
+
self.apply_function(handler, [String(sig_name)])
|
|
2965
|
+
except Exception as e:
|
|
2966
|
+
print(f"⚠️ Signal handler error: {str(e)}")
|
|
2967
|
+
|
|
2968
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
2969
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
2970
|
+
|
|
2971
|
+
try:
|
|
2972
|
+
# Keep running until interrupted
|
|
2973
|
+
while not shutdown_requested[0]:
|
|
2974
|
+
if callback:
|
|
2975
|
+
# Execute callback function with arguments
|
|
2976
|
+
result = self.apply_function(callback, callback_args)
|
|
2977
|
+
if is_error(result):
|
|
2978
|
+
print(f"⚠️ Callback error: {result.message}")
|
|
2979
|
+
|
|
2980
|
+
# Sleep for the interval
|
|
2981
|
+
time_module.sleep(interval)
|
|
2982
|
+
|
|
2983
|
+
except KeyboardInterrupt:
|
|
2984
|
+
print("\n⏹️ Interrupted. Exiting...")
|
|
2985
|
+
|
|
2986
|
+
except Exception as e:
|
|
2987
|
+
print(f"❌ Error in run loop: {str(e)}")
|
|
2988
|
+
return EvaluationError(f"run() error: {str(e)}")
|
|
2989
|
+
|
|
2990
|
+
finally:
|
|
2991
|
+
# Execute on_exit hooks
|
|
2992
|
+
for hook in self._lifecycle_hooks.get('on_exit', []):
|
|
2993
|
+
try:
|
|
2994
|
+
result = self.apply_function(hook, [])
|
|
2995
|
+
if is_error(result):
|
|
2996
|
+
print(f"⚠️ on_exit hook error: {result.message}")
|
|
2997
|
+
except Exception as e:
|
|
2998
|
+
print(f"⚠️ on_exit hook error: {str(e)}")
|
|
2999
|
+
|
|
3000
|
+
print("✅ Program terminated gracefully.")
|
|
3001
|
+
|
|
3002
|
+
return NULL
|
|
3003
|
+
|
|
3004
|
+
def _execute(*a):
|
|
3005
|
+
"""
|
|
3006
|
+
Alias for run() - keeps the program executing until interrupted.
|
|
3007
|
+
|
|
3008
|
+
Usage:
|
|
3009
|
+
if __MODULE__ == "__main__":
|
|
3010
|
+
execute()
|
|
3011
|
+
"""
|
|
3012
|
+
return _run(*a)
|
|
3013
|
+
|
|
3014
|
+
def _is_main(*a):
|
|
3015
|
+
"""
|
|
3016
|
+
Check if the current module is being run as the main program.
|
|
3017
|
+
Returns true if __MODULE__ == "__main__", false otherwise.
|
|
3018
|
+
|
|
3019
|
+
Usage:
|
|
3020
|
+
if is_main():
|
|
3021
|
+
print("Running as main program")
|
|
3022
|
+
"""
|
|
3023
|
+
env = getattr(self, '_current_env', None)
|
|
3024
|
+
if env:
|
|
3025
|
+
module_name = env.get('__MODULE__')
|
|
3026
|
+
if module_name and isinstance(module_name, String):
|
|
3027
|
+
return TRUE if module_name.value == "__main__" else FALSE
|
|
3028
|
+
return FALSE
|
|
3029
|
+
|
|
3030
|
+
def _exit_program(*a):
|
|
3031
|
+
"""
|
|
3032
|
+
Exit the program with an optional exit code.
|
|
3033
|
+
|
|
3034
|
+
Usage:
|
|
3035
|
+
exit_program() # Exit with code 0
|
|
3036
|
+
exit_program(1) # Exit with code 1
|
|
3037
|
+
"""
|
|
3038
|
+
exit_code = 0
|
|
3039
|
+
if len(a) >= 1:
|
|
3040
|
+
if isinstance(a[0], Integer):
|
|
3041
|
+
exit_code = a[0].value
|
|
3042
|
+
else:
|
|
3043
|
+
return EvaluationError("exit_program() exit code must be an integer")
|
|
3044
|
+
|
|
3045
|
+
# Execute on_exit hooks before exiting
|
|
3046
|
+
for hook in self._lifecycle_hooks.get('on_exit', []):
|
|
3047
|
+
try:
|
|
3048
|
+
result = self.apply_function(hook, [])
|
|
3049
|
+
if is_error(result):
|
|
3050
|
+
print(f"⚠️ on_exit hook error: {result.message}")
|
|
3051
|
+
except Exception as e:
|
|
3052
|
+
print(f"⚠️ on_exit hook error: {str(e)}")
|
|
3053
|
+
|
|
3054
|
+
print(f"👋 Exiting with code {exit_code}")
|
|
3055
|
+
sys.exit(exit_code)
|
|
3056
|
+
|
|
3057
|
+
def _on_start(*a):
|
|
3058
|
+
"""
|
|
3059
|
+
Register a callback to run when the program starts (before run loop).
|
|
3060
|
+
|
|
3061
|
+
Usage:
|
|
3062
|
+
on_start(lambda: print("Starting up..."))
|
|
3063
|
+
on_start(initialize_database)
|
|
3064
|
+
"""
|
|
3065
|
+
if len(a) != 1:
|
|
3066
|
+
return EvaluationError("on_start() requires exactly one function argument")
|
|
3067
|
+
|
|
3068
|
+
callback = a[0]
|
|
3069
|
+
if not isinstance(callback, (Action, LambdaFunction)):
|
|
3070
|
+
return EvaluationError("on_start() argument must be a function")
|
|
3071
|
+
|
|
3072
|
+
self._lifecycle_hooks['on_start'].append(callback)
|
|
3073
|
+
return NULL
|
|
3074
|
+
|
|
3075
|
+
def _on_exit(*a):
|
|
3076
|
+
"""
|
|
3077
|
+
Register a callback to run when the program exits (after run loop).
|
|
3078
|
+
|
|
3079
|
+
Usage:
|
|
3080
|
+
on_exit(lambda: print("Cleaning up..."))
|
|
3081
|
+
on_exit(close_connections)
|
|
3082
|
+
"""
|
|
3083
|
+
if len(a) != 1:
|
|
3084
|
+
return EvaluationError("on_exit() requires exactly one function argument")
|
|
3085
|
+
|
|
3086
|
+
callback = a[0]
|
|
3087
|
+
if not isinstance(callback, (Action, LambdaFunction)):
|
|
3088
|
+
return EvaluationError("on_exit() argument must be a function")
|
|
3089
|
+
|
|
3090
|
+
self._lifecycle_hooks['on_exit'].append(callback)
|
|
3091
|
+
return NULL
|
|
3092
|
+
|
|
3093
|
+
def _signal_handler(*a):
|
|
3094
|
+
"""
|
|
3095
|
+
Register a custom signal handler for specific signals.
|
|
3096
|
+
|
|
3097
|
+
Usage:
|
|
3098
|
+
signal_handler("SIGINT", lambda sig: print("Caught SIGINT"))
|
|
3099
|
+
signal_handler("SIGTERM", cleanup_handler)
|
|
3100
|
+
"""
|
|
3101
|
+
if len(a) != 2:
|
|
3102
|
+
return EvaluationError("signal_handler() requires signal name and callback function")
|
|
3103
|
+
|
|
3104
|
+
signal_name = _to_str(a[0])
|
|
3105
|
+
callback = a[1]
|
|
3106
|
+
|
|
3107
|
+
if not isinstance(callback, (Action, LambdaFunction)):
|
|
3108
|
+
return EvaluationError("signal_handler() callback must be a function")
|
|
3109
|
+
|
|
3110
|
+
if signal_name not in self._signal_handlers:
|
|
3111
|
+
self._signal_handlers[signal_name] = []
|
|
3112
|
+
|
|
3113
|
+
self._signal_handlers[signal_name].append(callback)
|
|
3114
|
+
return NULL
|
|
3115
|
+
|
|
3116
|
+
def _schedule(*a):
|
|
3117
|
+
"""
|
|
3118
|
+
Schedule multiple tasks with different intervals to run in parallel.
|
|
3119
|
+
|
|
3120
|
+
Usage:
|
|
3121
|
+
schedule([
|
|
3122
|
+
{interval: 1, action: check_queue},
|
|
3123
|
+
{interval: 5, action: save_state},
|
|
3124
|
+
{interval: 60, action: cleanup}
|
|
3125
|
+
])
|
|
3126
|
+
|
|
3127
|
+
Returns: List of task IDs
|
|
3128
|
+
"""
|
|
3129
|
+
if len(a) != 1:
|
|
3130
|
+
return EvaluationError("schedule() requires a list of task definitions")
|
|
3131
|
+
|
|
3132
|
+
tasks_arg = a[0]
|
|
3133
|
+
if not isinstance(tasks_arg, List):
|
|
3134
|
+
return EvaluationError("schedule() argument must be a list")
|
|
3135
|
+
|
|
3136
|
+
import threading
|
|
3137
|
+
import time as time_module
|
|
3138
|
+
|
|
3139
|
+
task_ids = []
|
|
3140
|
+
|
|
3141
|
+
for i, task_def in enumerate(tasks_arg.elements):
|
|
3142
|
+
if not isinstance(task_def, Map):
|
|
3143
|
+
return EvaluationError(f"Task {i} must be a map with 'interval' and 'action' keys")
|
|
3144
|
+
|
|
3145
|
+
# Extract interval and action - map keys can be strings or String objects
|
|
3146
|
+
interval_obj = None
|
|
3147
|
+
action_obj = None
|
|
3148
|
+
|
|
3149
|
+
for key, value in task_def.pairs.items():
|
|
3150
|
+
key_str = key if isinstance(key, str) else (key.value if hasattr(key, 'value') else str(key))
|
|
3151
|
+
if key_str == "interval":
|
|
3152
|
+
interval_obj = value
|
|
3153
|
+
elif key_str == "action":
|
|
3154
|
+
action_obj = value
|
|
3155
|
+
|
|
3156
|
+
if not interval_obj or not action_obj:
|
|
3157
|
+
return EvaluationError(f"Task {i} must have 'interval' and 'action' keys")
|
|
3158
|
+
|
|
3159
|
+
if isinstance(interval_obj, (Integer, Float)):
|
|
3160
|
+
interval = float(interval_obj.value)
|
|
3161
|
+
else:
|
|
3162
|
+
return EvaluationError(f"Task {i} interval must be a number")
|
|
3163
|
+
|
|
3164
|
+
if not isinstance(action_obj, (Action, LambdaFunction)):
|
|
3165
|
+
return EvaluationError(f"Task {i} action must be a function")
|
|
3166
|
+
|
|
3167
|
+
# Create task thread
|
|
3168
|
+
task_id = f"task_{i}_{id(action_obj)}"
|
|
3169
|
+
task_ids.append(String(task_id))
|
|
3170
|
+
|
|
3171
|
+
def task_worker(action, interval_sec, task_id):
|
|
3172
|
+
"""Worker function that runs the task at specified interval"""
|
|
3173
|
+
while True:
|
|
3174
|
+
try:
|
|
3175
|
+
time_module.sleep(interval_sec)
|
|
3176
|
+
result = self.apply_function(action, [])
|
|
3177
|
+
if is_error(result):
|
|
3178
|
+
print(f"⚠️ Task {task_id} error: {result.message}")
|
|
3179
|
+
except Exception as e:
|
|
3180
|
+
print(f"⚠️ Task {task_id} exception: {str(e)}")
|
|
3181
|
+
break
|
|
3182
|
+
|
|
3183
|
+
# Start thread in daemon mode so it exits when main program exits
|
|
3184
|
+
thread = threading.Thread(
|
|
3185
|
+
target=task_worker,
|
|
3186
|
+
args=(action_obj, interval, task_id),
|
|
3187
|
+
daemon=True
|
|
3188
|
+
)
|
|
3189
|
+
thread.start()
|
|
3190
|
+
|
|
3191
|
+
return List(task_ids)
|
|
3192
|
+
|
|
3193
|
+
def _sleep(*args):
|
|
3194
|
+
"""
|
|
3195
|
+
Sleep for specified seconds.
|
|
3196
|
+
|
|
3197
|
+
Usage:
|
|
3198
|
+
sleep(2) # Sleep for 2 seconds
|
|
3199
|
+
sleep(0.5) # Sleep for 0.5 seconds
|
|
3200
|
+
"""
|
|
3201
|
+
if len(args) != 1:
|
|
3202
|
+
return EvaluationError("sleep() requires exactly 1 argument (seconds)")
|
|
3203
|
+
|
|
3204
|
+
seconds_arg = args[0]
|
|
3205
|
+
if isinstance(seconds_arg, (Integer, Float)):
|
|
3206
|
+
try:
|
|
3207
|
+
time_module.sleep(float(seconds_arg.value))
|
|
3208
|
+
return NULL
|
|
3209
|
+
except Exception as e:
|
|
3210
|
+
return EvaluationError(f"sleep() error: {str(e)}")
|
|
3211
|
+
else:
|
|
3212
|
+
return EvaluationError("sleep() argument must be a number")
|
|
3213
|
+
|
|
3214
|
+
def _daemonize(*args):
|
|
3215
|
+
"""
|
|
3216
|
+
Run the current process as a background daemon.
|
|
3217
|
+
|
|
3218
|
+
Detaches from terminal and runs in background. On Unix systems, this
|
|
3219
|
+
performs a double fork to properly daemonize. On Windows, it's a no-op.
|
|
3220
|
+
|
|
3221
|
+
Usage:
|
|
3222
|
+
if is_main() {
|
|
3223
|
+
daemonize()
|
|
3224
|
+
# Now running as daemon
|
|
3225
|
+
run(my_server_task)
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
Optional arguments:
|
|
3229
|
+
daemonize() # Use defaults
|
|
3230
|
+
daemonize(working_dir) # Set working directory
|
|
3231
|
+
"""
|
|
3232
|
+
import os
|
|
3233
|
+
import sys
|
|
3234
|
+
|
|
3235
|
+
# Check if we're on a Unix-like system
|
|
3236
|
+
if not hasattr(os, 'fork'):
|
|
3237
|
+
return EvaluationError("daemonize() is only supported on Unix-like systems")
|
|
3238
|
+
|
|
3239
|
+
# Get optional working directory
|
|
3240
|
+
working_dir = None
|
|
3241
|
+
if len(args) > 0:
|
|
3242
|
+
if isinstance(args[0], String):
|
|
3243
|
+
working_dir = args[0].value
|
|
3244
|
+
else:
|
|
3245
|
+
return EvaluationError("daemonize() working_dir must be a string")
|
|
3246
|
+
|
|
3247
|
+
try:
|
|
3248
|
+
# First fork
|
|
3249
|
+
pid = os.fork()
|
|
3250
|
+
if pid > 0:
|
|
3251
|
+
# Parent process - exit
|
|
3252
|
+
sys.exit(0)
|
|
3253
|
+
except OSError as e:
|
|
3254
|
+
return EvaluationError(f"First fork failed: {str(e)}")
|
|
3255
|
+
|
|
3256
|
+
# Decouple from parent environment
|
|
3257
|
+
os.chdir(working_dir if working_dir else '/')
|
|
3258
|
+
os.setsid()
|
|
3259
|
+
os.umask(0)
|
|
3260
|
+
|
|
3261
|
+
# Second fork to prevent acquiring a controlling terminal
|
|
3262
|
+
try:
|
|
3263
|
+
pid = os.fork()
|
|
3264
|
+
if pid > 0:
|
|
3265
|
+
# Parent of second fork - exit
|
|
3266
|
+
sys.exit(0)
|
|
3267
|
+
except OSError as e:
|
|
3268
|
+
return EvaluationError(f"Second fork failed: {str(e)}")
|
|
3269
|
+
|
|
3270
|
+
# Redirect standard file descriptors to /dev/null
|
|
3271
|
+
sys.stdout.flush()
|
|
3272
|
+
sys.stderr.flush()
|
|
3273
|
+
|
|
3274
|
+
# Open /dev/null
|
|
3275
|
+
dev_null = os.open(os.devnull, os.O_RDWR)
|
|
3276
|
+
|
|
3277
|
+
# Redirect stdin, stdout, stderr
|
|
3278
|
+
os.dup2(dev_null, sys.stdin.fileno())
|
|
3279
|
+
os.dup2(dev_null, sys.stdout.fileno())
|
|
3280
|
+
os.dup2(dev_null, sys.stderr.fileno())
|
|
3281
|
+
|
|
3282
|
+
# Close the dev_null file descriptor
|
|
3283
|
+
if dev_null > 2:
|
|
3284
|
+
os.close(dev_null)
|
|
3285
|
+
|
|
3286
|
+
return NULL
|
|
3287
|
+
|
|
3288
|
+
def _watch_and_reload(*args):
|
|
3289
|
+
"""
|
|
3290
|
+
Watch files for changes and reload modules automatically.
|
|
3291
|
+
Useful for development to see code changes without restarting.
|
|
3292
|
+
|
|
3293
|
+
Usage:
|
|
3294
|
+
watch_and_reload([__file__]) # Watch current file
|
|
3295
|
+
watch_and_reload([file1, file2]) # Watch multiple files
|
|
3296
|
+
watch_and_reload([__file__], 1.0) # Custom check interval
|
|
3297
|
+
watch_and_reload([__file__], 1.0, my_callback) # With callback
|
|
3298
|
+
|
|
3299
|
+
Returns: Map with watch info
|
|
3300
|
+
"""
|
|
3301
|
+
import os
|
|
3302
|
+
import time as time_module
|
|
3303
|
+
import threading
|
|
3304
|
+
|
|
3305
|
+
if len(args) < 1:
|
|
3306
|
+
return EvaluationError("watch_and_reload() requires at least 1 argument (files)")
|
|
3307
|
+
|
|
3308
|
+
# Parse arguments
|
|
3309
|
+
files_arg = args[0]
|
|
3310
|
+
check_interval = 1.0 # Default: check every second
|
|
3311
|
+
reload_callback = None
|
|
3312
|
+
|
|
3313
|
+
if not isinstance(files_arg, List):
|
|
3314
|
+
return EvaluationError("watch_and_reload() files must be a list")
|
|
3315
|
+
|
|
3316
|
+
if len(args) >= 2:
|
|
3317
|
+
interval_obj = args[1]
|
|
3318
|
+
if isinstance(interval_obj, (Integer, Float)):
|
|
3319
|
+
check_interval = float(interval_obj.value)
|
|
3320
|
+
else:
|
|
3321
|
+
return EvaluationError("watch_and_reload() interval must be a number")
|
|
3322
|
+
|
|
3323
|
+
if len(args) >= 3:
|
|
3324
|
+
callback_obj = args[2]
|
|
3325
|
+
if isinstance(callback_obj, (Action, LambdaFunction)):
|
|
3326
|
+
reload_callback = callback_obj
|
|
3327
|
+
else:
|
|
3328
|
+
return EvaluationError("watch_and_reload() callback must be a function")
|
|
3329
|
+
|
|
3330
|
+
# Extract file paths
|
|
3331
|
+
file_paths = []
|
|
3332
|
+
for file_obj in files_arg.elements:
|
|
3333
|
+
if isinstance(file_obj, String):
|
|
3334
|
+
path = file_obj.value
|
|
3335
|
+
if os.path.exists(path):
|
|
3336
|
+
file_paths.append(path)
|
|
3337
|
+
else:
|
|
3338
|
+
return EvaluationError(f"File not found: {path}")
|
|
3339
|
+
else:
|
|
3340
|
+
return EvaluationError("watch_and_reload() file paths must be strings")
|
|
3341
|
+
|
|
3342
|
+
if not file_paths:
|
|
3343
|
+
return EvaluationError("No valid files to watch")
|
|
3344
|
+
|
|
3345
|
+
# Get initial modification times
|
|
3346
|
+
file_mtimes = {}
|
|
3347
|
+
for path in file_paths:
|
|
3348
|
+
try:
|
|
3349
|
+
file_mtimes[path] = os.path.getmtime(path)
|
|
3350
|
+
except OSError as e:
|
|
3351
|
+
return EvaluationError(f"Cannot stat {path}: {str(e)}")
|
|
3352
|
+
|
|
3353
|
+
reload_count = [0] # Use list for closure mutability
|
|
3354
|
+
|
|
3355
|
+
def watch_worker():
|
|
3356
|
+
"""Background thread that watches for file changes"""
|
|
3357
|
+
while True:
|
|
3358
|
+
time_module.sleep(check_interval)
|
|
3359
|
+
|
|
3360
|
+
for path in file_paths:
|
|
3361
|
+
try:
|
|
3362
|
+
current_mtime = os.path.getmtime(path)
|
|
3363
|
+
if current_mtime > file_mtimes[path]:
|
|
3364
|
+
# File was modified!
|
|
3365
|
+
print(f"\n🔄 File changed: {path}")
|
|
3366
|
+
file_mtimes[path] = current_mtime
|
|
3367
|
+
reload_count[0] += 1
|
|
3368
|
+
|
|
3369
|
+
# Execute reload callback if provided
|
|
3370
|
+
if reload_callback:
|
|
3371
|
+
try:
|
|
3372
|
+
result = self.apply_function(reload_callback, [String(path)])
|
|
3373
|
+
if is_error(result):
|
|
3374
|
+
print(f"⚠️ Reload callback error: {result.message}")
|
|
3375
|
+
except Exception as e:
|
|
3376
|
+
print(f"⚠️ Reload callback exception: {str(e)}")
|
|
3377
|
+
else:
|
|
3378
|
+
print(f" Reload #{reload_count[0]} - No auto-reload callback set")
|
|
3379
|
+
print(f" Tip: Restart the program to see changes")
|
|
3380
|
+
except OSError:
|
|
3381
|
+
# File might have been deleted/renamed
|
|
3382
|
+
pass
|
|
3383
|
+
|
|
3384
|
+
# Start watch thread
|
|
3385
|
+
watch_thread = threading.Thread(target=watch_worker, daemon=True)
|
|
3386
|
+
watch_thread.start()
|
|
3387
|
+
|
|
3388
|
+
print(f"👁️ Watching {len(file_paths)} file(s) for changes...")
|
|
3389
|
+
for path in file_paths:
|
|
3390
|
+
print(f" - {path}")
|
|
3391
|
+
print(f" Check interval: {check_interval}s")
|
|
3392
|
+
|
|
3393
|
+
# Return watch info
|
|
3394
|
+
return Map({
|
|
3395
|
+
"files": files_arg,
|
|
3396
|
+
"interval": Float(check_interval),
|
|
3397
|
+
"active": BooleanObj(True)
|
|
3398
|
+
})
|
|
3399
|
+
|
|
3400
|
+
def _get_module_name(*a):
|
|
3401
|
+
"""
|
|
3402
|
+
Get the current module name (__MODULE__).
|
|
3403
|
+
|
|
3404
|
+
Usage:
|
|
3405
|
+
name = get_module_name()
|
|
3406
|
+
print("Module: " + name)
|
|
3407
|
+
"""
|
|
3408
|
+
env = getattr(self, '_current_env', None)
|
|
3409
|
+
if not env:
|
|
3410
|
+
return String("")
|
|
3411
|
+
|
|
3412
|
+
module = env.get('__MODULE__')
|
|
3413
|
+
return module if module else String("")
|
|
3414
|
+
|
|
3415
|
+
def _get_module_path(*a):
|
|
3416
|
+
"""
|
|
3417
|
+
Get the current module file path (__file__).
|
|
3418
|
+
|
|
3419
|
+
Usage:
|
|
3420
|
+
path = get_module_path()
|
|
3421
|
+
print("Path: " + path)
|
|
3422
|
+
"""
|
|
3423
|
+
env = getattr(self, '_current_env', None)
|
|
3424
|
+
if not env:
|
|
3425
|
+
return String("")
|
|
3426
|
+
|
|
3427
|
+
file_path = env.get('__file__')
|
|
3428
|
+
if not file_path:
|
|
3429
|
+
file_path = env.get('__FILE__')
|
|
3430
|
+
return file_path if file_path else String("")
|
|
3431
|
+
|
|
3432
|
+
def _module_info(*a):
|
|
3433
|
+
"""
|
|
3434
|
+
Get information about the current module.
|
|
3435
|
+
Returns a map with module metadata.
|
|
3436
|
+
|
|
3437
|
+
Usage:
|
|
3438
|
+
info = module_info()
|
|
3439
|
+
print(info["name"]) # Module name
|
|
3440
|
+
print(info["file"]) # File path
|
|
3441
|
+
print(info["dir"]) # Directory
|
|
3442
|
+
print(info["package"]) # Package name
|
|
3443
|
+
"""
|
|
3444
|
+
env = getattr(self, '_current_env', None)
|
|
3445
|
+
if not env:
|
|
3446
|
+
return Map({})
|
|
3447
|
+
|
|
3448
|
+
result = {}
|
|
3449
|
+
|
|
3450
|
+
# Get module variables
|
|
3451
|
+
for var_name in ['__MODULE__', '__file__', '__FILE__', '__DIR__', '__PACKAGE__']:
|
|
3452
|
+
val = env.get(var_name)
|
|
3453
|
+
if val:
|
|
3454
|
+
key = var_name.strip('_').lower()
|
|
3455
|
+
result[String(key)] = val
|
|
3456
|
+
|
|
3457
|
+
return Map(result)
|
|
3458
|
+
|
|
3459
|
+
def _list_imports(*a):
|
|
3460
|
+
"""
|
|
3461
|
+
List all imported modules in the current environment.
|
|
3462
|
+
|
|
3463
|
+
Usage:
|
|
3464
|
+
imports = list_imports()
|
|
3465
|
+
print(imports) # ["math", "json", "./utils"]
|
|
3466
|
+
"""
|
|
3467
|
+
env = getattr(self, '_current_env', None)
|
|
3468
|
+
if not env:
|
|
3469
|
+
return List([])
|
|
3470
|
+
|
|
3471
|
+
# Collect all imported module names (this is a simplified version)
|
|
3472
|
+
# In a more complete implementation, we'd track imports explicitly
|
|
3473
|
+
imports = []
|
|
3474
|
+
|
|
3475
|
+
# Look for common module indicators in the environment
|
|
3476
|
+
for name, value in env.store.items():
|
|
3477
|
+
# Skip special variables and builtins
|
|
3478
|
+
if name.startswith('__') or name in self.builtins:
|
|
3479
|
+
continue
|
|
3480
|
+
# Check if it looks like an imported module
|
|
3481
|
+
if isinstance(value, Map) and len(value.pairs) > 3:
|
|
3482
|
+
imports.append(String(name))
|
|
3483
|
+
|
|
3484
|
+
return List(imports)
|
|
3485
|
+
|
|
3486
|
+
def _get_exported_names(*a):
|
|
3487
|
+
"""
|
|
3488
|
+
Get all exported variable names from the current module.
|
|
3489
|
+
|
|
3490
|
+
Usage:
|
|
3491
|
+
exports = get_exported_names()
|
|
3492
|
+
print(exports) # ["myFunction", "MY_CONSTANT", "MyClass"]
|
|
3493
|
+
"""
|
|
3494
|
+
env = getattr(self, '_current_env', None)
|
|
3495
|
+
if not env:
|
|
3496
|
+
return List([])
|
|
3497
|
+
|
|
3498
|
+
exports = []
|
|
3499
|
+
|
|
3500
|
+
# Get all user-defined names (skip special variables and builtins)
|
|
3501
|
+
for name in env.store.keys():
|
|
3502
|
+
if not name.startswith('__') and name not in self.builtins:
|
|
3503
|
+
exports.append(String(name))
|
|
3504
|
+
|
|
3505
|
+
return List(exports)
|
|
3506
|
+
|
|
3507
|
+
# Register the builtins
|
|
3508
|
+
self.builtins.update({
|
|
3509
|
+
"run": Builtin(_run, "run"),
|
|
3510
|
+
"execute": Builtin(_execute, "execute"),
|
|
3511
|
+
"is_main": Builtin(_is_main, "is_main"),
|
|
3512
|
+
"exit_program": Builtin(_exit_program, "exit_program"),
|
|
3513
|
+
"on_start": Builtin(_on_start, "on_start"),
|
|
3514
|
+
"on_exit": Builtin(_on_exit, "on_exit"),
|
|
3515
|
+
"signal_handler": Builtin(_signal_handler, "signal_handler"),
|
|
3516
|
+
"schedule": Builtin(_schedule, "schedule"),
|
|
3517
|
+
"sleep": Builtin(_sleep, "sleep"),
|
|
3518
|
+
"daemonize": Builtin(_daemonize, "daemonize"),
|
|
3519
|
+
"watch_and_reload": Builtin(_watch_and_reload, "watch_and_reload"),
|
|
3520
|
+
"get_module_name": Builtin(_get_module_name, "get_module_name"),
|
|
3521
|
+
"get_module_path": Builtin(_get_module_path, "get_module_path"),
|
|
3522
|
+
"module_info": Builtin(_module_info, "module_info"),
|
|
3523
|
+
"list_imports": Builtin(_list_imports, "list_imports"),
|
|
3524
|
+
"get_exported_names": Builtin(_get_exported_names, "get_exported_names"),
|
|
3525
|
+
})
|
|
3526
|
+
|
|
3527
|
+
def _register_renderer_builtins(self):
|
|
3528
|
+
"""Logic extracted from the original RENDER_REGISTRY and helper functions."""
|
|
3529
|
+
|
|
3530
|
+
# Mix
|
|
3531
|
+
def builtin_mix(*args):
|
|
3532
|
+
if len(args) != 3:
|
|
3533
|
+
return EvaluationError("mix(colorA, colorB, ratio)")
|
|
3534
|
+
a, b, ratio = args
|
|
3535
|
+
a_name = _to_str(a)
|
|
3536
|
+
b_name = _to_str(b)
|
|
3537
|
+
|
|
3538
|
+
try:
|
|
3539
|
+
ratio_val = float(ratio.value) if isinstance(ratio, (Integer, Float)) else float(str(ratio))
|
|
3540
|
+
except Exception:
|
|
3541
|
+
ratio_val = 0.5
|
|
3542
|
+
|
|
3543
|
+
if _BACKEND_AVAILABLE:
|
|
3544
|
+
try:
|
|
3545
|
+
res = _BACKEND.mix(a_name, b_name, ratio_val)
|
|
3546
|
+
return String(str(res))
|
|
3547
|
+
except Exception:
|
|
3548
|
+
pass
|
|
3549
|
+
|
|
3550
|
+
return String(f"mix({a_name},{b_name},{ratio_val})")
|
|
3551
|
+
|
|
3552
|
+
# Define Screen
|
|
3553
|
+
def builtin_define_screen(*args):
|
|
3554
|
+
if len(args) < 1:
|
|
3555
|
+
return EvaluationError("define_screen() requires at least a name")
|
|
3556
|
+
|
|
3557
|
+
name = _to_str(args[0])
|
|
3558
|
+
props = _zexus_to_python(args[1]) if len(args) > 1 else {}
|
|
3559
|
+
|
|
3560
|
+
if _BACKEND_AVAILABLE:
|
|
3561
|
+
try:
|
|
3562
|
+
_BACKEND.define_screen(name, props)
|
|
3563
|
+
return NULL
|
|
3564
|
+
except Exception as e:
|
|
3565
|
+
return EvaluationError(str(e))
|
|
3566
|
+
|
|
3567
|
+
self.render_registry['screens'].setdefault(name, {
|
|
3568
|
+
'properties': props,
|
|
3569
|
+
'components': [],
|
|
3570
|
+
'theme': None
|
|
3571
|
+
})
|
|
3572
|
+
return NULL
|
|
3573
|
+
|
|
3574
|
+
# Define Component
|
|
3575
|
+
def builtin_define_component(*args):
|
|
3576
|
+
if len(args) < 1:
|
|
3577
|
+
return EvaluationError("define_component() requires at least a name")
|
|
3578
|
+
|
|
3579
|
+
name = _to_str(args[0])
|
|
3580
|
+
props = _zexus_to_python(args[1]) if len(args) > 1 else {}
|
|
3581
|
+
|
|
3582
|
+
if _BACKEND_AVAILABLE:
|
|
3583
|
+
try:
|
|
3584
|
+
_BACKEND.define_component(name, props)
|
|
3585
|
+
return NULL
|
|
3586
|
+
except Exception as e:
|
|
3587
|
+
return EvaluationError(str(e))
|
|
3588
|
+
|
|
3589
|
+
self.render_registry['components'][name] = props
|
|
3590
|
+
return NULL
|
|
3591
|
+
|
|
3592
|
+
# Add to Screen
|
|
3593
|
+
def builtin_add_to_screen(*args):
|
|
3594
|
+
if len(args) != 2:
|
|
3595
|
+
return EvaluationError("add_to_screen() requires (screen_name, component_name)")
|
|
3596
|
+
|
|
3597
|
+
screen = _to_str(args[0])
|
|
3598
|
+
comp = _to_str(args[1])
|
|
3599
|
+
|
|
3600
|
+
if _BACKEND_AVAILABLE:
|
|
3601
|
+
try:
|
|
3602
|
+
_BACKEND.add_to_screen(screen, comp)
|
|
3603
|
+
return NULL
|
|
3604
|
+
except Exception as e:
|
|
3605
|
+
return EvaluationError(str(e))
|
|
3606
|
+
|
|
3607
|
+
if screen not in self.render_registry['screens']:
|
|
3608
|
+
return EvaluationError(f"Screen '{screen}' not found")
|
|
3609
|
+
|
|
3610
|
+
self.render_registry['screens'][screen]['components'].append(comp)
|
|
3611
|
+
return NULL
|
|
3612
|
+
|
|
3613
|
+
# Render Screen
|
|
3614
|
+
def builtin_render_screen(*args):
|
|
3615
|
+
if len(args) != 1:
|
|
3616
|
+
return EvaluationError("render_screen() requires exactly 1 argument")
|
|
3617
|
+
|
|
3618
|
+
name = _to_str(args[0])
|
|
3619
|
+
|
|
3620
|
+
if _BACKEND_AVAILABLE:
|
|
3621
|
+
try:
|
|
3622
|
+
out = _BACKEND.render_screen(name)
|
|
3623
|
+
return String(str(out))
|
|
3624
|
+
except Exception as e:
|
|
3625
|
+
return String(f"<render error: {str(e)}>")
|
|
3626
|
+
|
|
3627
|
+
screen = self.render_registry['screens'].get(name)
|
|
3628
|
+
if not screen:
|
|
3629
|
+
return String(f"<missing screen: {name}>")
|
|
3630
|
+
|
|
3631
|
+
return String(f"Screen:{name} props={screen.get('properties')} components={screen.get('components')}")
|
|
3632
|
+
|
|
3633
|
+
# Set Theme
|
|
3634
|
+
def builtin_set_theme(*args):
|
|
3635
|
+
if len(args) == 1:
|
|
3636
|
+
theme_name = _to_str(args[0])
|
|
3637
|
+
if _BACKEND_AVAILABLE:
|
|
3638
|
+
try:
|
|
3639
|
+
_BACKEND.set_theme(theme_name)
|
|
3640
|
+
return NULL
|
|
3641
|
+
except Exception as e:
|
|
3642
|
+
return EvaluationError(str(e))
|
|
3643
|
+
|
|
3644
|
+
self.render_registry['current_theme'] = theme_name
|
|
3645
|
+
return NULL
|
|
3646
|
+
|
|
3647
|
+
if len(args) == 2:
|
|
3648
|
+
target = _to_str(args[0])
|
|
3649
|
+
theme_name = _to_str(args[1])
|
|
3650
|
+
|
|
3651
|
+
if _BACKEND_AVAILABLE:
|
|
3652
|
+
try:
|
|
3653
|
+
_BACKEND.set_theme(target, theme_name)
|
|
3654
|
+
return NULL
|
|
3655
|
+
except Exception as e:
|
|
3656
|
+
return EvaluationError(str(e))
|
|
3657
|
+
|
|
3658
|
+
if target in self.render_registry['screens']:
|
|
3659
|
+
self.render_registry['screens'][target]['theme'] = theme_name
|
|
3660
|
+
else:
|
|
3661
|
+
self.render_registry['themes'].setdefault(theme_name, {})
|
|
3662
|
+
|
|
3663
|
+
return NULL
|
|
3664
|
+
|
|
3665
|
+
return EvaluationError("set_theme() requires 1 (theme) or 2 (target,theme) args")
|
|
3666
|
+
|
|
3667
|
+
# Canvas Ops
|
|
3668
|
+
def builtin_create_canvas(*args):
|
|
3669
|
+
if len(args) != 2:
|
|
3670
|
+
return EvaluationError("create_canvas(width, height)")
|
|
3671
|
+
|
|
3672
|
+
try:
|
|
3673
|
+
wid = int(args[0].value) if isinstance(args[0], Integer) else int(str(args[0]))
|
|
3674
|
+
hei = int(args[1].value) if isinstance(args[1], Integer) else int(str(args[1]))
|
|
3675
|
+
except Exception:
|
|
3676
|
+
return EvaluationError("Invalid numeric arguments to create_canvas()")
|
|
3677
|
+
|
|
3678
|
+
if _BACKEND_AVAILABLE:
|
|
3679
|
+
try:
|
|
3680
|
+
cid = _BACKEND.create_canvas(wid, hei)
|
|
3681
|
+
return String(str(cid))
|
|
3682
|
+
except Exception as e:
|
|
3683
|
+
return EvaluationError(str(e))
|
|
3684
|
+
|
|
3685
|
+
cid = f"canvas_{len(self.render_registry['canvases'])+1}"
|
|
3686
|
+
self.render_registry['canvases'][cid] = {
|
|
3687
|
+
'width': wid,
|
|
3688
|
+
'height': hei,
|
|
3689
|
+
'draw_ops': []
|
|
3690
|
+
}
|
|
3691
|
+
return String(cid)
|
|
3692
|
+
|
|
3693
|
+
def builtin_draw_line(*args):
|
|
3694
|
+
if len(args) != 5:
|
|
3695
|
+
return EvaluationError("draw_line(canvas_id,x1,y1,x2,y2)")
|
|
3696
|
+
|
|
3697
|
+
cid = _to_str(args[0])
|
|
3698
|
+
try:
|
|
3699
|
+
coords = [int(a.value) if isinstance(a, Integer) else int(str(a)) for a in args[1:]]
|
|
3700
|
+
except Exception:
|
|
3701
|
+
return EvaluationError("Invalid coordinates in draw_line()")
|
|
3702
|
+
|
|
3703
|
+
if _BACKEND_AVAILABLE:
|
|
3704
|
+
try:
|
|
3705
|
+
_BACKEND.draw_line(cid, *coords)
|
|
3706
|
+
return NULL
|
|
3707
|
+
except Exception as e:
|
|
3708
|
+
return EvaluationError(str(e))
|
|
3709
|
+
|
|
3710
|
+
canvas = self.render_registry['canvases'].get(cid)
|
|
3711
|
+
if not canvas:
|
|
3712
|
+
return EvaluationError(f"Canvas {cid} not found")
|
|
3713
|
+
|
|
3714
|
+
canvas['draw_ops'].append(('line', coords))
|
|
3715
|
+
return NULL
|
|
3716
|
+
|
|
3717
|
+
def builtin_draw_text(*args):
|
|
3718
|
+
if len(args) != 4:
|
|
3719
|
+
return EvaluationError("draw_text(canvas_id,x,y,text)")
|
|
3720
|
+
|
|
3721
|
+
cid = _to_str(args[0])
|
|
3722
|
+
try:
|
|
3723
|
+
x = int(args[1].value) if isinstance(args[1], Integer) else int(str(args[1]))
|
|
3724
|
+
y = int(args[2].value) if isinstance(args[2], Integer) else int(str(args[2]))
|
|
3725
|
+
except Exception:
|
|
3726
|
+
return EvaluationError("Invalid coordinates in draw_text()")
|
|
3727
|
+
|
|
3728
|
+
text = _to_str(args[3])
|
|
3729
|
+
|
|
3730
|
+
if _BACKEND_AVAILABLE:
|
|
3731
|
+
try:
|
|
3732
|
+
_BACKEND.draw_text(cid, x, y, text)
|
|
3733
|
+
return NULL
|
|
3734
|
+
except Exception as e:
|
|
3735
|
+
return EvaluationError(str(e))
|
|
3736
|
+
|
|
3737
|
+
canvas = self.render_registry['canvases'].get(cid)
|
|
3738
|
+
if not canvas:
|
|
3739
|
+
return EvaluationError(f"Canvas {cid} not found")
|
|
3740
|
+
|
|
3741
|
+
canvas['draw_ops'].append(('text', (x, y, text)))
|
|
3742
|
+
return NULL
|
|
3743
|
+
|
|
3744
|
+
# Register renderer builtins
|
|
3745
|
+
self.builtins.update({
|
|
3746
|
+
"mix": Builtin(builtin_mix, "mix"),
|
|
3747
|
+
"define_screen": Builtin(builtin_define_screen, "define_screen"),
|
|
3748
|
+
"define_component": Builtin(builtin_define_component, "define_component"),
|
|
3749
|
+
"add_to_screen": Builtin(builtin_add_to_screen, "add_to_screen"),
|
|
3750
|
+
"render_screen": Builtin(builtin_render_screen, "render_screen"),
|
|
3751
|
+
"set_theme": Builtin(builtin_set_theme, "set_theme"),
|
|
3752
|
+
"create_canvas": Builtin(builtin_create_canvas, "create_canvas"),
|
|
3753
|
+
"draw_line": Builtin(builtin_draw_line, "draw_line"),
|
|
3754
|
+
"draw_text": Builtin(builtin_draw_text, "draw_text"),
|
|
3755
|
+
})
|
|
3756
|
+
|
|
3757
|
+
def _register_verification_builtins(self):
|
|
3758
|
+
"""Register verification helper functions for VERIFY keyword"""
|
|
3759
|
+
import re
|
|
3760
|
+
import os
|
|
3761
|
+
|
|
3762
|
+
def _is_email(*a):
|
|
3763
|
+
"""Check if string is valid email format"""
|
|
3764
|
+
if len(a) != 1:
|
|
3765
|
+
return EvaluationError("is_email() takes 1 argument")
|
|
3766
|
+
|
|
3767
|
+
val = a[0]
|
|
3768
|
+
email_str = val.value if isinstance(val, String) else str(val)
|
|
3769
|
+
|
|
3770
|
+
# Simple email validation pattern
|
|
3771
|
+
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
3772
|
+
is_valid = bool(re.match(pattern, email_str))
|
|
3773
|
+
return TRUE if is_valid else FALSE
|
|
3774
|
+
|
|
3775
|
+
def _is_url(*a):
|
|
3776
|
+
"""Check if string is valid URL format"""
|
|
3777
|
+
if len(a) != 1:
|
|
3778
|
+
return EvaluationError("is_url() takes 1 argument")
|
|
3779
|
+
|
|
3780
|
+
val = a[0]
|
|
3781
|
+
url_str = val.value if isinstance(val, String) else str(val)
|
|
3782
|
+
|
|
3783
|
+
# Simple URL validation pattern
|
|
3784
|
+
pattern = r'^https?://[^\s/$.?#].[^\s]*$'
|
|
3785
|
+
is_valid = bool(re.match(pattern, url_str))
|
|
3786
|
+
return TRUE if is_valid else FALSE
|
|
3787
|
+
|
|
3788
|
+
def _is_phone(*a):
|
|
3789
|
+
"""Check if string is valid phone number format"""
|
|
3790
|
+
if len(a) != 1:
|
|
3791
|
+
return EvaluationError("is_phone() takes 1 argument")
|
|
3792
|
+
|
|
3793
|
+
val = a[0]
|
|
3794
|
+
phone_str = val.value if isinstance(val, String) else str(val)
|
|
3795
|
+
|
|
3796
|
+
# Remove common separators
|
|
3797
|
+
clean = re.sub(r'[\s\-\(\)\.]', '', phone_str)
|
|
3798
|
+
|
|
3799
|
+
# Check if it's digits and reasonable length
|
|
3800
|
+
is_valid = clean.isdigit() and 10 <= len(clean) <= 15
|
|
3801
|
+
return TRUE if is_valid else FALSE
|
|
3802
|
+
|
|
3803
|
+
def _is_numeric(*a):
|
|
3804
|
+
"""Check if string contains only numbers"""
|
|
3805
|
+
if len(a) != 1:
|
|
3806
|
+
return EvaluationError("is_numeric() takes 1 argument")
|
|
3807
|
+
|
|
3808
|
+
val = a[0]
|
|
3809
|
+
if isinstance(val, (Integer, Float)):
|
|
3810
|
+
return TRUE
|
|
3811
|
+
|
|
3812
|
+
str_val = val.value if isinstance(val, String) else str(val)
|
|
3813
|
+
|
|
3814
|
+
try:
|
|
3815
|
+
float(str_val)
|
|
3816
|
+
return TRUE
|
|
3817
|
+
except ValueError:
|
|
3818
|
+
return FALSE
|
|
3819
|
+
|
|
3820
|
+
def _is_alpha(*a):
|
|
3821
|
+
"""Check if string contains only alphabetic characters"""
|
|
3822
|
+
if len(a) != 1:
|
|
3823
|
+
return EvaluationError("is_alpha() takes 1 argument")
|
|
3824
|
+
|
|
3825
|
+
val = a[0]
|
|
3826
|
+
str_val = val.value if isinstance(val, String) else str(val)
|
|
3827
|
+
|
|
3828
|
+
is_valid = str_val.isalpha()
|
|
3829
|
+
return TRUE if is_valid else FALSE
|
|
3830
|
+
|
|
3831
|
+
def _is_alphanumeric(*a):
|
|
3832
|
+
"""Check if string contains only alphanumeric characters"""
|
|
3833
|
+
if len(a) != 1:
|
|
3834
|
+
return EvaluationError("is_alphanumeric() takes 1 argument")
|
|
3835
|
+
|
|
3836
|
+
val = a[0]
|
|
3837
|
+
str_val = val.value if isinstance(val, String) else str(val)
|
|
3838
|
+
|
|
3839
|
+
is_valid = str_val.isalnum()
|
|
3840
|
+
return TRUE if is_valid else FALSE
|
|
3841
|
+
|
|
3842
|
+
def _matches_pattern(*a):
|
|
3843
|
+
"""Check if string matches regex pattern: matches_pattern(value, pattern)"""
|
|
3844
|
+
if len(a) != 2:
|
|
3845
|
+
return EvaluationError("matches_pattern() takes 2 arguments: value, pattern")
|
|
3846
|
+
|
|
3847
|
+
val = a[0]
|
|
3848
|
+
pattern_obj = a[1]
|
|
3849
|
+
|
|
3850
|
+
str_val = val.value if isinstance(val, String) else str(val)
|
|
3851
|
+
pattern = pattern_obj.value if isinstance(pattern_obj, String) else str(pattern_obj)
|
|
3852
|
+
|
|
3853
|
+
try:
|
|
3854
|
+
is_valid = bool(re.match(pattern, str_val))
|
|
3855
|
+
return TRUE if is_valid else FALSE
|
|
3856
|
+
except Exception as e:
|
|
3857
|
+
return EvaluationError(f"Pattern matching error: {str(e)}")
|
|
3858
|
+
|
|
3859
|
+
def _env_get(*a):
|
|
3860
|
+
"""Get environment variable: env_get("VAR_NAME") or env_get("VAR_NAME", "default")"""
|
|
3861
|
+
if len(a) < 1 or len(a) > 2:
|
|
3862
|
+
return EvaluationError("env_get() takes 1 or 2 arguments: var_name, [default]")
|
|
3863
|
+
|
|
3864
|
+
var_name_obj = a[0]
|
|
3865
|
+
var_name = var_name_obj.value if isinstance(var_name_obj, String) else str(var_name_obj)
|
|
3866
|
+
|
|
3867
|
+
default = a[1] if len(a) == 2 else None
|
|
3868
|
+
|
|
3869
|
+
value = os.environ.get(var_name)
|
|
3870
|
+
|
|
3871
|
+
if value is None:
|
|
3872
|
+
return default if default is not None else NULL
|
|
3873
|
+
|
|
3874
|
+
return String(value)
|
|
3875
|
+
|
|
3876
|
+
def _env_set(*a):
|
|
3877
|
+
"""Set environment variable: env_set("VAR_NAME", "value")"""
|
|
3878
|
+
if len(a) != 2:
|
|
3879
|
+
return EvaluationError("env_set() takes 2 arguments: var_name, value")
|
|
3880
|
+
|
|
3881
|
+
var_name_obj = a[0]
|
|
3882
|
+
value_obj = a[1]
|
|
3883
|
+
|
|
3884
|
+
var_name = var_name_obj.value if isinstance(var_name_obj, String) else str(var_name_obj)
|
|
3885
|
+
value = value_obj.value if isinstance(value_obj, String) else str(value_obj)
|
|
3886
|
+
|
|
3887
|
+
os.environ[var_name] = value
|
|
3888
|
+
return TRUE
|
|
3889
|
+
|
|
3890
|
+
def _env_exists(*a):
|
|
3891
|
+
"""Check if environment variable exists: env_exists("VAR_NAME")"""
|
|
3892
|
+
if len(a) != 1:
|
|
3893
|
+
return EvaluationError("env_exists() takes 1 argument: var_name")
|
|
3894
|
+
|
|
3895
|
+
var_name_obj = a[0]
|
|
3896
|
+
var_name = var_name_obj.value if isinstance(var_name_obj, String) else str(var_name_obj)
|
|
3897
|
+
|
|
3898
|
+
exists = var_name in os.environ
|
|
3899
|
+
return TRUE if exists else FALSE
|
|
3900
|
+
|
|
3901
|
+
def _password_strength(*a):
|
|
3902
|
+
"""Check password strength: password_strength(password) -> "weak"/"medium"/"strong" """
|
|
3903
|
+
if len(a) != 1:
|
|
3904
|
+
return EvaluationError("password_strength() takes 1 argument")
|
|
3905
|
+
|
|
3906
|
+
val = a[0]
|
|
3907
|
+
password = val.value if isinstance(val, String) else str(val)
|
|
3908
|
+
|
|
3909
|
+
score = 0
|
|
3910
|
+
length = len(password)
|
|
3911
|
+
|
|
3912
|
+
# Length check
|
|
3913
|
+
if length >= 8:
|
|
3914
|
+
score += 1
|
|
3915
|
+
if length >= 12:
|
|
3916
|
+
score += 1
|
|
3917
|
+
|
|
3918
|
+
# Complexity checks
|
|
3919
|
+
if re.search(r'[a-z]', password):
|
|
3920
|
+
score += 1
|
|
3921
|
+
if re.search(r'[A-Z]', password):
|
|
3922
|
+
score += 1
|
|
3923
|
+
if re.search(r'[0-9]', password):
|
|
3924
|
+
score += 1
|
|
3925
|
+
if re.search(r'[^a-zA-Z0-9]', password):
|
|
3926
|
+
score += 1
|
|
3927
|
+
|
|
3928
|
+
if score <= 2:
|
|
3929
|
+
return String("weak")
|
|
3930
|
+
elif score <= 4:
|
|
3931
|
+
return String("medium")
|
|
3932
|
+
else:
|
|
3933
|
+
return String("strong")
|
|
3934
|
+
|
|
3935
|
+
def _sanitize_input(*a):
|
|
3936
|
+
"""Sanitize user input by removing dangerous characters"""
|
|
3937
|
+
if len(a) != 1:
|
|
3938
|
+
return EvaluationError("sanitize_input() takes 1 argument")
|
|
3939
|
+
|
|
3940
|
+
val = a[0]
|
|
3941
|
+
input_str = val.value if isinstance(val, String) else str(val)
|
|
3942
|
+
|
|
3943
|
+
# Remove potentially dangerous characters
|
|
3944
|
+
# Remove HTML tags
|
|
3945
|
+
sanitized = re.sub(r'<[^>]+>', '', input_str)
|
|
3946
|
+
# Remove script tags
|
|
3947
|
+
sanitized = re.sub(r'<script[^>]*>.*?</script>', '', sanitized, flags=re.IGNORECASE)
|
|
3948
|
+
# Remove SQL injection patterns
|
|
3949
|
+
sanitized = re.sub(r'(;|--|\'|\"|\bOR\b|\bAND\b)', '', sanitized, flags=re.IGNORECASE)
|
|
3950
|
+
|
|
3951
|
+
return String(sanitized)
|
|
3952
|
+
|
|
3953
|
+
def _validate_length(*a):
|
|
3954
|
+
"""Validate string length: validate_length(value, min, max)"""
|
|
3955
|
+
if len(a) != 3:
|
|
3956
|
+
return EvaluationError("validate_length() takes 3 arguments: value, min, max")
|
|
3957
|
+
|
|
3958
|
+
val = a[0]
|
|
3959
|
+
min_len_obj = a[1]
|
|
3960
|
+
max_len_obj = a[2]
|
|
3961
|
+
|
|
3962
|
+
str_val = val.value if isinstance(val, String) else str(val)
|
|
3963
|
+
min_len = min_len_obj.value if isinstance(min_len_obj, Integer) else int(min_len_obj)
|
|
3964
|
+
max_len = max_len_obj.value if isinstance(max_len_obj, Integer) else int(max_len_obj)
|
|
3965
|
+
|
|
3966
|
+
length = len(str_val)
|
|
3967
|
+
is_valid = min_len <= length <= max_len
|
|
3968
|
+
|
|
3969
|
+
return TRUE if is_valid else FALSE
|
|
3970
|
+
|
|
3971
|
+
# Register verification builtins
|
|
3972
|
+
self.builtins.update({
|
|
3973
|
+
"is_email": Builtin(_is_email, "is_email"),
|
|
3974
|
+
"is_url": Builtin(_is_url, "is_url"),
|
|
3975
|
+
"is_phone": Builtin(_is_phone, "is_phone"),
|
|
3976
|
+
"is_numeric": Builtin(_is_numeric, "is_numeric"),
|
|
3977
|
+
"is_alpha": Builtin(_is_alpha, "is_alpha"),
|
|
3978
|
+
"is_alphanumeric": Builtin(_is_alphanumeric, "is_alphanumeric"),
|
|
3979
|
+
"matches_pattern": Builtin(_matches_pattern, "matches_pattern"),
|
|
3980
|
+
"env_get": Builtin(_env_get, "env_get"),
|
|
3981
|
+
"env_set": Builtin(_env_set, "env_set"),
|
|
3982
|
+
"env_exists": Builtin(_env_exists, "env_exists"),
|
|
3983
|
+
"password_strength": Builtin(_password_strength, "password_strength"),
|
|
3984
|
+
"sanitize_input": Builtin(_sanitize_input, "sanitize_input"),
|
|
3985
|
+
"validate_length": Builtin(_validate_length, "validate_length"),
|
|
3986
|
+
})
|
|
3987
|
+
|
|
3988
|
+
# Register main entry point and event loop builtins
|
|
3989
|
+
self._register_main_entry_point_builtins()
|