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.
@@ -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[ASTNode]] = {} # NEW: Permanent function injections
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 CSSLRuntimeError(str(e), e.line)
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 CSSLRuntimeError(str(e), e.line)
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
- main_func = self.scope.get('main')
352
- if main_func and isinstance(main_func, ASTNode) and main_func.type == 'function':
353
- try:
354
- result = self._call_function(main_func, [])
355
- except CSSLReturn as ret:
356
- result = ret.value
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 string and dict formats
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
- if i < len(args):
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 True:
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
- # Evaluate source
1150
- source = self._evaluate(source_node)
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
- self._function_injections[func_name] = [code_block]
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
- # For now, this removes all injections for the function
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
- self._function_injections[func_name] = []
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
- value = self.scope.get(node.value)
1476
- if value is None and self.builtins.has_function(node.value):
1477
- return self.builtins.get_function(node.value)
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
- # Get function name for injection check
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
- # Execute injected code first (if any)
1765
- if has_injections:
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
- # If function is REPLACED (<<==), skip original body execution
1769
- if is_replaced:
1770
- return None # Injection already ran, don't run original
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 code_block in self._function_injections[func_name]:
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]):