IncludeCPP 3.4.2__py3-none-any.whl → 3.4.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
includecpp/__init__.py CHANGED
@@ -1,59 +1,59 @@
1
- from .core.cpp_api import CppApi
2
- from .core import cssl_bridge as CSSL
3
- import warnings
4
-
5
- __version__ = "3.4.2"
6
- __all__ = ["CppApi", "CSSL"]
7
-
8
- # Module-level cache for C++ modules
9
- _api_instance = None
10
- _loaded_modules = {}
11
-
12
- def _get_api():
13
- """Get or create singleton CppApi instance."""
14
- global _api_instance
15
- if _api_instance is None:
16
- _api_instance = CppApi()
17
- return _api_instance
18
-
19
- def __getattr__(name: str):
20
- """Enable: from includecpp import fast_list
21
-
22
- This hook is called when Python cannot find an attribute in this module.
23
- It allows dynamic C++ module loading via the import system.
24
- """
25
- if name.startswith('_'):
26
- raise AttributeError(f"module 'includecpp' has no attribute '{name}'")
27
-
28
- if name in _loaded_modules:
29
- return _loaded_modules[name]
30
-
31
- api = _get_api()
32
-
33
- if name not in api.registry:
34
- available = list(api.registry.keys())
35
- raise AttributeError(
36
- f"Module '{name}' not found. "
37
- f"Available: {available}. "
38
- f"Run 'includecpp rebuild' first."
39
- )
40
-
41
- if api.need_update(name):
42
- warnings.warn(
43
- f"Module '{name}' source files changed. "
44
- f"Run 'includecpp rebuild' to update.",
45
- UserWarning
46
- )
47
-
48
- module = api.include(name)
49
- _loaded_modules[name] = module
50
- return module
51
-
52
- def __dir__():
53
- """List available attributes including C++ modules."""
54
- base = ['CppApi', 'CSSL', '__version__']
55
- try:
56
- api = _get_api()
57
- return sorted(set(base + list(api.registry.keys())))
58
- except Exception:
59
- return base
1
+ from .core.cpp_api import CppApi
2
+ from .core import cssl_bridge as CSSL
3
+ import warnings
4
+
5
+ __version__ = "3.4.8"
6
+ __all__ = ["CppApi", "CSSL"]
7
+
8
+ # Module-level cache for C++ modules
9
+ _api_instance = None
10
+ _loaded_modules = {}
11
+
12
+ def _get_api():
13
+ """Get or create singleton CppApi instance."""
14
+ global _api_instance
15
+ if _api_instance is None:
16
+ _api_instance = CppApi()
17
+ return _api_instance
18
+
19
+ def __getattr__(name: str):
20
+ """Enable: from includecpp import fast_list
21
+
22
+ This hook is called when Python cannot find an attribute in this module.
23
+ It allows dynamic C++ module loading via the import system.
24
+ """
25
+ if name.startswith('_'):
26
+ raise AttributeError(f"module 'includecpp' has no attribute '{name}'")
27
+
28
+ if name in _loaded_modules:
29
+ return _loaded_modules[name]
30
+
31
+ api = _get_api()
32
+
33
+ if name not in api.registry:
34
+ available = list(api.registry.keys())
35
+ raise AttributeError(
36
+ f"Module '{name}' not found. "
37
+ f"Available: {available}. "
38
+ f"Run 'includecpp rebuild' first."
39
+ )
40
+
41
+ if api.need_update(name):
42
+ warnings.warn(
43
+ f"Module '{name}' source files changed. "
44
+ f"Run 'includecpp rebuild' to update.",
45
+ UserWarning
46
+ )
47
+
48
+ module = api.include(name)
49
+ _loaded_modules[name] = module
50
+ return module
51
+
52
+ def __dir__():
53
+ """List available attributes including C++ modules."""
54
+ base = ['CppApi', 'CSSL', '__version__']
55
+ try:
56
+ api = _get_api()
57
+ return sorted(set(base + list(api.registry.keys())))
58
+ except Exception:
59
+ return base
@@ -19,9 +19,9 @@ from .cssl_parser import (
19
19
  from .cssl_runtime import CSSLRuntime, CSSLRuntimeError, CSSLServiceRunner, run_cssl, run_cssl_file
20
20
  from .cssl_types import (
21
21
  DataStruct, Shuffled, Iterator, Combo, DataSpace, OpenQuote,
22
- OpenFind,
22
+ OpenFind, Parameter,
23
23
  create_datastruct, create_shuffled, create_iterator,
24
- create_combo, create_dataspace, create_openquote
24
+ create_combo, create_dataspace, create_openquote, create_parameter
25
25
  )
26
26
 
27
27
  __all__ = [
@@ -34,7 +34,7 @@ __all__ = [
34
34
  'run_cssl', 'run_cssl_file',
35
35
  # Data Types
36
36
  'DataStruct', 'Shuffled', 'Iterator', 'Combo', 'DataSpace', 'OpenQuote',
37
- 'OpenFind',
37
+ 'OpenFind', 'Parameter',
38
38
  'create_datastruct', 'create_shuffled', 'create_iterator',
39
- 'create_combo', 'create_dataspace', 'create_openquote'
39
+ 'create_combo', 'create_dataspace', 'create_openquote', 'create_parameter'
40
40
  ]
@@ -216,6 +216,9 @@ class CSSLLexer:
216
216
  self.column = 1
217
217
  elif char in '"\'':
218
218
  self._read_string(char)
219
+ elif char == '`':
220
+ # Raw string (no escape processing) - useful for JSON
221
+ self._read_raw_string()
219
222
  elif char.isdigit() or (char == '-' and self._peek(1).isdigit()):
220
223
  self._read_number()
221
224
  elif char == 'r' and self._peek(1) == '@':
@@ -224,7 +227,7 @@ class CSSLLexer:
224
227
  elif char == 's' and self._peek(1) == '@':
225
228
  # s@<name> self-reference to global struct
226
229
  self._read_self_ref()
227
- elif char.isalpha() or char == '_' or char == '-':
230
+ elif char.isalpha() or char == '_':
228
231
  self._read_identifier()
229
232
  elif char == '@':
230
233
  self._add_token(TokenType.AT, '@')
@@ -346,13 +349,52 @@ class CSSLLexer:
346
349
  def _read_string(self, quote_char: str):
347
350
  self._advance()
348
351
  start = self.pos
352
+ result = []
349
353
  while self.pos < len(self.source) and self.source[self.pos] != quote_char:
350
- if self.source[self.pos] == '\\':
354
+ if self.source[self.pos] == '\\' and self.pos + 1 < len(self.source):
355
+ # Handle escape sequences
356
+ next_char = self.source[self.pos + 1]
357
+ if next_char == 'n':
358
+ result.append('\n')
359
+ elif next_char == 't':
360
+ result.append('\t')
361
+ elif next_char == 'r':
362
+ result.append('\r')
363
+ elif next_char == '\\':
364
+ result.append('\\')
365
+ elif next_char == quote_char:
366
+ result.append(quote_char)
367
+ elif next_char == '"':
368
+ result.append('"')
369
+ elif next_char == "'":
370
+ result.append("'")
371
+ else:
372
+ result.append(self.source[self.pos])
373
+ result.append(next_char)
374
+ self._advance()
375
+ self._advance()
376
+ else:
377
+ result.append(self.source[self.pos])
351
378
  self._advance()
379
+ value = ''.join(result)
380
+ self._add_token(TokenType.STRING, value)
381
+ self._advance()
382
+
383
+ def _read_raw_string(self):
384
+ """Read raw string with backticks - no escape processing.
385
+
386
+ Useful for JSON: `{"id": "2819e1", "name": "test"}`
387
+ """
388
+ self._advance() # Skip opening backtick
389
+ start = self.pos
390
+ while self.pos < len(self.source) and self.source[self.pos] != '`':
391
+ if self.source[self.pos] == '\n':
392
+ self.line += 1
393
+ self.column = 0
352
394
  self._advance()
353
395
  value = self.source[start:self.pos]
354
396
  self._add_token(TokenType.STRING, value)
355
- self._advance()
397
+ self._advance() # Skip closing backtick
356
398
 
357
399
  def _read_number(self):
358
400
  start = self.pos
@@ -368,7 +410,7 @@ class CSSLLexer:
368
410
 
369
411
  def _read_identifier(self):
370
412
  start = self.pos
371
- while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] in '_-'):
413
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
372
414
  self._advance()
