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.
@@ -1533,9 +1533,40 @@ class ContextStackParser:
1533
1533
  default_val = BooleanLiteral(True)
1534
1534
  elif val_token.type == FALSE:
1535
1535
  default_val = BooleanLiteral(False)
1536
+ elif val_token.type == LBRACE:
1537
+ # Map literal: {}
1538
+ # Find matching RBRACE
1539
+ map_start = current_idx
1540
+ depth = 1
1541
+ current_idx += 1
1542
+ while current_idx < brace_end and depth > 0:
1543
+ if tokens[current_idx].type == LBRACE:
1544
+ depth += 1
1545
+ elif tokens[current_idx].type == RBRACE:
1546
+ depth -= 1
1547
+ current_idx += 1
1548
+ # Parse the map literal
1549
+ map_tokens = tokens[map_start:current_idx]
1550
+ default_val = self._parse_map_literal(map_tokens)
1551
+ elif val_token.type == LBRACKET:
1552
+ # List literal: []
1553
+ # Find matching RBRACKET
1554
+ list_start = current_idx
1555
+ depth = 1
1556
+ current_idx += 1
1557
+ while current_idx < brace_end and depth > 0:
1558
+ if tokens[current_idx].type == LBRACKET:
1559
+ depth += 1
1560
+ elif tokens[current_idx].type == RBRACKET:
1561
+ depth -= 1
1562
+ current_idx += 1
1563
+ # Parse the list literal
1564
+ list_tokens = tokens[list_start:current_idx]
1565
+ default_val = self._parse_list_literal(list_tokens)
1536
1566
  elif val_token.type == IDENT:
1537
1567
  default_val = Identifier(val_token.literal)
1538
- current_idx += 1
1568
+ current_idx += 1
1569
+ # Note: current_idx already advanced for LBRACE and LBRACKET cases
1539
1570
 
1540
1571
  # Use AstNodeShim for compatibility with evaluator
