IncludeCPP 3.5.2__tar.gz → 3.5.7__tar.gz

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.
Files changed (55) hide show
  1. {includecpp-3.5.2 → includecpp-3.5.7}/IncludeCPP.egg-info/PKG-INFO +1 -1
  2. {includecpp-3.5.2 → includecpp-3.5.7}/PKG-INFO +1 -1
  3. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/__init__.py +1 -1
  4. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/cssl_builtins.py +23 -0
  5. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/cssl_parser.py +108 -2
  6. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/cssl_runtime.py +214 -14
  7. {includecpp-3.5.2 → includecpp-3.5.7}/pyproject.toml +1 -1
  8. {includecpp-3.5.2 → includecpp-3.5.7}/IncludeCPP.egg-info/SOURCES.txt +0 -0
  9. {includecpp-3.5.2 → includecpp-3.5.7}/IncludeCPP.egg-info/dependency_links.txt +0 -0
  10. {includecpp-3.5.2 → includecpp-3.5.7}/IncludeCPP.egg-info/entry_points.txt +0 -0
  11. {includecpp-3.5.2 → includecpp-3.5.7}/IncludeCPP.egg-info/requires.txt +0 -0
  12. {includecpp-3.5.2 → includecpp-3.5.7}/IncludeCPP.egg-info/top_level.txt +0 -0
  13. {includecpp-3.5.2 → includecpp-3.5.7}/LICENSE +0 -0
  14. {includecpp-3.5.2 → includecpp-3.5.7}/MANIFEST.in +0 -0
  15. {includecpp-3.5.2 → includecpp-3.5.7}/README.md +0 -0
  16. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/__init__.pyi +0 -0
  17. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/__main__.py +0 -0
  18. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/cli/__init__.py +0 -0
  19. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/cli/commands.py +0 -0
  20. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/cli/config_parser.py +0 -0
  21. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/__init__.py +0 -0
  22. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/ai_integration.py +0 -0
  23. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/build_manager.py +0 -0
  24. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cpp_api.py +0 -0
  25. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cpp_api.pyi +0 -0
  26. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cppy_converter.py +0 -0
  27. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/CSSL_DOCUMENTATION.md +0 -0
  28. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/__init__.py +0 -0
  29. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/cssl_events.py +0 -0
  30. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/cssl_modules.py +0 -0
  31. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/cssl_syntax.py +0 -0
  32. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl/cssl_types.py +0 -0
  33. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl_bridge.py +0 -0
  34. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/cssl_bridge.pyi +0 -0
  35. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/error_catalog.py +0 -0
  36. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/error_formatter.py +0 -0
  37. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/exceptions.py +0 -0
  38. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/path_discovery.py +0 -0
  39. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/project_ui.py +0 -0
  40. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/core/settings_ui.py +0 -0
  41. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/generator/__init__.py +0 -0
  42. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/generator/parser.cpp +0 -0
  43. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/generator/parser.h +0 -0
  44. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/generator/type_resolver.cpp +0 -0
  45. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/generator/type_resolver.h +0 -0
  46. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/py.typed +0 -0
  47. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/templates/cpp.proj.template +0 -0
  48. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/vscode/__init__.py +0 -0
  49. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/vscode/cssl/__init__.py +0 -0
  50. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/vscode/cssl/language-configuration.json +0 -0
  51. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/vscode/cssl/package.json +0 -0
  52. {includecpp-3.5.2 → includecpp-3.5.7}/includecpp/vscode/cssl/syntaxes/cssl.tmLanguage.json +0 -0
  53. {includecpp-3.5.2 → includecpp-3.5.7}/requirements.txt +0 -0
  54. {includecpp-3.5.2 → includecpp-3.5.7}/setup.cfg +0 -0
  55. {includecpp-3.5.2 → includecpp-3.5.7}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: IncludeCPP
3
- Version: 3.5.2
3
+ Version: 3.5.7
4
4
  Summary: Professional C++ Python bindings with type-generic templates, pystubs and native threading
5
5
  Home-page: https://github.com/liliassg/IncludeCPP
6
6
  Author: Lilias Hatterscheidt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: IncludeCPP
