IncludeCPP 3.5.2__tar.gz → 3.5.8__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.8}/IncludeCPP.egg-info/PKG-INFO +1 -1
  2. {includecpp-3.5.2 → includecpp-3.5.8}/PKG-INFO +1 -1
  3. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/__init__.py +1 -1
  4. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/cssl_builtins.py +23 -0
  5. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/cssl_parser.py +112 -6
  6. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/cssl_runtime.py +250 -22
  7. {includecpp-3.5.2 → includecpp-3.5.8}/pyproject.toml +1 -1
  8. {includecpp-3.5.2 → includecpp-3.5.8}/IncludeCPP.egg-info/SOURCES.txt +0 -0
  9. {includecpp-3.5.2 → includecpp-3.5.8}/IncludeCPP.egg-info/dependency_links.txt +0 -0
  10. {includecpp-3.5.2 → includecpp-3.5.8}/IncludeCPP.egg-info/entry_points.txt +0 -0
  11. {includecpp-3.5.2 → includecpp-3.5.8}/IncludeCPP.egg-info/requires.txt +0 -0
  12. {includecpp-3.5.2 → includecpp-3.5.8}/IncludeCPP.egg-info/top_level.txt +0 -0
  13. {includecpp-3.5.2 → includecpp-3.5.8}/LICENSE +0 -0
  14. {includecpp-3.5.2 → includecpp-3.5.8}/MANIFEST.in +0 -0
  15. {includecpp-3.5.2 → includecpp-3.5.8}/README.md +0 -0
  16. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/__init__.pyi +0 -0
  17. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/__main__.py +0 -0
  18. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/cli/__init__.py +0 -0
  19. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/cli/commands.py +0 -0
  20. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/cli/config_parser.py +0 -0
  21. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/__init__.py +0 -0
  22. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/ai_integration.py +0 -0
  23. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/build_manager.py +0 -0
  24. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cpp_api.py +0 -0
  25. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cpp_api.pyi +0 -0
  26. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cppy_converter.py +0 -0
  27. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/CSSL_DOCUMENTATION.md +0 -0
  28. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/__init__.py +0 -0
  29. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/cssl_events.py +0 -0
  30. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/cssl_modules.py +0 -0
  31. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/cssl_syntax.py +0 -0
  32. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl/cssl_types.py +0 -0
  33. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl_bridge.py +0 -0
  34. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/cssl_bridge.pyi +0 -0
  35. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/error_catalog.py +0 -0
  36. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/error_formatter.py +0 -0
  37. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/exceptions.py +0 -0
  38. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/path_discovery.py +0 -0
  39. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/project_ui.py +0 -0
  40. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/core/settings_ui.py +0 -0
  41. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/generator/__init__.py +0 -0
  42. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/generator/parser.cpp +0 -0
  43. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/generator/parser.h +0 -0
  44. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/generator/type_resolver.cpp +0 -0
  45. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/generator/type_resolver.h +0 -0
  46. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/py.typed +0 -0
  47. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/templates/cpp.proj.template +0 -0
  48. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/vscode/__init__.py +0 -0
  49. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/vscode/cssl/__init__.py +0 -0
  50. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/vscode/cssl/language-configuration.json +0 -0
  51. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/vscode/cssl/package.json +0 -0
  52. {includecpp-3.5.2 → includecpp-3.5.8}/includecpp/vscode/cssl/syntaxes/cssl.tmLanguage.json +0 -0
  53. {includecpp-3.5.2 → includecpp-3.5.8}/requirements.txt +0 -0
  54. {includecpp-3.5.2 → includecpp-3.5.8}/setup.cfg +0 -0
  55. {includecpp-3.5.2 → includecpp-3.5.8}/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.8
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.8
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.8"
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) == '=':
@@ -541,10 +559,10 @@ class CSSLLexer:
541
559
  elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '+':
542
560
  self._add_token(TokenType.INJECT_PLUS_RIGHT, '==>+')
543
561
  for _ in range(4): self._advance()
544
- # Check for ===>- (injection right minus - moves & removes)
545
- elif self._peek(1) == '=' and self._peek(2) == '=' and self._peek(3) == '>' and self._peek(4) == '-':
546
- self._add_token(TokenType.INJECT_MINUS_RIGHT, '===>')
547
- for _ in range(5): self._advance()
562
+ # Check for ==>- (injection right minus - moves & removes)
563
+ elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '-':
564
+ self._add_token(TokenType.INJECT_MINUS_RIGHT, '==>-')
565
+ for _ in range(4): self._advance()
548
566
  # Check for ==> (basic injection right)
