zexus 1.6.2 → 1.6.4

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.
@@ -0,0 +1,291 @@
1
+ # src/zexus/evaluator/resource_limiter.py
2
+ """
3
+ Resource Limiter for Zexus Interpreter
4
+
5
+ Prevents resource exhaustion attacks by enforcing limits on:
6
+ - Loop iterations (prevents infinite loops)
7
+ - Execution time (prevents DoS via slow operations)
8
+ - Call stack depth (prevents stack overflow)
9
+ - Memory usage (prevents memory exhaustion)
10
+
11
+ Security Fix #7: Resource Limits
12
+ """
13
+
14
+ import time
15
+ import signal
16
+ import sys
17
+
18
+
19
+ class ResourceError(Exception):
20
+ """Raised when a resource limit is exceeded"""
21
+ pass
22
+
23
+
24
+ class TimeoutError(ResourceError):
25
+ """Raised when execution timeout is exceeded"""
26
+ pass
27
+
28
+
29
+ class ResourceLimiter:
30
+ """
31
+ Enforces resource limits during program execution.
32
+
33
+ Limits:
34
+ - max_iterations: Maximum loop iterations across all loops (default: 1,000,000)
35
+ - timeout_seconds: Maximum execution time (default: 30 seconds)
36
+ - max_call_depth: Maximum call stack depth (default: 1000)
37
+ - max_memory_mb: Maximum memory usage (default: 500 MB, not enforced by default)
38
+
39
+ Usage:
40
+ limiter = ResourceLimiter(max_iterations=100000, timeout_seconds=10)
41
+ limiter.start() # Start timeout timer
42
+
43
+ # In loops:
44
+ limiter.check_iterations()
45
+
46
+ # On function calls:
47
+ limiter.enter_call()
48
+ # ... function body ...
49
+ limiter.exit_call()
50
+
51
+ limiter.stop() # Stop timeout timer
52
+ """
53
+
54
+ # Default limits
55
+ DEFAULT_MAX_ITERATIONS = 1_000_000 # 1 million iterations
56
+ DEFAULT_TIMEOUT_SECONDS = 30 # 30 seconds
57
+ DEFAULT_MAX_CALL_DEPTH = 100 # 100 nested calls (Python interpreter uses many stack frames per Zexus call)
58
+ DEFAULT_MAX_MEMORY_MB = 500 # 500 MB (not enforced by default)
59
+
60
+ def __init__(self,
61
+ max_iterations=None,
62
+ timeout_seconds=None,
63
+ max_call_depth=None,
64
+ max_memory_mb=None,
65
+ enable_timeout=False,
66
+ enable_memory_check=False):
67
+ """
68
+ Initialize resource limiter.
69
+
70
+ Args:
71
+ max_iterations: Maximum total loop iterations (default: 1,000,000)
72
+ timeout_seconds: Maximum execution time (default: 30)
73
+ max_call_depth: Maximum call stack depth (default: 1000)
74
+ max_memory_mb: Maximum memory usage in MB (default: 500)
75
+ enable_timeout: Enable timeout enforcement (default: False, Linux only)
76
+ enable_memory_check: Enable memory checking (default: False, requires psutil)
77
+ """
78
+ self.max_iterations = max_iterations or self.DEFAULT_MAX_ITERATIONS
79
+ self.timeout_seconds = timeout_seconds or self.DEFAULT_TIMEOUT_SECONDS
80
+ self.max_call_depth = max_call_depth or self.DEFAULT_MAX_CALL_DEPTH
81
+ self.max_memory_mb = max_memory_mb or self.DEFAULT_MAX_MEMORY_MB
82
+
83
+ # Feature flags
84
+ self.enable_timeout = enable_timeout
85
+ self.enable_memory_check = enable_memory_check
86
+
87
+ # Runtime counters
88
+ self.iteration_count = 0
89
+ self.call_depth = 0
90
+ self.start_time = None
91
+ self.timeout_handler = None
92
+
93
+ # Memory checking (optional, requires psutil)
94
+ self.psutil_available = False
95
+ if enable_memory_check:
96
+ try:
97
+ import psutil
98
+ self.psutil = psutil
99
+ self.psutil_available = True
100
+ self.process = psutil.Process()
101
+ except ImportError:
102
+ print("⚠️ Warning: psutil not available, memory checking disabled")
103
+ self.enable_memory_check = False
104
+
105
+ def start(self):
106
+ """
107
+ Start resource monitoring (timeout timer, etc.)
108
+ Should be called at the beginning of script execution.
109
+ """
110
+ self.start_time = time.time()
111
+ self.iteration_count = 0
112
+ self.call_depth = 0
113
+
114
+ # Set timeout handler (Linux/Unix only)
115
+ if self.enable_timeout and hasattr(signal, 'SIGALRM'):
116
+ self._set_timeout_alarm()
117
+
118
+ def stop(self):
119
+ """
120
+ Stop resource monitoring and cleanup.
121
+ Should be called at the end of script execution.
122
+ """
123
+ # Cancel timeout alarm
124
+ if self.enable_timeout and hasattr(signal, 'SIGALRM'):
125
+ signal.alarm(0) # Cancel alarm
126
+
127
+ def reset(self):
128
+ """Reset iteration counter (useful for multiple script executions)"""
129
+ self.iteration_count = 0
130
+ self.call_depth = 0
131
+ self.start_time = None
132
+
133
+ def check_iterations(self):
134
+ """
135
+ Check if iteration limit has been exceeded.
136
+ Should be called at the beginning of each loop iteration.
137
+
138
+ Raises:
139
+ ResourceError: If iteration limit exceeded
140
+ """
141
+ self.iteration_count += 1
142
+
143
+ if self.iteration_count > self.max_iterations:
144
+ raise ResourceError(
145
+ f"Iteration limit exceeded: {self.max_iterations:,} iterations\n"
146
+ f"This prevents infinite loops and resource exhaustion.\n\n"
147
+ f"Suggestion: Review your loop conditions or increase the limit with:\n"
148
+ f" zx-run --max-iterations 10000000 script.zx"
149
+ )
150
+
151
+ def check_timeout(self):
152
+ """
153
+ Check if execution timeout has been exceeded.
154
+ Should be called periodically during long operations.
155
+
156
+ Raises:
157
+ TimeoutError: If timeout exceeded
158
+ """
159
+ if self.start_time is None:
160
+ return
161
+
162
+ elapsed = time.time() - self.start_time
163
+ if elapsed > self.timeout_seconds:
164
+ raise TimeoutError(
165
+ f"Execution timeout exceeded: {elapsed:.2f}s > {self.timeout_seconds}s\n"
166
+ f"This prevents denial-of-service via slow operations.\n\n"
167
+ f"Suggestion: Optimize your code or increase timeout with:\n"
168
+ f" zx-run --timeout 60 script.zx"
169
+ )
170
+
171
+ def check_memory(self):
172
+ """
173
+ Check if memory limit has been exceeded.
174
+ Should be called periodically (e.g., every 1000 iterations).
175
+
176
+ Raises:
177
+ ResourceError: If memory limit exceeded
178
+ """
179
+ if not self.enable_memory_check or not self.psutil_available:
180
+ return
181
+
182
+ try:
183
+ memory_mb = self.process.memory_info().rss / 1024 / 1024
184
+
185
+ if memory_mb > self.max_memory_mb:
186
+ raise ResourceError(
187
+ f"Memory limit exceeded: {memory_mb:.2f}MB > {self.max_memory_mb}MB\n"
188
+ f"This prevents memory exhaustion attacks.\n\n"
189
+ f"Suggestion: Reduce memory usage or increase limit with:\n"
190
+ f" zx-run --max-memory 1000 script.zx"
191
+ )
192
+ except Exception as e:
193
+ # Don't crash on memory check failure
194
+ print(f"⚠️ Warning: Memory check failed: {e}")
195
+
196
+ def enter_call(self, function_name=None):
197
+ """
198
+ Called when entering a function/action call.
199
+ Tracks call depth to prevent stack overflow.
200
+
201
+ Args:
202
+ function_name: Optional name of function being called
203
+
204
+ Raises:
205
+ ResourceError: If call depth limit exceeded
206
+ """
207
+ self.call_depth += 1
208
+
209
+ if self.call_depth > self.max_call_depth:
210
+ func_info = f" ({function_name})" if function_name else ""
211
+ raise ResourceError(
212
+ f"Call depth limit exceeded: {self.max_call_depth} nested calls{func_info}\n"
213
+ f"This prevents stack overflow from excessive recursion.\n\n"
214
+ f"Suggestion: Review your recursion or increase limit with:\n"
215
+ f" zx-run --max-call-depth 5000 script.zx"
216
+ )
217
+
218
+ def exit_call(self):
219
+ """
220
+ Called when exiting a function/action call.
221
+ Decrements call depth counter.
222
+ """
223
+ if self.call_depth > 0:
224
+ self.call_depth -= 1
225
+
226
+ def get_stats(self):
227
+ """
228
+ Get current resource usage statistics.
229
+
230
+ Returns:
231
+ dict: Resource usage stats
232
+ """
233
+ stats = {
234
+ 'iterations': self.iteration_count,
235
+ 'max_iterations': self.max_iterations,
236
+ 'iteration_percent': (self.iteration_count / self.max_iterations) * 100,
237
+ 'call_depth': self.call_depth,
238
+ 'max_call_depth': self.max_call_depth,
239
+ }
240
+
241
+ if self.start_time:
242
+ elapsed = time.time() - self.start_time
243
+ stats['elapsed_seconds'] = elapsed
244
+ stats['timeout_seconds'] = self.timeout_seconds
245
+ stats['timeout_percent'] = (elapsed / self.timeout_seconds) * 100
246
+
247
+ if self.enable_memory_check and self.psutil_available:
248
+ try:
249
+ memory_mb = self.process.memory_info().rss / 1024 / 1024
250
+ stats['memory_mb'] = memory_mb
251
+ stats['max_memory_mb'] = self.max_memory_mb
252
+ stats['memory_percent'] = (memory_mb / self.max_memory_mb) * 100
253
+ except:
254
+ pass
255
+
256
+ return stats
257
+
258
+ def _set_timeout_alarm(self):
259
+ """
260
+ Set SIGALRM timeout handler (Linux/Unix only).
261
+ This is automatically called by start() if enable_timeout is True.
262
+ """
263
+ def timeout_handler(signum, frame):
264
+ raise TimeoutError(
265
+ f"Execution timeout: {self.timeout_seconds}s exceeded\n"
266
+ f"This prevents denial-of-service via slow operations.\n\n"
267
+ f"Suggestion: Optimize your code or increase timeout with:\n"
268
+ f" zx-run --timeout 60 script.zx"
269
+ )
270
+
271
+ self.timeout_handler = timeout_handler
272
+ signal.signal(signal.SIGALRM, timeout_handler)
273
+ signal.alarm(self.timeout_seconds)
274
+
275
+
276
+ # Default global limiter (can be overridden)
277
+ _default_limiter = None
278
+
279
+
280
+ def get_default_limiter():
281
+ """Get the default global resource limiter"""
282
+ global _default_limiter
283
+ if _default_limiter is None:
284
+ _default_limiter = ResourceLimiter()
285
+ return _default_limiter
286
+
287
+
288
+ def set_default_limiter(limiter):
289
+ """Set the default global resource limiter"""
290
+ global _default_limiter
291
+ _default_limiter = limiter
@@ -757,15 +757,23 @@ class StatementEvaluatorMixin:
757
757
  if is_error(obj):