1541
1572
  storage_vars.append(AstNodeShim(
@@ -3234,9 +3265,10 @@ class ContextStackParser:
3234
3265
  # Parse REQUIRE statement: require(condition, message) or require condition { tolerance_block }
3235
3266
  j = i + 1
3236
3267
 
3237
- # Collect tokens until semicolon OR until after tolerance block closes
3268
+ # Collect tokens until semicolon OR until after tolerance block closes OR after closing paren
3238
3269
  require_tokens = [token]
3239
3270
  brace_nest = 0
3271
+ paren_nest = 0
3240
3272
  while j < len(tokens):
3241
3273
  tj = tokens[j]
3242
3274
 
@@ -3246,6 +3278,12 @@ class ContextStackParser:
3246
3278
  elif tj.type == RBRACE:
3247
3279
  brace_nest -= 1
3248
3280
 
3281
+ # Track paren nesting for require(condition, message) form
3282
+ if tj.type == LPAREN:
3283
+ paren_nest += 1
3284
+ elif tj.type == RPAREN:
3285
+ paren_nest -= 1
3286
+
3249
3287
  require_tokens.append(tj)
3250
3288
  j += 1
3251
3289
 
@@ -3253,6 +3291,10 @@ class ContextStackParser:
3253
3291
  if tj.type == SEMICOLON and brace_nest == 0:
3254
3292
  break
3255
3293
 
3294
+ # Stop after closing paren of require(...) form (when paren_nest returns to 0)
3295
+ if tj.type == RPAREN and paren_nest == 0 and brace_nest == 0:
3296
+ break
3297
+
3256
3298
  # Stop after tolerance block closes (if there was one)
3257
3299
  if brace_nest == 0 and len(require_tokens) > 1 and require_tokens[-2].type == RBRACE:
3258
3300
  break
@@ -3921,6 +3963,24 @@ class ContextStackParser:
3921
3963
  if i < n and tokens[i].type == RPAREN:
3922
3964
  i += 1 # Skip RPAREN
3923
3965
  return CallExpression(Identifier(name), args, type_args=type_args)
3966
+
3967
+ # Check for constructor call with map literal: Entity{field: value, ...}
3968
+ elif i < n and tokens[i].type == LBRACE:
3969
+ # Parse the map literal as the single argument
3970
+ start = i
3971
+ depth = 1
3972
+ i += 1 # Skip LBRACE
3973
+ # Find matching RBRACE
3974
+ while i < n and depth > 0:
3975
+ if tokens[i].type == LBRACE:
3976
+ depth += 1
3977
+ elif tokens[i].type == RBRACE:
3978
+ depth -= 1
3979
+ i += 1
3980
+ # Parse the map literal tokens (including braces)
3981
+ map_literal = self._parse_map_literal(tokens[start:i])
3982
+ return CallExpression(Identifier(name), [map_literal], type_args=type_args)
3983
+
3924
3984
  else:
3925
3985
  return Identifier(name)
3926
3986
 
@@ -3986,7 +4046,8 @@ class ContextStackParser:
3986
4046
  # Property access: expr.name
3987
4047
  current_expr = PropertyAccessExpression(
3988
4048
  object=current_expr,
3989
- property=Identifier(name_token.literal)
4049
+ property=Identifier(name_token.literal),
4050
+ computed=False
3990
4051
  )
3991
4052
  continue
3992
4053
 
@@ -4041,7 +4102,8 @@ class ContextStackParser:
4041
4102
  prop_expr = self._parse_expression(inner_tokens) if inner_tokens else Identifier('')
4042
4103
  current_expr = PropertyAccessExpression(
4043
4104
  object=current_expr,
4044
- property=prop_expr
4105
+ property=prop_expr,
4106
+ computed=True
4045
4107
  )
4046
4108
  continue
4047
4109
 
@@ -4597,7 +4659,7 @@ class ContextStackParser:
4597
4659
 
4598
4660
  Returns a SanitizeStatement which can be evaluated as an expression.
4599
4661
  """
4600
- print(" 🔧 [Sanitize Expression] Parsing sanitize expression")
4662
+ # print(" 🔧 [Sanitize Expression] Parsing sanitize expression")
4601
4663
  if not tokens or tokens[0].type != SANITIZE:
4602
4664
  return None
4603
4665
 
@@ -6155,8 +6217,22 @@ class ContextStackParser:
6155
6217
 
6156
6218
  # Check for parenthesized form: require(condition, message)
6157
6219
  if start_idx < len(tokens) and tokens[start_idx].type == LPAREN:
6158
- # Extract tokens between LPAREN and RPAREN
6159
- inner = tokens[start_idx+1:-1] if len(tokens) > start_idx+1 and tokens[-1].type == RPAREN else tokens[start_idx+1:]
6220
+ # Find matching RPAREN
6221
+ paren_depth = 1
6222
+ end_idx = start_idx + 1
6223
+ while end_idx < len(tokens) and paren_depth > 0:
6224
+ if tokens[end_idx].type == LPAREN:
6225
+ paren_depth += 1
6226
+ elif tokens[end_idx].type == RPAREN:
6227
+ paren_depth -= 1
6228
+ end_idx += 1
6229
+
6230
+ if paren_depth != 0:
6231
+ parser_debug(" ❌ Unmatched parentheses in require")
6232
+ return None
6233
+
6234
+ # Extract tokens between LPAREN and matching RPAREN
6235
+ inner = tokens[start_idx+1:end_idx-1]
6160
6236
 
6161
6237
  # Split by comma to get condition and optional message
6162
6238
  args = self._parse_argument_list(inner)
@@ -609,7 +609,14 @@ class StructuralAnalyzer:
609
609
  elif tokens[k].type in {LBRACE, COLON}:
610
610
  # Found statement form indicators
611
611
  break
612
- if not (in_assignment and (allow_in_assignment or allow_debug_call or allow_if_then_else)):
612
+
613
+ # FIX #4: After seeing SANITIZE in assignment, also check if previous token was SANITIZE
614
+ # This allows collecting the sanitize expression arguments
615
+ prev_was_sanitize = False
616
+ if j > 0 and tokens[j - 1].type == SANITIZE:
617
+ prev_was_sanitize = True
618
+
619
+ if not (in_assignment and (allow_in_assignment or allow_debug_call or allow_if_then_else or prev_was_sanitize)):
613
620
  break
614
621
 
615
622
  # CRITICAL FIX: Also break on modifier tokens at nesting 0 when followed by statement keywords
@@ -647,9 +654,10 @@ class StructuralAnalyzer:
647
654
  stmt_tokens.append(tj)
648
655
  j += 1
649
656
 
650
- # MODIFIED: For RETURN, CONTINUE, and PRINT, stop after closing parens at nesting 0
651
- # PRINT can have multiple comma-separated arguments inside the parens
652
- if t.type in {RETURN, CONTINUE, PRINT} and nesting == 0 and tj.type == RPAREN:
657
+ # MODIFIED: For RETURN, CONTINUE, PRINT, and REQUIRE, stop after closing parens at nesting 0
658
+ # These can have multiple comma-separated arguments inside the parens
659
+ # NOTE: 't' is the statement starter token (first token), 'tj' is the just-collected token
660
+ if t.type in {RETURN, CONTINUE, PRINT, REQUIRE} and nesting == 0 and tj.type == RPAREN:
653
661
  break
654
662
 
655
663
  # If we just closed a brace block and are back at nesting 0, stop
@@ -128,15 +128,35 @@ def disable_memory_tracking():
128
128
  # PERSISTENT STORAGE BACKEND
129
129
  # ===============================================
130
130
 
131
+ class StorageLimitError(Exception):
132
+ """Raised when persistent storage limits are exceeded"""
133
+ pass
134
+
135
+
131
136
  class PersistentStorage:
132
- """Persistent storage for variables using SQLite"""
137
+ """Persistent storage for variables using SQLite with size limits"""
133
138
 
134
- def __init__(self, scope_id: str, storage_dir: str = PERSISTENCE_DIR):
139
+ # Default limits (configurable)
140
+ DEFAULT_MAX_ITEMS = 10000 # Maximum number of stored variables
141
+ DEFAULT_MAX_SIZE_MB = 100 # Maximum storage size in MB
142
+
143
+ def __init__(self, scope_id: str, storage_dir: str = PERSISTENCE_DIR,
144
+ max_items: int = None, max_size_mb: int = None):
135
145
  self.scope_id = scope_id
136
146
  self.db_path = os.path.join(storage_dir, f"{scope_id}.sqlite")
137
147
  self.conn = None
138
148
  self.lock = Lock()
149
+
150
+ # Storage limits
151
+ self.max_items = max_items if max_items is not None else self.DEFAULT_MAX_ITEMS
152
+ self.max_size_bytes = (max_size_mb if max_size_mb is not None else self.DEFAULT_MAX_SIZE_MB) * 1024 * 1024
153
+
154
+ # Usage tracking
155
+ self.current_item_count = 0
156
+ self.current_size_bytes = 0
157
+
139
158
  self._init_db()
159
+ self._update_usage_stats()
140
160
 
141
161
  def _init_db(self):
142
162
  """Initialize SQLite database"""
@@ -147,6 +167,7 @@ class PersistentStorage:
147
167
  name TEXT PRIMARY KEY,
148
168
  type TEXT NOT NULL,
149
169
  value TEXT NOT NULL,
170
+ size_bytes INTEGER DEFAULT 0,
150
171
  is_const INTEGER DEFAULT 0,
151
172
  created_at REAL NOT NULL,
152
173
  updated_at REAL NOT NULL
@@ -157,24 +178,96 @@ class PersistentStorage:
157
178
  ''')
158
179
  self.conn.commit()
159
180
 
181
+ def _update_usage_stats(self):
182
+ """Update current usage statistics"""
183
+ with self.lock:
184
+ cursor = self.conn.cursor()
185
+
186
+ # Count items
187
+ cursor.execute('SELECT COUNT(*) FROM variables')
188
+ self.current_item_count = cursor.fetchone()[0]
189
+
190
+ # Calculate total size
191
+ cursor.execute('SELECT SUM(size_bytes) FROM variables')
192
+ result = cursor.fetchone()[0]
193
+ self.current_size_bytes = result if result else 0
194
+
195
+ def _calculate_size(self, serialized: Dict[str, str]) -> int:
196
+ """Calculate size of serialized data in bytes"""
197
+ # Approximate size: length of type + value strings
198
+ size = len(serialized['type']) + len(serialized['value'])
199
+ return size
200
+
201
+ def _check_limits(self, name: str, new_size: int) -> None:
202
+ """Check if adding/updating a variable would exceed limits"""
203
+ # Get current size of existing variable if it exists
204
+ cursor = self.conn.cursor()
205
+ cursor.execute('SELECT size_bytes FROM variables WHERE name = ?', (name,))
206
+ row = cursor.fetchone()
207
+ existing_size = row[0] if row else 0
208
+ is_update = row is not None
209
+
210
+ # Calculate new totals
211
+ new_item_count = self.current_item_count if is_update else self.current_item_count + 1
212
+ new_total_size = self.current_size_bytes - existing_size + new_size
213
+
214
+ # Check item limit
215
+ if new_item_count > self.max_items:
216
+ raise StorageLimitError(
217
+ f"Persistent storage item limit exceeded: {new_item_count} > {self.max_items}. "
218
+ f"Cannot store variable '{name}'. "
219
+ f"Consider increasing max_items or cleaning up old variables."
220
+ )
221
+
222
+ # Check size limit
223
+ if new_total_size > self.max_size_bytes:
224
+ size_mb = new_total_size / (1024 * 1024)
225
+ limit_mb = self.max_size_bytes / (1024 * 1024)
226
+ raise StorageLimitError(
227
+ f"Persistent storage size limit exceeded: {size_mb:.2f}MB > {limit_mb:.2f}MB. "
228
+ f"Cannot store variable '{name}' ({new_size} bytes). "
229
+ f"Consider increasing max_size_mb or cleaning up old data."
230
+ )
231
+
232
+ def get_usage_stats(self) -> Dict[str, Any]:
233
+ """Get current storage usage statistics"""
234
+ return {
235
+ 'item_count': self.current_item_count,
236
+ 'max_items': self.max_items,
237
+ 'items_remaining': self.max_items - self.current_item_count,
238
+ 'size_bytes': self.current_size_bytes,
239
+ 'size_mb': self.current_size_bytes / (1024 * 1024),
240
+ 'max_size_mb': self.max_size_bytes / (1024 * 1024),
241
+ 'size_remaining_mb': (self.max_size_bytes - self.current_size_bytes) / (1024 * 1024),
242
+ 'usage_percent': (self.current_size_bytes / self.max_size_bytes * 100) if self.max_size_bytes > 0 else 0
243
+ }
244
+
160
245
  def set(self, name: str, value: Object, is_const: bool = False):
161
- """Persist a variable"""
246
+ """Persist a variable with size limit checks"""
162
247
  with self.lock:
163
248
  serialized = self._serialize(value)
249
+ size_bytes = self._calculate_size(serialized)
250
+
251
+ # Check limits before storing
252
+ self._check_limits(name, size_bytes)
253
+
164
254
  cursor = self.conn.cursor()
165
255
 
166
256
  import time
167
257
  timestamp = time.time()
168
258
 
169
259
  cursor.execute('''
170
- INSERT OR REPLACE INTO variables (name, type, value, is_const, created_at, updated_at)
171
- VALUES (?, ?, ?, ?,
260
+ INSERT OR REPLACE INTO variables (name, type, value, size_bytes, is_const, created_at, updated_at)
261
+ VALUES (?, ?, ?, ?, ?,
172
262
  COALESCE((SELECT created_at FROM variables WHERE name = ?), ?),
173
263
  ?)
174
- ''', (name, serialized['type'], serialized['value'], 1 if is_const else 0,
264
+ ''', (name, serialized['type'], serialized['value'], size_bytes, 1 if is_const else 0,
175
265
  name, timestamp, timestamp))
176
266
 
177
267
  self.conn.commit()
268
+
269
+ # Update usage stats
270
+ self._update_usage_stats()
178
271
 
179
272
  def get(self, name: str) -> Optional[Object]:
180
273
  """Retrieve a persisted variable"""
@@ -186,6 +279,9 @@ class PersistentStorage:
186
279
  if row is None:
187
280
  return None
188
281
 
282
+
283
+ # Update usage stats
284
+ self._update_usage_stats()
189
285
  return self._deserialize({'type': row[0], 'value': row[1]})
190
286
 
191
287
  def delete(self, name: str):
@@ -213,6 +309,9 @@ class PersistentStorage:
213
309
  def clear(self):
214
310
  """Clear all persisted variables"""
215
311
  with self.lock:
312
+
313
+ # Update usage stats
314
+ self._update_usage_stats()
216
315
  cursor = self.conn.cursor()
217
316
  cursor.execute('DELETE FROM variables')
218
317
  self.conn.commit()
@@ -734,8 +734,10 @@ class StorageBackend:
734
734
  class InMemoryBackend(StorageBackend):
735
735
  def __init__(self):
736
736
  self.data = {}
737
- def set(self, key, value): self.data[key] = value
738
- def get(self, key): return self.data.get(key)
737
+ def set(self, key, value):
738
+ self.data[key] = value
739
+ def get(self, key):
740
+ return self.data.get(key)
739
741
  def delete(self, key):
740
742
  if key in self.data: del self.data[key]
741
743
 
@@ -954,8 +956,24 @@ class SmartContract:
954
956
 
955
957
  print(f" 🔗 Contract Address: {new_address}")
956
958
 
957
- # Deploy the instance (initialize storage)
958
- instance.deploy()
959
+ # Copy initial storage values from the template contract
960
+ # This ensures instances get the evaluated initial values
961
+ initial_storage = {}
962
+ for var_node in self.storage_vars:
963
+ var_name = None
964
+ if hasattr(var_node, 'name'):
965
+ var_name = var_node.name.value if hasattr(var_node.name, 'value') else var_node.name
966
+ elif isinstance(var_node, dict):
967
+ var_name = var_node.get("name")
968
+
969
+ if var_name:
970
+ # Get the initial value from the template contract's storage
971
+ value = self.storage.get(var_name)
972
+ if value is not None:
973
+ initial_storage[var_name] = value
974
+
975
+ # Deploy the instance with the copied initial values
976
+ instance.deploy(evaluated_storage_values=initial_storage)
959
977
  instance.parent_contract = self
960
978
 
961
979
  print(f" Available actions: {list(self.actions.keys())}")
@@ -964,32 +982,32 @@ class SmartContract:
964
982
  def __call__(self, *args):
965
983
  return self.instantiate(args)
966
984
 
967
- def deploy(self):
968
- """Deploy the contract and initialize persistent storage"""
985
+ def deploy(self, evaluated_storage_values=None):
986
+ """Deploy the contract and initialize persistent storage
987
+
988
+ Args:
989
+ evaluated_storage_values: Optional dict of evaluated initial values
990
+ """
969
991
  # Checks if we should reset storage or strictly load existing
970
992
  # For simplicity in this VM, subsequent runs act like "loading" if DB exists
971
993
  self.is_deployed = True
972
994
 
973
- # Initialize storage only if key doesn't exist (preserve persistence)
974
- for var_node in self.storage_vars:
975
- var_name = None
976
- default_value = None
977
-
978
- if hasattr(var_node, 'initial_value'):
979
- var_name = var_node.name.value if hasattr(var_node.name, 'value') else var_node.name
980
- default_value = var_node.initial_value
981
- elif isinstance(var_node, dict) and "initial_value" in var_node:
982
- var_name = var_node.get("name")
983
- default_value = var_node["initial_value"]
984
-
985
- if var_name:
986
- # ONLY set if not already in DB (Persistence Logic)
995
+ # If evaluated values are provided, use them (from evaluator)
996
+ if evaluated_storage_values:
997
+ for var_name, value in evaluated_storage_values.items():
987
998
  if self.storage.get(var_name) is None:
988
- if default_value is not None:
989
- self.storage.set(var_name, default_value)
990
- else:
991
- # Set reasonable defaults for types if null
992
- self.storage.set(var_name, Null)
999
+ self.storage.set(var_name, value)
1000
+ else:
1001
+ # Fallback: Initialize storage with NULL for declared variables
1002
+ for var_node in self.storage_vars:
1003
+ var_name = None
1004
+ if hasattr(var_node, 'name'):
1005
+ var_name = var_node.name.value if hasattr(var_node.name, 'value') else var_node.name
1006
+ elif isinstance(var_node, dict):
1007
+ var_name = var_node.get("name")
1008
+
1009
+ if var_name and self.storage.get(var_name) is None:
1010
+ self.storage.set(var_name, Null)
993
1011
 
994
1012
  def call_method(self, action_name, args):
995
1013
  """Call a contract action - similar to EntityInstance.call_method"""