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.
@@ -162,6 +162,10 @@ class CSSLBuiltins:
162
162
  self._functions['abspath'] = self.builtin_abspath
163
163
  self._functions['normpath'] = self.builtin_normpath
164
164
  # File I/O functions
165
+ self._functions['read'] = self.builtin_read
166
+ self._functions['readline'] = self.builtin_readline
167
+ self._functions['write'] = self.builtin_write
168
+ self._functions['writeline'] = self.builtin_writeline
165
169
  self._functions['readfile'] = self.builtin_readfile
166
170
  self._functions['writefile'] = self.builtin_writefile
167
171
  self._functions['appendfile'] = self.builtin_appendfile
@@ -177,6 +181,18 @@ class CSSLBuiltins:
177
181
  # JSON functions
178
182
  self._functions['tojson'] = self.builtin_tojson
179
183
  self._functions['fromjson'] = self.builtin_fromjson
184
+ # JSON namespace functions (json::read, json::write, etc.)
185
+ self._functions['json::read'] = self.builtin_json_read
186
+ self._functions['json::write'] = self.builtin_json_write
187
+ self._functions['json::parse'] = self.builtin_fromjson
188
+ self._functions['json::stringify'] = self.builtin_tojson
189
+ self._functions['json::pretty'] = self.builtin_json_pretty
190
+ self._functions['json::keys'] = self.builtin_json_keys
191
+ self._functions['json::values'] = self.builtin_json_values
192
+ self._functions['json::get'] = self.builtin_json_get
193
+ self._functions['json::set'] = self.builtin_json_set
194
+ self._functions['json::has'] = self.builtin_json_has
195
+ self._functions['json::merge'] = self.builtin_json_merge
180
196
 
181
197
  # Regex functions
182
198
  self._functions['match'] = self.builtin_match
@@ -801,6 +817,54 @@ class CSSLBuiltins:
801
817
 
802
818
  # ============= File I/O Functions =============
803
819
 
820
+ def builtin_read(self, path: str, encoding: str = 'utf-8') -> str:
821
+ """Read entire file content.
822
+ Usage: read('/path/to/file.txt')
823
+ """
824
+ with open(path, 'r', encoding=encoding) as f:
825
+ return f.read()
826
+
827
+ def builtin_readline(self, line: int, path: str, encoding: str = 'utf-8') -> str:
828
+ """Read specific line from file (1-indexed).
829
+ Usage: readline(5, '/path/to/file.txt') -> returns line 5
830
+ """
831
+ with open(path, 'r', encoding=encoding) as f:
832
+ for i, file_line in enumerate(f, 1):
833
+ if i == line:
834
+ return file_line.rstrip('\n\r')
835
+ return "" # Line not found
836
+
837
+ def builtin_write(self, path: str, content: str, encoding: str = 'utf-8') -> int:
838
+ """Write content to file, returns chars written.
839
+ Usage: write('/path/to/file.txt', 'Hello World')
840
+ """
841
+ with open(path, 'w', encoding=encoding) as f:
842
+ return f.write(content)
843
+
844
+ def builtin_writeline(self, line: int, content: str, path: str, encoding: str = 'utf-8') -> bool:
845
+ """Write/replace specific line in file (1-indexed).
846
+ Usage: writeline(5, 'New content', '/path/to/file.txt')
847
+ """
848
+ # Read all lines
849
+ lines = []
850
+ if os.path.exists(path):
851
+ with open(path, 'r', encoding=encoding) as f:
852
+ lines = f.readlines()
853
+
854
+ # Ensure we have enough lines
855
+ while len(lines) < line:
856
+ lines.append('\n')
857
+
858
+ # Replace the specific line (1-indexed)
859
+ if not content.endswith('\n'):
860
+ content = content + '\n'
861
+ lines[line - 1] = content
862
+
863
+ # Write back
864
+ with open(path, 'w', encoding=encoding) as f:
865
+ f.writelines(lines)
866
+ return True
867
+
804
868
  def builtin_readfile(self, path: str, encoding: str = 'utf-8') -> str:
805
869
  """Read entire file content"""
806
870
  with open(path, 'r', encoding=encoding) as f:
@@ -862,6 +926,106 @@ class CSSLBuiltins:
862
926
  def builtin_fromjson(self, s: str) -> Any:
863
927
  return json.loads(s)
864
928
 
929
+ # JSON namespace functions (json::read, json::write, etc.)
930
+ def builtin_json_read(self, path: str, encoding: str = 'utf-8') -> Any:
931
+ """Read and parse JSON file.
932
+ Usage: json::read('/path/to/file.json')
933
+ """
934
+ with open(path, 'r', encoding=encoding) as f:
935
+ return json.load(f)
936
+
937
+ def builtin_json_write(self, path: str, data: Any, indent: int = 2, encoding: str = 'utf-8') -> bool:
938
+ """Write data to JSON file.
939
+ Usage: json::write('/path/to/file.json', data)
940
+ """
941
+ with open(path, 'w', encoding=encoding) as f:
942
+ json.dump(data, f, indent=indent, ensure_ascii=False)
943
+ return True
944
+
945
+ def builtin_json_pretty(self, value: Any, indent: int = 2) -> str:
946
+ """Pretty print JSON.
947
+ Usage: json::pretty(data)
948
+ """
949
+ return json.dumps(value, indent=indent, ensure_ascii=False)
950
+
951
+ def builtin_json_keys(self, data: Any) -> list:
952
+ """Get all keys from JSON object.
953
+ Usage: json::keys(data)
954
+ """
955
+ if isinstance(data, dict):
956
+ return list(data.keys())
957
+ return []
958
+
959
+ def builtin_json_values(self, data: Any) -> list:
960
+ """Get all values from JSON object.
961
+ Usage: json::values(data)
962
+ """
963
+ if isinstance(data, dict):
964
+ return list(data.values())
965
+ return []
966
+
967
+ def builtin_json_get(self, data: Any, path: str, default: Any = None) -> Any:
968
+ """Get value by dot-path from JSON.
969
+ Usage: json::get(data, 'user.name')
970
+ """
971
+ if not isinstance(data, dict):
972
+ return default
973
+ keys = path.split('.')
974
+ current = data
975
+ for key in keys:
976
+ if isinstance(current, dict) and key in current:
977
+ current = current[key]
978
+ elif isinstance(current, list):
979
+ try:
980
+ current = current[int(key)]
981
+ except (ValueError, IndexError):
982
+ return default
983
+ else:
984
+ return default
985
+ return current
986
+
987
+ def builtin_json_set(self, data: Any, path: str, value: Any) -> Any:
988
+ """Set value by dot-path in JSON object.
989
+ Usage: json::set(data, 'user.name', 'John')
990
+ """
991
+ if not isinstance(data, dict):
992
+ return data
993
+ data = dict(data) # Copy
994
+ keys = path.split('.')
995
+ current = data
996
+ for i, key in enumerate(keys[:-1]):
997
+ if key not in current or not isinstance(current[key], dict):
998
+ current[key] = {}
999
+ current = current[key]
1000
+ current[keys[-1]] = value
1001
+ return data
1002
+
1003
+ def builtin_json_has(self, data: Any, path: str) -> bool:
1004
+ """Check if path exists in JSON.
1005
+ Usage: json::has(data, 'user.name')
1006
+ """
1007
+ result = self.builtin_json_get(data, path, _MISSING := object())
1008
+ return result is not _MISSING
1009
+
1010
+ def builtin_json_merge(self, *dicts) -> dict:
1011
+ """Deep merge multiple JSON objects.
1012
+ Usage: json::merge(obj1, obj2, obj3)
1013
+ """
1014
+ def deep_merge(base, update):
1015
+ result = dict(base)
1016
+ for key, value in update.items():
1017
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
1018
+ result[key] = deep_merge(result[key], value)
1019
+ else:
1020
+ result[key] = value
1021
+ return result
1022
+
1023
+ result = {}
1024
+ for d in dicts:
1025
+ if isinstance(d, dict):
1026
+ result = deep_merge(result, d)
1027
+ return result
1028
+
865
1029
  # ============= Regex Functions =============
866
1030
 
867
1031
  def builtin_match(self, pattern: str, string: str) -> Optional[dict]:
@@ -913,6 +1077,29 @@ class CSSLBuiltins:
913
1077
  else:
914
1078
  raise SystemExit(code)
915
1079
 
1080
+ def builtin_original(self, func_name: str, *args) -> Any:
1081
+ """Call the original version of a replaced function.
1082
+
1083
+ Usage:
1084
+ exit <<== { printl("custom exit"); }
1085
+ original("exit"); // Calls the ORIGINAL exit, not the replacement
1086
+
1087
+ // In an injection that was defined BEFORE replacement:
1088
+ old_exit <<== { original("exit"); } // Calls original exit
1089
+ """
1090
+ if self.runtime and hasattr(self.runtime, '_original_functions'):
1091
+ original_func = self.runtime._original_functions.get(func_name)
1092
+ if original_func is not None:
1093
+ if callable(original_func):
1094
+ return original_func(*args)
1095
+ elif isinstance(original_func, type(lambda: None).__class__.__bases__[0]): # Check if bound method
1096
+ return original_func(*args)
1097
+ # Fallback: try to call builtin directly
1098
+ builtin_method = getattr(self, f'builtin_{func_name}', None)
1099
+ if builtin_method:
1100
+ return builtin_method(*args)
1101
+ raise CSSLBuiltinError(f"No original function '{func_name}' found")
1102
+
916
1103
  def builtin_env(self, name: str, default: str = None) -> Optional[str]:
917
1104
  return os.environ.get(name, default)
918
1105
 
@@ -1902,15 +2089,54 @@ class CSSLBuiltins:
1902
2089
  from .cssl_types import OpenQuote
1903
2090
  return OpenQuote(db_ref)
1904
2091
 
1905
- def builtin_openfind(self, combo_or_type: Any, index: int = 0) -> Any:
2092
+ def builtin_openfind(self, combo_or_type: Any, index: int = 0, params: list = None) -> Any:
1906
2093
  """Find open parameter by type or combo space.
1907
2094
 
1908
- Usage: OpenFind<string>(0) or OpenFind(&@comboSpace)
2095
+ Usage:
2096
+ OpenFind<string>(0) # Find first string at position 0
2097
+ OpenFind(&@comboSpace) # Find using combo filter
2098
+
2099
+ When using with open parameters:
2100
+ open define myFunc(open Params) {
2101
+ string name = OpenFind<string>(0); // Find nearest string at index 0
2102
+ int age = OpenFind<int>(1); // Find nearest int at index 1
2103
+ }
1909
2104
  """
1910
2105
  from .cssl_types import Combo
1911
2106
 
1912
2107
  if isinstance(combo_or_type, Combo):
2108
+ # Find by combo space
2109
+ if params:
2110
+ return combo_or_type.find_match(params)
1913
2111
  return combo_or_type.find_match([])
2112
+
2113
+ # Type-based search
2114
+ target_type = combo_or_type
2115
+ if params is None:
2116
+ params = []
2117
+
2118
+ # Map type names to Python types
2119
+ type_map = {
2120
+ 'string': str, 'str': str,
2121
+ 'int': int, 'integer': int,
2122
+ 'float': float, 'double': float,
2123
+ 'bool': bool, 'boolean': bool,
2124
+ 'list': list, 'array': list,
2125
+ 'dict': dict, 'dictionary': dict
2126
+ }
2127
+
2128
+ python_type = type_map.get(str(target_type).lower(), None)
2129
+ if python_type is None:
2130
+ return None
2131
+
2132
+ # Find the nearest matching type from index position
2133
+ matches_found = 0
2134
+ for i, param in enumerate(params):
2135
+ if isinstance(param, python_type):
2136
+ if matches_found == index:
2137
+ return param
2138
+ matches_found += 1
2139
+
1914
2140
  return None
1915
2141
 
1916
2142
 
@@ -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()
@@ -125,6 +126,7 @@ KEYWORDS = {
125
126
  'package', 'package-includes', 'exec', 'as', 'global',
126
127
  # CSSL Type Keywords
127
128
  'int', 'string', 'float', 'bool', 'void', 'json', 'array', 'vector', 'stack',
129
+ 'list', 'dictionary', 'dict', # Python-like types
128
130
  'dynamic', # No type declaration (slow but flexible)
129
131
  'undefined', # Function errors ignored
130
132
  'open', # Accept any parameter type
@@ -152,7 +154,7 @@ TYPE_LITERALS = {'list', 'dict'}
152
154
  # Generic type keywords that use <T> syntax
153
155
  TYPE_GENERICS = {
154
156
  'datastruct', 'dataspace', 'shuffled', 'iterator', 'combo',
155
- 'vector', 'stack', 'array', 'openquote'
157
+ 'vector', 'stack', 'array', 'openquote', 'list', 'dictionary'
156
158
  }
157
159
 
158
160
  # Functions that accept type parameters: FuncName<type>(args)
@@ -241,6 +243,9 @@ class CSSLLexer:
241
243
  elif char == '$':
242
244
  # $<name> shared object reference
243
245
  self._read_shared_ref()
246
+ elif char == '%':
247
+ # %<name> captured reference (for infusion)
248
+ self._read_captured_ref()
244
249
  elif char == '&':
245
250
  # & for references
246
251
  if self._peek(1) == '&':
@@ -493,6 +498,20 @@ class CSSLLexer:
493
498
  self.error("Expected identifier after '$'")
494
499
  self._add_token(TokenType.SHARED_REF, value)
495
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
+
496
515
  def _read_less_than(self):
497
516
  # Check for <<== (code infusion left)
498
517
  if self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
@@ -540,10 +559,10 @@ class CSSLLexer:
540
559
  elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '+':
541
560
  self._add_token(TokenType.INJECT_PLUS_RIGHT, '==>+')
542
561
  for _ in range(4): self._advance()
543
- # Check for ===>- (injection right minus - moves & removes)
544
- elif self._peek(1) == '=' and self._peek(2) == '=' and self._peek(3) == '>' and self._peek(4) == '-':
545
- self._add_token(TokenType.INJECT_MINUS_RIGHT, '===>')
546
- 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()
547
566
  # Check for ==> (basic injection right)
548
567
  elif self._peek(1) == '=' and self._peek(2) == '>':
549
568
  self._add_token(TokenType.INJECT_RIGHT, '==>')
@@ -660,6 +679,7 @@ class CSSLParser:
660
679
  def _is_type_keyword(self, value: str) -> bool:
661
680
  """Check if a keyword is a type declaration"""
662
681
  return value in ('int', 'string', 'float', 'bool', 'void', 'json', 'array', 'vector', 'stack',
682
+ 'list', 'dictionary', 'dict',
663
683
  'dynamic', 'datastruct', 'dataspace', 'shuffled', 'iterator', 'combo', 'structure')
664
684
 
665
685
  def _looks_like_function_declaration(self) -> bool:
@@ -877,7 +897,8 @@ class CSSLParser:
877
897
  # Skip known type keywords
878
898
  type_keywords = {'int', 'string', 'float', 'bool', 'dynamic', 'void',
879
899
  'stack', 'vector', 'datastruct', 'dataspace', 'shuffled',
880
- 'iterator', 'combo', 'array', 'openquote', 'json'}
900
+ 'iterator', 'combo', 'array', 'openquote', 'json',
901
+ 'list', 'dictionary', 'dict'}
881
902
  if type_name not in type_keywords:
882
903
  return False
883
904
 
@@ -1291,7 +1312,9 @@ class CSSLParser:
1291
1312
  elif self._looks_like_function_declaration():
1292
1313
  # Nested typed function (e.g., void Level2() { ... })
1293
1314
  return self._parse_typed_function()
1294
- 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)):
1295
1318
  return self._parse_expression_statement()
1296
1319
  else:
1297
1320
  self._advance()
@@ -1505,7 +1528,13 @@ class CSSLParser:
1505
1528
  return ASTNode('c_for_update', value={'var': var_name, 'op': 'none'})
1506
1529
 
1507
1530
  def _parse_python_style_for(self) -> ASTNode:
1508
- """Parse Python-style for loop: for (i in range(start, end)) { }"""
1531
+ """Parse Python-style for loop: for (i in range(...)) { }
1532
+
1533
+ Supports:
1534
+ for (i in range(n)) { } - 0 to n-1
1535
+ for (i in range(start, end)) { } - start to end-1
1536
+ for (i in range(start, end, step)) { }
1537
+ """
1509
1538
  var_name = self._advance().value
1510
1539
  self._expect(TokenType.KEYWORD) # 'in'
1511
1540
 
@@ -1518,15 +1547,27 @@ class CSSLParser:
1518
1547
  self.error(f"Expected 'range', got {self._peek().value}")
1519
1548
 
1520
1549
  self._expect(TokenType.PAREN_START)
1521
- start = self._parse_expression()
1522
- self._expect(TokenType.COMMA)
1523
- end = self._parse_expression()
1550
+ first_arg = self._parse_expression()
1524
1551
 
1525
- # Optional step parameter: range(start, end, step)
1552
+ # Check if there are more arguments
1553
+ start = None
1554
+ end = None
1526
1555
  step = None
1556
+
1527
1557
  if self._check(TokenType.COMMA):
1558
+ # range(start, end) or range(start, end, step)
1528
1559
  self._advance() # consume comma
1529
- step = self._parse_expression()
1560
+ start = first_arg
1561
+ end = self._parse_expression()
1562
+
1563
+ # Optional step parameter
1564
+ if self._check(TokenType.COMMA):
1565
+ self._advance() # consume comma
1566
+ step = self._parse_expression()
1567
+ else:
1568
+ # range(n) - single argument means 0 to n-1
1569
+ start = ASTNode('literal', value={'type': 'int', 'value': 0})
1570
+ end = first_arg
1530
1571
 
1531
1572
  self._expect(TokenType.PAREN_END)
1532
1573
  self._expect(TokenType.PAREN_END)
@@ -1734,12 +1775,26 @@ class CSSLParser:
1734
1775
  self._match(TokenType.SEMICOLON)
1735
1776
  return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'add', 'filter': filter_info})
1736
1777
 
1737
- # === MINUS INJECTION: -<== (move & remove from source) ===
1778
+ # === MINUS INJECTION: -<== or -<==[n] (move & remove from source) ===
1738
1779
  if self._match(TokenType.INJECT_MINUS_LEFT):
1780
+ # Check for indexed deletion: -<==[n] (only numbers, not filters)
1781
+ remove_index = None
1782
+ if self._check(TokenType.BRACKET_START):
1783
+ # Peek ahead to see if this is an index [n] or a filter [type::helper=...]
1784
+ # Only consume if it's a simple number index
1785
+ saved_pos = self._current
1786
+ self._advance() # consume [
1787
+ if self._check(TokenType.NUMBER):
1788
+ remove_index = int(self._advance().value)
1789
+ self._expect(TokenType.BRACKET_END)
1790
+ else:
1791
+ # Not a number - restore position for filter parsing
1792
+ self._current = saved_pos
1793
+
1739
1794
  filter_info = self._parse_injection_filter()
1740
1795
  source = self._parse_expression()
1741
1796
  self._match(TokenType.SEMICOLON)
1742
- return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'move', 'filter': filter_info})
1797
+ return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'move', 'filter': filter_info, 'index': remove_index})
1743
1798
 
1744
1799
  # === CODE INFUSION: <<== (inject code into function) ===
1745
1800
  if self._match(TokenType.INFUSE_LEFT):
@@ -1763,16 +1818,29 @@ class CSSLParser:
1763
1818
  self._match(TokenType.SEMICOLON)
1764
1819
  return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'add'})
1765
1820
 
1766
- # === CODE INFUSION MINUS: -<<== (remove code from function) ===
1821
+ # === CODE INFUSION MINUS: -<<== or -<<==[n] (remove code from function) ===
1767
1822
  if self._match(TokenType.INFUSE_MINUS_LEFT):
1823
+ # Check for indexed deletion: -<<==[n] (only numbers)
1824
+ remove_index = None
1825
+ if self._check(TokenType.BRACKET_START):
1826
+ # Peek ahead to see if this is an index [n] or something else
1827
+ saved_pos = self._current
1828
+ self._advance() # consume [
1829
+ if self._check(TokenType.NUMBER):
1830
+ remove_index = int(self._advance().value)
1831
+ self._expect(TokenType.BRACKET_END)
1832
+ else:
1833
+ # Not a number - restore position
1834
+ self._current = saved_pos
1835
+
1768
1836
  if self._check(TokenType.BLOCK_START):
1769
1837
  code_block = self._parse_action_block()
1770
1838
  self._match(TokenType.SEMICOLON)
1771
- return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'remove'})
1839
+ return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'remove', 'index': remove_index})
1772
1840
  else:
1773
1841
  source = self._parse_expression()
1774
1842
  self._match(TokenType.SEMICOLON)
1775
- return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'remove'})
1843
+ return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'remove', 'index': remove_index})
1776
1844
 
1777
1845
  # === RIGHT-SIDE OPERATORS ===
1778
1846
 
@@ -2010,6 +2078,31 @@ class CSSLParser:
2010
2078
  break
2011
2079
  return node
2012
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
+
2013
2106
  if self._check(TokenType.NUMBER):
2014
2107
  return ASTNode('literal', value=self._advance().value)
2015
2108
 
@@ -2034,7 +2127,13 @@ class CSSLParser:
2034
2127
  return expr
2035
2128
 
2036
2129
  if self._match(TokenType.BLOCK_START):
2037
- 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()
2038
2137
 
2039
2138
  if self._match(TokenType.BRACKET_START):
2040
2139
  return self._parse_array()
@@ -2079,9 +2178,48 @@ class CSSLParser:
2079
2178
 
2080
2179
  return node
2081
2180
 
2181
+ def _parse_call_arguments(self) -> tuple:
2182
+ """Parse function call arguments, supporting both positional and named (key=value).
2183
+
2184
+ Returns: (args, kwargs) where:
2185
+ args = list of positional argument expressions
2186
+ kwargs = dict of {name: expression} for named arguments
2187
+ """
2188
+ args = []
2189
+ kwargs = {}
2190
+
2191
+ while not self._check(TokenType.PAREN_END) and not self._is_at_end():
2192
+ # Check for named argument: identifier = expression
2193
+ if self._check(TokenType.IDENTIFIER):
2194
+ saved_pos = self._current
2195
+ name_token = self._advance()
2196
+
2197
+ if self._check(TokenType.EQUALS):
2198
+ # Named argument: name=value
2199
+ self._advance() # consume =
2200
+ value = self._parse_expression()
2201
+ kwargs[name_token.value] = value
2202
+ else:
2203
+ # Not named, restore and parse as expression
2204
+ self._current = saved_pos
2205
+ args.append(self._parse_expression())
2206
+ else:
2207
+ args.append(self._parse_expression())
2208
+
2209
+ if not self._check(TokenType.PAREN_END):
2210
+ self._expect(TokenType.COMMA)
2211
+
2212
+ return args, kwargs
2213
+
2082
2214
  def _parse_identifier_or_call(self) -> ASTNode:
2083
2215
  name = self._advance().value
2084
2216
 
2217
+ # Check for namespace syntax: json::read, string::cut, etc.
2218
+ if self._match(TokenType.DOUBLE_COLON):
2219
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
2220
+ namespace_member = self._advance().value
2221
+ name = f"{name}::{namespace_member}"
2222
+
2085
2223
  # Check for type generic instantiation: stack<string>, vector<int>, etc.
2086
2224
  # This creates a new instance of the type with the specified element type
2087
2225
  if name in TYPE_GENERICS and self._check(TokenType.COMPARE_LT):
@@ -2127,13 +2265,9 @@ class CSSLParser:
2127
2265
  member = self._advance().value
2128
2266
  node = ASTNode('member_access', value={'object': node, 'member': member})
2129
2267
  elif self._match(TokenType.PAREN_START):
2130
- args = []
2131
- while not self._check(TokenType.PAREN_END):
2132
- args.append(self._parse_expression())
2133
- if not self._check(TokenType.PAREN_END):
2134
- self._expect(TokenType.COMMA)
2268
+ args, kwargs = self._parse_call_arguments()
2135
2269
  self._expect(TokenType.PAREN_END)
2136
- node = ASTNode('call', value={'callee': node, 'args': args})
2270
+ node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
2137
2271
  elif self._match(TokenType.BRACKET_START):
2138
2272
  index = self._parse_expression()
2139
2273
  self._expect(TokenType.BRACKET_END)
@@ -2143,6 +2277,61 @@ class CSSLParser:
2143
2277
 
2144
2278
  return node
2145
2279
 
2280
+ def _is_object_literal(self) -> bool:
2281
+ """Check if current position is an object literal { key = value } vs action block { expr; }
2282
+
2283
+ Object literal: { name = value; } or { "key" = value; }
2284
+ Action block: { %version; } or { "1.0.0" } or { call(); }
2285
+ """
2286
+ # Empty block is action block
2287
+ if self._check(TokenType.BLOCK_END):
2288
+ return False
2289
+
2290
+ # Save position for lookahead
2291
+ saved_pos = self._current
2292
+
2293
+ # Check if it looks like key = value pattern
2294
+ is_object = False
2295
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.STRING):
2296
+ self._advance() # skip key
2297
+ if self._check(TokenType.EQUALS):
2298
+ # Looks like object literal: { key = ...
2299
+ is_object = True
2300
+
2301
+ # Restore position
2302
+ self._current = saved_pos
2303
+ return is_object
2304
+
2305
+ def _parse_action_block_expression(self) -> ASTNode:
2306
+ """Parse an action block expression: { expr; expr2; } returns last value
2307
+
2308
+ Used for: v <== { %version; } or v <== { "1.0.0" }
2309
+ """
2310
+ children = []
2311
+
2312
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
2313
+ # Parse statement or expression
2314
+ if (self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or
2315
+ self._check(TokenType.CAPTURED_REF) or self._check(TokenType.SHARED_REF) or
2316
+ self._check(TokenType.GLOBAL_REF) or self._check(TokenType.SELF_REF) or
2317
+ self._check(TokenType.STRING) or self._check(TokenType.NUMBER) or
2318
+ self._check(TokenType.BOOLEAN) or self._check(TokenType.NULL) or
2319
+ self._check(TokenType.PAREN_START)):
2320
+ # Parse as expression and wrap in expression node for _execute_node
2321
+ expr = self._parse_expression()
2322
+ self._match(TokenType.SEMICOLON)
2323
+ children.append(ASTNode('expression', value=expr))
2324
+ elif self._check(TokenType.KEYWORD):
2325
+ # Parse as statement
2326
+ stmt = self._parse_statement()
2327
+ if stmt:
2328
+ children.append(stmt)
2329
+ else:
2330
+ self._advance()
2331
+
2332
+ self._expect(TokenType.BLOCK_END)
2333
+ return ASTNode('action_block', children=children)
2334
+
2146
2335
  def _parse_object(self) -> ASTNode:
2147
2336
  properties = {}
2148
2337