3
- Version: 3.5.2
3
+ Version: 3.5.7
4
4
  Summary: Professional C++ Python bindings with type-generic templates, pystubs and native threading
5
5
  Home-page: https://github.com/liliassg/IncludeCPP
6
6
  Author: Lilias Hatterscheidt
@@ -2,7 +2,7 @@ from .core.cpp_api import CppApi
2
2
  from .core import cssl_bridge as CSSL
3
3
  import warnings
4
4
 
5
- __version__ = "3.5.2"
5
+ __version__ = "3.5.7"
6
6
  __all__ = ["CppApi", "CSSL"]
7
7
 
8
8
  # Module-level cache for C++ modules
@@ -913,6 +913,29 @@ class CSSLBuiltins:
913
913
  else:
914
914
  raise SystemExit(code)
915
915
 
916
+ def builtin_original(self, func_name: str, *args) -> Any:
917
+ """Call the original version of a replaced function.
918
+
919
+ Usage:
920
+ exit <<== { printl("custom exit"); }
921
+ original("exit"); // Calls the ORIGINAL exit, not the replacement
922
+
923
+ // In an injection that was defined BEFORE replacement:
924
+ old_exit <<== { original("exit"); } // Calls original exit
925
+ """
926
+ if self.runtime and hasattr(self.runtime, '_original_functions'):
927
+ original_func = self.runtime._original_functions.get(func_name)
928
+ if original_func is not None:
929
+ if callable(original_func):
930
+ return original_func(*args)
931
+ elif isinstance(original_func, type(lambda: None).__class__.__bases__[0]): # Check if bound method
932
+ return original_func(*args)
933
+ # Fallback: try to call builtin directly
934
+ builtin_method = getattr(self, f'builtin_{func_name}', None)
935
+ if builtin_method:
936
+ return builtin_method(*args)
937
+ raise CSSLBuiltinError(f"No original function '{func_name}' found")
938
+
916
939
  def builtin_env(self, name: str, default: str = None) -> Optional[str]:
917
940
  return os.environ.get(name, default)
918
941
 
@@ -100,6 +100,7 @@ class TokenType(Enum):
100
100
  GLOBAL_REF = auto() # r@<name> global variable declaration
101
101
  SELF_REF = auto() # s@<name> self-reference to global struct
102
102
  SHARED_REF = auto() # $<name> shared object reference
103
+ CAPTURED_REF = auto() # %<name> captured reference (for infusion)
103
104
  PACKAGE = auto()
104
105
  PACKAGE_INCLUDES = auto()
105
106
  AS = auto()
@@ -242,6 +243,9 @@ class CSSLLexer:
242
243
  elif char == '$':
243
244
  # $<name> shared object reference
244
245
  self._read_shared_ref()
246
+ elif char == '%':
247
+ # %<name> captured reference (for infusion)
248
+ self._read_captured_ref()
245
249
  elif char == '&':
246
250
  # & for references
247
251
  if self._peek(1) == '&':
@@ -494,6 +498,20 @@ class CSSLLexer:
494
498
  self.error("Expected identifier after '$'")
495
499
  self._add_token(TokenType.SHARED_REF, value)
496
500
 
501
+ def _read_captured_ref(self):
502
+ """Read %<name> captured reference (captures value at definition time for infusions)"""
503
+ self._advance() # skip '%'
504
+
505
+ # Read the identifier (captured reference name)
506
+ name_start = self.pos
507
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
508
+ self._advance()
509
+
510
+ value = self.source[name_start:self.pos]
511
+ if not value:
512
+ self.error("Expected identifier after '%'")
513
+ self._add_token(TokenType.CAPTURED_REF, value)
514
+
497
515
  def _read_less_than(self):
498
516
  # Check for <<== (code infusion left)
499
517
  if self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
@@ -1294,7 +1312,9 @@ class CSSLParser:
1294
1312
  elif self._looks_like_function_declaration():
1295
1313
  # Nested typed function (e.g., void Level2() { ... })
1296
1314
  return self._parse_typed_function()