373
415
  value = self.source[start:self.pos]
374
416
 
@@ -588,6 +630,176 @@ class CSSLParser:
588
630
  self._match(TokenType.BLOCK_END)
589
631
  return root
590
632
 
633
+ def _is_function_modifier(self, value: str) -> bool:
634
+ """Check if a keyword is a function modifier"""
635
+ return value in ('undefined', 'open', 'meta', 'super', 'closed', 'private', 'virtual', 'sqlbased')
636
+
637
+ def _is_type_keyword(self, value: str) -> bool:
638
+ """Check if a keyword is a type declaration"""
639
+ return value in ('int', 'string', 'float', 'bool', 'void', 'json', 'array', 'vector', 'stack',
640
+ 'dynamic', 'datastruct', 'dataspace', 'shuffled', 'iterator', 'combo', 'structure')
641
+
642
+ def _looks_like_function_declaration(self) -> bool:
643
+ """Check if current position looks like a C-style function declaration.
644
+
645
+ Patterns:
646
+ - int funcName(...)
647
+ - undefined int funcName(...)
648
+ - vector<string> funcName(...)
649
+ - undefined void funcName(...)
650
+ """
651
+ saved_pos = self.pos
652
+
653
+ # Skip modifiers (undefined, open, meta, super, closed, private, virtual)
654
+ while self._check(TokenType.KEYWORD) and self._is_function_modifier(self._current().value):
655
+ self._advance()
656
+
657
+ # Check for type keyword
658
+ if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
659
+ self._advance()
660
+
661
+ # Skip generic type parameters <T>
662
+ if self._check(TokenType.COMPARE_LT):
663
+ depth = 1
664
+ self._advance()
665
+ while depth > 0 and not self._is_at_end():
666
+ if self._check(TokenType.COMPARE_LT):
667
+ depth += 1
668
+ elif self._check(TokenType.COMPARE_GT):
669
+ depth -= 1
670
+ self._advance()
671
+
672
+ # Check for identifier followed by (
673
+ if self._check(TokenType.IDENTIFIER):
674
+ self._advance()
675
+ is_func = self._check(TokenType.PAREN_START)
676
+ self.pos = saved_pos
677
+ return is_func
678
+
679
+ self.pos = saved_pos
680
+ return False
681
+
682
+ def _parse_typed_function(self) -> ASTNode:
683
+ """Parse C-style typed function declaration.
684
+
685
+ Patterns:
686
+ - int Add(int a, int b) { }
687
+ - undefined int Func() { }
688
+ - open void Handler(open Params) { }
689
+ - vector<string> GetNames() { }
690
+ """
691
+ modifiers = []
692
+ return_type = None
693
+ generic_type = None
694
+
695
+ # Collect modifiers (undefined, open, meta, super, closed, private, virtual)
696
+ while self._check(TokenType.KEYWORD) and self._is_function_modifier(self._current().value):
697
+ modifiers.append(self._advance().value)
698
+
699
+ # Get return type
700
+ if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
701
+ return_type = self._advance().value
702
+
703
+ # Check for generic type <T>
704
+ if self._check(TokenType.COMPARE_LT):
705
+ self._advance() # skip <
706
+ generic_parts = []
707
+ depth = 1
708
+ while depth > 0 and not self._is_at_end():
709
+ if self._check(TokenType.COMPARE_LT):
710
+ depth += 1
711
+ generic_parts.append('<')
712
+ elif self._check(TokenType.COMPARE_GT):
713
+ depth -= 1
714
+ if depth > 0:
715
+ generic_parts.append('>')
716
+ elif self._check(TokenType.COMMA):
717
+ generic_parts.append(',')
718
+ else:
719
+ generic_parts.append(self._current().value)
720
+ self._advance()
721
+ generic_type = ''.join(generic_parts)
722
+
723
+ # Get function name
724
+ name = self._advance().value
725
+
726
+ # Parse parameters
727
+ params = []
728
+ self._expect(TokenType.PAREN_START)
729
+
730
+ while not self._check(TokenType.PAREN_END) and not self._is_at_end():
731
+ param_info = {}
732
+
733
+ # Handle 'open' keyword for open parameters
734
+ if self._match_keyword('open'):
735
+ param_info['open'] = True
736
+
737
+ # Handle type annotations
738
+ if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
739
+ param_info['type'] = self._advance().value
740
+
741
+ # Check for generic type parameter <T>
742
+ if self._check(TokenType.COMPARE_LT):
743
+ self._advance()
744
+ generic_parts = []
745
+ depth = 1
746
+ while depth > 0 and not self._is_at_end():
747
+ if self._check(TokenType.COMPARE_LT):
748
+ depth += 1
749
+ generic_parts.append('<')
750
+ elif self._check(TokenType.COMPARE_GT):
751
+ depth -= 1
752
+ if depth > 0:
753
+ generic_parts.append('>')
754
+ elif self._check(TokenType.COMMA):
755
+ generic_parts.append(',')
756
+ else:
757
+ generic_parts.append(self._current().value)
758
+ self._advance()
759
+ param_info['generic'] = ''.join(generic_parts)
760
+
761
+ # Handle reference operator &
762
+ if self._match(TokenType.AMPERSAND):
763
+ param_info['ref'] = True
764
+
765
+ # Get parameter name
766
+ if self._check(TokenType.IDENTIFIER):
767
+ param_name = self._advance().value
768
+ if param_info:
769
+ params.append({'name': param_name, **param_info})
770
+ else:
771
+ params.append(param_name)
772
+ elif self._check(TokenType.KEYWORD):
773
+ # Parameter name could be a keyword
774
+ param_name = self._advance().value
775
+ if param_info:
776
+ params.append({'name': param_name, **param_info})
777
+ else:
778
+ params.append(param_name)
779
+
780
+ self._match(TokenType.COMMA)
781
+
782
+ self._expect(TokenType.PAREN_END)
783
+
784
+ # Parse function body
785
+ node = ASTNode('function', value={
786
+ 'name': name,
787
+ 'params': params,
788
+ 'return_type': return_type,
789
+ 'generic_type': generic_type,
790
+ 'modifiers': modifiers
791
+ }, children=[])
792
+
793
+ self._expect(TokenType.BLOCK_START)
794
+
795
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
796
+ stmt = self._parse_statement()
797
+ if stmt:
798
+ node.children.append(stmt)
799
+
800
+ self._expect(TokenType.BLOCK_END)
801
+ return node
802
+
591
803
  def parse_program(self) -> ASTNode:
592
804
  """Parse a standalone program (no service wrapper)"""
593
805
  root = ASTNode('program', children=[])
@@ -597,6 +809,30 @@ class CSSLParser:
597
809
  root.children.append(self._parse_struct())
598
810
  elif self._match_keyword('define'):
599
811
  root.children.append(self._parse_define())
812
+ # Check for C-style typed function declarations
813
+ elif self._looks_like_function_declaration():
814
+ root.children.append(self._parse_typed_function())
815
+ # Handle service blocks
816
+ elif self._match_keyword('service-init'):
817
+ root.children.append(self._parse_service_init())
818
+ elif self._match_keyword('service-include'):
819
+ root.children.append(self._parse_service_include())
820
+ elif self._match_keyword('service-run'):
821
+ root.children.append(self._parse_service_run())
822
+ elif self._match_keyword('package'):
823
+ root.children.append(self._parse_package())
824
+ elif self._match_keyword('package-includes'):
825
+ root.children.append(self._parse_package_includes())
826
+ # Handle global declarations
827
+ elif self._match_keyword('global'):
828
+ stmt = self._parse_expression_statement()
829
+ if stmt:
830
+ root.children.append(stmt)
831
+ elif self._check(TokenType.GLOBAL_REF):
832
+ stmt = self._parse_expression_statement()
833
+ if stmt:
834
+ root.children.append(stmt)
835
+ # Handle statements
600
836
  elif self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or self._check(TokenType.SELF_REF):
