IncludeCPP 3.5.0__py3-none-any.whl → 3.6.0__py3-none-any.whl
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.
- includecpp/__init__.py +1 -1
- includecpp/core/cssl/CSSL_DOCUMENTATION.md +351 -20
- includecpp/core/cssl/cssl_builtins.py +228 -2
- includecpp/core/cssl/cssl_parser.py +214 -25
- includecpp/core/cssl/cssl_runtime.py +365 -38
- includecpp/core/cssl/cssl_types.py +339 -2
- includecpp/core/cssl_bridge.py +100 -4
- includecpp/core/cssl_bridge.pyi +177 -0
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/METADATA +1 -1
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/RECORD +14 -14
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/WHEEL +0 -0
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/entry_points.txt +0 -0
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/licenses/LICENSE +0 -0
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/top_level.txt +0 -0
|
@@ -14,7 +14,7 @@ from .cssl_builtins import CSSLBuiltins
|
|
|
14
14
|
from .cssl_modules import get_module_registry, get_standard_module
|
|
15
15
|
from .cssl_types import (
|
|
16
16
|
Parameter, DataStruct, Shuffled, Iterator, Combo,
|
|
17
|
-
Stack, Vector, Array, DataSpace, OpenQuote
|
|
17
|
+
Stack, Vector, Array, DataSpace, OpenQuote, List, Dictionary
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
@@ -139,8 +139,11 @@ class CSSLRuntime:
|
|
|
139
139
|
self.services: Dict[str, ServiceDefinition] = {}
|
|
140
140
|
self._modules: Dict[str, Any] = {}
|
|
141
141
|
self._global_structs: Dict[str, Any] = {} # Global structs for s@<name> references
|
|
142
|
-
self._function_injections: Dict[str, List[
|
|
142
|
+
self._function_injections: Dict[str, List[tuple]] = {} # List of (code_block, captured_values_dict)
|
|
143
143
|
self._function_replaced: Dict[str, bool] = {} # NEW: Track replaced functions (<<==)
|
|
144
|
+
self._original_functions: Dict[str, Any] = {} # Store originals before replacement
|
|
145
|
+
self._injection_captures: Dict[str, Dict[str, Any]] = {} # Captured %vars per injection
|
|
146
|
+
self._current_captured_values: Dict[str, Any] = {} # Current captured values during injection execution
|
|
144
147
|
self._promoted_globals: Dict[str, Any] = {} # NEW: Variables promoted via global()
|
|
145
148
|
self._running = False
|
|
146
149
|
self._exit_code = 0
|
|
@@ -374,8 +377,13 @@ class CSSLRuntime:
|
|
|
374
377
|
- top-level statements (assignments, function calls, control flow)
|
|
375
378
|
"""
|
|
376
379
|
result = None
|
|
380
|
+
self._running = True # Start running
|
|
377
381
|
|
|
378
382
|
for child in node.children:
|
|
383
|
+
# Check if exit() was called
|
|
384
|
+
if not self._running:
|
|
385
|
+
break
|
|
386
|
+
|
|
379
387
|
if child.type == 'struct':
|
|
380
388
|
self._exec_struct(child)
|
|
381
389
|
elif child.type == 'function':
|
|
@@ -398,13 +406,14 @@ class CSSLRuntime:
|
|
|
398
406
|
except CSSLRuntimeError:
|
|
399
407
|
pass # Ignore unknown nodes in program mode
|
|
400
408
|
|
|
401
|
-
# Look for and execute main() if defined
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
409
|
+
# Look for and execute main() if defined (only if still running)
|
|
410
|
+
if self._running:
|
|
411
|
+
main_func = self.scope.get('main')
|
|
412
|
+
if main_func and isinstance(main_func, ASTNode) and main_func.type == 'function':
|
|
413
|
+
try:
|
|
414
|
+
result = self._call_function(main_func, [])
|
|
415
|
+
except CSSLReturn as ret:
|
|
416
|
+
result = ret.value
|
|
408
417
|
|
|
409
418
|
return result
|
|
410
419
|
|
|
@@ -701,6 +710,10 @@ class CSSLRuntime:
|
|
|
701
710
|
instance = {} if value_node is None else self._evaluate(value_node)
|
|
702
711
|
elif type_name == 'array':
|
|
703
712
|
instance = Array(element_type)
|
|
713
|
+
elif type_name == 'list':
|
|
714
|
+
instance = List(element_type)
|
|
715
|
+
elif type_name in ('dictionary', 'dict'):
|
|
716
|
+
instance = Dictionary(element_type)
|
|
704
717
|
else:
|
|
705
718
|
# Default: evaluate the value or set to None
|
|
706
719
|
instance = self._evaluate(value_node) if value_node else None
|
|
@@ -762,11 +775,18 @@ class CSSLRuntime:
|
|
|
762
775
|
# Fallback: execute normally
|
|
763
776
|
return self._execute_node(inner)
|
|
764
777
|
|
|
765
|
-
def _call_function(self, func_node: ASTNode, args: List[Any]) -> Any:
|
|
766
|
-
"""Call a function node with arguments
|
|
778
|
+
def _call_function(self, func_node: ASTNode, args: List[Any], kwargs: Dict[str, Any] = None) -> Any:
|
|
779
|
+
"""Call a function node with arguments (positional and named)
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
func_node: The function AST node
|
|
783
|
+
args: List of positional arguments
|
|
784
|
+
kwargs: Dict of named arguments (param_name -> value)
|
|
785
|
+
"""
|
|
767
786
|
func_info = func_node.value
|
|
768
787
|
params = func_info.get('params', [])
|
|
769
788
|
modifiers = func_info.get('modifiers', [])
|
|
789
|
+
kwargs = kwargs or {}
|
|
770
790
|
|
|
771
791
|
# Check for undefined modifier - suppress errors if present
|
|
772
792
|
is_undefined = 'undefined' in modifiers
|
|
@@ -774,11 +794,16 @@ class CSSLRuntime:
|
|
|
774
794
|
# Create new scope
|
|
775
795
|
new_scope = Scope(parent=self.scope)
|
|
776
796
|
|
|
777
|
-
# Bind parameters - handle both
|
|
797
|
+
# Bind parameters - handle both positional and named arguments
|
|
778
798
|
for i, param in enumerate(params):
|
|
779
799
|
# Extract param name from dict format: {'name': 'a', 'type': 'int'}
|
|
780
800
|
param_name = param['name'] if isinstance(param, dict) else param
|
|
781
|
-
|
|
801
|
+
|
|
802
|
+
if param_name in kwargs:
|
|
803
|
+
# Named argument takes priority
|
|
804
|
+
new_scope.set(param_name, kwargs[param_name])
|
|
805
|
+
elif i < len(args):
|
|
806
|
+
# Positional argument
|
|
782
807
|
new_scope.set(param_name, args[i])
|
|
783
808
|
else:
|
|
784
809
|
new_scope.set(param_name, None)
|
|
@@ -789,6 +814,9 @@ class CSSLRuntime:
|
|
|
789
814
|
|
|
790
815
|
try:
|
|
791
816
|
for child in func_node.children:
|
|
817
|
+
# Check if exit() was called
|
|
818
|
+
if not self._running:
|
|
819
|
+
break
|
|
792
820
|
self._execute_node(child)
|
|
793
821
|
except CSSLReturn as ret:
|
|
794
822
|
return ret.value
|
|
@@ -824,9 +852,11 @@ class CSSLRuntime:
|
|
|
824
852
|
|
|
825
853
|
def _exec_while(self, node: ASTNode) -> Any:
|
|
826
854
|
"""Execute while loop"""
|
|
827
|
-
while self._evaluate(node.value.get('condition')):
|
|
855
|
+
while self._running and self._evaluate(node.value.get('condition')):
|
|
828
856
|
try:
|
|
829
857
|
for child in node.children:
|
|
858
|
+
if not self._running:
|
|
859
|
+
break
|
|
830
860
|
self._execute_node(child)
|
|
831
861
|
except CSSLBreak:
|
|
832
862
|
break
|
|
@@ -846,9 +876,13 @@ class CSSLRuntime:
|
|
|
846
876
|
step = int(self._evaluate(step_node)) if step_node else 1
|
|
847
877
|
|
|
848
878
|
for i in range(start, end, step):
|
|
879
|
+
if not self._running:
|
|
880
|
+
break
|
|
849
881
|
self.scope.set(var_name, i)
|
|
850
882
|
try:
|
|
851
883
|
for child in node.children:
|
|
884
|
+
if not self._running:
|
|
885
|
+
break
|
|
852
886
|
self._execute_node(child)
|
|
853
887
|
except CSSLBreak:
|
|
854
888
|
break
|
|
@@ -879,7 +913,7 @@ class CSSLRuntime:
|
|
|
879
913
|
var_name = None
|
|
880
914
|
|
|
881
915
|
# Main loop
|
|
882
|
-
while
|
|
916
|
+
while self._running:
|
|
883
917
|
# Check condition
|
|
884
918
|
if condition:
|
|
885
919
|
cond_result = self._evaluate(condition)
|
|
@@ -890,6 +924,8 @@ class CSSLRuntime:
|
|
|
890
924
|
# Execute body
|
|
891
925
|
try:
|
|
892
926
|
for child in node.children:
|
|
927
|
+
if not self._running:
|
|
928
|
+
break
|
|
893
929
|
self._execute_node(child)
|
|
894
930
|
except CSSLBreak:
|
|
895
931
|
break
|
|
@@ -933,9 +969,13 @@ class CSSLRuntime:
|
|
|
933
969
|
return None
|
|
934
970
|
|
|
935
971
|
for item in iterable:
|
|
972
|
+
if not self._running:
|
|
973
|
+
break
|
|
936
974
|
self.scope.set(var_name, item)
|
|
937
975
|
try:
|
|
938
976
|
for child in node.children:
|
|
977
|
+
if not self._running:
|
|
978
|
+
break
|
|
939
979
|
self._execute_node(child)
|
|
940
980
|
except CSSLBreak:
|
|
941
981
|
break
|
|
@@ -1109,6 +1149,76 @@ class CSSLRuntime:
|
|
|
1109
1149
|
result = result if len(result) == filter_val else None
|
|
1110
1150
|
elif isinstance(result, list):
|
|
1111
1151
|
result = [item for item in result if isinstance(item, str) and len(item) == filter_val]
|
|
1152
|
+
elif helper == 'cut':
|
|
1153
|
+
# Cut string - returns the part BEFORE the index/substring
|
|
1154
|
+
# x = <==[string::cut=2] "20:200-1" --> x = "20"
|
|
1155
|
+
# x = <==[string::cut="1.0"] "1.0.0" --> x = "" (before "1.0")
|
|
1156
|
+
if isinstance(result, str):
|
|
1157
|
+
if isinstance(filter_val, str):
|
|
1158
|
+
# Cut at substring position
|
|
1159
|
+
idx = result.find(filter_val)
|
|
1160
|
+
result = result[:idx] if idx >= 0 else result
|
|
1161
|
+
else:
|
|
1162
|
+
# Cut at integer index
|
|
1163
|
+
idx = int(filter_val)
|
|
1164
|
+
result = result[:idx] if 0 <= idx <= len(result) else result
|
|
1165
|
+
elif isinstance(result, list):
|
|
1166
|
+
def cut_item(item):
|
|
1167
|
+
if not isinstance(item, str):
|
|
1168
|
+
return item
|
|
1169
|
+
if isinstance(filter_val, str):
|
|
1170
|
+
idx = item.find(filter_val)
|
|
1171
|
+
return item[:idx] if idx >= 0 else item
|
|
1172
|
+
return item[:int(filter_val)]
|
|
1173
|
+
result = [cut_item(item) for item in result]
|
|
1174
|
+
elif helper == 'cutAfter':
|
|
1175
|
+
# Get the part AFTER the index/substring
|
|
1176
|
+
# x = <==[string::cutAfter=2] "20:200-1" --> x = ":200-1"
|
|
1177
|
+
# x = <==[string::cutAfter="1.0"] "1.0.0" --> x = ".0" (after "1.0")
|
|
1178
|
+
if isinstance(result, str):
|
|
1179
|
+
if isinstance(filter_val, str):
|
|
1180
|
+
# Cut after substring
|
|
1181
|
+
idx = result.find(filter_val)
|
|
1182
|
+
result = result[idx + len(filter_val):] if idx >= 0 else result
|
|
1183
|
+
else:
|
|
1184
|
+
# Cut after integer index
|
|
1185
|
+
idx = int(filter_val)
|
|
1186
|
+
result = result[idx:] if 0 <= idx <= len(result) else result
|
|
1187
|
+
elif isinstance(result, list):
|
|
1188
|
+
def cut_after_item(item):
|
|
1189
|
+
if not isinstance(item, str):
|
|
1190
|
+
return item
|
|
1191
|
+
if isinstance(filter_val, str):
|
|
1192
|
+
idx = item.find(filter_val)
|
|
1193
|
+
return item[idx + len(filter_val):] if idx >= 0 else item
|
|
1194
|
+
return item[int(filter_val):]
|
|
1195
|
+
result = [cut_after_item(item) for item in result]
|
|
1196
|
+
elif helper == 'slice':
|
|
1197
|
+
# Slice string with start:end format (e.g., "2:5")
|
|
1198
|
+
if isinstance(result, str) and isinstance(filter_val, str) and ':' in filter_val:
|
|
1199
|
+
parts = filter_val.split(':')
|
|
1200
|
+
start = int(parts[0]) if parts[0] else 0
|
|
1201
|
+
end = int(parts[1]) if parts[1] else len(result)
|
|
1202
|
+
result = result[start:end]
|
|
1203
|
+
elif helper == 'split':
|
|
1204
|
+
# Split string by delimiter
|
|
1205
|
+
if isinstance(result, str):
|
|
1206
|
+
result = result.split(str(filter_val))
|
|
1207
|
+
elif helper == 'replace':
|
|
1208
|
+
# Replace in string (format: "old:new")
|
|
1209
|
+
if isinstance(result, str) and isinstance(filter_val, str) and ':' in filter_val:
|
|
1210
|
+
parts = filter_val.split(':', 1)
|
|
1211
|
+
if len(parts) == 2:
|
|
1212
|
+
result = result.replace(parts[0], parts[1])
|
|
1213
|
+
elif helper == 'upper':
|
|
1214
|
+
if isinstance(result, str):
|
|
1215
|
+
result = result.upper()
|
|
1216
|
+
elif helper == 'lower':
|
|
1217
|
+
if isinstance(result, str):
|
|
1218
|
+
result = result.lower()
|
|
1219
|
+
elif helper == 'trim':
|
|
1220
|
+
if isinstance(result, str):
|
|
1221
|
+
result = result.strip()
|
|
1112
1222
|
|
|
1113
1223
|
# === INTEGER HELPERS ===
|
|
1114
1224
|
elif filter_type == 'integer':
|
|
@@ -1220,8 +1330,21 @@ class CSSLRuntime:
|
|
|
1220
1330
|
self.register_function_injection(func_name, source_node)
|
|
1221
1331
|
return None
|
|
1222
1332
|
|
|
1223
|
-
#
|
|
1224
|
-
|
|
1333
|
+
# Check if source is an action_block with %<name> captures
|
|
1334
|
+
# If so, capture values NOW and evaluate the block with those captures
|
|
1335
|
+
if isinstance(source_node, ASTNode) and source_node.type == 'action_block':
|
|
1336
|
+
# Scan for %<name> captured references and capture their current values
|
|
1337
|
+
captured_values = self._scan_and_capture_refs(source_node)
|
|
1338
|
+
old_captured = self._current_captured_values.copy()
|
|
1339
|
+
self._current_captured_values = captured_values
|
|
1340
|
+
try:
|
|
1341
|
+
# Execute the action block and get the last expression's value
|
|
1342
|
+
source = self._evaluate_action_block(source_node)
|
|
1343
|
+
finally:
|
|
1344
|
+
self._current_captured_values = old_captured
|
|
1345
|
+
else:
|
|
1346
|
+
# Evaluate source normally
|
|
1347
|
+
source = self._evaluate(source_node)
|
|
1225
1348
|
|
|
1226
1349
|
# Apply filter if present
|
|
1227
1350
|
if filter_info:
|
|
@@ -1360,11 +1483,20 @@ class CSSLRuntime:
|
|
|
1360
1483
|
- replace: func <<== { code } - REPLACES function body (original won't execute)
|
|
1361
1484
|
- add: func +<<== { code } - ADDS code to function (both execute)
|
|
1362
1485
|
- remove: func -<<== { code } - REMOVES matching code from function
|
|
1486
|
+
|
|
1487
|
+
Also supports expression form: func <<== %exit() (wraps in action_block)
|
|
1363
1488
|
"""
|
|
1364
1489
|
target = node.value.get('target')
|
|
1365
1490
|
code_block = node.value.get('code')
|
|
1491
|
+
source_expr = node.value.get('source') # For expression form: func <<== expr
|
|
1366
1492
|
mode = node.value.get('mode', 'replace') # Default is REPLACE for <<==
|
|
1367
1493
|
|
|
1494
|
+
# If source expression is provided instead of code block, wrap it
|
|
1495
|
+
if code_block is None and source_expr is not None:
|
|
1496
|
+
# Wrap in expression node so _execute_node can handle it
|
|
1497
|
+
expr_node = ASTNode('expression', value=source_expr)
|
|
1498
|
+
code_block = ASTNode('action_block', children=[expr_node])
|
|
1499
|
+
|
|
1368
1500
|
# Get function name from target
|
|
1369
1501
|
func_name = None
|
|
1370
1502
|
if isinstance(target, ASTNode):
|
|
@@ -1375,7 +1507,7 @@ class CSSLRuntime:
|
|
|
1375
1507
|
if isinstance(callee, ASTNode) and callee.type == 'identifier':
|
|
1376
1508
|
func_name = callee.value
|
|
1377
1509
|
|
|
1378
|
-
if not func_name:
|
|
1510
|
+
if not func_name or code_block is None:
|
|
1379
1511
|
return None
|
|
1380
1512
|
|
|
1381
1513
|
if mode == 'add':
|
|
@@ -1384,16 +1516,31 @@ class CSSLRuntime:
|
|
|
1384
1516
|
self._function_replaced[func_name] = False # Don't replace, just add
|
|
1385
1517
|
elif mode == 'replace':
|
|
1386
1518
|
# <<== : Replace function body (only injection executes, original skipped)
|
|
1387
|
-
|
|
1519
|
+
# Save original function BEFORE replacing (for original() access)
|
|
1520
|
+
if func_name not in self._original_functions:
|
|
1521
|
+
# Try to find original in scope or builtins
|
|
1522
|
+
original = self.scope.get(func_name)
|
|
1523
|
+
if original is None:
|
|
1524
|
+
original = getattr(self.builtins, f'builtin_{func_name}', None)
|
|
1525
|
+
if original is not None:
|
|
1526
|
+
self._original_functions[func_name] = original
|
|
1527
|
+
# Capture %<name> references at registration time
|
|
1528
|
+
captured_values = self._scan_and_capture_refs(code_block)
|
|
1529
|
+
self._function_injections[func_name] = [(code_block, captured_values)]
|
|
1388
1530
|
self._function_replaced[func_name] = True # Mark as replaced
|
|
1389
1531
|
elif mode == 'remove':
|
|
1390
|
-
# -<<== : Remove matching code from function body
|
|
1391
|
-
|
|
1532
|
+
# -<<== or -<<==[n] : Remove matching code from function body
|
|
1533
|
+
remove_index = node.value.get('index')
|
|
1534
|
+
|
|
1392
1535
|
if func_name in self._function_injections:
|
|
1393
|
-
|
|
1536
|
+
if remove_index is not None:
|
|
1537
|
+
# Indexed removal: -<<==[n] removes only the nth injection
|
|
1538
|
+
if 0 <= remove_index < len(self._function_injections[func_name]):
|
|
1539
|
+
self._function_injections[func_name].pop(remove_index)
|
|
1540
|
+
else:
|
|
1541
|
+
# No index: -<<== removes all injections
|
|
1542
|
+
self._function_injections[func_name] = []
|
|
1394
1543
|
self._function_replaced[func_name] = False
|
|
1395
|
-
# Note: Removing from actual function body would require AST manipulation
|
|
1396
|
-
# which is complex - for now we just clear injections
|
|
1397
1544
|
|
|
1398
1545
|
return None
|
|
1399
1546
|
|
|
@@ -1546,9 +1693,17 @@ class CSSLRuntime:
|
|
|
1546
1693
|
return None
|
|
1547
1694
|
|
|
1548
1695
|
if node.type == 'identifier':
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1696
|
+
name = node.value
|
|
1697
|
+
value = self.scope.get(name)
|
|
1698
|
+
# Fallback to global scope
|
|
1699
|
+
if value is None:
|
|
1700
|
+
value = self.global_scope.get(name)
|
|
1701
|
+
# Fallback to promoted globals (from 'global' keyword)
|
|
1702
|
+
if value is None:
|
|
1703
|
+
value = self._promoted_globals.get(name)
|
|
1704
|
+
# Fallback to builtins
|
|
1705
|
+
if value is None and self.builtins.has_function(name):
|
|
1706
|
+
return self.builtins.get_function(name)
|
|
1552
1707
|
return value
|
|
1553
1708
|
|
|
1554
1709
|
if node.type == 'module_ref':
|
|
@@ -1585,6 +1740,33 @@ class CSSLRuntime:
|
|
|
1585
1740
|
return scoped_val
|
|
1586
1741
|
raise CSSLRuntimeError(f"Shared object '${name}' not found. Use share() to share objects.")
|
|
1587
1742
|
|
|
1743
|
+
if node.type == 'captured_ref':
|
|
1744
|
+
# %<name> captured reference - use value captured at infusion registration time
|
|
1745
|
+
name = node.value
|
|
1746
|
+
# First check captured values from current injection context
|
|
1747
|
+
if name in self._current_captured_values:
|
|
1748
|
+
captured_value = self._current_captured_values[name]
|
|
1749
|
+
# Only use captured value if it's not None
|
|
1750
|
+
if captured_value is not None:
|
|
1751
|
+
return captured_value
|
|
1752
|
+
# Fall back to normal resolution if not captured or capture was None
|
|
1753
|
+
value = self.scope.get(name)
|
|
1754
|
+
if value is None:
|
|
1755
|
+
value = self.global_scope.get(name)
|
|
1756
|
+
if value is None:
|
|
1757
|
+
# For critical builtins like 'exit', create direct wrapper
|
|
1758
|
+
if name == 'exit':
|
|
1759
|
+
runtime = self
|
|
1760
|
+
value = lambda code=0, rt=runtime: rt.exit(code)
|
|
1761
|
+
else:
|
|
1762
|
+
value = getattr(self.builtins, f'builtin_{name}', None)
|
|
1763
|
+
if value is None:
|
|
1764
|
+
# Check original functions (for replaced functions)
|
|
1765
|
+
value = self._original_functions.get(name)
|
|
1766
|
+
if value is not None:
|
|
1767
|
+
return value
|
|
1768
|
+
raise CSSLRuntimeError(f"Captured reference '%{name}' not found.")
|
|
1769
|
+
|
|
1588
1770
|
if node.type == 'type_instantiation':
|
|
1589
1771
|
# Create new instance of a type: stack<string>, vector<int>, etc.
|
|
1590
1772
|
type_name = node.value.get('type')
|
|
@@ -1608,6 +1790,10 @@ class CSSLRuntime:
|
|
|
1608
1790
|
return OpenQuote()
|
|
1609
1791
|
elif type_name == 'array':
|
|
1610
1792
|
return Array(element_type)
|
|
1793
|
+
elif type_name == 'list':
|
|
1794
|
+
return List(element_type)
|
|
1795
|
+
elif type_name in ('dictionary', 'dict'):
|
|
1796
|
+
return Dictionary(element_type)
|
|
1611
1797
|
else:
|
|
1612
1798
|
return None
|
|
1613
1799
|
|
|
@@ -1647,8 +1833,41 @@ class CSSLRuntime:
|
|
|
1647
1833
|
return {'__ref__': True, 'name': inner.value, 'value': self.get_module(inner.value)}
|
|
1648
1834
|
return {'__ref__': True, 'value': self._evaluate(inner)}
|
|
1649
1835
|
|
|
1836
|
+
# Handle action_block - execute and return last expression value
|
|
1837
|
+
if node.type == 'action_block':
|
|
1838
|
+
return self._evaluate_action_block(node)
|
|
1839
|
+
|
|
1650
1840
|
return None
|
|
1651
1841
|
|
|
1842
|
+
def _evaluate_action_block(self, node: ASTNode) -> Any:
|
|
1843
|
+
"""Evaluate an action block and return the last expression's value.
|
|
1844
|
+
|
|
1845
|
+
Used for: v <== { %version; } - captures %version at this moment
|
|
1846
|
+
|
|
1847
|
+
Returns the value of the last expression in the block.
|
|
1848
|
+
If the block contains a captured_ref (%name), that's what gets returned.
|
|
1849
|
+
"""
|
|
1850
|
+
last_value = None
|
|
1851
|
+
for child in node.children:
|
|
1852
|
+
if child.type == 'captured_ref':
|
|
1853
|
+
# Direct captured reference - return its value
|
|
1854
|
+
last_value = self._evaluate(child)
|
|
1855
|
+
elif child.type == 'expression':
|
|
1856
|
+
# Expression statement - evaluate and keep value
|
|
1857
|
+
last_value = self._evaluate(child.value if hasattr(child, 'value') else child)
|
|
1858
|
+
elif child.type == 'identifier':
|
|
1859
|
+
# Just an identifier - evaluate it
|
|
1860
|
+
last_value = self._evaluate(child)
|
|
1861
|
+
elif child.type in ('call', 'member_access', 'binary', 'unary'):
|
|
1862
|
+
# Expression types
|
|
1863
|
+
last_value = self._evaluate(child)
|
|
1864
|
+
else:
|
|
1865
|
+
# Execute other statements
|
|
1866
|
+
result = self._execute_node(child)
|
|
1867
|
+
if result is not None:
|
|
1868
|
+
last_value = result
|
|
1869
|
+
return last_value
|
|
1870
|
+
|
|
1652
1871
|
def _eval_binary(self, node: ASTNode) -> Any:
|
|
1653
1872
|
"""Evaluate binary operation with auto-casting support"""
|
|
1654
1873
|
op = node.value.get('op')
|
|
@@ -1818,12 +2037,15 @@ class CSSLRuntime:
|
|
|
1818
2037
|
return None
|
|
1819
2038
|
|
|
1820
2039
|
def _eval_call(self, node: ASTNode) -> Any:
|
|
1821
|
-
"""Evaluate function call"""
|
|
2040
|
+
"""Evaluate function call with optional named arguments"""
|
|
1822
2041
|
callee_node = node.value.get('callee')
|
|
1823
|
-
callee = self._evaluate(callee_node)
|
|
1824
2042
|
args = [self._evaluate(a) for a in node.value.get('args', [])]
|
|
1825
2043
|
|
|
1826
|
-
#
|
|
2044
|
+
# Evaluate named arguments (kwargs)
|
|
2045
|
+
kwargs_raw = node.value.get('kwargs', {})
|
|
2046
|
+
kwargs = {k: self._evaluate(v) for k, v in kwargs_raw.items()} if kwargs_raw else {}
|
|
2047
|
+
|
|
2048
|
+
# Get function name for injection check FIRST (before evaluating callee)
|
|
1827
2049
|
func_name = None
|
|
1828
2050
|
if isinstance(callee_node, ASTNode):
|
|
1829
2051
|
if callee_node.type == 'identifier':
|
|
@@ -1835,20 +2057,27 @@ class CSSLRuntime:
|
|
|
1835
2057
|
has_injections = func_name and func_name in self._function_injections
|
|
1836
2058
|
is_replaced = func_name and self._function_replaced.get(func_name, False)
|
|
1837
2059
|
|
|
1838
|
-
#
|
|
1839
|
-
|
|
2060
|
+
# If function is FULLY REPLACED (<<==), run injection and skip original
|
|
2061
|
+
# This allows creating new functions via infusion: new_func <<== { ... }
|
|
2062
|
+
if is_replaced:
|
|
1840
2063
|
self._execute_function_injections(func_name)
|
|
2064
|
+
return None # Injection ran, don't try to find original
|
|
1841
2065
|
|
|
1842
|
-
#
|
|
1843
|
-
|
|
1844
|
-
|
|
2066
|
+
# Now evaluate the callee (only if not replaced)
|
|
2067
|
+
callee = self._evaluate(callee_node)
|
|
2068
|
+
|
|
2069
|
+
# Execute added injections (+<<==) before original
|
|
2070
|
+
if has_injections and not is_replaced:
|
|
2071
|
+
self._execute_function_injections(func_name)
|
|
1845
2072
|
|
|
1846
2073
|
# Execute original function
|
|
1847
2074
|
if callable(callee):
|
|
2075
|
+
if kwargs:
|
|
2076
|
+
return callee(*args, **kwargs)
|
|
1848
2077
|
return callee(*args)
|
|
1849
2078
|
|
|
1850
2079
|
if isinstance(callee, ASTNode) and callee.type == 'function':
|
|
1851
|
-
return self._call_function(callee, args)
|
|
2080
|
+
return self._call_function(callee, args, kwargs)
|
|
1852
2081
|
|
|
1853
2082
|
callee_name = callee_node.value if isinstance(callee_node, ASTNode) and hasattr(callee_node, 'value') else str(callee_node)
|
|
1854
2083
|
raise CSSLRuntimeError(
|
|
@@ -2256,22 +2485,107 @@ class CSSLRuntime:
|
|
|
2256
2485
|
# Also store in promoted globals for string interpolation
|
|
2257
2486
|
self._promoted_globals[base_name] = value
|
|
2258
2487
|
|
|
2488
|
+
# NEW: Scan for captured_ref nodes and capture their current values
|
|
2489
|
+
def _scan_and_capture_refs(self, node: ASTNode) -> Dict[str, Any]:
|
|
2490
|
+
"""Scan AST for %<name> captured references and capture their current values.
|
|
2491
|
+
|
|
2492
|
+
This is called at infusion registration time to capture values.
|
|
2493
|
+
Example: old_exit <<== { %exit(); } captures 'exit' at definition time.
|
|
2494
|
+
"""
|
|
2495
|
+
captured = {}
|
|
2496
|
+
|
|
2497
|
+
def scan_node(n):
|
|
2498
|
+
if not isinstance(n, ASTNode):
|
|
2499
|
+
return
|
|
2500
|
+
|
|
2501
|
+
# Found a captured_ref - capture its current value
|
|
2502
|
+
if n.type == 'captured_ref':
|
|
2503
|
+
name = n.value
|
|
2504
|
+
if name not in captured:
|
|
2505
|
+
# Try to find value - check multiple sources
|
|
2506
|
+
value = None
|
|
2507
|
+
|
|
2508
|
+
# 1. Check _original_functions first (for functions that were JUST replaced)
|
|
2509
|
+
if value is None:
|
|
2510
|
+
value = self._original_functions.get(name)
|
|
2511
|
+
|
|
2512
|
+
# 2. Check scope
|
|
2513
|
+
if value is None:
|
|
2514
|
+
value = self.scope.get(name)
|
|
2515
|
+
|
|
2516
|
+
# 3. Check global_scope
|
|
2517
|
+
if value is None:
|
|
2518
|
+
value = self.global_scope.get(name)
|
|
2519
|
+
|
|
2520
|
+
# 4. Check builtins (most common case for exit, print, etc.)
|
|
2521
|
+
if value is None:
|
|
2522
|
+
# For critical builtins like 'exit', create a direct wrapper
|
|
2523
|
+
# that captures the runtime reference to ensure correct behavior
|
|
2524
|
+
if name == 'exit':
|
|
2525
|
+
runtime = self # Capture runtime in closure
|
|
2526
|
+
value = lambda code=0, rt=runtime: rt.exit(code)
|
|
2527
|
+
else:
|
|
2528
|
+
value = getattr(self.builtins, f'builtin_{name}', None)
|
|
2529
|
+
|
|
2530
|
+
# 5. Check if there's a user-defined function in scope
|
|
2531
|
+
if value is None:
|
|
2532
|
+
# Look for function definitions
|
|
2533
|
+
func_def = self.global_scope.get(f'__func_{name}')
|
|
2534
|
+
if func_def is not None:
|
|
2535
|
+
value = func_def
|
|
2536
|
+
|
|
2537
|
+
# Only capture if we found something
|
|
2538
|
+
if value is not None:
|
|
2539
|
+
captured[name] = value
|
|
2540
|
+
|
|
2541
|
+
# Check call node's callee
|
|
2542
|
+
if n.type == 'call':
|
|
2543
|
+
callee = n.value.get('callee')
|
|
2544
|
+
if callee:
|
|
2545
|
+
scan_node(callee)
|
|
2546
|
+
for arg in n.value.get('args', []):
|
|
2547
|
+
scan_node(arg)
|
|
2548
|
+
|
|
2549
|
+
# Recurse into children
|
|
2550
|
+
if hasattr(n, 'children') and n.children:
|
|
2551
|
+
for child in n.children:
|
|
2552
|
+
scan_node(child)
|
|
2553
|
+
|
|
2554
|
+
# Check value dict for nested nodes
|
|
2555
|
+
if hasattr(n, 'value') and isinstance(n.value, dict):
|
|
2556
|
+
for key, val in n.value.items():
|
|
2557
|
+
if isinstance(val, ASTNode):
|
|
2558
|
+
scan_node(val)
|
|
2559
|
+
elif isinstance(val, list):
|
|
2560
|
+
for item in val:
|
|
2561
|
+
if isinstance(item, ASTNode):
|
|
2562
|
+
scan_node(item)
|
|
2563
|
+
|
|
2564
|
+
scan_node(node)
|
|
2565
|
+
return captured
|
|
2566
|
+
|
|
2259
2567
|
# NEW: Register permanent function injection
|
|
2260
2568
|
def register_function_injection(self, func_name: str, code_block: ASTNode):
|
|
2261
2569
|
"""Register code to be permanently injected into a function - NEW
|
|
2262
2570
|
|
|
2263
2571
|
Example: exit() <== { println("Cleanup..."); }
|
|
2264
2572
|
Makes every call to exit() also execute the injected code
|
|
2573
|
+
|
|
2574
|
+
Captures %<name> references at registration time.
|
|
2265
2575
|
"""
|
|
2576
|
+
# Scan for %<name> captured references and capture their current values
|
|
2577
|
+
captured_values = self._scan_and_capture_refs(code_block)
|
|
2578
|
+
|
|
2266
2579
|
if func_name not in self._function_injections:
|
|
2267
2580
|
self._function_injections[func_name] = []
|
|
2268
|
-
self._function_injections[func_name].append(code_block)
|
|
2581
|
+
self._function_injections[func_name].append((code_block, captured_values))
|
|
2269
2582
|
|
|
2270
2583
|
# NEW: Execute injected code for a function
|
|
2271
2584
|
def _execute_function_injections(self, func_name: str):
|
|
2272
2585
|
"""Execute all injected code blocks for a function - NEW
|
|
2273
2586
|
|
|
2274
2587
|
Includes protection against recursive execution to prevent doubled output.
|
|
2588
|
+
Uses captured values for %<name> references.
|
|
2275
2589
|
"""
|
|
2276
2590
|
# Prevent recursive injection execution (fixes doubled output bug)
|
|
2277
2591
|
if getattr(self, '_injection_executing', False):
|
|
@@ -2279,16 +2593,29 @@ class CSSLRuntime:
|
|
|
2279
2593
|
|
|
2280
2594
|
if func_name in self._function_injections:
|
|
2281
2595
|
self._injection_executing = True
|
|
2596
|
+
old_captured = self._current_captured_values.copy()
|
|
2282
2597
|
try:
|
|
2283
|
-
for
|
|
2598
|
+
for injection in self._function_injections[func_name]:
|
|
2599
|
+
# Handle both tuple format (code_block, captured_values) and legacy ASTNode format
|
|
2600
|
+
if isinstance(injection, tuple):
|
|
2601
|
+
code_block, captured_values = injection
|
|
2602
|
+
self._current_captured_values = captured_values
|
|
2603
|
+
else:
|
|
2604
|
+
code_block = injection
|
|
2605
|
+
self._current_captured_values = {}
|
|
2606
|
+
|
|
2284
2607
|
if isinstance(code_block, ASTNode):
|
|
2285
2608
|
if code_block.type == 'action_block':
|
|
2286
2609
|
for child in code_block.children:
|
|
2610
|
+
# Check if exit() was called
|
|
2611
|
+
if not self._running:
|
|
2612
|
+
break
|
|
2287
2613
|
self._execute_node(child)
|
|
2288
2614
|
else:
|
|
2289
2615
|
self._execute_node(code_block)
|
|
2290
2616
|
finally:
|
|
2291
2617
|
self._injection_executing = False
|
|
2618
|
+
self._current_captured_values = old_captured
|
|
2292
2619
|
|
|
2293
2620
|
# Output functions for builtins
|
|
2294
2621
|
def set_output_callback(self, callback: Callable[[str, str], None]):
|