758
758
  return obj
759
759
 
760
- # Safely extract property key
761
- if hasattr(node.name.property, 'value'):
762
- prop_key = node.name.property.value
763
- else:
764
- # Evaluate property expression
760
+ # Determine property key based on whether it's computed (obj[expr]) or literal (obj.prop)
761
+ if hasattr(node.name, 'computed') and node.name.computed:
762
+ # Computed property (obj[expr]) - evaluate the expression
765
763
  prop_result = self.eval_node(node.name.property, env, stack_trace)
766
764
  if is_error(prop_result):
767
765
  return prop_result
768
766
  prop_key = prop_result.value if hasattr(prop_result, 'value') else str(prop_result)
767
+ else:
768
+ # Literal property (obj.prop) - use the identifier name directly
769
+ if hasattr(node.name.property, 'value'):
770
+ prop_key = node.name.property.value
771
+ else:
772
+ # Fallback: evaluate it
773
+ prop_result = self.eval_node(node.name.property, env, stack_trace)
774
+ if is_error(prop_result):
775
+ return prop_result
776
+ prop_key = prop_result.value if hasattr(prop_result, 'value') else str(prop_result)
769
777
 
770
778
  # Evaluate value first
771
779
  value = self.eval_node(node.value, env, stack_trace)
@@ -877,6 +885,16 @@ class StatementEvaluatorMixin:
877
885
  def eval_while_statement(self, node, env, stack_trace):
