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.
@@ -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[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
@@ -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
- main_func = self.scope.get('main')
403
- if main_func and isinstance(main_func, ASTNode) and main_func.type == 'function':
404
- try:
405
- result = self._call_function(main_func, [])
406
- except CSSLReturn as ret:
407
- 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
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 string and dict formats
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
- 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
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 True:
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
- # Evaluate source
1224
- 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)
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
- 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)]
1388
1530
  self._function_replaced[func_name] = True # Mark as replaced
1389
1531
  elif mode == 'remove':
1390
- # -<<== : Remove matching code from function body
1391
- # 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
+
1392
1535
  if func_name in self._function_injections:
1393
- 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] = []
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
- value = self.scope.get(node.value)
1550
- if value is None and self.builtins.has_function(node.value):
1551
- 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)
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
- # 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)
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
- # Execute injected code first (if any)
1839
- 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:
1840
2063
  self._execute_function_injections(func_name)
2064
+ return None # Injection ran, don't try to find original
1841
2065
 
1842
- # If function is REPLACED (<<==), skip original body execution
1843
- if is_replaced:
1844
- 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)
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 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
+
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]):