zexus 1.8.0 → 1.8.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/README.md +34 -6
- package/bin/zexus +12 -2
- package/bin/zpics +12 -2
- package/bin/zpm +12 -2
- package/bin/zx +12 -2
- package/bin/zx-deploy +12 -2
- package/bin/zx-dev +12 -2
- package/bin/zx-run +12 -2
- package/package.json +2 -1
- package/rust_core/Cargo.lock +603 -0
- package/rust_core/Cargo.toml +26 -0
- package/rust_core/README.md +15 -0
- package/rust_core/pyproject.toml +25 -0
- package/rust_core/src/binary_bytecode.rs +543 -0
- package/rust_core/src/contract_vm.rs +643 -0
- package/rust_core/src/executor.rs +847 -0
- package/rust_core/src/hasher.rs +90 -0
- package/rust_core/src/lib.rs +71 -0
- package/rust_core/src/merkle.rs +128 -0
- package/rust_core/src/rust_vm.rs +2313 -0
- package/rust_core/src/signature.rs +79 -0
- package/rust_core/src/state_adapter.rs +281 -0
- package/rust_core/src/validator.rs +116 -0
- package/scripts/postinstall.js +204 -21
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/evaluator/bytecode_compiler.py +150 -52
- package/src/zexus/evaluator/core.py +151 -809
- package/src/zexus/evaluator/expressions.py +27 -22
- package/src/zexus/evaluator/functions.py +171 -126
- package/src/zexus/evaluator/statements.py +55 -112
- package/src/zexus/module_cache.py +20 -9
- package/src/zexus/object.py +330 -38
- package/src/zexus/parser/parser.py +103 -23
- package/src/zexus/parser/strategy_context.py +318 -6
- package/src/zexus/parser/strategy_structural.py +2 -2
- package/src/zexus/persistence.py +46 -17
- package/src/zexus/security.py +140 -234
- package/src/zexus/type_checker.py +44 -5
- package/src/zexus/vm/binary_bytecode.py +7 -3
- package/src/zexus/vm/bytecode.py +6 -0
- package/src/zexus/vm/cache.py +24 -46
- package/src/zexus/vm/compiler.py +549 -68
- package/src/zexus/vm/memory_pool.py +21 -9
- package/src/zexus/vm/vm.py +609 -95
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +56 -12
- package/src/zexus.egg-info/SOURCES.txt +14 -0
- package/src/zexus.egg-info/entry_points.txt +5 -1
- package/src/zexus.egg-info/requires.txt +26 -0
|
@@ -15,7 +15,9 @@ STATEMENT_STARTERS = {
|
|
|
15
15
|
STORAGE, AUDIT, RESTRICT, SANDBOX, TRAIL, NATIVE, GC, INLINE, BUFFER,
|
|
16
16
|
SIMD, DEFER, PATTERN, ENUM, STREAM, WATCH, LOG, CAPABILITY, GRANT,
|
|
17
17
|
REVOKE, VALIDATE, SANITIZE, IMMUTABLE, INTERFACE, TYPE_ALIAS, MODULE,
|
|
18
|
-
PACKAGE, USING, MIDDLEWARE, AUTH, THROTTLE, CACHE, REQUIRE
|
|
18
|
+
PACKAGE, USING, MIDDLEWARE, AUTH, THROTTLE, CACHE, REQUIRE,
|
|
19
|
+
EMIT, PROTOCOL, SEAL,
|
|
20
|
+
CHANNEL, SEND, RECEIVE, ATOMIC, ASYNC,
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
_MEANINGFUL_TOKEN_TYPES = {
|
|
@@ -162,6 +164,10 @@ class ContextStackParser:
|
|
|
162
164
|
ANIMATION: self._parse_animation_statement,
|
|
163
165
|
CLOCK: self._parse_clock_statement,
|
|
164
166
|
GC: self._parse_gc_statement_block,
|
|
167
|
+
# EMIT handler
|
|
168
|
+
EMIT: self._parse_emit_statement_block,
|
|
169
|
+
# PROTOCOL handler
|
|
170
|
+
PROTOCOL: self._parse_protocol_statement_block,
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
def push_context(self, context_type, context_name=None):
|
|
@@ -1262,7 +1268,8 @@ class ContextStackParser:
|
|
|
1262
1268
|
EXPORT, USE, DEBUG, ENTITY, CONTRACT, AUDIT, RESTRICT, SANDBOX, TRAIL, NATIVE, GC, INLINE,
|
|
1263
1269
|
BUFFER, SIMD, DEFER, PATTERN, ENUM, STREAM, WATCH, CAPABILITY, GRANT,
|
|
1264
1270
|
REVOKE, VALIDATE, SANITIZE, IMMUTABLE, INTERFACE, TYPE_ALIAS, MODULE,
|
|
1265
|
-
PACKAGE, USING
|
|
1271
|
+
PACKAGE, USING,
|
|
1272
|
+
CHANNEL, SEND, RECEIVE, ATOMIC, ASYNC,
|
|
1266
1273
|
}
|
|
1267
1274
|
j = assign_idx + 1
|
|
1268
1275
|
nesting_depth = 0
|
|
@@ -1558,9 +1565,18 @@ class ContextStackParser:
|
|
|
1558
1565
|
except Exception:
|
|
1559
1566
|
pass
|
|
1560
1567
|
|
|
1561
|
-
# 1. Extract Name
|
|
1568
|
+
# 1. Extract Name and optional implements clause
|
|
1562
1569
|
contract_name = tokens[1].literal if tokens[1].type == IDENT else "UnknownContract"
|
|
1563
|
-
|
|
1570
|
+
implements = None
|
|
1571
|
+
|
|
1572
|
+
# Check for 'implements' keyword: contract Name implements ProtocolName { ... }
|
|
1573
|
+
for idx in range(2, min(len(tokens), 5)):
|
|
1574
|
+
if idx < len(tokens) and tokens[idx].type == IMPLEMENTS:
|
|
1575
|
+
if idx + 1 < len(tokens) and tokens[idx + 1].type == IDENT:
|
|
1576
|
+
implements = Identifier(tokens[idx + 1].literal)
|
|
1577
|
+
break
|
|
1578
|
+
|
|
1579
|
+
parser_debug(f" 📝 Contract Name: {contract_name}" + (f" implements {implements}" if implements else ""))
|
|
1564
1580
|
|
|
1565
1581
|
# 2. Identify Block Boundaries
|
|
1566
1582
|
brace_start = -1
|
|
@@ -1935,7 +1951,8 @@ class ContextStackParser:
|
|
|
1935
1951
|
contract_stmt = ContractStatement(
|
|
1936
1952
|
name=Identifier(contract_name),
|
|
1937
1953
|
body=body_block,
|
|
1938
|
-
modifiers=None
|
|
1954
|
+
modifiers=None,
|
|
1955
|
+
implements=implements
|
|
1939
1956
|
)
|
|
1940
1957
|
|
|
1941
1958
|
# Add backward compatibility attributes
|
|
@@ -3156,6 +3173,67 @@ class ContextStackParser:
|
|
|
3156
3173
|
i = j
|
|
3157
3174
|
continue
|
|
3158
3175
|
|
|
3176
|
+
elif token.type == EMIT:
|
|
3177
|
+
# Parse EMIT statement: emit EventName(args)
|
|
3178
|
+
j = i + 1
|
|
3179
|
+
emit_tokens = [token]
|
|
3180
|
+
paren_nest = 0
|
|
3181
|
+
|
|
3182
|
+
# Collect until end of emit statement (close paren or semicolon/newline boundary)
|
|
3183
|
+
while j < len(tokens):
|
|
3184
|
+
tj = tokens[j]
|
|
3185
|
+
if tj.type == LPAREN:
|
|
3186
|
+
paren_nest += 1
|
|
3187
|
+
elif tj.type == RPAREN:
|
|
3188
|
+
paren_nest -= 1
|
|
3189
|
+
if paren_nest == 0:
|
|
3190
|
+
emit_tokens.append(tj)
|
|
3191
|
+
j += 1
|
|
3192
|
+
break
|
|
3193
|
+
elif tj.type == SEMICOLON and paren_nest == 0:
|
|
3194
|
+
j += 1
|
|
3195
|
+
break
|
|
3196
|
+
emit_tokens.append(tj)
|
|
3197
|
+
j += 1
|
|
3198
|
+
|
|
3199
|
+
parser_debug(f" 📝 Found emit statement: {[t.literal for t in emit_tokens]}")
|
|
3200
|
+
|
|
3201
|
+
block_info = {'tokens': emit_tokens}
|
|
3202
|
+
stmt = self._parse_emit_statement_block(block_info, tokens)
|
|
3203
|
+
if stmt:
|
|
3204
|
+
statements.append(stmt)
|
|
3205
|
+
|
|
3206
|
+
i = j
|
|
3207
|
+
continue
|
|
3208
|
+
|
|
3209
|
+
elif token.type == PROTOCOL:
|
|
3210
|
+
# Parse PROTOCOL statement: protocol Name { action method1() ... }
|
|
3211
|
+
j = i + 1
|
|
3212
|
+
proto_tokens = [token]
|
|
3213
|
+
brace_nest = 0
|
|
3214
|
+
|
|
3215
|
+
while j < len(tokens):
|
|
3216
|
+
tj = tokens[j]
|
|
3217
|
+
proto_tokens.append(tj)
|
|
3218
|
+
if tj.type == LBRACE:
|
|
3219
|
+
brace_nest += 1
|
|
3220
|
+
elif tj.type == RBRACE:
|
|
3221
|
+
brace_nest -= 1
|
|
3222
|
+
if brace_nest == 0:
|
|
3223
|
+
j += 1
|
|
3224
|
+
break
|
|
3225
|
+
j += 1
|
|
3226
|
+
|
|
3227
|
+
parser_debug(f" 📝 Found protocol statement: {[t.literal for t in proto_tokens[:10]]}")
|
|
3228
|
+
|
|
3229
|
+
block_info = {'tokens': proto_tokens}
|
|
3230
|
+
stmt = self._parse_protocol_statement_block(block_info, tokens)
|
|
3231
|
+
if stmt:
|
|
3232
|
+
statements.append(stmt)
|
|
3233
|
+
|
|
3234
|
+
i = j
|
|
3235
|
+
continue
|
|
3236
|
+
|
|
3159
3237
|
elif token.type == WATCH:
|
|
3160
3238
|
j = i + 1
|
|
3161
3239
|
stmt_tokens = [token]
|
|
@@ -3907,6 +3985,35 @@ class ContextStackParser:
|
|
|
3907
3985
|
i = j
|
|
3908
3986
|
continue
|
|
3909
3987
|
|
|
3988
|
+
elif token.type == CHANNEL:
|
|
3989
|
+
# Parse CHANNEL declaration: channel<type>[capacity] name
|
|
3990
|
+
j = i + 1
|
|
3991
|
+
channel_tokens = [token]
|
|
3992
|
+
# Collect until we hit a statement starter or semicolon at nesting 0
|
|
3993
|
+
nesting = 0
|
|
3994
|
+
while j < len(tokens):
|
|
3995
|
+
tj = tokens[j]
|
|
3996
|
+
if tj.type in (LBRACKET, LT):
|
|
3997
|
+
nesting += 1
|
|
3998
|
+
elif tj.type in (RBRACKET, GT):
|
|
3999
|
+
nesting -= 1
|
|
4000
|
+
elif nesting == 0 and tj.type == SEMICOLON:
|
|
4001
|
+
j += 1 # skip semicolon
|
|
4002
|
+
break
|
|
4003
|
+
elif nesting == 0 and tj.type in statement_starters and len(channel_tokens) > 1:
|
|
4004
|
+
break
|
|
4005
|
+
channel_tokens.append(tj)
|
|
4006
|
+
j += 1
|
|
4007
|
+
|
|
4008
|
+
# Use the context handler to parse the channel tokens
|
|
4009
|
+
block_info = {'tokens': channel_tokens}
|
|
4010
|
+
stmt = self._parse_channel_statement(block_info, tokens)
|
|
4011
|
+
if stmt:
|
|
4012
|
+
statements.append(stmt)
|
|
4013
|
+
|
|
4014
|
+
i = j
|
|
4015
|
+
continue
|
|
4016
|
+
|
|
3910
4017
|
elif token.type == ATOMIC:
|
|
3911
4018
|
# Parse ATOMIC statement: atomic { statements }
|
|
3912
4019
|
j = i + 1
|
|
@@ -3939,6 +4046,48 @@ class ContextStackParser:
|
|
|
3939
4046
|
continue
|
|
3940
4047
|
|
|
3941
4048
|
elif token.type == ASYNC:
|
|
4049
|
+
# Check if this is "async action" — treat as action with async modifier
|
|
4050
|
+
if i + 1 < len(tokens) and tokens[i + 1].type == ACTION:
|
|
4051
|
+
# Collect entire async action declaration including brace body
|
|
4052
|
+
j = i + 1 # skip to ACTION token
|
|
4053
|
+
stmt_tokens = [tokens[j]] # Start with ACTION token
|
|
4054
|
+
j += 1
|
|
4055
|
+
brace_nest = 0
|
|
4056
|
+
paren_nest = 0
|
|
4057
|
+
while j < len(tokens):
|
|
4058
|
+
tj = tokens[j]
|
|
4059
|
+
stmt_tokens.append(tj)
|
|
4060
|
+
if tj.type == LPAREN:
|
|
4061
|
+
paren_nest += 1
|
|
4062
|
+
elif tj.type == RPAREN:
|
|
4063
|
+
if paren_nest > 0:
|
|
4064
|
+
paren_nest -= 1
|
|
4065
|
+
elif tj.type == LBRACE:
|
|
4066
|
+
brace_nest += 1
|
|
4067
|
+
elif tj.type == RBRACE:
|
|
4068
|
+
brace_nest -= 1
|
|
4069
|
+
if brace_nest == 0:
|
|
4070
|
+
j += 1
|
|
4071
|
+
break
|
|
4072
|
+
j += 1
|
|
4073
|
+
|
|
4074
|
+
# Parse as action statement and set is_async flag
|
|
4075
|
+
block_info = {'tokens': stmt_tokens}
|
|
4076
|
+
stmt = self._parse_action_statement(block_info, tokens)
|
|
4077
|
+
if stmt:
|
|
4078
|
+
stmt.is_async = True
|
|
4079
|
+
try:
|
|
4080
|
+
existing_modifiers = list(getattr(stmt, 'modifiers', []) or [])
|
|
4081
|
+
if 'async' not in existing_modifiers:
|
|
4082
|
+
existing_modifiers.append('async')
|
|
4083
|
+
stmt.modifiers = existing_modifiers
|
|
4084
|
+
except Exception:
|
|
4085
|
+
stmt.modifiers = ['async']
|
|
4086
|
+
statements.append(stmt)
|
|
4087
|
+
|
|
4088
|
+
i = j
|
|
4089
|
+
continue
|
|
4090
|
+
|
|
3942
4091
|
# Parse ASYNC expression: async <expression>
|
|
3943
4092
|
j = i + 1
|
|
3944
4093
|
|
|
@@ -6081,6 +6230,22 @@ class ContextStackParser:
|
|
|
6081
6230
|
# If this is "async function", let _parse_function_statement_context handle it
|
|
6082
6231
|
if len(tokens) > 1 and tokens[1].type == FUNCTION:
|
|
6083
6232
|
return self._parse_function_statement_context(block_info, all_tokens)
|
|
6233
|
+
|
|
6234
|
+
# If this is "async action", let _parse_action_statement handle it with async flag
|
|
6235
|
+
if len(tokens) > 1 and tokens[1].type == ACTION:
|
|
6236
|
+
action_tokens = tokens[1:] # Strip ASYNC
|
|
6237
|
+
action_block = {'tokens': action_tokens}
|
|
6238
|
+
stmt = self._parse_action_statement(action_block, all_tokens)
|
|
6239
|
+
if stmt:
|
|
6240
|
+
stmt.is_async = True
|
|
6241
|
+
try:
|
|
6242
|
+
existing_modifiers = list(getattr(stmt, 'modifiers', []) or [])
|
|
6243
|
+
if 'async' not in existing_modifiers:
|
|
6244
|
+
existing_modifiers.append('async')
|
|
6245
|
+
stmt.modifiers = existing_modifiers
|
|
6246
|
+
except Exception:
|
|
6247
|
+
stmt.modifiers = ['async']
|
|
6248
|
+
return stmt
|
|
6084
6249
|
|
|
6085
6250
|
# The tokens are [ASYNC, ...expression tokens...]
|
|
6086
6251
|
# We can just call parse_async_expression from the main parser!
|
|
@@ -7667,10 +7832,157 @@ class ContextStackParser:
|
|
|
7667
7832
|
|
|
7668
7833
|
parser_debug(" ✅ Watch statement (explicit)")
|
|
7669
7834
|
return WatchStatement(reaction=reaction, watched_expr=watched_expr)
|
|
7670
|
-
|
|
7835
|
+
|
|
7836
|
+
# Form 3: watch expr { ... } (expression followed by block, no arrow)
|
|
7837
|
+
brace_idx = -1
|
|
7838
|
+
for i, t in enumerate(tokens):
|
|
7839
|
+
if t.type == LBRACE:
|
|
7840
|
+
brace_idx = i
|
|
7841
|
+
break
|
|
7842
|
+
|
|
7843
|
+
if brace_idx > 1:
|
|
7844
|
+
expr_tokens = tokens[1:brace_idx]
|
|
7845
|
+
reaction_tokens = tokens[brace_idx:]
|
|
7846
|
+
watched_expr = self._parse_expression(expr_tokens)
|
|
7847
|
+
if reaction_tokens and reaction_tokens[0].type == LBRACE:
|
|
7848
|
+
inner = reaction_tokens[1:-1] if reaction_tokens[-1].type == RBRACE else reaction_tokens[1:]
|
|
7849
|
+
stmts = self._parse_block_statements(inner)
|
|
7850
|
+
reaction = BlockStatement()
|
|
7851
|
+
reaction.statements = stmts
|
|
7852
|
+
else:
|
|
7853
|
+
reaction = self._parse_expression(reaction_tokens)
|
|
7854
|
+
parser_debug(" ✅ Watch statement (variable)")
|
|
7855
|
+
return WatchStatement(reaction=reaction, watched_expr=watched_expr)
|
|
7856
|
+
|
|
7671
7857
|
parser_debug(" ❌ Invalid watch syntax")
|
|
7672
7858
|
return None
|
|
7673
7859
|
|
|
7860
|
+
def _parse_emit_statement_block(self, block_info, all_tokens):
|
|
7861
|
+
"""Parse emit statement.
|
|
7862
|
+
|
|
7863
|
+
Form: emit EventName(arg1, arg2, ...)
|
|
7864
|
+
Example: emit Transfer("0xALICE", "0xBOB", 100)
|
|
7865
|
+
"""
|
|
7866
|
+
parser_debug("🔧 [Context] Parsing emit statement")
|
|
7867
|
+
tokens = block_info.get('tokens', [])
|
|
7868
|
+
|
|
7869
|
+
if not tokens or tokens[0].type != EMIT:
|
|
7870
|
+
parser_debug(" ❌ Expected EMIT keyword")
|
|
7871
|
+
return None
|
|
7872
|
+
|
|
7873
|
+
# Token 1 should be the event name (IDENT)
|
|
7874
|
+
if len(tokens) < 2 or tokens[1].type != IDENT:
|
|
7875
|
+
parser_debug(" ❌ Expected event name after emit")
|
|
7876
|
+
return None
|
|
7877
|
+
|
|
7878
|
+
event_name = Identifier(tokens[1].literal)
|
|
7879
|
+
|
|
7880
|
+
# Parse arguments if present: emit EventName(arg1, arg2)
|
|
7881
|
+
arguments = []
|
|
7882
|
+
if len(tokens) > 2 and tokens[2].type == LPAREN:
|
|
7883
|
+
# Find matching RPAREN
|
|
7884
|
+
paren_depth = 0
|
|
7885
|
+
arg_start = 3
|
|
7886
|
+
for idx in range(2, len(tokens)):
|
|
7887
|
+
if tokens[idx].type == LPAREN:
|
|
7888
|
+
paren_depth += 1
|
|
7889
|
+
elif tokens[idx].type == RPAREN:
|
|
7890
|
+
paren_depth -= 1
|
|
7891
|
+
if paren_depth == 0:
|
|
7892
|
+
# Parse arguments between parens
|
|
7893
|
+
arg_tokens = tokens[3:idx]
|
|
7894
|
+
if arg_tokens:
|
|
7895
|
+
# Split by commas at depth 0
|
|
7896
|
+
current_arg = []
|
|
7897
|
+
inner_depth = 0
|
|
7898
|
+
for at in arg_tokens:
|
|
7899
|
+
if at.type == LPAREN:
|
|
7900
|
+
inner_depth += 1
|
|
7901
|
+
elif at.type == RPAREN:
|
|
7902
|
+
inner_depth -= 1
|
|
7903
|
+
if at.type == COMMA and inner_depth == 0:
|
|
7904
|
+
if current_arg:
|
|
7905
|
+
expr = self._parse_expression(current_arg)
|
|
7906
|
+
if expr:
|
|
7907
|
+
arguments.append(expr)
|
|
7908
|
+
current_arg = []
|
|
7909
|
+
else:
|
|
7910
|
+
current_arg.append(at)
|
|
7911
|
+
if current_arg:
|
|
7912
|
+
expr = self._parse_expression(current_arg)
|
|
7913
|
+
if expr:
|
|
7914
|
+
arguments.append(expr)
|
|
7915
|
+
break
|
|
7916
|
+
|
|
7917
|
+
parser_debug(f" ✅ Emit statement: {event_name} with {len(arguments)} args")
|
|
7918
|
+
return EmitStatement(event_name, arguments)
|
|
7919
|
+
|
|
7920
|
+
def _parse_protocol_statement_block(self, block_info, all_tokens):
|
|
7921
|
+
"""Parse protocol statement.
|
|
7922
|
+
|
|
7923
|
+
Form: protocol Name { action method1() action method2(arg) -> type }
|
|
7924
|
+
Example: protocol Greetable { action greet() -> string }
|
|
7925
|
+
"""
|
|
7926
|
+
parser_debug("🔧 [Context] Parsing protocol statement")
|
|
7927
|
+
tokens = block_info.get('tokens', [])
|
|
7928
|
+
|
|
7929
|
+
if not tokens or tokens[0].type != PROTOCOL:
|
|
7930
|
+
parser_debug(" ❌ Expected PROTOCOL keyword")
|
|
7931
|
+
return None
|
|
7932
|
+
|
|
7933
|
+
# Token 1 should be protocol name
|
|
7934
|
+
if len(tokens) < 2 or tokens[1].type != IDENT:
|
|
7935
|
+
parser_debug(" ❌ Expected protocol name")
|
|
7936
|
+
return None
|
|
7937
|
+
|
|
7938
|
+
protocol_name = Identifier(tokens[1].literal)
|
|
7939
|
+
|
|
7940
|
+
# Parse method signatures from brace block
|
|
7941
|
+
methods = []
|
|
7942
|
+
brace_start = -1
|
|
7943
|
+
for idx, t in enumerate(tokens):
|
|
7944
|
+
if t.type == LBRACE:
|
|
7945
|
+
brace_start = idx
|
|
7946
|
+
break
|
|
7947
|
+
|
|
7948
|
+
if brace_start != -1:
|
|
7949
|
+
# Find matching RBRACE
|
|
7950
|
+
brace_end = len(tokens) - 1
|
|
7951
|
+
for idx in range(len(tokens) - 1, brace_start, -1):
|
|
7952
|
+
if tokens[idx].type == RBRACE:
|
|
7953
|
+
brace_end = idx
|
|
7954
|
+
break
|
|
7955
|
+
|
|
7956
|
+
# Parse inner tokens for method signatures
|
|
7957
|
+
inner = tokens[brace_start + 1:brace_end]
|
|
7958
|
+
i = 0
|
|
7959
|
+
while i < len(inner):
|
|
7960
|
+
t = inner[i]
|
|
7961
|
+
# Look for action/function keyword or bare identifiers as method names
|
|
7962
|
+
if t.type in (ACTION, FUNCTION):
|
|
7963
|
+
i += 1
|
|
7964
|
+
if i < len(inner) and inner[i].type == IDENT:
|
|
7965
|
+
methods.append(inner[i].literal)
|
|
7966
|
+
# Skip past params and return type
|
|
7967
|
+
while i < len(inner) and inner[i].type != SEMICOLON and inner[i].type != ACTION and inner[i].type != FUNCTION:
|
|
7968
|
+
i += 1
|
|
7969
|
+
if i < len(inner) and inner[i].type == SEMICOLON:
|
|
7970
|
+
i += 1
|
|
7971
|
+
continue
|
|
7972
|
+
elif t.type == IDENT:
|
|
7973
|
+
methods.append(t.literal)
|
|
7974
|
+
# Skip to next method
|
|
7975
|
+
i += 1
|
|
7976
|
+
while i < len(inner) and inner[i].type != SEMICOLON and inner[i].type != ACTION and inner[i].type != FUNCTION:
|
|
7977
|
+
i += 1
|
|
7978
|
+
if i < len(inner) and inner[i].type == SEMICOLON:
|
|
7979
|
+
i += 1
|
|
7980
|
+
continue
|
|
7981
|
+
i += 1
|
|
7982
|
+
|
|
7983
|
+
parser_debug(f" ✅ Protocol statement: {protocol_name} with methods: {methods}")
|
|
7984
|
+
return ProtocolStatement(name=protocol_name, methods=methods)
|
|
7985
|
+
|
|
7674
7986
|
def _parse_protect_statement(self, block_info, all_tokens):
|
|
7675
7987
|
"""Parse protect statement.
|
|
7676
7988
|
|
|
@@ -46,7 +46,7 @@ class StructuralAnalyzer:
|
|
|
46
46
|
DEFER, PATTERN, ENUM, STREAM, WATCH, MATCH,
|
|
47
47
|
CAPABILITY, GRANT, REVOKE, VALIDATE, SANITIZE, IMMUTABLE,
|
|
48
48
|
INTERFACE, TYPE_ALIAS, MODULE, PACKAGE, USING,
|
|
49
|
-
CHANNEL, ATOMIC,
|
|
49
|
+
CHANNEL, ATOMIC, EMIT, PROTOCOL,
|
|
50
50
|
# Blockchain keywords
|
|
51
51
|
LEDGER, STATE, REQUIRE, REVERT, LIMIT
|
|
52
52
|
}
|
|
@@ -1212,7 +1212,7 @@ class StructuralAnalyzer:
|
|
|
1212
1212
|
DEFER, PATTERN, ENUM, STREAM, WATCH,
|
|
1213
1213
|
CAPABILITY, GRANT, REVOKE, VALIDATE, SANITIZE, IMMUTABLE,
|
|
1214
1214
|
INTERFACE, TYPE_ALIAS, MODULE, PACKAGE, USING,
|
|
1215
|
-
CHANNEL, ATOMIC, ASYNC,
|
|
1215
|
+
CHANNEL, ATOMIC, ASYNC, EMIT, PROTOCOL,
|
|
1216
1216
|
LBRACE # Added LBRACE to recognize standalone blocks as statements
|
|
1217
1217
|
}
|
|
1218
1218
|
|
package/src/zexus/persistence.py
CHANGED
|
@@ -12,12 +12,21 @@ from threading import Lock
|
|
|
12
12
|
from typing import Dict, Any, Optional, Set
|
|
13
13
|
from .object import (
|
|
14
14
|
Object, Integer, Float, String, Boolean as BooleanObj,
|
|
15
|
-
Null, NULL, List, Map, EntityInstance
|
|
15
|
+
Null, NULL, List, Map, EntityInstance,
|
|
16
|
+
_sanitize_identifier
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
# Storage directory for persistent data
|
|
19
20
|
PERSISTENCE_DIR = os.path.expanduser("~/.zexus/persistence")
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure_persistence_dir(path: str = PERSISTENCE_DIR) -> None:
|
|
24
|
+
"""Ensure persistence directory exists.
|
|
25
|
+
|
|
26
|
+
MEDIUM (M3): Directory creation is deferred until persistence is actually used
|
|
27
|
+
to avoid surprising filesystem writes at import time.
|
|
28
|
+
"""
|
|
29
|
+
os.makedirs(path, exist_ok=True)
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
# ===============================================
|
|
@@ -142,8 +151,10 @@ class PersistentStorage:
|
|
|
142
151
|
|
|
143
152
|
def __init__(self, scope_id: str, storage_dir: str = PERSISTENCE_DIR,
|
|
144
153
|
max_items: int = None, max_size_mb: int = None):
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
_ensure_persistence_dir(storage_dir)
|
|
155
|
+
# SECURITY (C5): Sanitize scope_id to prevent path traversal
|
|
156
|
+
self.scope_id = _sanitize_identifier(scope_id, "scope_id")
|
|
157
|
+
self.db_path = os.path.join(storage_dir, f"{self.scope_id}.sqlite")
|
|
147
158
|
self.conn = None
|
|
148
159
|
self.lock = Lock()
|
|
149
160
|
|
|
@@ -278,10 +289,9 @@ class PersistentStorage:
|
|
|
278
289
|
|
|
279
290
|
if row is None:
|
|
280
291
|
return None
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
#
|
|
284
|
-
self._update_usage_stats()
|
|
292
|
+
|
|
293
|
+
# LI2: Reads should not force a full usage-stats refresh.
|
|
294
|
+
# Usage is updated on writes/deletes/clear.
|
|
285
295
|
return self._deserialize({'type': row[0], 'value': row[1]})
|
|
286
296
|
|
|
287
297
|
def delete(self, name: str):
|
|
@@ -290,6 +300,7 @@ class PersistentStorage:
|
|
|
290
300
|
cursor = self.conn.cursor()
|
|
291
301
|
cursor.execute('DELETE FROM variables WHERE name = ?', (name,))
|
|
292
302
|
self.conn.commit()
|
|
303
|
+
self._update_usage_stats()
|
|
293
304
|
|
|
294
305
|
def is_const(self, name: str) -> bool:
|
|
295
306
|
"""Check if a variable is const"""
|
|
@@ -309,12 +320,11 @@ class PersistentStorage:
|
|
|
309
320
|
def clear(self):
|
|
310
321
|
"""Clear all persisted variables"""
|
|
311
322
|
with self.lock:
|
|
312
|
-
|
|
313
|
-
# Update usage stats
|
|
314
|
-
self._update_usage_stats()
|
|
315
323
|
cursor = self.conn.cursor()
|
|
316
324
|
cursor.execute('DELETE FROM variables')
|
|
317
325
|
self.conn.commit()
|
|
326
|
+
# LI1: update stats after mutation, not before.
|
|
327
|
+
self._update_usage_stats()
|
|
318
328
|
|
|
319
329
|
def _key_to_str(self, key):
|
|
320
330
|
"""Convert a Zexus object key to a Python string for JSON serialization."""
|
|
@@ -350,11 +360,24 @@ class PersistentStorage:
|
|
|
350
360
|
return {'type': 'null', 'value': json.dumps(None)}
|
|
351
361
|
elif isinstance(obj, EntityInstance):
|
|
352
362
|
serialized_values = {k: self._serialize(v) for k, v in obj.values.items()}
|
|
363
|
+
|
|
364
|
+
# LI3: Preserve entity shape so deserialization can rebuild a usable
|
|
365
|
+
# EntityDefinition (properties + inheritance layout) even if methods
|
|
366
|
+
# can't be persisted.
|
|
367
|
+
prop_names = []
|
|
368
|
+
try:
|
|
369
|
+
if hasattr(obj, 'entity_def') and hasattr(obj.entity_def, 'get_all_properties'):
|
|
370
|
+
prop_names = list(obj.entity_def.get_all_properties().keys())
|
|
371
|
+
except Exception:
|
|
372
|
+
prop_names = []
|
|
373
|
+
if not prop_names:
|
|
374
|
+
prop_names = list(serialized_values.keys())
|
|
353
375
|
return {
|
|
354
376
|
'type': 'entity_instance',
|
|
355
377
|
'value': json.dumps({
|
|
356
378
|
'entity_name': obj.entity_def.name,
|
|
357
|
-
'values': serialized_values
|
|
379
|
+
'values': serialized_values,
|
|
380
|
+
'properties': prop_names,
|
|
358
381
|
})
|
|
359
382
|
}
|
|
360
383
|
else:
|
|
@@ -383,15 +406,19 @@ class PersistentStorage:
|
|
|
383
406
|
pairs = {k: self._deserialize(v) for k, v in value.items()}
|
|
384
407
|
return Map(pairs)
|
|
385
408
|
elif obj_type == 'entity_instance':
|
|
386
|
-
#
|
|
387
|
-
#
|
|
409
|
+
# LI3: Rebuild a minimally useful EntityDefinition from persisted
|
|
410
|
+
# property names. Methods/computed properties are not persisted.
|
|
388
411
|
from .object import EntityDefinition
|
|
389
412
|
entity_name = value['entity_name']
|
|
390
413
|
serialized_values = value['values']
|
|
391
414
|
deserialized_values = {k: self._deserialize(v) for k, v in serialized_values.items()}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
415
|
+
|
|
416
|
+
prop_names = value.get('properties') or list(deserialized_values.keys())
|
|
417
|
+
try:
|
|
418
|
+
props = {name: {"type": "any", "default_value": NULL} for name in prop_names}
|
|
419
|
+
except Exception:
|
|
420
|
+
props = {}
|
|
421
|
+
entity_def = EntityDefinition(entity_name, props)
|
|
395
422
|
return EntityInstance(entity_def, deserialized_values)
|
|
396
423
|
else:
|
|
397
424
|
return String(str(value))
|
|
@@ -499,6 +526,8 @@ def list_persistent_scopes() -> list:
|
|
|
499
526
|
|
|
500
527
|
def delete_persistent_scope(scope_name: str):
|
|
501
528
|
"""Delete a persistent storage scope"""
|
|
529
|
+
# SECURITY (C7): Sanitize scope_name to prevent path traversal / arbitrary deletion
|
|
530
|
+
scope_name = _sanitize_identifier(scope_name, "scope_name")
|
|
502
531
|
db_path = os.path.join(PERSISTENCE_DIR, f"{scope_name}.sqlite")
|
|
503
532
|
if os.path.exists(db_path):
|
|
504
533
|
os.remove(db_path)
|