878
886
  result = NULL
879
887
  while True:
888
+ # Resource limit check (Security Fix #7)
889
+ try:
890
+ self.resource_limiter.check_iterations()
891
+ except Exception as e:
892
+ # Convert ResourceError to EvaluationError
893
+ from ..evaluator.resource_limiter import ResourceError, TimeoutError
894
+ if isinstance(e, (ResourceError, TimeoutError)):
895
+ return EvaluationError(str(e))
896
+ raise # Re-raise if not a resource error
897
+
880
898
  cond = self.eval_node(node.condition, env, stack_trace)
881
899
  if is_error(cond):
882
900
  return cond
@@ -904,6 +922,16 @@ class StatementEvaluatorMixin:
904
922
 
905
923
  result = NULL
906
924
  for item in iterable.elements:
925
+ # Resource limit check (Security Fix #7)
926
+ try:
927
+ self.resource_limiter.check_iterations()
928
+ except Exception as e:
929
+ # Convert ResourceError to EvaluationError
930
+ from ..evaluator.resource_limiter import ResourceError, TimeoutError
931
+ if isinstance(e, (ResourceError, TimeoutError)):
932
+ return EvaluationError(str(e))
933
+ raise # Re-raise if not a resource error
934
+
907
935
  env.set(node.item.value, item)
