IncludeCPP 3.4.21__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/cli/commands.py +100 -6
- 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 +441 -40
- includecpp/core/cssl/cssl_types.py +339 -2
- includecpp/core/cssl_bridge.py +106 -5
- includecpp/core/cssl_bridge.pyi +177 -0
- includecpp/vscode/__init__.py +1 -0
- includecpp/vscode/cssl/__init__.py +1 -0
- includecpp/vscode/cssl/language-configuration.json +38 -0
- includecpp/vscode/cssl/package.json +30 -0
- includecpp/vscode/cssl/syntaxes/cssl.tmLanguage.json +221 -0
- {includecpp-3.4.21.dist-info → includecpp-3.6.0.dist-info}/METADATA +1 -1
- {includecpp-3.4.21.dist-info → includecpp-3.6.0.dist-info}/RECORD +20 -15
- {includecpp-3.4.21.dist-info → includecpp-3.6.0.dist-info}/WHEEL +0 -0
- {includecpp-3.4.21.dist-info → includecpp-3.6.0.dist-info}/entry_points.txt +0 -0
- {includecpp-3.4.21.dist-info → includecpp-3.6.0.dist-info}/licenses/LICENSE +0 -0
- {includecpp-3.4.21.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,12 +139,17 @@ 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
|
|
147
150
|
self._output_callback = output_callback # Callback for console output (text, level)
|
|
151
|
+
self._source_lines: List[str] = [] # Store source code lines for error reporting
|
|
152
|
+
self._current_file: str = "<code>" # Current file being executed
|
|
148
153
|
|
|
149
154
|
self._setup_modules()
|
|
150
155
|
self._setup_builtins()
|
|
@@ -230,23 +235,68 @@ class CSSLRuntime:
|
|
|
230
235
|
|
|
231
236
|
return obj
|
|
232
237
|
|
|
238
|
+
def _format_error(self, line: int, message: str, hint: str = None) -> CSSLRuntimeError:
|
|
239
|
+
"""Format a detailed error with source context"""
|
|
240
|
+
error_parts = []
|
|
241
|
+
|
|
242
|
+
# Main error header
|
|
243
|
+
if line and line > 0:
|
|
244
|
+
error_parts.append(f"Error at line {line} in {self._current_file}:")
|
|
245
|
+
else:
|
|
246
|
+
error_parts.append(f"Error in {self._current_file}:")
|
|
247
|
+
|
|
248
|
+
# Extract message without existing line info
|
|
249
|
+
clean_msg = message
|
|
250
|
+
if "at line" in clean_msg.lower():
|
|
251
|
+
# Remove redundant line info from message
|
|
252
|
+
clean_msg = clean_msg.split(":", 1)[-1].strip() if ":" in clean_msg else clean_msg
|
|
253
|
+
|
|
254
|
+
error_parts.append(f" {clean_msg}")
|
|
255
|
+
|
|
256
|
+
# Show source context (3 lines before and after)
|
|
257
|
+
if self._source_lines and line and line > 0:
|
|
258
|
+
error_parts.append("")
|
|
259
|
+
start = max(0, line - 3)
|
|
260
|
+
end = min(len(self._source_lines), line + 2)
|
|
261
|
+
|
|
262
|
+
for i in range(start, end):
|
|
263
|
+
line_num = i + 1
|
|
264
|
+
source_line = self._source_lines[i] if i < len(self._source_lines) else ""
|
|
265
|
+
marker = ">>>" if line_num == line else " "
|
|
266
|
+
error_parts.append(f" {marker} {line_num:4d} | {source_line}")
|
|
267
|
+
|
|
268
|
+
# Add hint
|
|
269
|
+
if hint:
|
|
270
|
+
error_parts.append("")
|
|
271
|
+
error_parts.append(f" Hint: {hint}")
|
|
272
|
+
|
|
273
|
+
return CSSLRuntimeError("\n".join(error_parts), line)
|
|
274
|
+
|
|
275
|
+
def _get_source_line(self, line: int) -> str:
|
|
276
|
+
"""Get source line by number (1-indexed)"""
|
|
277
|
+
if self._source_lines and 0 < line <= len(self._source_lines):
|
|
278
|
+
return self._source_lines[line - 1]
|
|
279
|
+
return ""
|
|
280
|
+
|
|
233
281
|
def execute(self, source: str) -> Any:
|
|
234
282
|
"""Execute CSSL service source code"""
|
|
283
|
+
self._source_lines = source.splitlines()
|
|
235
284
|
try:
|
|
236
285
|
ast = parse_cssl(source)
|
|
237
286
|
return self._execute_node(ast)
|
|
238
287
|
except CSSLSyntaxError as e:
|
|
239
|
-
raise
|
|
288
|
+
raise self._format_error(e.line, str(e))
|
|
240
289
|
except SyntaxError as e:
|
|
241
290
|
raise CSSLRuntimeError(f"Syntax error: {e}")
|
|
242
291
|
|
|
243
292
|
def execute_program(self, source: str) -> Any:
|
|
244
293
|
"""Execute standalone CSSL program (no service wrapper)"""
|
|
294
|
+
self._source_lines = source.splitlines()
|
|
245
295
|
try:
|
|
246
296
|
ast = parse_cssl_program(source)
|
|
247
297
|
return self._exec_program(ast)
|
|
248
298
|
except CSSLSyntaxError as e:
|
|
249
|
-
raise
|
|
299
|
+
raise self._format_error(e.line, str(e))
|
|
250
300
|
except SyntaxError as e:
|
|
251
301
|
raise CSSLRuntimeError(f"Syntax error: {e}")
|
|
252
302
|
|
|
@@ -256,12 +306,16 @@ class CSSLRuntime:
|
|
|
256
306
|
|
|
257
307
|
def execute_file(self, filepath: str) -> Any:
|
|
258
308
|
"""Execute a CSSL service file"""
|
|
309
|
+
import os
|
|
310
|
+
self._current_file = os.path.basename(filepath)
|
|
259
311
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
260
312
|
source = f.read()
|
|
261
313
|
return self.execute(source)
|
|
262
314
|
|
|
263
315
|
def execute_program_file(self, filepath: str) -> Any:
|
|
264
316
|
"""Execute a standalone CSSL program file"""
|
|
317
|
+
import os
|
|
318
|
+
self._current_file = os.path.basename(filepath)
|
|
265
319
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
266
320
|
source = f.read()
|
|
267
321
|
return self.execute_program(source)
|
|
@@ -323,8 +377,13 @@ class CSSLRuntime:
|
|
|
323
377
|
- top-level statements (assignments, function calls, control flow)
|
|
324
378
|
"""
|
|
325
379
|
result = None
|
|
380
|
+
self._running = True # Start running
|
|
326
381
|
|
|
327
382
|
for child in node.children:
|
|
383
|
+
# Check if exit() was called
|
|
384
|
+
if not self._running:
|
|
385
|
+
break
|
|
386
|
+
|
|
328
387
|
if child.type == 'struct':
|
|
329
388
|
self._exec_struct(child)
|
|
330
389
|
elif child.type == 'function':
|
|
@@ -347,13 +406,14 @@ class CSSLRuntime:
|
|
|
347
406
|
except CSSLRuntimeError:
|
|
348
407
|
pass # Ignore unknown nodes in program mode
|
|
349
408
|
|
|
350
|
-
# Look for and execute main() if defined
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
357
417
|
|
|
358
418
|
return result
|
|
359
419
|
|
|
@@ -650,6 +710,10 @@ class CSSLRuntime:
|
|
|
650
710
|
instance = {} if value_node is None else self._evaluate(value_node)
|
|
651
711
|
elif type_name == 'array':
|
|
652
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)
|
|
653
717
|
else:
|
|
654
718
|
# Default: evaluate the value or set to None
|
|
655
719
|
instance = self._evaluate(value_node) if value_node else None
|
|
@@ -711,11 +775,18 @@ class CSSLRuntime:
|
|
|
711
775
|
# Fallback: execute normally
|
|
712
776
|
return self._execute_node(inner)
|
|
713
777
|
|
|
714
|
-
def _call_function(self, func_node: ASTNode, args: List[Any]) -> Any:
|
|
715
|
-
"""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
|
+
"""
|
|
716
786
|
func_info = func_node.value
|
|
717
787
|
params = func_info.get('params', [])
|
|
718
788
|
modifiers = func_info.get('modifiers', [])
|
|
789
|
+
kwargs = kwargs or {}
|
|
719
790
|
|
|
720
791
|
# Check for undefined modifier - suppress errors if present
|
|
721
792
|
is_undefined = 'undefined' in modifiers
|
|
@@ -723,11 +794,16 @@ class CSSLRuntime:
|
|
|
723
794
|
# Create new scope
|
|
724
795
|
new_scope = Scope(parent=self.scope)
|
|
725
796
|
|
|
726
|
-
# Bind parameters - handle both
|
|
797
|
+
# Bind parameters - handle both positional and named arguments
|
|
727
798
|
for i, param in enumerate(params):
|
|
728
799
|
# Extract param name from dict format: {'name': 'a', 'type': 'int'}
|
|
729
800
|
param_name = param['name'] if isinstance(param, dict) else param
|
|
730
|
-
|
|
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
|
|
731
807
|
new_scope.set(param_name, args[i])
|
|
732
808
|
else:
|
|
733
809
|
new_scope.set(param_name, None)
|
|
@@ -738,6 +814,9 @@ class CSSLRuntime:
|
|
|
738
814
|
|
|
739
815
|
try:
|
|
740
816
|
for child in func_node.children:
|
|
817
|
+
# Check if exit() was called
|
|
818
|
+
if not self._running:
|
|
819
|
+
break
|
|
741
820
|
self._execute_node(child)
|
|
742
821
|
except CSSLReturn as ret:
|
|
743
822
|
return ret.value
|
|
@@ -773,9 +852,11 @@ class CSSLRuntime:
|
|
|
773
852
|
|
|
774
853
|
def _exec_while(self, node: ASTNode) -> Any:
|
|
775
854
|
"""Execute while loop"""
|
|
776
|
-
while self._evaluate(node.value.get('condition')):
|
|
855
|
+
while self._running and self._evaluate(node.value.get('condition')):
|
|
777
856
|
try:
|
|
778
857
|
for child in node.children:
|
|
858
|
+
if not self._running:
|
|
859
|
+
break
|
|
779
860
|
self._execute_node(child)
|
|
780
861
|
except CSSLBreak:
|
|
781
862
|
break
|
|
@@ -795,9 +876,13 @@ class CSSLRuntime:
|
|
|
795
876
|
step = int(self._evaluate(step_node)) if step_node else 1
|
|
796
877
|
|
|
797
878
|
for i in range(start, end, step):
|
|
879
|
+
if not self._running:
|
|
880
|
+
break
|
|
798
881
|
self.scope.set(var_name, i)
|
|
799
882
|
try:
|
|
800
883
|
for child in node.children:
|
|
884
|
+
if not self._running:
|
|
885
|
+
break
|
|
801
886
|
self._execute_node(child)
|
|
802
887
|
except CSSLBreak:
|
|
803
888
|
break
|
|
@@ -828,7 +913,7 @@ class CSSLRuntime:
|
|
|
828
913
|
var_name = None
|
|
829
914
|
|
|
830
915
|
# Main loop
|
|
831
|
-
while
|
|
916
|
+
while self._running:
|
|
832
917
|
# Check condition
|
|
833
918
|
if condition:
|
|
834
919
|
cond_result = self._evaluate(condition)
|
|
@@ -839,6 +924,8 @@ class CSSLRuntime:
|
|
|
839
924
|
# Execute body
|
|
840
925
|
try:
|
|
841
926
|
for child in node.children:
|
|
927
|
+
if not self._running:
|
|
928
|
+
break
|
|
842
929
|
self._execute_node(child)
|
|
843
930
|
except CSSLBreak:
|
|
844
931
|
break
|
|
@@ -882,9 +969,13 @@ class CSSLRuntime:
|
|
|
882
969
|
return None
|
|
883
970
|
|
|
884
971
|
for item in iterable:
|
|
972
|
+
if not self._running:
|
|
973
|
+
break
|
|
885
974
|
self.scope.set(var_name, item)
|
|
886
975
|
try:
|
|
887
976
|
for child in node.children:
|
|
977
|
+
if not self._running:
|
|
978
|
+
break
|
|
888
979
|
self._execute_node(child)
|
|
889
980
|
except CSSLBreak:
|
|
890
981
|
break
|
|
@@ -1026,15 +1117,108 @@ class CSSLRuntime:
|
|
|
1026
1117
|
# === STRING HELPERS ===
|
|
1027
1118
|
if filter_type == 'string':
|
|
1028
1119
|
if helper == 'where':
|
|
1120
|
+
# Exact match
|
|
1121
|
+
if isinstance(result, str):
|
|
1122
|
+
result = result if result == filter_val else None
|
|
1123
|
+
elif isinstance(result, list):
|
|
1124
|
+
result = [item for item in result if isinstance(item, str) and item == filter_val]
|
|
1125
|
+
elif helper == 'contains':
|
|
1126
|
+
# Contains substring
|
|
1029
1127
|
if isinstance(result, str):
|
|
1030
1128
|
result = result if filter_val in result else None
|
|
1031
1129
|
elif isinstance(result, list):
|
|
1032
1130
|
result = [item for item in result if isinstance(item, str) and filter_val in item]
|
|
1131
|
+
elif helper == 'not':
|
|
1132
|
+
# Exclude matching
|
|
1133
|
+
if isinstance(result, str):
|
|
1134
|
+
result = result if result != filter_val else None
|
|
1135
|
+
elif isinstance(result, list):
|
|
1136
|
+
result = [item for item in result if not (isinstance(item, str) and item == filter_val)]
|
|
1137
|
+
elif helper == 'startsWith':
|
|
1138
|
+
if isinstance(result, str):
|
|
1139
|
+
result = result if result.startswith(filter_val) else None
|
|
1140
|
+
elif isinstance(result, list):
|
|
1141
|
+
result = [item for item in result if isinstance(item, str) and item.startswith(filter_val)]
|
|
1142
|
+
elif helper == 'endsWith':
|
|
1143
|
+
if isinstance(result, str):
|
|
1144
|
+
result = result if result.endswith(filter_val) else None
|
|
1145
|
+
elif isinstance(result, list):
|
|
1146
|
+
result = [item for item in result if isinstance(item, str) and item.endswith(filter_val)]
|
|
1033
1147
|
elif helper in ('length', 'lenght'): # Support common typo
|
|
1034
1148
|
if isinstance(result, str):
|
|
1035
1149
|
result = result if len(result) == filter_val else None
|
|
1036
1150
|
elif isinstance(result, list):
|
|
1037
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()
|
|
1038
1222
|
|
|
1039
1223
|
# === INTEGER HELPERS ===
|
|
1040
1224
|
elif filter_type == 'integer':
|
|
@@ -1146,8 +1330,21 @@ class CSSLRuntime:
|
|
|
1146
1330
|
self.register_function_injection(func_name, source_node)
|
|
1147
1331
|
return None
|
|
1148
1332
|
|
|
1149
|
-
#
|
|
1150
|
-
|
|
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)
|
|
1151
1348
|
|
|
1152
1349
|
# Apply filter if present
|
|
1153
1350
|
if filter_info:
|
|
@@ -1286,11 +1483,20 @@ class CSSLRuntime:
|
|
|
1286
1483
|
- replace: func <<== { code } - REPLACES function body (original won't execute)
|
|
1287
1484
|
- add: func +<<== { code } - ADDS code to function (both execute)
|
|
1288
1485
|
- remove: func -<<== { code } - REMOVES matching code from function
|
|
1486
|
+
|
|
1487
|
+
Also supports expression form: func <<== %exit() (wraps in action_block)
|
|
1289
1488
|
"""
|
|
1290
1489
|
target = node.value.get('target')
|
|
1291
1490
|
code_block = node.value.get('code')
|
|
1491
|
+
source_expr = node.value.get('source') # For expression form: func <<== expr
|
|
1292
1492
|
mode = node.value.get('mode', 'replace') # Default is REPLACE for <<==
|
|
1293
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
|
+
|
|
1294
1500
|
# Get function name from target
|
|
1295
1501
|
func_name = None
|
|
1296
1502
|
if isinstance(target, ASTNode):
|
|
@@ -1301,7 +1507,7 @@ class CSSLRuntime:
|
|
|
1301
1507
|
if isinstance(callee, ASTNode) and callee.type == 'identifier':
|
|
1302
1508
|
func_name = callee.value
|
|
1303
1509
|
|
|
1304
|
-
if not func_name:
|
|
1510
|
+
if not func_name or code_block is None:
|
|
1305
1511
|
return None
|
|
1306
1512
|
|
|
1307
1513
|
if mode == 'add':
|
|
@@ -1310,16 +1516,31 @@ class CSSLRuntime:
|
|
|
1310
1516
|
self._function_replaced[func_name] = False # Don't replace, just add
|
|
1311
1517
|
elif mode == 'replace':
|
|
1312
1518
|
# <<== : Replace function body (only injection executes, original skipped)
|
|
1313
|
-
|
|
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)]
|
|
1314
1530
|
self._function_replaced[func_name] = True # Mark as replaced
|
|
1315
1531
|
elif mode == 'remove':
|
|
1316
|
-
# -<<== : Remove matching code from function body
|
|
1317
|
-
|
|
1532
|
+
# -<<== or -<<==[n] : Remove matching code from function body
|
|
1533
|
+
remove_index = node.value.get('index')
|
|
1534
|
+
|
|
1318
1535
|
if func_name in self._function_injections:
|
|
1319
|
-
|
|
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] = []
|
|
1320
1543
|
self._function_replaced[func_name] = False
|
|
1321
|
-
# Note: Removing from actual function body would require AST manipulation
|
|
1322
|
-
# which is complex - for now we just clear injections
|
|
1323
1544
|
|
|
1324
1545
|
return None
|
|
1325
1546
|
|
|
@@ -1472,9 +1693,17 @@ class CSSLRuntime:
|
|
|
1472
1693
|
return None
|
|
1473
1694
|
|
|
1474
1695
|
if node.type == 'identifier':
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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)
|
|
1478
1707
|
return value
|
|
1479
1708
|
|
|
1480
1709
|
if node.type == 'module_ref':
|
|
@@ -1511,6 +1740,33 @@ class CSSLRuntime:
|
|
|
1511
1740
|
return scoped_val
|
|
1512
1741
|
raise CSSLRuntimeError(f"Shared object '${name}' not found. Use share() to share objects.")
|
|
1513
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
|
+
|
|
1514
1770
|
if node.type == 'type_instantiation':
|
|
1515
1771
|
# Create new instance of a type: stack<string>, vector<int>, etc.
|
|
1516
1772
|
type_name = node.value.get('type')
|
|
@@ -1534,6 +1790,10 @@ class CSSLRuntime:
|
|
|
1534
1790
|
return OpenQuote()
|
|
1535
1791
|
elif type_name == 'array':
|
|
1536
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)
|
|
1537
1797
|
else:
|
|
1538
1798
|
return None
|
|
1539
1799
|
|
|
@@ -1573,8 +1833,41 @@ class CSSLRuntime:
|
|
|
1573
1833
|
return {'__ref__': True, 'name': inner.value, 'value': self.get_module(inner.value)}
|
|
1574
1834
|
return {'__ref__': True, 'value': self._evaluate(inner)}
|
|
1575
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
|
+
|
|
1576
1840
|
return None
|
|
1577
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
|
+
|
|
1578
1871
|
def _eval_binary(self, node: ASTNode) -> Any:
|
|
1579
1872
|
"""Evaluate binary operation with auto-casting support"""
|
|
1580
1873
|
op = node.value.get('op')
|
|
@@ -1744,12 +2037,15 @@ class CSSLRuntime:
|
|
|
1744
2037
|
return None
|
|
1745
2038
|
|
|
1746
2039
|
def _eval_call(self, node: ASTNode) -> Any:
|
|
1747
|
-
"""Evaluate function call"""
|
|
2040
|
+
"""Evaluate function call with optional named arguments"""
|
|
1748
2041
|
callee_node = node.value.get('callee')
|
|
1749
|
-
callee = self._evaluate(callee_node)
|
|
1750
2042
|
args = [self._evaluate(a) for a in node.value.get('args', [])]
|
|
1751
2043
|
|
|
1752
|
-
#
|
|
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)
|
|
1753
2049
|
func_name = None
|
|
1754
2050
|
if isinstance(callee_node, ASTNode):
|
|
1755
2051
|
if callee_node.type == 'identifier':
|
|
@@ -1761,20 +2057,27 @@ class CSSLRuntime:
|
|
|
1761
2057
|
has_injections = func_name and func_name in self._function_injections
|
|
1762
2058
|
is_replaced = func_name and self._function_replaced.get(func_name, False)
|
|
1763
2059
|
|
|
1764
|
-
#
|
|
1765
|
-
|
|
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:
|
|
1766
2063
|
self._execute_function_injections(func_name)
|
|
2064
|
+
return None # Injection ran, don't try to find original
|
|
1767
2065
|
|
|
1768
|
-
#
|
|
1769
|
-
|
|
1770
|
-
|
|
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)
|
|
1771
2072
|
|
|
1772
2073
|
# Execute original function
|
|
1773
2074
|
if callable(callee):
|
|
2075
|
+
if kwargs:
|
|
2076
|
+
return callee(*args, **kwargs)
|
|
1774
2077
|
return callee(*args)
|
|
1775
2078
|
|
|
1776
2079
|
if isinstance(callee, ASTNode) and callee.type == 'function':
|
|
1777
|
-
return self._call_function(callee, args)
|
|
2080
|
+
return self._call_function(callee, args, kwargs)
|
|
1778
2081
|
|
|
1779
2082
|
callee_name = callee_node.value if isinstance(callee_node, ASTNode) and hasattr(callee_node, 'value') else str(callee_node)
|
|
1780
2083
|
raise CSSLRuntimeError(
|
|
@@ -2182,22 +2485,107 @@ class CSSLRuntime:
|
|
|
2182
2485
|
# Also store in promoted globals for string interpolation
|
|
2183
2486
|
self._promoted_globals[base_name] = value
|
|
2184
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
|
+
|
|
2185
2567
|
# NEW: Register permanent function injection
|
|
2186
2568
|
def register_function_injection(self, func_name: str, code_block: ASTNode):
|
|
2187
2569
|
"""Register code to be permanently injected into a function - NEW
|
|
2188
2570
|
|
|
2189
2571
|
Example: exit() <== { println("Cleanup..."); }
|
|
2190
2572
|
Makes every call to exit() also execute the injected code
|
|
2573
|
+
|
|
2574
|
+
Captures %<name> references at registration time.
|
|
2191
2575
|
"""
|
|
2576
|
+
# Scan for %<name> captured references and capture their current values
|
|
2577
|
+
captured_values = self._scan_and_capture_refs(code_block)
|
|
2578
|
+
|
|
2192
2579
|
if func_name not in self._function_injections:
|
|
2193
2580
|
self._function_injections[func_name] = []
|
|
2194
|
-
self._function_injections[func_name].append(code_block)
|
|
2581
|
+
self._function_injections[func_name].append((code_block, captured_values))
|
|
2195
2582
|
|
|
2196
2583
|
# NEW: Execute injected code for a function
|
|
2197
2584
|
def _execute_function_injections(self, func_name: str):
|
|
2198
2585
|
"""Execute all injected code blocks for a function - NEW
|
|
2199
2586
|
|
|
2200
2587
|
Includes protection against recursive execution to prevent doubled output.
|
|
2588
|
+
Uses captured values for %<name> references.
|
|
2201
2589
|
"""
|
|
2202
2590
|
# Prevent recursive injection execution (fixes doubled output bug)
|
|
2203
2591
|
if getattr(self, '_injection_executing', False):
|
|
@@ -2205,16 +2593,29 @@ class CSSLRuntime:
|
|
|
2205
2593
|
|
|
2206
2594
|
if func_name in self._function_injections:
|
|
2207
2595
|
self._injection_executing = True
|
|
2596
|
+
old_captured = self._current_captured_values.copy()
|
|
2208
2597
|
try:
|
|
2209
|
-
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
|
+
|
|
2210
2607
|
if isinstance(code_block, ASTNode):
|
|
2211
2608
|
if code_block.type == 'action_block':
|
|
2212
2609
|
for child in code_block.children:
|
|
2610
|
+
# Check if exit() was called
|
|
2611
|
+
if not self._running:
|
|
2612
|
+
break
|
|
2213
2613
|
self._execute_node(child)
|
|
2214
2614
|
else:
|
|
2215
2615
|
self._execute_node(code_block)
|
|
2216
2616
|
finally:
|
|
2217
2617
|
self._injection_executing = False
|
|
2618
|
+
self._current_captured_values = old_captured
|
|
2218
2619
|
|
|
2219
2620
|
# Output functions for builtins
|
|
2220
2621
|
def set_output_callback(self, callback: Callable[[str, str], None]):
|