601
837
  stmt = self._parse_expression_statement()
602
838
  if stmt:
@@ -609,6 +845,9 @@ class CSSLParser:
609
845
  root.children.append(self._parse_for())
610
846
  elif self._match_keyword('foreach'):
611
847
  root.children.append(self._parse_foreach())
848
+ # Skip comments and newlines
849
+ elif self._check(TokenType.COMMENT) or self._check(TokenType.NEWLINE):
850
+ self._advance()
612
851
  else:
613
852
  self._advance()
614
853
 
@@ -837,8 +1076,31 @@ class CSSLParser:
837
1076
 
838
1077
  if self._match(TokenType.PAREN_START):
839
1078
  while not self._check(TokenType.PAREN_END):
1079
+ param_info = {}
1080
+ # Handle 'open' keyword for open parameters
1081
+ if self._match_keyword('open'):
1082
+ param_info['open'] = True
1083
+ # Handle type annotations (e.g., string, int, dynamic, etc.)
1084
+ if self._check(TokenType.KEYWORD):
1085
+ param_info['type'] = self._advance().value
1086
+ # Handle reference operator &
1087
+ if self._match(TokenType.AMPERSAND):
1088
+ param_info['ref'] = True
1089
+ # Get parameter name
840
1090
  if self._check(TokenType.IDENTIFIER):