908
936
  result = self.eval_node(node.body, env, stack_trace)
909
937
  if isinstance(result, ReturnValue):
@@ -1688,11 +1716,10 @@ class StatementEvaluatorMixin:
1688
1716
 
1689
1717
  # Pass the AST nodes as storage_vars, not the storage dict
1690
1718
  contract = SmartContract(node.name.value, node.storage_vars, actions)
1691
- contract.deploy()
1719
+ # Deploy with evaluated storage values to avoid storing AST nodes
1720
+ contract.deploy(evaluated_storage_values=storage)
1692
1721
 
1693
- # Initialize storage with evaluated initial values
1694
- for var_name, init_val in storage.items():
1695
- contract.storage.set(var_name, init_val)
1722
+ # Storage values are now set during deploy(), no need to set again
1696
1723
 
1697
1724
  # Check if contract has a constructor and execute it
1698
1725
  if 'constructor' in actions:
@@ -3115,12 +3142,14 @@ class StatementEvaluatorMixin:
3115
3142
  else:
3116
3143
  data_str = str(data)
3117
3144
 
3118
- # Determine encoding
3145
+ # Determine encoding/context
3119
3146
  encoding = Encoding.HTML # Default
3147
+ context_name = "html" # For marking sanitized_for
3120
3148
  if node.encoding:
3121
3149
  enc_val = self.eval_node(node.encoding, env, stack_trace)
3122
3150
  if hasattr(enc_val, 'value'):
3123
3151
  enc_name = enc_val.value.upper()
3152
+ context_name = enc_val.value.lower()
3124
3153
  try:
3125
3154
  encoding = Encoding[enc_name]
3126
3155
  except KeyError:
@@ -3130,10 +3159,16 @@ class StatementEvaluatorMixin:
3130
3159
  try:
3131
3160
  sanitized = Sanitizer.sanitize_string(data_str, encoding)
3132
3161
  debug_log("eval_sanitize_statement", f"Sanitized {len(data_str)} chars with {encoding.value}")
3133
- return String(sanitized)
3162
+ result = String(sanitized)
3163
+ # SECURITY ENFORCEMENT: Mark as sanitized for this context
3164
+ result.mark_sanitized(context_name)
3165
+ return result
3134
3166
  except Exception as e:
3135
3167
  debug_log("eval_sanitize_statement", f"Sanitization error: {e}")
3136
- return String(data_str) # Return original if sanitization fails
3168
+ # Even on error, mark as sanitized to prevent double-sanitization loops
3169
+ result = String(data_str)
3170
+ result.mark_sanitized(context_name)
3171
+ return result
3137
3172
 
3138
3173
  def eval_inject_statement(self, node, env, stack_trace):
3139
3174
  """Evaluate inject statement - full dependency injection with mode-aware resolution."""
@@ -89,20 +89,26 @@ def _zexus_to_python(value):
89
89
  else:
90
90
  return str(value)
91
91
 
92
- def _python_to_zexus(value):
93
- """Convert Python native types to Zexus objects"""
92
+ def _python_to_zexus(value, mark_untrusted=False):
93
+ """Convert Python native types to Zexus objects
94
+
95
+ Args:
96
+ value: Python value to convert
97
+ mark_untrusted: If True, mark strings as untrusted (external data)
98
+ """
94
99
  from ..object import Map, List, String, Integer, Float, Boolean as BooleanObj
95
100
 
96
101
  if isinstance(value, dict):
97
102
  pairs = {}
98
103
  for k, v in value.items():
99
- pairs[k] = _python_to_zexus(v)
104
+ pairs[k] = _python_to_zexus(v, mark_untrusted)
100
105
  return Map(pairs)
101
106
  elif isinstance(value, list):
102
- zexus_list = List([_python_to_zexus(item) for item in value])
107
+ zexus_list = List([_python_to_zexus(item, mark_untrusted) for item in value])
103
108
  return zexus_list
104
109
  elif isinstance(value, str):
105
- return String(value)
110
+ # Mark strings as untrusted if from external source (HTTP, DB, etc.)
111
+ return String(value, is_trusted=not mark_untrusted)
106
112
  elif isinstance(value, int):
107
113
  return Integer(value)
108
114
  elif isinstance(value, float):
@@ -112,7 +118,7 @@ def _python_to_zexus(value):
112
118
  elif value is None:
113
119
  return NULL
114
120
  else:
115
- return String(str(value))
121
+ return String(str(value), is_trusted=not mark_untrusted)
116
122
 
117
123
  def _to_str(obj):
118
124
  """Helper to convert Zexus object to string"""
@@ -87,7 +87,7 @@ if PYGLS_AVAILABLE:
87
87
  """Zexus Language Server implementation."""
88
88
 
89
89
  def __init__(self):
90
- super().__init__('zexus-language-server', 'v1.6.2')
90
+ super().__init__('zexus-language-server', 'v1.6.3')
91
91
  self.completion_provider = CompletionProvider()
92
92
  self.symbol_provider = SymbolProvider()
93
93
  self.hover_provider = HoverProvider()
@@ -31,7 +31,12 @@ class Null(Object):
31
31
  def type(self): return "NULL"
32
32
 
33
33
  class String(Object):
34
- def __init__(self, value): self.value = value
34
+ def __init__(self, value, sanitized_for=None, is_trusted=False):
35
+ self.value = value
36
+ # Track sanitization status for security enforcement
37
+ self.sanitized_for = sanitized_for # None, 'sql', 'html', 'url', 'shell', etc.
38
+ self.is_trusted = is_trusted # True for literals, False for external input
39
+
35
40
  def inspect(self): return self.value
36
41
  def type(self): return "STRING"
37
42
  def __str__(self): return self.value
@@ -43,6 +48,19 @@ class String(Object):
43
48
  def __hash__(self):
44
49
  """Enable String objects to be used as dict keys"""