549
567
  elif self._peek(1) == '=' and self._peek(2) == '>':
550
568
  self._add_token(TokenType.INJECT_RIGHT, '==>')
@@ -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
@@ -1132,21 +1138,49 @@ class CSSLRuntime:
1132
1138
  elif isinstance(result, list):
1133
1139
  result = [item for item in result if isinstance(item, str) and len(item) == filter_val]
1134
1140
  elif helper == 'cut':
1135
- # Cut string at given index - returns the part BEFORE the index
1141
+ # Cut string - returns the part BEFORE the index/substring
1136
1142
  # x = <==[string::cut=2] "20:200-1" --> x = "20"
1143
+ # x = <==[string::cut="1.0"] "1.0.0" --> x = "" (before "1.0")
1137
1144
  if isinstance(result, str):
1138
- idx = int(filter_val) if not isinstance(filter_val, int) else filter_val
1139
- result = result[:idx] if 0 <= idx <= len(result) else result
1145
+ if isinstance(filter_val, str):
1146
+ # Cut at substring position
1147
+ idx = result.find(filter_val)
1148
+ result = result[:idx] if idx >= 0 else result
1149
+ else:
1150
+ # Cut at integer index
1151
+ idx = int(filter_val)
1152
+ result = result[:idx] if 0 <= idx <= len(result) else result
1140
1153
  elif isinstance(result, list):
1141
- result = [item[:int(filter_val)] if isinstance(item, str) else item for item in result]
1154
+ def cut_item(item):
1155
+ if not isinstance(item, str):
1156
+ return item
1157
+ if isinstance(filter_val, str):
1158
+ idx = item.find(filter_val)
1159
+ return item[:idx] if idx >= 0 else item
1160
+ return item[:int(filter_val)]
1161
+ result = [cut_item(item) for item in result]
1142
1162
  elif helper == 'cutAfter':
1143
- # Get the part AFTER the index
1163
+ # Get the part AFTER the index/substring
1144
1164
  # x = <==[string::cutAfter=2] "20:200-1" --> x = ":200-1"
1165
+ # x = <==[string::cutAfter="1.0"] "1.0.0" --> x = ".0" (after "1.0")
1145
1166
  if isinstance(result, str):
1146
- idx = int(filter_val) if not isinstance(filter_val, int) else filter_val
1147
- result = result[idx:] if 0 <= idx <= len(result) else result
1167
+ if isinstance(filter_val, str):
1168
+ # Cut after substring
1169
+ idx = result.find(filter_val)
1170
+ result = result[idx + len(filter_val):] if idx >= 0 else result
1171
+ else:
1172
+ # Cut after integer index
1173
+ idx = int(filter_val)
1174
+ result = result[idx:] if 0 <= idx <= len(result) else result
1148
1175
  elif isinstance(result, list):
1149
- result = [item[int(filter_val):] if isinstance(item, str) else item for item in result]
1176
+ def cut_after_item(item):
1177
+ if not isinstance(item, str):
1178
+ return item
1179
+ if isinstance(filter_val, str):
1180
+ idx = item.find(filter_val)
1181
+ return item[idx + len(filter_val):] if idx >= 0 else item
1182
+ return item[int(filter_val):]
1183
+ result = [cut_after_item(item) for item in result]
1150
1184
  elif helper == 'slice':
1151
1185
  # Slice string with start:end format (e.g., "2:5")
1152
1186
  if isinstance(result, str) and isinstance(filter_val, str) and ':' in filter_val:
@@ -1284,8 +1318,21 @@ class CSSLRuntime:
1284
1318
  self.register_function_injection(func_name, source_node)
1285
1319
  return None
1286
1320
 
1287
- # Evaluate source
1288
- source = self._evaluate(source_node)
1321
+ # Check if source is an action_block with %<name> captures
1322
+ # If so, capture values NOW and evaluate the block with those captures
1323
+ if isinstance(source_node, ASTNode) and source_node.type == 'action_block':
1324
+ # Scan for %<name> captured references and capture their current values
1325
+ captured_values = self._scan_and_capture_refs(source_node)
1326
+ old_captured = self._current_captured_values.copy()
1327
+ self._current_captured_values = captured_values
1328
+ try:
1329
+ # Execute the action block and get the last expression's value
1330
+ source = self._evaluate_action_block(source_node)
1331
+ finally:
1332
+ self._current_captured_values = old_captured
1333
+ else:
1334
+ # Evaluate source normally
1335
+ source = self._evaluate(source_node)
1289
1336
 
1290
1337
  # Apply filter if present
1291
1338
  if filter_info:
@@ -1424,11 +1471,20 @@ class CSSLRuntime:
1424
1471
  - replace: func <<== { code } - REPLACES function body (original won't execute)
1425
1472
  - add: func +<<== { code } - ADDS code to function (both execute)
1426
1473
  - remove: func -<<== { code } - REMOVES matching code from function
1474
+
1475
+ Also supports expression form: func <<== %exit() (wraps in action_block)
1427
1476
  """
1428
1477
  target = node.value.get('target')
1429
1478
  code_block = node.value.get('code')
1479
+ source_expr = node.value.get('source') # For expression form: func <<== expr
1430
1480
  mode = node.value.get('mode', 'replace') # Default is REPLACE for <<==
1431
1481
 
1482
+ # If source expression is provided instead of code block, wrap it
1483
+ if code_block is None and source_expr is not None:
1484
+ # Wrap in expression node so _execute_node can handle it
1485
+ expr_node = ASTNode('expression', value=source_expr)
1486
+ code_block = ASTNode('action_block', children=[expr_node])
1487
+
1432
1488
  # Get function name from target
1433
1489
  func_name = None
1434
1490
  if isinstance(target, ASTNode):
@@ -1439,7 +1495,7 @@ class CSSLRuntime:
1439
1495
  if isinstance(callee, ASTNode) and callee.type == 'identifier':
1440
1496
  func_name = callee.value
1441
1497
 
1442
- if not func_name:
1498
+ if not func_name or code_block is None:
1443
1499
  return None
1444
1500
 
1445
1501
  if mode == 'add':
@@ -1448,7 +1504,17 @@ class CSSLRuntime:
1448
1504
  self._function_replaced[func_name] = False # Don't replace, just add
1449
1505
  elif mode == 'replace':
1450
1506
  # <<== : Replace function body (only injection executes, original skipped)
1451
- self._function_injections[func_name] = [code_block]
1507
+ # Save original function BEFORE replacing (for original() access)
1508
+ if func_name not in self._original_functions:
1509
+ # Try to find original in scope or builtins
1510
+ original = self.scope.get(func_name)
1511
+ if original is None:
1512
+ original = getattr(self.builtins, f'builtin_{func_name}', None)
1513
+ if original is not None:
1514
+ self._original_functions[func_name] = original
1515
+ # Capture %<name> references at registration time
1516
+ captured_values = self._scan_and_capture_refs(code_block)
1517
+ self._function_injections[func_name] = [(code_block, captured_values)]
1452
1518
  self._function_replaced[func_name] = True # Mark as replaced
1453
1519
  elif mode == 'remove':
1454
1520
  # -<<== or -<<==[n] : Remove matching code from function body
@@ -1654,6 +1720,33 @@ class CSSLRuntime:
1654
1720
  return scoped_val
1655
1721
  raise CSSLRuntimeError(f"Shared object '${name}' not found. Use share() to share objects.")
1656
1722
 
1723
+ if node.type == 'captured_ref':
1724
+ # %<name> captured reference - use value captured at infusion registration time
1725
+ name = node.value
1726
+ # First check captured values from current injection context
1727
+ if name in self._current_captured_values:
1728
+ captured_value = self._current_captured_values[name]
1729
+ # Only use captured value if it's not None
1730
+ if captured_value is not None:
1731
+ return captured_value
1732
+ # Fall back to normal resolution if not captured or capture was None
1733
+ value = self.scope.get(name)
1734
+ if value is None:
1735
+ value = self.global_scope.get(name)
1736
+ if value is None:
1737
+ # For critical builtins like 'exit', create direct wrapper
1738
+ if name == 'exit':
1739
+ runtime = self
1740
+ value = lambda code=0, rt=runtime: rt.exit(code)
1741
+ else:
1742
+ value = getattr(self.builtins, f'builtin_{name}', None)
1743
+ if value is None:
1744
+ # Check original functions (for replaced functions)
1745
+ value = self._original_functions.get(name)
1746
+ if value is not None:
1747
+ return value
1748
+ raise CSSLRuntimeError(f"Captured reference '%{name}' not found.")
1749
+
1657
1750
  if node.type == 'type_instantiation':
1658
1751
  # Create new instance of a type: stack<string>, vector<int>, etc.
1659
1752
  type_name = node.value.get('type')
@@ -1720,8 +1813,41 @@ class CSSLRuntime:
1720
1813
  return {'__ref__': True, 'name': inner.value, 'value': self.get_module(inner.value)}
1721
1814
  return {'__ref__': True, 'value': self._evaluate(inner)}
1722
1815
 
1816
+ # Handle action_block - execute and return last expression value
1817
+ if node.type == 'action_block':
1818
+ return self._evaluate_action_block(node)
1819
+
1723
1820
  return None
1724
1821
 
1822
+ def _evaluate_action_block(self, node: ASTNode) -> Any:
1823
+ """Evaluate an action block and return the last expression's value.
1824
+
1825
+ Used for: v <== { %version; } - captures %version at this moment
1826
+
1827
+ Returns the value of the last expression in the block.
1828
+ If the block contains a captured_ref (%name), that's what gets returned.
1829
+ """
1830
+ last_value = None
1831
+ for child in node.children:
1832
+ if child.type == 'captured_ref':
1833
+ # Direct captured reference - return its value
1834
+ last_value = self._evaluate(child)
1835
+ elif child.type == 'expression':
1836
+ # Expression statement - evaluate and keep value
1837
+ last_value = self._evaluate(child.value if hasattr(child, 'value') else child)
1838
+ elif child.type == 'identifier':
1839
+ # Just an identifier - evaluate it
1840
+ last_value = self._evaluate(child)
1841
+ elif child.type in ('call', 'member_access', 'binary', 'unary'):
1842
+ # Expression types
1843
+ last_value = self._evaluate(child)
1844
+ else:
1845
+ # Execute other statements
1846
+ result = self._execute_node(child)
1847
+ if result is not None:
1848
+ last_value = result
1849
+ return last_value
1850
+
1725
1851
  def _eval_binary(self, node: ASTNode) -> Any:
1726
1852
  """Evaluate binary operation with auto-casting support"""
1727
1853
  op = node.value.get('op')
@@ -1893,10 +2019,9 @@ class CSSLRuntime:
1893
2019
  def _eval_call(self, node: ASTNode) -> Any:
1894
2020
  """Evaluate function call"""
1895
2021
  callee_node = node.value.get('callee')
1896
- callee = self._evaluate(callee_node)
1897
2022
  args = [self._evaluate(a) for a in node.value.get('args', [])]
1898
2023
 
1899
- # Get function name for injection check
2024
+ # Get function name for injection check FIRST (before evaluating callee)
1900
2025
  func_name = None
1901
2026
  if isinstance(callee_node, ASTNode):
1902
2027
  if callee_node.type == 'identifier':
@@ -1908,13 +2033,18 @@ class CSSLRuntime:
1908
2033
  has_injections = func_name and func_name in self._function_injections
1909
2034
  is_replaced = func_name and self._function_replaced.get(func_name, False)
1910
2035
 
1911
- # Execute injected code first (if any)
1912
- if has_injections:
2036
+ # If function is FULLY REPLACED (<<==), run injection and skip original
2037
+ # This allows creating new functions via infusion: new_func <<== { ... }
2038
+ if is_replaced:
1913
2039
  self._execute_function_injections(func_name)
2040
+ return None # Injection ran, don't try to find original
1914
2041
 
1915
- # If function is REPLACED (<<==), skip original body execution
1916
- if is_replaced:
1917
- return None # Injection already ran, don't run original
2042
+ # Now evaluate the callee (only if not replaced)
2043
+ callee = self._evaluate(callee_node)
2044
+
2045
+ # Execute added injections (+<<==) before original
2046
+ if has_injections and not is_replaced:
2047
+ self._execute_function_injections(func_name)
1918
2048
 
1919
2049
  # Execute original function
1920
2050
  if callable(callee):
@@ -2329,22 +2459,107 @@ class CSSLRuntime:
2329
2459
  # Also store in promoted globals for string interpolation
2330
2460
  self._promoted_globals[base_name] = value
2331
2461
 
2462
+ # NEW: Scan for captured_ref nodes and capture their current values
2463
+ def _scan_and_capture_refs(self, node: ASTNode) -> Dict[str, Any]:
2464
+ """Scan AST for %<name> captured references and capture their current values.
2465
+
2466
+ This is called at infusion registration time to capture values.
2467
+ Example: old_exit <<== { %exit(); } captures 'exit' at definition time.
2468
+ """
2469
+ captured = {}
2470
+
2471
+ def scan_node(n):
2472
+ if not isinstance(n, ASTNode):
2473
+ return
2474
+
2475
+ # Found a captured_ref - capture its current value
2476
+ if n.type == 'captured_ref':
2477
+ name = n.value
2478
+ if name not in captured:
2479
+ # Try to find value - check multiple sources
2480
+ value = None
2481
+
2482
+ # 1. Check _original_functions first (for functions that were JUST replaced)
2483
+ if value is None:
2484
+ value = self._original_functions.get(name)
2485
+
2486
+ # 2. Check scope
2487
+ if value is None:
2488
+ value = self.scope.get(name)
2489
+
2490
+ # 3. Check global_scope
2491
+ if value is None:
2492
+ value = self.global_scope.get(name)
2493
+
2494
+ # 4. Check builtins (most common case for exit, print, etc.)
2495
+ if value is None:
2496
+ # For critical builtins like 'exit', create a direct wrapper
2497
+ # that captures the runtime reference to ensure correct behavior
2498
+ if name == 'exit':
2499
+ runtime = self # Capture runtime in closure
2500
+ value = lambda code=0, rt=runtime: rt.exit(code)
2501
+ else:
2502
+ value = getattr(self.builtins, f'builtin_{name}', None)
2503
+
2504
+ # 5. Check if there's a user-defined function in scope
2505
+ if value is None:
2506
+ # Look for function definitions
2507
+ func_def = self.global_scope.get(f'__func_{name}')
2508
+ if func_def is not None:
2509
+ value = func_def
2510
+
2511
+ # Only capture if we found something
2512
+ if value is not None:
2513
+ captured[name] = value
2514
+
2515
+ # Check call node's callee
2516
+ if n.type == 'call':
2517
+ callee = n.value.get('callee')
2518
+ if callee:
2519
+ scan_node(callee)
2520
+ for arg in n.value.get('args', []):
2521
+ scan_node(arg)
2522
+
2523
+ # Recurse into children
2524
+ if hasattr(n, 'children') and n.children:
2525
+ for child in n.children:
2526
+ scan_node(child)
2527
+
2528
+ # Check value dict for nested nodes
2529
+ if hasattr(n, 'value') and isinstance(n.value, dict):
2530
+ for key, val in n.value.items():
2531
+ if isinstance(val, ASTNode):
2532
+ scan_node(val)
2533
+ elif isinstance(val, list):
2534
+ for item in val:
2535
+ if isinstance(item, ASTNode):
2536
+ scan_node(item)
2537
+
2538
+ scan_node(node)
2539
+ return captured
2540
+
2332
2541
  # NEW: Register permanent function injection
2333
2542
  def register_function_injection(self, func_name: str, code_block: ASTNode):
2334
2543
  """Register code to be permanently injected into a function - NEW
2335
2544
 
2336
2545
  Example: exit() <== { println("Cleanup..."); }
2337
2546
  Makes every call to exit() also execute the injected code
2547
+
2548
+ Captures %<name> references at registration time.
2338
2549
  """
2550
+ # Scan for %<name> captured references and capture their current values
2551
+ captured_values = self._scan_and_capture_refs(code_block)
2552
+
2339
2553
  if func_name not in self._function_injections:
2340
2554
  self._function_injections[func_name] = []
2341
- self._function_injections[func_name].append(code_block)
2555
+ self._function_injections[func_name].append((code_block, captured_values))
2342
2556
 
2343
2557
  # NEW: Execute injected code for a function
2344
2558
  def _execute_function_injections(self, func_name: str):
2345
2559
  """Execute all injected code blocks for a function - NEW
2346
2560
 
2347
2561
  Includes protection against recursive execution to prevent doubled output.
2562
+ Uses captured values for %<name> references.
2348
2563
  """
2349
2564
  # Prevent recursive injection execution (fixes doubled output bug)
2350
2565
  if getattr(self, '_injection_executing', False):
@@ -2352,16 +2567,29 @@ class CSSLRuntime:
2352
2567
 
2353
2568
  if func_name in self._function_injections:
2354
2569
  self._injection_executing = True
2570
+ old_captured = self._current_captured_values.copy()
2355
2571
  try:
2356
- for code_block in self._function_injections[func_name]:
2572
+ for injection in self._function_injections[func_name]:
2573
+ # Handle both tuple format (code_block, captured_values) and legacy ASTNode format
2574
+ if isinstance(injection, tuple):
2575
+ code_block, captured_values = injection
2576
+ self._current_captured_values = captured_values
2577
+ else:
2578
+ code_block = injection
2579
+ self._current_captured_values = {}
2580
+
2357
2581
  if isinstance(code_block, ASTNode):
2358
2582
  if code_block.type == 'action_block':
2359
2583
  for child in code_block.children:
2584
+ # Check if exit() was called
2585
+ if not self._running:
2586
+ break
2360
2587
  self._execute_node(child)
2361
2588
  else:
2362
2589
  self._execute_node(code_block)
2363
2590
  finally:
2364
2591
  self._injection_executing = False
2592
+ self._current_captured_values = old_captured
2365
2593
 
2366
2594
  # Output functions for builtins
2367
2595
  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.8"
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