841
- params.append(self._advance().value)
1091
+ param_name = self._advance().value
1092
+ if param_info:
1093
+ params.append({'name': param_name, **param_info})
1094
+ else:
1095
+ params.append(param_name)
1096
+ self._match(TokenType.COMMA)
1097
+ elif self._check(TokenType.KEYWORD):
1098
+ # Parameter name could be a keyword like 'Params'
1099
+ param_name = self._advance().value
1100
+ if param_info:
1101
+ params.append({'name': param_name, **param_info})
1102
+ else:
1103
+ params.append(param_name)
842
1104
  self._match(TokenType.COMMA)
843
1105
  else:
844
1106
  break
@@ -878,6 +1140,12 @@ class CSSLParser:
878
1140
  return self._parse_try()
879
1141
  elif self._match_keyword('await'):
880
1142
  return self._parse_await()
1143
+ elif self._match_keyword('define'):
1144
+ # Nested define function
1145
+ return self._parse_define()
1146
+ elif self._looks_like_function_declaration():
1147
+ # Nested typed function (e.g., void Level2() { ... })
1148
+ return self._parse_typed_function()
881
1149
  elif self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT):
882
1150
  return self._parse_expression_statement()
883
1151
  else:
@@ -1077,6 +1345,9 @@ class CSSLParser:
1077
1345
  # Check for define statements inside action block
1078
1346
  if self._match_keyword('define'):
1079
1347
  node.children.append(self._parse_define())
1348
+ # Check for typed function definitions (nested functions)
1349
+ elif self._looks_like_function_declaration():
1350
+ node.children.append(self._parse_typed_function())
1080
1351
  else:
1081
1352
  stmt = self._parse_statement()
1082
1353
  if stmt:
@@ -1320,12 +1591,39 @@ class CSSLParser:
1320
1591
  if self._match(TokenType.MINUS):
1321
1592
  operand = self._parse_unary()
1322
1593
  return ASTNode('unary', value={'op': '-', 'operand': operand})
1594
+ if self._match(TokenType.AMPERSAND):
1595
+ # Reference operator: &variable or &@module
1596
+ operand = self._parse_unary()
1597
+ return ASTNode('reference', value=operand)
1323
1598
 
1324
1599
  return self._parse_primary()
1325
1600
 
1326
1601
  def _parse_primary(self) -> ASTNode:
1327
1602
  if self._match(TokenType.AT):
1328
- return self._parse_module_reference()
1603
+ node = self._parse_module_reference()
1604
+ # Continue to check for calls, indexing, member access on module refs
1605
+ while True:
1606
+ if self._match(TokenType.PAREN_START):
1607
+ # Function call on module ref: @Module.method()
1608
+ args = []
1609
+ while not self._check(TokenType.PAREN_END) and not self._is_at_end():
1610
+ args.append(self._parse_expression())
1611
+ if not self._check(TokenType.PAREN_END):
1612
+ self._expect(TokenType.COMMA)
1613
+ self._expect(TokenType.PAREN_END)
1614
+ node = ASTNode('call', value={'callee': node, 'args': args})
1615
+ elif self._match(TokenType.DOT):
1616
+ # Member access: @Module.property
1617
+ member = self._advance().value
1618
+ node = ASTNode('member_access', value={'object': node, 'member': member})
1619
+ elif self._match(TokenType.BRACKET_START):
1620
+ # Index access: @Module[index]
1621
+ index = self._parse_expression()
1622
+ self._expect(TokenType.BRACKET_END)
1623
+ node = ASTNode('index_access', value={'object': node, 'index': index})
1624
+ else:
1625
+ break
1626
+ return node
1329
1627
 
1330
1628
  if self._check(TokenType.SELF_REF):
1331
1629
  # s@<name> self-reference to global struct
@@ -12,6 +12,7 @@ from .cssl_parser import ASTNode, parse_cssl, parse_cssl_program, CSSLSyntaxErro
12
12
  from .cssl_events import CSSLEventManager, EventType, EventData, get_event_manager
13
13
  from .cssl_builtins import CSSLBuiltins
14
14
  from .cssl_modules import get_module_registry, get_standard_module
15
+ from .cssl_types import Parameter, DataStruct, Shuffled, Iterator, Combo
15
16
 
16
17
 
17
18
  class CSSLRuntimeError(Exception):