45
50
  return hash(self.value)
51
+
52
+ def mark_sanitized(self, context):
53
+ """Mark this string as sanitized for a specific context"""
54
+ self.sanitized_for = context
55
+ return self
56
+
57
+ def is_safe_for(self, context):
58
+ """Check if string is safe to use in given context"""
59
+ # Trusted strings (literals) are always safe
60
+ if self.is_trusted:
61
+ return True
62
+ # Check if sanitized for this specific context
63
+ return self.sanitized_for == context
46
64
 
47
65
  class List(Object):
48
66
  def __init__(self, elements): self.elements = elements
@@ -425,7 +443,8 @@ class File(Object):
425
443
  if isinstance(path, String):
426
444
  path = path.value
427
445
  with open(path, 'r', encoding='utf-8') as f:
428
- return String(f.read())
446
+ # Files are external data sources - return untrusted strings
447
+ return String(f.read(), is_trusted=False)
429
448
  except Exception as e:
430
449
  return EvaluationError(f"File read error: {str(e)}")
431
450
 
@@ -27,6 +27,7 @@ precedences = {
27
27
  SLASH: PRODUCT, STAR: PRODUCT, MOD: PRODUCT,
28
28
  LPAREN: CALL,
29
29
  LBRACKET: CALL,
30
+ LBRACE: CALL, # Entity{...} constructor syntax
30
31
  DOT: CALL,
31
32
  }
32
33
 
@@ -82,6 +83,7 @@ class UltimateParser:
82
83
  TRY: self.parse_try_catch_statement,
83
84
  EXTERNAL: self.parse_external_declaration,
84
85
  ASYNC: self.parse_async_expression, # Support async <expression>
86
+ SANITIZE: self.parse_sanitize_expression, # FIX #4: Support sanitize as expression
85
87
  }
86
88
  self.infix_parse_fns = {
87
89
  PLUS: self.parse_infix_expression,
@@ -102,6 +104,7 @@ class UltimateParser:
102
104
  ASSIGN: self.parse_assignment_expression,
103
105
  LAMBDA: self.parse_lambda_infix, # support arrow-style lambdas: params => body
104
106
  LPAREN: self.parse_call_expression,
107
+ LBRACE: self.parse_constructor_call_expression, # Entity{field: value} syntax
105
108
  LBRACKET: self.parse_index_expression,
106
109
  DOT: self.parse_method_call_expression,
107
110
  }
@@ -511,7 +514,8 @@ class UltimateParser:
511
514
  elif self.cur_token_is(STATE):
512
515
  print(f"[PARSE_STMT] Matched STATE", file=sys.stderr, flush=True)
513
516
  node = self.parse_state_statement()
514
- # REQUIRE is now handled by ContextStackParser for enhanced syntax support
517
+ elif self.cur_token_is(REQUIRE):
518
+ node = self.parse_require_statement()
515
519
  elif self.cur_token_is(REVERT):
516
520
  print(f"[PARSE_STMT] Matched REVERT", file=sys.stderr, flush=True)
517
521
  node = self.parse_revert_statement()
@@ -1504,7 +1508,7 @@ class UltimateParser:
1504
1508
  arguments = self.parse_expression_list(RPAREN)
1505
1509
  return MethodCallExpression(object=left, method=method, arguments=arguments)
1506
1510
  else:
1507
- return PropertyAccessExpression(object=left, property=method)
1511
+ return PropertyAccessExpression(object=left, property=method, computed=False)
1508
1512
 
1509
1513
  def parse_export_statement(self):
1510
1514
  token = self.cur_token
@@ -1707,7 +1711,7 @@ class UltimateParser:
1707
1711
  return None
1708
1712
 
1709
1713
  field_name = Identifier(self.cur_token.literal)
1710
- target = PropertyAccessExpression(obj_name, field_name)
1714
+ target = PropertyAccessExpression(obj_name, field_name, computed=False)
1711
1715
 
1712
1716
  # Expect assignment
1713
1717
  if not self.expect_peek(ASSIGN):
