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.
- includecpp/__init__.py +1 -1
- includecpp/core/cssl/CSSL_DOCUMENTATION.md +351 -20
- includecpp/core/cssl/cssl_builtins.py +228 -2
- includecpp/core/cssl/cssl_parser.py +214 -25
- includecpp/core/cssl/cssl_runtime.py +365 -38
- includecpp/core/cssl/cssl_types.py +339 -2
- includecpp/core/cssl_bridge.py +100 -4
- includecpp/core/cssl_bridge.pyi +177 -0
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/METADATA +1 -1
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/RECORD +14 -14
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/WHEEL +0 -0
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/entry_points.txt +0 -0
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/licenses/LICENSE +0 -0
- {includecpp-3.5.0.dist-info → includecpp-3.6.0.dist-info}/top_level.txt +0 -0
|
@@ -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:
|
|
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
|
|
544
|
-
elif self._peek(1) == '=' and self._peek(2) == '
|
|
545
|
-
self._add_token(TokenType.INJECT_MINUS_RIGHT, '
|
|
546
|
-
for _ in range(
|
|
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(
|
|
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
|
-
|
|
1522
|
-
self._expect(TokenType.COMMA)
|
|
1523
|
-
end = self._parse_expression()
|
|
1550
|
+
first_arg = self._parse_expression()
|
|
1524
1551
|
|
|
1525
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|