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.
- package/README.md +165 -5
- package/package.json +1 -1
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/access_control_system/__init__.py +38 -0
- package/src/zexus/access_control_system/access_control.py +237 -0
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/debug_sanitizer.py +250 -0
- package/src/zexus/error_reporter.py +22 -2
- package/src/zexus/evaluator/core.py +17 -21
- package/src/zexus/evaluator/expressions.py +116 -57
- package/src/zexus/evaluator/functions.py +613 -170
- package/src/zexus/evaluator/resource_limiter.py +291 -0
- package/src/zexus/evaluator/statements.py +47 -12
- package/src/zexus/evaluator/utils.py +12 -6
- package/src/zexus/lsp/server.py +1 -1
- package/src/zexus/object.py +21 -2
- package/src/zexus/parser/parser.py +56 -4
- package/src/zexus/parser/strategy_context.py +83 -7
- package/src/zexus/parser/strategy_structural.py +12 -4
- package/src/zexus/persistence.py +105 -6
- package/src/zexus/security.py +43 -25
- package/src/zexus/security_enforcement.py +237 -0
- package/src/zexus/stdlib/fs.py +120 -22
- package/src/zexus/zexus_ast.py +3 -2
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +499 -13
- package/src/zexus.egg-info/SOURCES.txt +258 -152
|
@@ -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
|
-
|
|
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
|
-
#
|
|
6159
|
-
|
|
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
|
-
|
|
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
|
|
651
|
-
#
|
|
652
|
-
|
|
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
|
package/src/zexus/persistence.py
CHANGED
|
@@ -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
|
-
|
|
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()
|
package/src/zexus/security.py
CHANGED
|
@@ -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):
|
|
738
|
-
|
|
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
|
-
#
|
|
958
|
-
|
|
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
|
-
#
|
|
974
|
-
|
|
975
|
-
var_name
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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"""
|