@@ -2726,6 +2730,18 @@ class UltimateParser:
2726
2730
  exp.arguments = self.parse_expression_list(RPAREN)
2727
2731
  return exp
2728
2732
 
2733
+ def parse_constructor_call_expression(self, function):
2734
+ """Parse constructor call with map literal syntax: Entity{field: value, ...}
2735
+
2736
+ This converts Entity{a: 1, b: 2} into Entity({a: 1, b: 2})
2737
+ """
2738
+ # Current token is LBRACE, parse it as a map literal
2739
+ map_literal = self.parse_map_literal()
2740
+
2741
+ # Create a call expression with the map as the single argument
2742
+ exp = CallExpression(function=function, arguments=[map_literal])
2743
+ return exp
2744
+
2729
2745
  def parse_prefix_expression(self):
2730
2746
  expression = PrefixExpression(operator=self.cur_token.literal, right=None)
2731
2747
  self.next_token()
@@ -2798,7 +2814,7 @@ class UltimateParser:
2798
2814
  # Expect closing bracket
2799
2815
  if not self.expect_peek(RBRACKET):
2800
2816
  return None
2801
- return PropertyAccessExpression(object=left, property=index_expr)
2817
+ return PropertyAccessExpression(object=left, property=index_expr, computed=True)
2802
2818
 
2803
2819
  def _lookahead_token_after_matching_paren(self):
2804
2820
  """Character-level lookahead: detect if the matching ')' is followed by '=>' (arrow).
@@ -3299,6 +3315,40 @@ class UltimateParser:
3299
3315
 
3300
3316
  return ValidateStatement(data=data_expr, schema=schema_expr)
3301
3317
 
3318
+ def parse_sanitize_expression(self):
3319
+ """Parse sanitize as expression - can be used in assignments
3320
+
3321
+ Supports both:
3322
+ let safe = sanitize data, "sql"
3323
+ let safe = sanitize data as sql
3324
+ """
3325
+ token = self.cur_token
3326
+ self.next_token()
3327
+
3328
+ # Parse data expression
3329
+ data_expr = self.parse_expression(LOWEST)
3330
+ if data_expr is None:
3331
+ self.errors.append(f"Line {token.line}:{token.column} - Expected expression to sanitize")
3332
+ return None
3333
+
3334
+ # Expect comma or 'as'
3335
+ encoding = None
3336
+ if self.cur_token_is(COMMA):
3337
+ self.next_token()
3338
+ # Parse encoding as expression (can be string literal or identifier)
3339
+ encoding = self.parse_expression(LOWEST)
3340
+ elif self.cur_token_is(IDENT) and self.cur_token.literal == 'as':
3341
+ self.next_token()
3342
+ if self.cur_token_is(IDENT):
3343
+ # Convert identifier to string literal
3344
+ encoding = StringLiteral(value=self.cur_token.literal)
3345
+ self.next_token()
3346
+ elif self.cur_token_is(STRING):
3347
+ encoding = self.parse_string_literal()
3348
+
3349
+ result = SanitizeStatement(data=data_expr, rules=None, encoding=encoding)
3350
+ return result
3351
+
3302
3352
  def parse_sanitize_statement(self):
3303
3353
  """Parse sanitize statement - sanitize data"""
3304
3354
  token = self.cur_token
@@ -3698,6 +3748,7 @@ class UltimateParser:
3698
3748
 
3699
3749
  Asserts condition, reverts transaction if false.
3700
3750
  """
3751
+ print(f"[DEBUG PARSER] parse_require_statement called", flush=True)
3701
3752
  token = self.cur_token
3702
3753
 
3703
3754
  if not self.expect_peek(LPAREN):
@@ -3721,6 +3772,7 @@ class UltimateParser:
3721
3772
  if self.peek_token_is(SEMICOLON):
3722
3773
  self.next_token()
3723
3774
 
3775
+ print(f"[DEBUG PARSER] Creating RequireStatement with condition={condition}, message={message}", flush=True)
3724
3776
  return RequireStatement(condition=condition, message=message)
3725
3777
 
3726
3778
  def parse_revert_statement(self):