@@ -567,16 +568,22 @@ class CSSLRuntime:
567
568
  """Call a function node with arguments"""
568
569
  func_info = func_node.value
569
570
  params = func_info.get('params', [])
571
+ modifiers = func_info.get('modifiers', [])
572
+
573
+ # Check for undefined modifier - suppress errors if present
574
+ is_undefined = 'undefined' in modifiers
570
575
 
571
576
  # Create new scope
572
577
  new_scope = Scope(parent=self.scope)
573
578
 
574
- # Bind parameters
579
+ # Bind parameters - handle both string and dict formats
575
580
  for i, param in enumerate(params):
581
+ # Extract param name from dict format: {'name': 'a', 'type': 'int'}
582
+ param_name = param['name'] if isinstance(param, dict) else param
576
583
  if i < len(args):
577
- new_scope.set(param, args[i])
584
+ new_scope.set(param_name, args[i])
578
585
  else:
579
- new_scope.set(param, None)
586
+ new_scope.set(param_name, None)
580
587
 
581
588
  # Execute body
582
589
  old_scope = self.scope
@@ -587,6 +594,11 @@ class CSSLRuntime:
587
594
  self._execute_node(child)
588
595
  except CSSLReturn as ret:
589
596
  return ret.value
597
+ except Exception as e:
598
+ # If undefined modifier, suppress all errors
599
+ if is_undefined:
600
+ return None
601
+ raise
590
602
  finally:
591
603
  self.scope = old_scope
592
604
 
@@ -881,7 +893,11 @@ class CSSLRuntime:
881
893
  # Get current target value for add/move modes
882
894
  current_value = None
883
895
  if mode in ('add', 'move'):
884
- current_value = self._evaluate(target)
896
+ try:
897
+ current_value = self._evaluate(target)
898
+ except Exception:
899
+ # Target might not exist yet, that's okay for add mode
900
+ current_value = None
885
901
 
886
902
  # Determine final value based on mode
887
903
  if mode == 'replace':
@@ -1188,6 +1204,17 @@ class CSSLRuntime:
1188
1204
  if node.type == 'object':
1189
1205
  return {k: self._evaluate(v) for k, v in node.value.items()}
1190
1206
 
1207
+ if node.type == 'reference':
1208
+ # &variable - return a reference object wrapping the actual value
1209
+ inner = node.value
1210
+ if isinstance(inner, ASTNode):
1211
+ if inner.type == 'identifier':
1212
+ # Return a reference wrapper with the variable name
1213
+ return {'__ref__': True, 'name': inner.value, 'value': self.scope.get(inner.value)}
1214
+ elif inner.type == 'module_ref':
1215
+ return {'__ref__': True, 'name': inner.value, 'value': self.get_module(inner.value)}
1216
+ return {'__ref__': True, 'value': self._evaluate(inner)}
1217
+
1191
1218
  return None
1192
1219
 
1193
1220
  def _eval_binary(self, node: ASTNode) -> Any:
@@ -1388,15 +1415,26 @@ class CSSLRuntime:
1388
1415
 
1389
1416
  # NEW: Execute injected code for a function
1390
1417
  def _execute_function_injections(self, func_name: str):
1391
- """Execute all injected code blocks for a function - NEW"""
1418
+ """Execute all injected code blocks for a function - NEW
1419
+
1420
+ Includes protection against recursive execution to prevent doubled output.
1421
+ """
1422
+ # Prevent recursive injection execution (fixes doubled output bug)
1423
+ if getattr(self, '_injection_executing', False):
1424
+ return
1425
+
1392
1426
  if func_name in self._function_injections:
1393
- for code_block in self._function_injections[func_name]:
1394
- if isinstance(code_block, ASTNode):
1395
- if code_block.type == 'action_block':
1396
- for child in code_block.children:
1397
- self._execute_node(child)
1398
- else:
1399
- self._execute_node(code_block)
1427
+ self._injection_executing = True
1428
+ try:
1429
+ for code_block in self._function_injections[func_name]:
1430
+ if isinstance(code_block, ASTNode):
1431
+ if code_block.type == 'action_block':
1432
+ for child in code_block.children:
1433
+ self._execute_node(child)
1434
+ else:
1435
+ self._execute_node(code_block)
1436
+ finally:
1437
+ self._injection_executing = False
1400
1438
 
1401
1439
  # Output functions for builtins
1402
1440
  def set_output_callback(self, callback: Callable[[str, str], None]):