1297
- elif self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT):
1315
+ elif (self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or
1316
+ self._check(TokenType.CAPTURED_REF) or self._check(TokenType.SHARED_REF) or
1317
+ self._check(TokenType.GLOBAL_REF) or self._check(TokenType.SELF_REF)):
1298
1318
  return self._parse_expression_statement()
1299
1319
  else:
1300
1320
  self._advance()
@@ -2058,6 +2078,31 @@ class CSSLParser:
2058
2078
  break
2059
2079
  return node
2060
2080
 
2081
+ if self._check(TokenType.CAPTURED_REF):
2082
+ # %<name> captured reference (captures value at infusion registration time)
2083
+ token = self._advance()
2084
+ node = ASTNode('captured_ref', value=token.value, line=token.line, column=token.column)
2085
+ # Check for member access, calls, indexing
2086
+ while True:
2087
+ if self._match(TokenType.PAREN_START):
2088
+ args = []
2089
+ while not self._check(TokenType.PAREN_END):
2090
+ args.append(self._parse_expression())
2091
+ if not self._check(TokenType.PAREN_END):
2092
+ self._expect(TokenType.COMMA)
2093
+ self._expect(TokenType.PAREN_END)
2094
+ node = ASTNode('call', value={'callee': node, 'args': args})
2095
+ elif self._match(TokenType.DOT):
2096
+ member = self._advance().value
2097
+ node = ASTNode('member_access', value={'object': node, 'member': member})
2098
+ elif self._match(TokenType.BRACKET_START):
2099
+ index = self._parse_expression()
2100
+ self._expect(TokenType.BRACKET_END)
2101
+ node = ASTNode('index_access', value={'object': node, 'index': index})
2102
+ else:
2103
+ break
2104
+ return node
2105
+
2061
2106
  if self._check(TokenType.NUMBER):
2062
2107
  return ASTNode('literal', value=self._advance().value)
2063
2108
 
@@ -2082,7 +2127,13 @@ class CSSLParser:
2082
2127
  return expr
2083
2128
 
2084
2129
  if self._match(TokenType.BLOCK_START):
2085
- return self._parse_object()
2130
+ # Distinguish between object literal { key = value } and action block { expr; }
2131
+ # Object literal: starts with IDENTIFIER = or STRING =
2132
+ # Action block: starts with expression (captured_ref, call, literal, etc.)
2133
+ if self._is_object_literal():
2134
+ return self._parse_object()
2135
+ else:
2136
+ return self._parse_action_block_expression()
2086
2137
 
2087
2138
  if self._match(TokenType.BRACKET_START):
2088
2139
  return self._parse_array()
@@ -2191,6 +2242,61 @@ class CSSLParser:
2191
2242
 
2192
2243
  return node
2193
2244
 
2245
+ def _is_object_literal(self) -> bool:
2246
+ """Check if current position is an object literal { key = value } vs action block { expr; }
2247
+
2248
+ Object literal: { name = value; } or { "key" = value; }
2249
+ Action block: { %version; } or { "1.0.0" } or { call(); }
2250
+ """
2251
+ # Empty block is action block
2252
+ if self._check(TokenType.BLOCK_END):
2253
+ return False
2254
+
2255
+ # Save position for lookahead
2256
+ saved_pos = self._current
2257
+
2258
+ # Check if it looks like key = value pattern
2259
+ is_object = False
2260
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.STRING):
2261
+ self._advance() # skip key
2262
+ if self._check(TokenType.EQUALS):
2263
+ # Looks like object literal: { key = ...
2264
+ is_object = True
2265
+
2266
+ # Restore position
2267
+ self._current = saved_pos
2268
+ return is_object
2269
+
2270
+ def _parse_action_block_expression(self) -> ASTNode:
2271
+ """Parse an action block expression: { expr; expr2; } returns last value
2272
+
2273
+ Used for: v <== { %version; } or v <== { "1.0.0" }
2274
+ """
2275
+ children = []
2276
+
2277
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
2278
+ # Parse statement or expression
2279
+ if (self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or
2280
+ self._check(TokenType.CAPTURED_REF) or self._check(TokenType.SHARED_REF) or
2281
+ self._check(TokenType.GLOBAL_REF) or self._check(TokenType.SELF_REF) or
2282
+ self._check(TokenType.STRING) or self._check(TokenType.NUMBER) or
2283
+ self._check(TokenType.BOOLEAN) or self._check(TokenType.NULL) or
2284
+ self._check(TokenType.PAREN_START)):
2285
+ # Parse as expression and wrap in expression node for _execute_node
2286
+ expr = self._parse_expression()
2287
+ self._match(TokenType.SEMICOLON)
2288
+ children.append(ASTNode('expression', value=expr))
2289
+ elif self._check(TokenType.KEYWORD):
2290
+ # Parse as statement
2291
+ stmt = self._parse_statement()
2292
+ if stmt:
2293
+ children.append(stmt)
2294
+ else:
2295
+ self._advance()
2296
+
2297
+ self._expect(TokenType.BLOCK_END)
2298
+ return ASTNode('action_block', children=children)
2299
+
2194
2300
  def _parse_object(self) -> ASTNode:
2195
2301
  properties = {}
2196
2302
 
@@ -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
@@ -799,6 +802,9 @@ class CSSLRuntime:
799
802
 
800
803
  try:
801
804
  for child in func_node.children:
805
+ # Check if exit() was called
806
+ if not self._running:
807
+ break
802
808
  self._execute_node(child)
803
809
  except CSSLReturn as ret:
804
810
  return ret.value
@@ -1284,8 +1290,21 @@ class CSSLRuntime:
1284
1290
  self.register_function_injection(func_name, source_node)
1285
1291
  return None
1286
1292
 
1287
- # Evaluate source
1288
- source = self._evaluate(source_node)
1293
+ # Check if source is an action_block with %<name> captures
1294
+ # If so, capture values NOW and evaluate the block with those captures
1295
+ if isinstance(source_node, ASTNode) and source_node.type == 'action_block':
1296
+ # Scan for %<name> captured references and capture their current values
1297
+ captured_values = self._scan_and_capture_refs(source_node)
1298
+ old_captured = self._current_captured_values.copy()
1299
+ self._current_captured_values = captured_values
1300
+ try:
1301
+ # Execute the action block and get the last expression's value
1302
+ source = self._evaluate_action_block(source_node)
1303
+ finally:
1304
+ self._current_captured_values = old_captured
1305
+ else:
1306
+ # Evaluate source normally
1307
+ source = self._evaluate(source_node)
1289
1308
 
1290
1309
  # Apply filter if present
1291
1310
  if filter_info:
@@ -1424,11 +1443,20 @@ class CSSLRuntime:
1424
1443
  - replace: func <<== { code } - REPLACES function body (original won't execute)
1425
1444
  - add: func +<<== { code } - ADDS code to function (both execute)
1426
1445
  - remove: func -<<== { code } - REMOVES matching code from function
1446
+
1447
+ Also supports expression form: func <<== %exit() (wraps in action_block)
1427
1448
  """
1428
1449
  target = node.value.get('target')
1429
1450
  code_block = node.value.get('code')
1451
+ source_expr = node.value.get('source') # For expression form: func <<== expr
1430
1452
  mode = node.value.get('mode', 'replace') # Default is REPLACE for <<==
1431
1453
 
1454
+ # If source expression is provided instead of code block, wrap it
1455
+ if code_block is None and source_expr is not None:
1456
+ # Wrap in expression node so _execute_node can handle it
1457
+ expr_node = ASTNode('expression', value=source_expr)
1458
+ code_block = ASTNode('action_block', children=[expr_node])
1459
+
1432
1460
  # Get function name from target
1433
1461
  func_name = None
1434
1462
  if isinstance(target, ASTNode):
@@ -1439,7 +1467,7 @@ class CSSLRuntime:
1439
1467
  if isinstance(callee, ASTNode) and callee.type == 'identifier':
1440
1468
  func_name = callee.value
1441
1469
 
1442
- if not func_name:
1470
+ if not func_name or code_block is None:
1443
1471
  return None
1444
1472
 
1445
1473
  if mode == 'add':
@@ -1448,7 +1476,17 @@ class CSSLRuntime:
1448
1476
  self._function_replaced[func_name] = False # Don't replace, just add
1449
1477
  elif mode == 'replace':
1450
1478
  # <<== : Replace function body (only injection executes, original skipped)
1451
- self._function_injections[func_name] = [code_block]
1479
+ # Save original function BEFORE replacing (for original() access)
1480
+ if func_name not in self._original_functions:
1481
+ # Try to find original in scope or builtins
1482
+ original = self.scope.get(func_name)
1483
+ if original is None:
1484
+ original = getattr(self.builtins, f'builtin_{func_name}', None)
1485
+ if original is not None:
1486
+ self._original_functions[func_name] = original
1487
+ # Capture %<name> references at registration time
1488
+ captured_values = self._scan_and_capture_refs(code_block)
1489
+ self._function_injections[func_name] = [(code_block, captured_values)]
1452
1490
  self._function_replaced[func_name] = True # Mark as replaced
1453
1491
  elif mode == 'remove':
1454
1492
  # -<<== or -<<==[n] : Remove matching code from function body
@@ -1654,6 +1692,33 @@ class CSSLRuntime:
1654
1692
  return scoped_val
1655
1693
  raise CSSLRuntimeError(f"Shared object '${name}' not found. Use share() to share objects.")
1656
1694
 
1695
+ if node.type == 'captured_ref':
1696
+ # %<name> captured reference - use value captured at infusion registration time
1697
+ name = node.value
1698
+ # First check captured values from current injection context
1699
+ if name in self._current_captured_values:
1700
+ captured_value = self._current_captured_values[name]
1701
+ # Only use captured value if it's not None
1702
+ if captured_value is not None:
1703
+ return captured_value
1704
+ # Fall back to normal resolution if not captured or capture was None
1705
+ value = self.scope.get(name)
1706
+ if value is None:
1707
+ value = self.global_scope.get(name)
1708
+ if value is None:
1709
+ # For critical builtins like 'exit', create direct wrapper
1710
+ if name == 'exit':
1711
+ runtime = self
1712
+ value = lambda code=0, rt=runtime: rt.exit(code)
1713
+ else:
1714
+ value = getattr(self.builtins, f'builtin_{name}', None)
1715
+ if value is None:
1716
+ # Check original functions (for replaced functions)
1717
+ value = self._original_functions.get(name)
1718
+ if value is not None:
1719
+ return value
1720
+ raise CSSLRuntimeError(f"Captured reference '%{name}' not found.")
1721
+
1657
1722
  if node.type == 'type_instantiation':
1658
1723
  # Create new instance of a type: stack<string>, vector<int>, etc.
1659
1724
  type_name = node.value.get('type')
@@ -1720,8 +1785,41 @@ class CSSLRuntime:
1720
1785
  return {'__ref__': True, 'name': inner.value, 'value': self.get_module(inner.value)}
1721
1786
  return {'__ref__': True, 'value': self._evaluate(inner)}
1722
1787
 
1788
+ # Handle action_block - execute and return last expression value
1789
+ if node.type == 'action_block':
1790
+ return self._evaluate_action_block(node)
1791
+
1723
1792
  return None
1724
1793
 
1794
+ def _evaluate_action_block(self, node: ASTNode) -> Any:
1795
+ """Evaluate an action block and return the last expression's value.
1796
+
1797
+ Used for: v <== { %version; } - captures %version at this moment
1798
+
1799
+ Returns the value of the last expression in the block.
1800
+ If the block contains a captured_ref (%name), that's what gets returned.
1801
+ """
1802
+ last_value = None
1803
+ for child in node.children:
1804
+ if child.type == 'captured_ref':
1805
+ # Direct captured reference - return its value
1806
+ last_value = self._evaluate(child)
1807
+ elif child.type == 'expression':
1808
+ # Expression statement - evaluate and keep value
1809
+ last_value = self._evaluate(child.value if hasattr(child, 'value') else child)
1810
+ elif child.type == 'identifier':
1811
+ # Just an identifier - evaluate it
1812
+ last_value = self._evaluate(child)
1813
+ elif child.type in ('call', 'member_access', 'binary', 'unary'):
1814
+ # Expression types
1815
+ last_value = self._evaluate(child)
1816
+ else:
1817
+ # Execute other statements
1818
+ result = self._execute_node(child)
1819
+ if result is not None:
1820
+ last_value = result
1821
+ return last_value
1822
+
1725
1823
  def _eval_binary(self, node: ASTNode) -> Any:
1726
1824
  """Evaluate binary operation with auto-casting support"""
1727
1825
  op = node.value.get('op')
@@ -1893,10 +1991,9 @@ class CSSLRuntime:
1893
1991
  def _eval_call(self, node: ASTNode) -> Any:
1894
1992
  """Evaluate function call"""
1895
1993
  callee_node = node.value.get('callee')
1896
- callee = self._evaluate(callee_node)
1897
1994
  args = [self._evaluate(a) for a in node.value.get('args', [])]
1898
1995
 
1899
- # Get function name for injection check
1996
+ # Get function name for injection check FIRST (before evaluating callee)
1900
1997
  func_name = None
1901
1998
  if isinstance(callee_node, ASTNode):
1902
1999
  if callee_node.type == 'identifier':
@@ -1908,13 +2005,18 @@ class CSSLRuntime:
1908
2005
  has_injections = func_name and func_name in self._function_injections
1909
2006
  is_replaced = func_name and self._function_replaced.get(func_name, False)
1910
2007
 
1911
- # Execute injected code first (if any)
1912
- if has_injections:
2008
+ # If function is FULLY REPLACED (<<==), run injection and skip original
2009
+ # This allows creating new functions via infusion: new_func <<== { ... }
2010
+ if is_replaced:
1913
2011
  self._execute_function_injections(func_name)
2012
+ return None # Injection ran, don't try to find original
1914
2013
 
1915
- # If function is REPLACED (<<==), skip original body execution
1916
- if is_replaced:
1917
- return None # Injection already ran, don't run original
2014
+ # Now evaluate the callee (only if not replaced)
2015
+ callee = self._evaluate(callee_node)
2016
+
2017
+ # Execute added injections (+<<==) before original
2018
+ if has_injections and not is_replaced:
2019
+ self._execute_function_injections(func_name)
1918
2020
 
1919
2021
  # Execute original function
1920
2022
  if callable(callee):
@@ -2329,22 +2431,107 @@ class CSSLRuntime:
2329
2431
  # Also store in promoted globals for string interpolation
2330
2432
  self._promoted_globals[base_name] = value
2331
2433
 
2434
+ # NEW: Scan for captured_ref nodes and capture their current values
2435
+ def _scan_and_capture_refs(self, node: ASTNode) -> Dict[str, Any]:
2436
+ """Scan AST for %<name> captured references and capture their current values.
2437
+
2438
+ This is called at infusion registration time to capture values.
2439
+ Example: old_exit <<== { %exit(); } captures 'exit' at definition time.
2440
+ """
2441
+ captured = {}
2442
+
2443
+ def scan_node(n):
2444
+ if not isinstance(n, ASTNode):
2445
+ return
2446
+
2447
+ # Found a captured_ref - capture its current value
2448
+ if n.type == 'captured_ref':
2449
+ name = n.value
2450
+ if name not in captured:
2451
+ # Try to find value - check multiple sources
2452
+ value = None
2453
+
2454
+ # 1. Check _original_functions first (for functions that were JUST replaced)
2455
+ if value is None:
2456
+ value = self._original_functions.get(name)
2457
+
2458
+ # 2. Check scope
2459
+ if value is None:
2460
+ value = self.scope.get(name)
2461
+
2462
+ # 3. Check global_scope
2463
+ if value is None:
2464
+ value = self.global_scope.get(name)
2465
+
2466
+ # 4. Check builtins (most common case for exit, print, etc.)
2467
+ if value is None:
2468
+ # For critical builtins like 'exit', create a direct wrapper
2469
+ # that captures the runtime reference to ensure correct behavior
2470
+ if name == 'exit':
2471
+ runtime = self # Capture runtime in closure
2472
+ value = lambda code=0, rt=runtime: rt.exit(code)
2473
+ else:
2474
+ value = getattr(self.builtins, f'builtin_{name}', None)
2475
+
2476
+ # 5. Check if there's a user-defined function in scope
2477
+ if value is None:
2478
+ # Look for function definitions
2479
+ func_def = self.global_scope.get(f'__func_{name}')
2480
+ if func_def is not None:
2481
+ value = func_def
2482
+
2483
+ # Only capture if we found something
2484
+ if value is not None:
2485
+ captured[name] = value
2486
+
2487
+ # Check call node's callee
2488
+ if n.type == 'call':
2489
+ callee = n.value.get('callee')
2490
+ if callee:
2491
+ scan_node(callee)
2492
+ for arg in n.value.get('args', []):
2493
+ scan_node(arg)
2494
+
2495
+ # Recurse into children
2496
+ if hasattr(n, 'children') and n.children:
2497
+ for child in n.children:
2498
+ scan_node(child)
2499
+
2500
+ # Check value dict for nested nodes
2501
+ if hasattr(n, 'value') and isinstance(n.value, dict):
2502
+ for key, val in n.value.items():
2503
+ if isinstance(val, ASTNode):
2504
+ scan_node(val)
2505
+ elif isinstance(val, list):
2506
+ for item in val:
2507
+ if isinstance(item, ASTNode):
2508
+ scan_node(item)
2509
+
2510
+ scan_node(node)
2511
+ return captured
2512
+
2332
2513
  # NEW: Register permanent function injection
2333
2514
  def register_function_injection(self, func_name: str, code_block: ASTNode):
2334
2515
  """Register code to be permanently injected into a function - NEW
2335
2516
 
2336
2517
  Example: exit() <== { println("Cleanup..."); }
2337
2518
  Makes every call to exit() also execute the injected code
2519
+
2520
+ Captures %<name> references at registration time.
2338
2521
  """
2522
+ # Scan for %<name> captured references and capture their current values
2523
+ captured_values = self._scan_and_capture_refs(code_block)
2524
+
2339
2525
  if func_name not in self._function_injections:
2340
2526
  self._function_injections[func_name] = []
2341
- self._function_injections[func_name].append(code_block)
2527
+ self._function_injections[func_name].append((code_block, captured_values))
2342
2528
 
2343
2529
  # NEW: Execute injected code for a function
2344
2530
  def _execute_function_injections(self, func_name: str):
2345
2531
  """Execute all injected code blocks for a function - NEW
2346
2532
 
2347
2533
  Includes protection against recursive execution to prevent doubled output.
2534
+ Uses captured values for %<name> references.
2348
2535
  """
2349
2536
  # Prevent recursive injection execution (fixes doubled output bug)
2350
2537
  if getattr(self, '_injection_executing', False):
@@ -2352,16 +2539,29 @@ class CSSLRuntime:
2352
2539
 
2353
2540
  if func_name in self._function_injections:
2354
2541
  self._injection_executing = True
2542
+ old_captured = self._current_captured_values.copy()
2355
2543
  try:
2356
- for code_block in self._function_injections[func_name]:
2544
+ for injection in self._function_injections[func_name]:
2545
+ # Handle both tuple format (code_block, captured_values) and legacy ASTNode format
2546
+ if isinstance(injection, tuple):
2547
+ code_block, captured_values = injection
2548
+ self._current_captured_values = captured_values
2549
+ else:
2550
+ code_block = injection
2551
+ self._current_captured_values = {}
2552
+
2357
2553
  if isinstance(code_block, ASTNode):
2358
2554
  if code_block.type == 'action_block':
2359
2555
  for child in code_block.children:
2556
+ # Check if exit() was called
2557
+ if not self._running:
2558
+ break
2360
2559
  self._execute_node(child)
2361
2560
  else:
2362
2561
  self._execute_node(code_block)
2363
2562
  finally:
2364
2563
  self._injection_executing = False
2564
+ self._current_captured_values = old_captured
2365
2565
 
2366
2566
  # Output functions for builtins
2367
2567
  def set_output_callback(self, callback: Callable[[str, str], None]):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "IncludeCPP"
7
- version = "3.5.2"
7
+ version = "3.5.7"
8
8
  description = "Professional C++ Python bindings with type-generic templates, pystubs and native threading"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes