xvcl 2.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.
- xvcl/__init__.py +15 -0
- xvcl/compiler.py +1541 -0
- xvcl/py.typed +0 -0
- xvcl-2.6.0.dist-info/METADATA +1579 -0
- xvcl-2.6.0.dist-info/RECORD +8 -0
- xvcl-2.6.0.dist-info/WHEEL +4 -0
- xvcl-2.6.0.dist-info/entry_points.txt +2 -0
- xvcl-2.6.0.dist-info/licenses/LICENSE +21 -0
xvcl/compiler.py
ADDED
|
@@ -0,0 +1,1541 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
xvcl - Extended VCL compiler with metaprogramming features
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- #inline directive for zero-overhead text substitution macros
|
|
7
|
+
- Automatic parenthesization to prevent operator precedence issues
|
|
8
|
+
- Macros work in any expression context (unlike functions)
|
|
9
|
+
- #include directive for code reuse across files
|
|
10
|
+
- #const directive for named constants
|
|
11
|
+
- Better error messages with line numbers and context
|
|
12
|
+
- --debug mode for tracing expansion
|
|
13
|
+
- Source maps (optional)
|
|
14
|
+
- For loops, conditionals, template expressions
|
|
15
|
+
- Functions with single/tuple returns
|
|
16
|
+
- #let directive for variable declaration
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from difflib import get_close_matches
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ANSI color codes for terminal output
|
|
29
|
+
class Colors:
|
|
30
|
+
RED = "\033[91m"
|
|
31
|
+
YELLOW = "\033[93m"
|
|
32
|
+
GREEN = "\033[92m"
|
|
33
|
+
BLUE = "\033[94m"
|
|
34
|
+
CYAN = "\033[96m"
|
|
35
|
+
BOLD = "\033[1m"
|
|
36
|
+
RESET = "\033[0m"
|
|
37
|
+
GRAY = "\033[90m"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SourceLocation:
|
|
42
|
+
"""Tracks source location for error reporting."""
|
|
43
|
+
|
|
44
|
+
file: str
|
|
45
|
+
line: int
|
|
46
|
+
|
|
47
|
+
def __str__(self):
|
|
48
|
+
return f"{self.file}:{self.line}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PreprocessorError(Exception):
|
|
52
|
+
"""Base exception for preprocessor errors."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
message: str,
|
|
57
|
+
location: Optional[SourceLocation] = None,
|
|
58
|
+
context_lines: Optional[list[tuple[int, str]]] = None,
|
|
59
|
+
):
|
|
60
|
+
self.message = message
|
|
61
|
+
self.location = location
|
|
62
|
+
self.context_lines = context_lines
|
|
63
|
+
super().__init__(message)
|
|
64
|
+
|
|
65
|
+
def format_error(self, use_colors: bool = True) -> str:
|
|
66
|
+
"""Format error with context for display."""
|
|
67
|
+
parts = []
|
|
68
|
+
|
|
69
|
+
# Error header
|
|
70
|
+
if use_colors:
|
|
71
|
+
parts.append(f"{Colors.RED}{Colors.BOLD}Error{Colors.RESET}")
|
|
72
|
+
else:
|
|
73
|
+
parts.append("Error")
|
|
74
|
+
|
|
75
|
+
if self.location:
|
|
76
|
+
parts.append(f" at {self.location}:")
|
|
77
|
+
else:
|
|
78
|
+
parts.append(":")
|
|
79
|
+
|
|
80
|
+
parts.append(f"\n {self.message}\n")
|
|
81
|
+
|
|
82
|
+
# Context lines
|
|
83
|
+
if self.context_lines and self.location:
|
|
84
|
+
parts.append("\n Context:\n")
|
|
85
|
+
for line_num, line_text in self.context_lines:
|
|
86
|
+
prefix = " → " if line_num == self.location.line else " "
|
|
87
|
+
if use_colors and line_num == self.location.line:
|
|
88
|
+
parts.append(f"{Colors.BOLD}{prefix}{line_num}: {line_text}{Colors.RESET}\n")
|
|
89
|
+
else:
|
|
90
|
+
parts.append(f"{prefix}{line_num}: {line_text}\n")
|
|
91
|
+
|
|
92
|
+
return "".join(parts)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Macro:
|
|
96
|
+
"""Represents an inline macro definition."""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self, name: str, params: list[str], body: str, location: Optional[SourceLocation] = None
|
|
100
|
+
):
|
|
101
|
+
self.name = name
|
|
102
|
+
self.params = params # [param_name, ...]
|
|
103
|
+
self.body = body # Single expression (string)
|
|
104
|
+
self.location = location
|
|
105
|
+
|
|
106
|
+
def expand(self, args: list[str]) -> str:
|
|
107
|
+
"""Expand macro by substituting parameters with arguments."""
|
|
108
|
+
if len(args) != len(self.params):
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Macro {self.name} expects {len(self.params)} arguments, got {len(args)}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Start with body
|
|
114
|
+
result = self.body
|
|
115
|
+
|
|
116
|
+
# Replace each parameter with its argument
|
|
117
|
+
# Use word boundaries to avoid partial replacements
|
|
118
|
+
for param, arg in zip(self.params, args):
|
|
119
|
+
# Only wrap argument in parentheses if it contains operators
|
|
120
|
+
# This avoids creating grouped expressions for simple values
|
|
121
|
+
if any(op in arg for op in ["+", "-", "*", "/", "%", "==", "!=", "<", ">", "&&", "||"]):
|
|
122
|
+
wrapped_arg = f"({arg})"
|
|
123
|
+
else:
|
|
124
|
+
wrapped_arg = arg
|
|
125
|
+
result = re.sub(rf"\b{re.escape(param)}\b", wrapped_arg, result)
|
|
126
|
+
|
|
127
|
+
# Don't wrap entire expression - let VCL handle operator precedence naturally
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Function:
|
|
132
|
+
"""Represents a VCL function definition."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
name: str,
|
|
137
|
+
params: list[tuple[str, str]],
|
|
138
|
+
return_type,
|
|
139
|
+
body: list[str],
|
|
140
|
+
location: Optional[SourceLocation] = None,
|
|
141
|
+
):
|
|
142
|
+
self.name = name
|
|
143
|
+
self.params = params # [(param_name, param_type), ...]
|
|
144
|
+
self.return_type = return_type # str for single return, List[str] for tuple
|
|
145
|
+
self.body = body
|
|
146
|
+
self.location = location
|
|
147
|
+
|
|
148
|
+
def get_param_global(self, param_name: str) -> str:
|
|
149
|
+
"""Get the global header name for a parameter."""
|
|
150
|
+
return f"req.http.X-Func-{self.name}-{param_name}"
|
|
151
|
+
|
|
152
|
+
def get_return_global(self, index: Optional[int] = None) -> str:
|
|
153
|
+
"""Get the global header name for the return value."""
|
|
154
|
+
if isinstance(self.return_type, list):
|
|
155
|
+
# Tuple return - use indexed global
|
|
156
|
+
if index is None:
|
|
157
|
+
raise ValueError(f"Function {self.name} returns tuple, index required")
|
|
158
|
+
return f"req.http.X-Func-{self.name}-Return{index}"
|
|
159
|
+
else:
|
|
160
|
+
# Single return
|
|
161
|
+
return f"req.http.X-Func-{self.name}-Return"
|
|
162
|
+
|
|
163
|
+
def is_tuple_return(self) -> bool:
|
|
164
|
+
"""Check if function returns a tuple."""
|
|
165
|
+
return isinstance(self.return_type, list)
|
|
166
|
+
|
|
167
|
+
def get_return_types(self) -> list[str]:
|
|
168
|
+
"""Get return types as a list (single type becomes 1-element list)."""
|
|
169
|
+
if isinstance(self.return_type, list):
|
|
170
|
+
return self.return_type
|
|
171
|
+
else:
|
|
172
|
+
return [self.return_type]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class XVCLCompiler:
|
|
176
|
+
"""Extended VCL compiler with loops, conditionals, templates, functions, includes, and constants."""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
include_paths: Optional[list[str]] = None,
|
|
181
|
+
debug: bool = False,
|
|
182
|
+
source_maps: bool = False,
|
|
183
|
+
):
|
|
184
|
+
self.include_paths = include_paths or ["."]
|
|
185
|
+
self.debug = debug
|
|
186
|
+
self.source_maps = source_maps
|
|
187
|
+
|
|
188
|
+
# State
|
|
189
|
+
self.variables: dict[str, Any] = {}
|
|
190
|
+
self.constants: dict[str, Any] = {} # Constants defined with #const
|
|
191
|
+
self.macros: dict[str, Macro] = {} # NEW in v2.4: Inline macros
|
|
192
|
+
self.functions: dict[str, Function] = {}
|
|
193
|
+
self.output: list[str] = []
|
|
194
|
+
|
|
195
|
+
# Include tracking
|
|
196
|
+
self.included_files: set[str] = set() # Absolute paths of included files
|
|
197
|
+
self.include_stack: list[str] = [] # Stack for cycle detection
|
|
198
|
+
|
|
199
|
+
# Current source location for error reporting
|
|
200
|
+
self.current_file: str = ""
|
|
201
|
+
self.current_line: int = 0
|
|
202
|
+
self.current_lines: list[str] = [] # All lines for context
|
|
203
|
+
|
|
204
|
+
def log_debug(self, message: str, indent: int = 0):
|
|
205
|
+
"""Log debug message if debug mode is enabled."""
|
|
206
|
+
if self.debug:
|
|
207
|
+
prefix = " " * indent
|
|
208
|
+
print(f"{Colors.GRAY}[DEBUG]{Colors.RESET} {prefix}{message}")
|
|
209
|
+
|
|
210
|
+
def get_context_lines(self, line_num: int, context: int = 3) -> list[tuple[int, str]]:
|
|
211
|
+
"""Get context lines around the given line number."""
|
|
212
|
+
if not self.current_lines:
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
start = max(0, line_num - context - 1)
|
|
216
|
+
end = min(len(self.current_lines), line_num + context)
|
|
217
|
+
|
|
218
|
+
result = []
|
|
219
|
+
for i in range(start, end):
|
|
220
|
+
result.append((i + 1, self.current_lines[i].rstrip()))
|
|
221
|
+
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
def make_error(self, message: str, line_num: Optional[int] = None) -> PreprocessorError:
|
|
225
|
+
"""Create a PreprocessorError with context."""
|
|
226
|
+
loc = SourceLocation(self.current_file, line_num or self.current_line)
|
|
227
|
+
context = self.get_context_lines(line_num or self.current_line)
|
|
228
|
+
return PreprocessorError(message, loc, context)
|
|
229
|
+
|
|
230
|
+
def process_file(self, input_path: str, output_path: str) -> None:
|
|
231
|
+
"""Process a VCL template file and write the result."""
|
|
232
|
+
self.log_debug(f"Processing file: {input_path}")
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
with open(input_path) as f:
|
|
236
|
+
template = f.read()
|
|
237
|
+
except FileNotFoundError:
|
|
238
|
+
raise FileNotFoundError(f"XVCL file not found: {input_path}")
|
|
239
|
+
except Exception as e:
|
|
240
|
+
raise Exception(f"Error reading file {input_path}: {e}")
|
|
241
|
+
|
|
242
|
+
self.current_file = input_path
|
|
243
|
+
result = self.process(template, input_path)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
with open(output_path, "w") as f:
|
|
247
|
+
f.write(result)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
raise Exception(f"Error writing output file {output_path}: {e}")
|
|
250
|
+
|
|
251
|
+
# Summary
|
|
252
|
+
print(f"{Colors.GREEN}✓{Colors.RESET} Compiled {input_path} -> {output_path}")
|
|
253
|
+
if self.constants:
|
|
254
|
+
print(f" Constants: {len(self.constants)}")
|
|
255
|
+
if self.macros:
|
|
256
|
+
print(f" Macros: {len(self.macros)} ({', '.join(self.macros.keys())})")
|
|
257
|
+
if self.functions:
|
|
258
|
+
print(f" Functions: {len(self.functions)} ({', '.join(self.functions.keys())})")
|
|
259
|
+
if len(self.included_files) > 1: # More than just the main file
|
|
260
|
+
print(f" Included files: {len(self.included_files) - 1}")
|
|
261
|
+
|
|
262
|
+
def process(self, template: str, filename: str = "<string>") -> str:
|
|
263
|
+
"""Process a template string and return the result."""
|
|
264
|
+
self.log_debug(f"Starting processing of {filename}")
|
|
265
|
+
|
|
266
|
+
self.output = []
|
|
267
|
+
self.current_file = filename
|
|
268
|
+
self.current_lines = template.split("\n")
|
|
269
|
+
|
|
270
|
+
# Add to included files (using absolute path)
|
|
271
|
+
abs_path = os.path.abspath(filename) if os.path.exists(filename) else filename
|
|
272
|
+
self.included_files.add(abs_path)
|
|
273
|
+
|
|
274
|
+
lines = self.current_lines
|
|
275
|
+
|
|
276
|
+
# First pass: extract constants
|
|
277
|
+
self.log_debug("Pass 1: Extracting constants")
|
|
278
|
+
lines = self._extract_constants(lines)
|
|
279
|
+
|
|
280
|
+
# Second pass: process includes
|
|
281
|
+
self.log_debug("Pass 2: Processing includes")
|
|
282
|
+
lines = self._process_includes(lines, filename)
|
|
283
|
+
|
|
284
|
+
# Third pass: extract inline macros (NEW in v2.4)
|
|
285
|
+
self.log_debug("Pass 3: Extracting inline macros")
|
|
286
|
+
lines = self._extract_macros(lines)
|
|
287
|
+
|
|
288
|
+
# Fourth pass: extract function definitions
|
|
289
|
+
self.log_debug("Pass 4: Extracting functions")
|
|
290
|
+
lines = self._extract_functions(lines)
|
|
291
|
+
|
|
292
|
+
# NEW: Fourth-and-a-half pass: join multi-line function calls
|
|
293
|
+
self.log_debug("Pass 4.5: Joining multi-line function calls")
|
|
294
|
+
lines = self._join_multiline_function_calls(lines)
|
|
295
|
+
|
|
296
|
+
# Fifth pass: process loops/conditionals and replace function calls/macros
|
|
297
|
+
self.log_debug("Pass 5: Processing directives and generating code")
|
|
298
|
+
self._process_lines(lines, 0, len(lines), {})
|
|
299
|
+
|
|
300
|
+
# Sixth pass: append function subroutine implementations
|
|
301
|
+
self.log_debug("Pass 6: Generating function subroutines")
|
|
302
|
+
self._generate_function_subroutines()
|
|
303
|
+
|
|
304
|
+
self.log_debug(f"Processing complete: {len(self.output)} lines generated")
|
|
305
|
+
return "\n".join(self.output)
|
|
306
|
+
|
|
307
|
+
def _extract_constants(self, lines: list[str]) -> list[str]:
|
|
308
|
+
"""
|
|
309
|
+
Extract #const declarations and store them.
|
|
310
|
+
Returns lines with const declarations removed.
|
|
311
|
+
"""
|
|
312
|
+
result = []
|
|
313
|
+
i = 0
|
|
314
|
+
while i < len(lines):
|
|
315
|
+
line = lines[i]
|
|
316
|
+
stripped = line.strip()
|
|
317
|
+
self.current_line = i + 1
|
|
318
|
+
|
|
319
|
+
if stripped.startswith("#const "):
|
|
320
|
+
# Parse: #const NAME TYPE = value
|
|
321
|
+
match = re.match(r"#const\s+(\w+)\s+(\w+)\s*=\s*(.+)", stripped)
|
|
322
|
+
if not match:
|
|
323
|
+
# Try without type: #const NAME = value (infer type)
|
|
324
|
+
match = re.match(r"#const\s+(\w+)\s*=\s*(.+)", stripped)
|
|
325
|
+
if not match:
|
|
326
|
+
raise self.make_error(f"Invalid #const syntax: {stripped}")
|
|
327
|
+
|
|
328
|
+
name = match.group(1)
|
|
329
|
+
const_type = None # Infer from value
|
|
330
|
+
value_expr = match.group(2)
|
|
331
|
+
else:
|
|
332
|
+
name = match.group(1)
|
|
333
|
+
const_type = match.group(2)
|
|
334
|
+
value_expr = match.group(3)
|
|
335
|
+
|
|
336
|
+
# Evaluate the expression
|
|
337
|
+
try:
|
|
338
|
+
value = self._evaluate_expression(value_expr, {})
|
|
339
|
+
except Exception as e:
|
|
340
|
+
raise self.make_error(f"Error evaluating constant '{name}': {e}")
|
|
341
|
+
|
|
342
|
+
# Type checking if type was specified
|
|
343
|
+
if const_type:
|
|
344
|
+
expected_type = self._python_type_from_vcl(const_type)
|
|
345
|
+
if not isinstance(value, expected_type):
|
|
346
|
+
raise self.make_error(
|
|
347
|
+
f"Constant '{name}' type mismatch: expected {const_type}, "
|
|
348
|
+
f"got {type(value).__name__}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
self.constants[name] = value
|
|
352
|
+
self.log_debug(f"Defined constant: {name} = {value}")
|
|
353
|
+
i += 1
|
|
354
|
+
else:
|
|
355
|
+
result.append(line)
|
|
356
|
+
i += 1
|
|
357
|
+
|
|
358
|
+
return result
|
|
359
|
+
|
|
360
|
+
def _python_type_from_vcl(self, vcl_type: str) -> type:
|
|
361
|
+
"""Convert VCL type name to Python type for validation."""
|
|
362
|
+
type_map = {
|
|
363
|
+
"INTEGER": int,
|
|
364
|
+
"STRING": str,
|
|
365
|
+
"FLOAT": float,
|
|
366
|
+
"BOOL": bool,
|
|
367
|
+
}
|
|
368
|
+
return type_map.get(vcl_type, object)
|
|
369
|
+
|
|
370
|
+
def _process_includes(self, lines: list[str], current_file: str) -> list[str]:
|
|
371
|
+
"""
|
|
372
|
+
Process #include directives and insert included file contents.
|
|
373
|
+
Returns lines with includes expanded.
|
|
374
|
+
"""
|
|
375
|
+
result = []
|
|
376
|
+
i = 0
|
|
377
|
+
|
|
378
|
+
while i < len(lines):
|
|
379
|
+
line = lines[i]
|
|
380
|
+
stripped = line.strip()
|
|
381
|
+
self.current_line = i + 1
|
|
382
|
+
|
|
383
|
+
if stripped.startswith("#include "):
|
|
384
|
+
# Parse: #include "path/to/file.xvcl"
|
|
385
|
+
match = re.match(r'#include\s+["\'](.+?)["\']', stripped)
|
|
386
|
+
if not match:
|
|
387
|
+
# Try without quotes: #include <stdlib/file.xvcl>
|
|
388
|
+
match = re.match(r"#include\s+<(.+?)>", stripped)
|
|
389
|
+
|
|
390
|
+
if not match:
|
|
391
|
+
raise self.make_error(f"Invalid #include syntax: {stripped}")
|
|
392
|
+
|
|
393
|
+
include_path = match.group(1)
|
|
394
|
+
|
|
395
|
+
# Resolve include path
|
|
396
|
+
resolved_path = self._resolve_include_path(include_path, current_file)
|
|
397
|
+
|
|
398
|
+
if not resolved_path:
|
|
399
|
+
raise self.make_error(f"Cannot find included file: {include_path}")
|
|
400
|
+
|
|
401
|
+
abs_path = os.path.abspath(resolved_path)
|
|
402
|
+
|
|
403
|
+
# Check for cycles
|
|
404
|
+
if abs_path in self.include_stack:
|
|
405
|
+
cycle = " -> ".join(self.include_stack + [abs_path])
|
|
406
|
+
raise self.make_error(f"Circular include detected: {cycle}")
|
|
407
|
+
|
|
408
|
+
# Check if already included (include-once semantics)
|
|
409
|
+
if abs_path in self.included_files:
|
|
410
|
+
self.log_debug(f"Skipping already included file: {resolved_path}")
|
|
411
|
+
i += 1
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
self.log_debug(f"Including file: {resolved_path}")
|
|
415
|
+
|
|
416
|
+
# Read and process included file
|
|
417
|
+
try:
|
|
418
|
+
with open(resolved_path) as f:
|
|
419
|
+
included_content = f.read()
|
|
420
|
+
except Exception as e:
|
|
421
|
+
raise self.make_error(f"Error reading included file {resolved_path}: {e}")
|
|
422
|
+
|
|
423
|
+
# Add to included files and stack
|
|
424
|
+
self.included_files.add(abs_path)
|
|
425
|
+
self.include_stack.append(abs_path)
|
|
426
|
+
|
|
427
|
+
# Save current state
|
|
428
|
+
saved_file = self.current_file
|
|
429
|
+
saved_line = self.current_line
|
|
430
|
+
saved_lines = self.current_lines
|
|
431
|
+
|
|
432
|
+
# Process included file
|
|
433
|
+
self.current_file = resolved_path
|
|
434
|
+
self.current_lines = included_content.split("\n")
|
|
435
|
+
|
|
436
|
+
# Recursively process includes in the included file
|
|
437
|
+
included_lines = self._extract_constants(self.current_lines)
|
|
438
|
+
included_lines = self._process_includes(included_lines, resolved_path)
|
|
439
|
+
|
|
440
|
+
# Restore state
|
|
441
|
+
self.current_file = saved_file
|
|
442
|
+
self.current_line = saved_line
|
|
443
|
+
self.current_lines = saved_lines
|
|
444
|
+
|
|
445
|
+
# Pop from stack
|
|
446
|
+
self.include_stack.pop()
|
|
447
|
+
|
|
448
|
+
# Add comment marker if source maps enabled
|
|
449
|
+
if self.source_maps:
|
|
450
|
+
result.append(f"// BEGIN INCLUDE: {include_path}")
|
|
451
|
+
|
|
452
|
+
# Add included lines to result
|
|
453
|
+
result.extend(included_lines)
|
|
454
|
+
|
|
455
|
+
if self.source_maps:
|
|
456
|
+
result.append(f"// END INCLUDE: {include_path}")
|
|
457
|
+
|
|
458
|
+
i += 1
|
|
459
|
+
else:
|
|
460
|
+
result.append(line)
|
|
461
|
+
i += 1
|
|
462
|
+
|
|
463
|
+
return result
|
|
464
|
+
|
|
465
|
+
def _resolve_include_path(self, include_path: str, current_file: str) -> Optional[str]:
|
|
466
|
+
"""Resolve include path by searching include paths."""
|
|
467
|
+
# Try relative to current file first
|
|
468
|
+
if current_file and current_file != "<string>":
|
|
469
|
+
current_dir = os.path.dirname(os.path.abspath(current_file))
|
|
470
|
+
candidate = os.path.join(current_dir, include_path)
|
|
471
|
+
if os.path.exists(candidate):
|
|
472
|
+
return candidate
|
|
473
|
+
|
|
474
|
+
# Try include paths
|
|
475
|
+
for search_path in self.include_paths:
|
|
476
|
+
candidate = os.path.join(search_path, include_path)
|
|
477
|
+
if os.path.exists(candidate):
|
|
478
|
+
return candidate
|
|
479
|
+
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
def _extract_macros(self, lines: list[str]) -> list[str]:
|
|
483
|
+
"""
|
|
484
|
+
Extract #inline...#endinline blocks and store them as macros.
|
|
485
|
+
Returns lines with macro definitions removed.
|
|
486
|
+
"""
|
|
487
|
+
result = []
|
|
488
|
+
i = 0
|
|
489
|
+
while i < len(lines):
|
|
490
|
+
line = lines[i]
|
|
491
|
+
stripped = line.strip()
|
|
492
|
+
self.current_line = i + 1
|
|
493
|
+
|
|
494
|
+
if stripped.startswith("#inline "):
|
|
495
|
+
# Parse: #inline name(param1, param2, ...)
|
|
496
|
+
match = re.match(r"#inline\s+(\w+)\s*\(([^)]*)\)", stripped)
|
|
497
|
+
if not match:
|
|
498
|
+
raise self.make_error(f"Invalid #inline syntax: {stripped}")
|
|
499
|
+
|
|
500
|
+
name = match.group(1)
|
|
501
|
+
params_str = match.group(2).strip()
|
|
502
|
+
|
|
503
|
+
# Check for duplicate macro names
|
|
504
|
+
if name in self.macros:
|
|
505
|
+
raise self.make_error(
|
|
506
|
+
f"Macro '{name}' already defined at {self.macros[name].location}"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Parse parameters (comma-separated)
|
|
510
|
+
params = []
|
|
511
|
+
if params_str:
|
|
512
|
+
params = [p.strip() for p in params_str.split(",")]
|
|
513
|
+
|
|
514
|
+
# Find matching #endinline
|
|
515
|
+
try:
|
|
516
|
+
endinline_idx = self._find_matching_end(
|
|
517
|
+
lines, i, len(lines), "#inline", "#endinline"
|
|
518
|
+
)
|
|
519
|
+
except SyntaxError as e:
|
|
520
|
+
raise self.make_error(str(e))
|
|
521
|
+
|
|
522
|
+
# Extract macro body (should be a single expression)
|
|
523
|
+
body_lines = lines[i + 1 : endinline_idx]
|
|
524
|
+
# Join all lines and strip whitespace
|
|
525
|
+
body = " ".join(line.strip() for line in body_lines).strip()
|
|
526
|
+
|
|
527
|
+
if not body:
|
|
528
|
+
raise self.make_error(f"Macro '{name}' has empty body")
|
|
529
|
+
|
|
530
|
+
# Store macro
|
|
531
|
+
location = SourceLocation(self.current_file, i + 1)
|
|
532
|
+
self.macros[name] = Macro(name, params, body, location)
|
|
533
|
+
|
|
534
|
+
self.log_debug(f"Defined macro: {name}({', '.join(params)})")
|
|
535
|
+
|
|
536
|
+
# Skip past #endinline
|
|
537
|
+
i = endinline_idx + 1
|
|
538
|
+
else:
|
|
539
|
+
result.append(line)
|
|
540
|
+
i += 1
|
|
541
|
+
|
|
542
|
+
return result
|
|
543
|
+
|
|
544
|
+
def _extract_functions(self, lines: list[str]) -> list[str]:
|
|
545
|
+
"""
|
|
546
|
+
Extract #def...#enddef blocks and store them as functions.
|
|
547
|
+
Returns lines with function definitions removed.
|
|
548
|
+
"""
|
|
549
|
+
result = []
|
|
550
|
+
i = 0
|
|
551
|
+
while i < len(lines):
|
|
552
|
+
line = lines[i]
|
|
553
|
+
stripped = line.strip()
|
|
554
|
+
self.current_line = i + 1
|
|
555
|
+
|
|
556
|
+
if stripped.startswith("#def "):
|
|
557
|
+
# Parse function definition
|
|
558
|
+
func_def = self._parse_function_def(stripped)
|
|
559
|
+
if not func_def:
|
|
560
|
+
raise self.make_error(f"Invalid #def syntax: {stripped}")
|
|
561
|
+
|
|
562
|
+
name, params, return_type = func_def
|
|
563
|
+
|
|
564
|
+
# Check for duplicate function names
|
|
565
|
+
if name in self.functions:
|
|
566
|
+
raise self.make_error(
|
|
567
|
+
f"Function '{name}' already defined at {self.functions[name].location}"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Find matching #enddef
|
|
571
|
+
try:
|
|
572
|
+
enddef_idx = self._find_matching_end(lines, i, len(lines), "#def", "#enddef")
|
|
573
|
+
except SyntaxError as e:
|
|
574
|
+
raise self.make_error(str(e))
|
|
575
|
+
|
|
576
|
+
# Extract function body
|
|
577
|
+
body = lines[i + 1 : enddef_idx]
|
|
578
|
+
|
|
579
|
+
# Store function
|
|
580
|
+
location = SourceLocation(self.current_file, i + 1)
|
|
581
|
+
self.functions[name] = Function(name, params, return_type, body, location)
|
|
582
|
+
|
|
583
|
+
self.log_debug(
|
|
584
|
+
f"Defined function: {name}({', '.join(p[0] for p in params)}) -> {return_type}"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Skip past #enddef
|
|
588
|
+
i = enddef_idx + 1
|
|
589
|
+
else:
|
|
590
|
+
result.append(line)
|
|
591
|
+
i += 1
|
|
592
|
+
|
|
593
|
+
return result
|
|
594
|
+
|
|
595
|
+
def _parse_function_def(self, line: str):
|
|
596
|
+
"""
|
|
597
|
+
Parse function definition line.
|
|
598
|
+
Format: #def name(param1 TYPE, param2 TYPE) -> RETURN_TYPE
|
|
599
|
+
#def name(param1 TYPE, param2 TYPE) -> (TYPE1, TYPE2, ...)
|
|
600
|
+
Returns: (name, [(param, type), ...], return_type)
|
|
601
|
+
return_type is str for single, List[str] for tuple
|
|
602
|
+
"""
|
|
603
|
+
# Try tuple return first: #def name(params) -> (TYPE1, TYPE2, ...)
|
|
604
|
+
tuple_match = re.match(r"#def\s+(\w+)\s*\((.*?)\)\s*->\s*\((.*?)\)", line)
|
|
605
|
+
if tuple_match:
|
|
606
|
+
name = tuple_match.group(1)
|
|
607
|
+
params_str = tuple_match.group(2).strip()
|
|
608
|
+
return_types_str = tuple_match.group(3).strip()
|
|
609
|
+
|
|
610
|
+
# Parse return types
|
|
611
|
+
return_types = [rt.strip() for rt in return_types_str.split(",") if rt.strip()]
|
|
612
|
+
|
|
613
|
+
# Parse parameters
|
|
614
|
+
params = []
|
|
615
|
+
if params_str:
|
|
616
|
+
for param in params_str.split(","):
|
|
617
|
+
param = param.strip()
|
|
618
|
+
if " " in param:
|
|
619
|
+
param_name, param_type = param.rsplit(" ", 1)
|
|
620
|
+
params.append((param_name.strip(), param_type.strip()))
|
|
621
|
+
else:
|
|
622
|
+
params.append((param, "STRING"))
|
|
623
|
+
|
|
624
|
+
return (name, params, return_types) # List for tuple return
|
|
625
|
+
|
|
626
|
+
# Try single return: #def name(params) -> TYPE
|
|
627
|
+
single_match = re.match(r"#def\s+(\w+)\s*\((.*?)\)\s*->\s*(\w+)", line)
|
|
628
|
+
if not single_match:
|
|
629
|
+
return None
|
|
630
|
+
|
|
631
|
+
name = single_match.group(1)
|
|
632
|
+
params_str = single_match.group(2).strip()
|
|
633
|
+
return_type = single_match.group(3)
|
|
634
|
+
|
|
635
|
+
# Parse parameters: "param1 TYPE, param2 TYPE"
|
|
636
|
+
params = []
|
|
637
|
+
if params_str:
|
|
638
|
+
for param in params_str.split(","):
|
|
639
|
+
param = param.strip()
|
|
640
|
+
if " " in param:
|
|
641
|
+
param_name, param_type = param.rsplit(" ", 1)
|
|
642
|
+
params.append((param_name.strip(), param_type.strip()))
|
|
643
|
+
else:
|
|
644
|
+
# Just param name, assume STRING
|
|
645
|
+
params.append((param, "STRING"))
|
|
646
|
+
|
|
647
|
+
return (name, params, return_type) # str for single return
|
|
648
|
+
|
|
649
|
+
def _generate_function_subroutines(self) -> None:
|
|
650
|
+
"""Generate VCL subroutines for all defined functions."""
|
|
651
|
+
if not self.functions:
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
if self.output and self.output[-1].strip():
|
|
655
|
+
self.output.append("")
|
|
656
|
+
self.output.append(
|
|
657
|
+
"// ============================================================================"
|
|
658
|
+
)
|
|
659
|
+
self.output.append("// GENERATED FUNCTION SUBROUTINES")
|
|
660
|
+
self.output.append(
|
|
661
|
+
"// ============================================================================"
|
|
662
|
+
)
|
|
663
|
+
self.output.append("")
|
|
664
|
+
|
|
665
|
+
for func in self.functions.values():
|
|
666
|
+
self._generate_function_subroutine(func)
|
|
667
|
+
|
|
668
|
+
def _generate_function_subroutine(self, func: Function) -> None:
|
|
669
|
+
"""Generate a VCL subroutine for a function."""
|
|
670
|
+
if self.source_maps and func.location:
|
|
671
|
+
self.output.append(f"// Generated from {func.location}")
|
|
672
|
+
|
|
673
|
+
self.output.append(f"// Function: {func.name}")
|
|
674
|
+
self.output.append("//@recv, hash, hit, miss, pass, fetch, error, deliver, log")
|
|
675
|
+
self.output.append(f"sub {func.name} {{")
|
|
676
|
+
self.output.append("")
|
|
677
|
+
|
|
678
|
+
# Declare local variables for parameters
|
|
679
|
+
for param_name, param_type in func.params:
|
|
680
|
+
self.output.append(f" declare local var.{param_name} {param_type};")
|
|
681
|
+
|
|
682
|
+
if func.params:
|
|
683
|
+
self.output.append("")
|
|
684
|
+
|
|
685
|
+
# Read parameters from globals with type conversion
|
|
686
|
+
for param_name, param_type in func.params:
|
|
687
|
+
global_name = func.get_param_global(param_name)
|
|
688
|
+
if param_type == "INTEGER":
|
|
689
|
+
self.output.append(f" set var.{param_name} = std.atoi({global_name});")
|
|
690
|
+
elif param_type == "FLOAT":
|
|
691
|
+
self.output.append(f" set var.{param_name} = std.atof({global_name});")
|
|
692
|
+
elif param_type == "BOOL":
|
|
693
|
+
self.output.append(f' set var.{param_name} = ({global_name} == "true");')
|
|
694
|
+
else:
|
|
695
|
+
# STRING and others
|
|
696
|
+
self.output.append(f" set var.{param_name} = {global_name};")
|
|
697
|
+
|
|
698
|
+
if func.params:
|
|
699
|
+
self.output.append("")
|
|
700
|
+
|
|
701
|
+
# Declare return variable(s)
|
|
702
|
+
return_types = func.get_return_types()
|
|
703
|
+
if func.is_tuple_return():
|
|
704
|
+
# Multiple return values
|
|
705
|
+
for idx, ret_type in enumerate(return_types):
|
|
706
|
+
self.output.append(f" declare local var.return_value{idx} {ret_type};")
|
|
707
|
+
else:
|
|
708
|
+
# Single return value
|
|
709
|
+
self.output.append(f" declare local var.return_value {return_types[0]};")
|
|
710
|
+
self.output.append("")
|
|
711
|
+
|
|
712
|
+
# Process function body
|
|
713
|
+
param_substituted_body = []
|
|
714
|
+
for line in func.body:
|
|
715
|
+
processed_line = line
|
|
716
|
+
for param_name, _ in func.params:
|
|
717
|
+
# Replace standalone parameter references
|
|
718
|
+
processed_line = re.sub(rf"\b{param_name}\b", f"var.{param_name}", processed_line)
|
|
719
|
+
param_substituted_body.append(processed_line)
|
|
720
|
+
|
|
721
|
+
# Save current output and process body
|
|
722
|
+
saved_output = self.output
|
|
723
|
+
self.output = []
|
|
724
|
+
|
|
725
|
+
self._process_lines(param_substituted_body, 0, len(param_substituted_body), {})
|
|
726
|
+
|
|
727
|
+
body_output = self.output
|
|
728
|
+
self.output = saved_output
|
|
729
|
+
|
|
730
|
+
# Post-process the body output to handle return statements
|
|
731
|
+
for line in body_output:
|
|
732
|
+
# Replace "return expr1, expr2;" with multiple assignments
|
|
733
|
+
if re.match(r"\s*return\s+", line):
|
|
734
|
+
if func.is_tuple_return():
|
|
735
|
+
# Parse: return expr1, expr2, expr3;
|
|
736
|
+
return_match = re.search(r"\breturn\s+(.+);", line)
|
|
737
|
+
if return_match:
|
|
738
|
+
exprs_str = return_match.group(1)
|
|
739
|
+
exprs = [e.strip() for e in exprs_str.split(",")]
|
|
740
|
+
|
|
741
|
+
if len(exprs) != len(return_types):
|
|
742
|
+
raise ValueError(
|
|
743
|
+
f"Function {func.name} expects {len(return_types)} return values, got {len(exprs)}"
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
match_indent = re.match(r"(\s*)", line)
|
|
747
|
+
indent = match_indent.group(1) if match_indent else ""
|
|
748
|
+
for idx, expr in enumerate(exprs):
|
|
749
|
+
self.output.append(f"{indent}set var.return_value{idx} = {expr};")
|
|
750
|
+
continue
|
|
751
|
+
else:
|
|
752
|
+
# Single return
|
|
753
|
+
line = re.sub(r"\breturn\s+(.+);", r"set var.return_value = \1;", line)
|
|
754
|
+
|
|
755
|
+
self.output.append(line)
|
|
756
|
+
|
|
757
|
+
# Write return value(s) to global(s)
|
|
758
|
+
self.output.append("")
|
|
759
|
+
if func.is_tuple_return():
|
|
760
|
+
for idx, ret_type in enumerate(return_types):
|
|
761
|
+
return_global = func.get_return_global(idx)
|
|
762
|
+
self._write_return_conversion(return_global, f"var.return_value{idx}", ret_type)
|
|
763
|
+
else:
|
|
764
|
+
return_global = func.get_return_global()
|
|
765
|
+
self._write_return_conversion(return_global, "var.return_value", return_types[0])
|
|
766
|
+
|
|
767
|
+
self.output.append("}")
|
|
768
|
+
self.output.append("")
|
|
769
|
+
|
|
770
|
+
def _write_return_conversion(self, global_var: str, local_var: str, var_type: str) -> None:
|
|
771
|
+
"""Helper to write type conversion for return value."""
|
|
772
|
+
if var_type == "INTEGER":
|
|
773
|
+
self.output.append(f" set {global_var} = std.itoa({local_var});")
|
|
774
|
+
elif var_type == "FLOAT":
|
|
775
|
+
self.output.append(f' set {global_var} = "" + {local_var};')
|
|
776
|
+
elif var_type == "BOOL":
|
|
777
|
+
self.output.append(f" if ({local_var}) {{")
|
|
778
|
+
self.output.append(f' set {global_var} = "true";')
|
|
779
|
+
self.output.append(" } else {")
|
|
780
|
+
self.output.append(f' set {global_var} = "false";')
|
|
781
|
+
self.output.append(" }")
|
|
782
|
+
else:
|
|
783
|
+
# STRING and others
|
|
784
|
+
self.output.append(f" set {global_var} = {local_var};")
|
|
785
|
+
|
|
786
|
+
def _process_lines(
|
|
787
|
+
self, lines: list[str], start: int, end: int, context: dict[str, Any]
|
|
788
|
+
) -> int:
|
|
789
|
+
"""
|
|
790
|
+
Process lines from start to end with given context.
|
|
791
|
+
Returns the index of the last processed line.
|
|
792
|
+
"""
|
|
793
|
+
i = start
|
|
794
|
+
while i < end:
|
|
795
|
+
line = lines[i]
|
|
796
|
+
stripped = line.strip()
|
|
797
|
+
self.current_line = i + 1
|
|
798
|
+
|
|
799
|
+
# Handle #for loops
|
|
800
|
+
if stripped.startswith("#for "):
|
|
801
|
+
self.log_debug(f"Processing #for at line {self.current_line}", indent=1)
|
|
802
|
+
i = self._process_for_loop(lines, i, end, context)
|
|
803
|
+
|
|
804
|
+
# Handle #if conditionals
|
|
805
|
+
elif stripped.startswith("#if "):
|
|
806
|
+
self.log_debug(f"Processing #if at line {self.current_line}", indent=1)
|
|
807
|
+
i = self._process_if(lines, i, end, context)
|
|
808
|
+
|
|
809
|
+
# Handle #let (declare + initialize)
|
|
810
|
+
elif stripped.startswith("#let "):
|
|
811
|
+
self.log_debug(f"Processing #let at line {self.current_line}", indent=1)
|
|
812
|
+
self._process_let(line)
|
|
813
|
+
i += 1
|
|
814
|
+
|
|
815
|
+
# Skip control flow keywords
|
|
816
|
+
elif stripped in ("#else", "#endif", "#endfor", "#enddef", "#endinline"):
|
|
817
|
+
return i
|
|
818
|
+
|
|
819
|
+
# Regular line - process function calls and template substitutions
|
|
820
|
+
else:
|
|
821
|
+
processed_line = self._process_function_calls(line)
|
|
822
|
+
processed_line = self._substitute_expressions(processed_line, context)
|
|
823
|
+
self.output.append(processed_line)
|
|
824
|
+
i += 1
|
|
825
|
+
|
|
826
|
+
return i
|
|
827
|
+
|
|
828
|
+
def _process_let(self, line: str) -> None:
|
|
829
|
+
"""
|
|
830
|
+
Process #let directive (declare + initialize).
|
|
831
|
+
Format: #let name TYPE = expression;
|
|
832
|
+
Generates: declare local var.name TYPE;
|
|
833
|
+
set var.name = expression;
|
|
834
|
+
"""
|
|
835
|
+
# Match: #let name TYPE = expression;
|
|
836
|
+
match = re.match(r"(\s*)#let\s+(\w+)\s+(\w+)\s*=\s*(.+);", line)
|
|
837
|
+
if not match:
|
|
838
|
+
raise self.make_error(f"Invalid #let syntax: {line}")
|
|
839
|
+
|
|
840
|
+
indent = match.group(1)
|
|
841
|
+
var_name = match.group(2)
|
|
842
|
+
var_type = match.group(3)
|
|
843
|
+
expression = match.group(4)
|
|
844
|
+
|
|
845
|
+
self.log_debug(f"Declaring variable: var.{var_name} {var_type} = {expression}", indent=2)
|
|
846
|
+
|
|
847
|
+
# Generate declare statement
|
|
848
|
+
self.output.append(f"{indent}declare local var.{var_name} {var_type};")
|
|
849
|
+
|
|
850
|
+
# Generate set statement and process any function calls in the expression
|
|
851
|
+
set_statement = f"{indent}set var.{var_name} = {expression};"
|
|
852
|
+
processed_set = self._process_function_calls(set_statement)
|
|
853
|
+
|
|
854
|
+
# The processed_set might be multi-line if it contains function calls
|
|
855
|
+
if "\n" in processed_set:
|
|
856
|
+
self.output.extend(processed_set.split("\n"))
|
|
857
|
+
else:
|
|
858
|
+
self.output.append(processed_set)
|
|
859
|
+
|
|
860
|
+
def _count_unquoted_parens(self, text: str) -> int:
|
|
861
|
+
"""
|
|
862
|
+
Count parentheses balance, ignoring those inside string literals.
|
|
863
|
+
Returns positive number for more opens than closes.
|
|
864
|
+
"""
|
|
865
|
+
depth = 0
|
|
866
|
+
in_string = False
|
|
867
|
+
string_char = None
|
|
868
|
+
i = 0
|
|
869
|
+
while i < len(text):
|
|
870
|
+
char = text[i]
|
|
871
|
+
if in_string:
|
|
872
|
+
if char == "\\" and i + 1 < len(text):
|
|
873
|
+
# Skip escaped character
|
|
874
|
+
i += 2
|
|
875
|
+
continue
|
|
876
|
+
elif char == string_char:
|
|
877
|
+
in_string = False
|
|
878
|
+
else:
|
|
879
|
+
if char in ('"', "'"):
|
|
880
|
+
in_string = True
|
|
881
|
+
string_char = char
|
|
882
|
+
elif char == "(":
|
|
883
|
+
depth += 1
|
|
884
|
+
elif char == ")":
|
|
885
|
+
depth -= 1
|
|
886
|
+
i += 1
|
|
887
|
+
return depth
|
|
888
|
+
|
|
889
|
+
def _join_multiline_function_calls(self, lines: list[str]) -> list[str]:
|
|
890
|
+
"""
|
|
891
|
+
Join multi-line function calls into single lines.
|
|
892
|
+
Transforms:
|
|
893
|
+
set var.x = func(
|
|
894
|
+
arg1,
|
|
895
|
+
arg2
|
|
896
|
+
);
|
|
897
|
+
Into:
|
|
898
|
+
set var.x = func(arg1, arg2);
|
|
899
|
+
"""
|
|
900
|
+
result = []
|
|
901
|
+
i = 0
|
|
902
|
+
|
|
903
|
+
while i < len(lines):
|
|
904
|
+
line = lines[i]
|
|
905
|
+
|
|
906
|
+
# Check if this line contains an opening parenthesis
|
|
907
|
+
if "(" not in line:
|
|
908
|
+
result.append(line)
|
|
909
|
+
i += 1
|
|
910
|
+
continue
|
|
911
|
+
|
|
912
|
+
# Count parentheses (ignoring those in strings) to see if they balance
|
|
913
|
+
paren_depth = self._count_unquoted_parens(line)
|
|
914
|
+
|
|
915
|
+
if paren_depth == 0:
|
|
916
|
+
# Balanced on this line, no joining needed
|
|
917
|
+
result.append(line)
|
|
918
|
+
i += 1
|
|
919
|
+
continue
|
|
920
|
+
|
|
921
|
+
# Unbalanced - need to join with following lines
|
|
922
|
+
accumulated = [line]
|
|
923
|
+
i += 1
|
|
924
|
+
|
|
925
|
+
while i < len(lines) and paren_depth > 0:
|
|
926
|
+
next_line = lines[i]
|
|
927
|
+
accumulated.append(next_line)
|
|
928
|
+
paren_depth += self._count_unquoted_parens(next_line)
|
|
929
|
+
i += 1
|
|
930
|
+
|
|
931
|
+
# Join the accumulated lines
|
|
932
|
+
# Preserve the indentation of the first line
|
|
933
|
+
leading_ws = len(line) - len(line.lstrip())
|
|
934
|
+
indent = line[:leading_ws]
|
|
935
|
+
|
|
936
|
+
# Join all lines, removing leading/trailing whitespace from each
|
|
937
|
+
joined_parts = []
|
|
938
|
+
for part in accumulated:
|
|
939
|
+
stripped = part.strip()
|
|
940
|
+
if stripped:
|
|
941
|
+
joined_parts.append(stripped)
|
|
942
|
+
|
|
943
|
+
joined = " ".join(joined_parts)
|
|
944
|
+
|
|
945
|
+
# Add back the original indentation
|
|
946
|
+
result.append(indent + joined)
|
|
947
|
+
|
|
948
|
+
return result
|
|
949
|
+
|
|
950
|
+
def _find_matching_paren(self, text: str, start: int) -> Optional[int]:
|
|
951
|
+
"""Find the matching ')' for '(' at index start, ignoring strings."""
|
|
952
|
+
if start < 0 or start >= len(text) or text[start] != "(":
|
|
953
|
+
return None
|
|
954
|
+
|
|
955
|
+
depth = 1
|
|
956
|
+
pos = start + 1
|
|
957
|
+
in_string = False
|
|
958
|
+
string_char = None
|
|
959
|
+
|
|
960
|
+
while pos < len(text) and depth > 0:
|
|
961
|
+
char = text[pos]
|
|
962
|
+
if in_string:
|
|
963
|
+
if char == "\\" and pos + 1 < len(text):
|
|
964
|
+
pos += 2
|
|
965
|
+
continue
|
|
966
|
+
elif char == string_char:
|
|
967
|
+
in_string = False
|
|
968
|
+
else:
|
|
969
|
+
if char in ('"', "'"):
|
|
970
|
+
in_string = True
|
|
971
|
+
string_char = char
|
|
972
|
+
elif char == "(":
|
|
973
|
+
depth += 1
|
|
974
|
+
elif char == ")":
|
|
975
|
+
depth -= 1
|
|
976
|
+
pos += 1
|
|
977
|
+
|
|
978
|
+
if depth != 0:
|
|
979
|
+
return None
|
|
980
|
+
|
|
981
|
+
return pos - 1
|
|
982
|
+
|
|
983
|
+
def _parse_set_function_call(
|
|
984
|
+
self, line: str
|
|
985
|
+
) -> Optional[tuple[str, list[str], str, list[str], str]]:
|
|
986
|
+
"""
|
|
987
|
+
Parse a line of the form:
|
|
988
|
+
<prefix>set a, b = func(arg1, arg2);
|
|
989
|
+
Returns (prefix, result_vars, func_name, args, suffix) or None.
|
|
990
|
+
"""
|
|
991
|
+
for match in re.finditer(r"\bset\s+", line):
|
|
992
|
+
prefix = line[: match.start()]
|
|
993
|
+
rest = line[match.end() :]
|
|
994
|
+
|
|
995
|
+
if "=" not in rest:
|
|
996
|
+
continue
|
|
997
|
+
|
|
998
|
+
lhs, rhs = rest.split("=", 1)
|
|
999
|
+
lhs = lhs.strip()
|
|
1000
|
+
rhs = rhs.lstrip()
|
|
1001
|
+
|
|
1002
|
+
if not lhs:
|
|
1003
|
+
continue
|
|
1004
|
+
|
|
1005
|
+
func_match = re.match(r"(\w+)\s*\(", rhs)
|
|
1006
|
+
if not func_match:
|
|
1007
|
+
continue
|
|
1008
|
+
|
|
1009
|
+
func_name = func_match.group(1)
|
|
1010
|
+
open_paren_index = func_match.end() - 1
|
|
1011
|
+
close_paren_index = self._find_matching_paren(rhs, open_paren_index)
|
|
1012
|
+
if close_paren_index is None:
|
|
1013
|
+
continue
|
|
1014
|
+
|
|
1015
|
+
args_str = rhs[open_paren_index + 1 : close_paren_index]
|
|
1016
|
+
tail = rhs[close_paren_index + 1 :]
|
|
1017
|
+
|
|
1018
|
+
semicolon_match = re.match(r"\s*;(?P<suffix>.*)", tail, flags=re.DOTALL)
|
|
1019
|
+
if not semicolon_match:
|
|
1020
|
+
continue
|
|
1021
|
+
|
|
1022
|
+
suffix = semicolon_match.group("suffix")
|
|
1023
|
+
|
|
1024
|
+
# Split result vars (tuple unpacking if comma-separated)
|
|
1025
|
+
result_vars = [v.strip() for v in lhs.split(",")] if "," in lhs else [lhs]
|
|
1026
|
+
if any(not v for v in result_vars):
|
|
1027
|
+
continue
|
|
1028
|
+
|
|
1029
|
+
args = []
|
|
1030
|
+
if args_str.strip():
|
|
1031
|
+
args = self._parse_macro_args(args_str)
|
|
1032
|
+
|
|
1033
|
+
return prefix, result_vars, func_name, args, suffix
|
|
1034
|
+
|
|
1035
|
+
return None
|
|
1036
|
+
|
|
1037
|
+
def _process_function_calls(self, line: str) -> str:
|
|
1038
|
+
"""Replace function calls with VCL subroutine calls using globals."""
|
|
1039
|
+
# First, expand any macros in the line (NEW in v2.4)
|
|
1040
|
+
line = self._expand_macros(line)
|
|
1041
|
+
|
|
1042
|
+
parsed = self._parse_set_function_call(line)
|
|
1043
|
+
if not parsed:
|
|
1044
|
+
return line
|
|
1045
|
+
|
|
1046
|
+
prefix, result_vars, func_name, args, suffix = parsed
|
|
1047
|
+
|
|
1048
|
+
if func_name not in self.functions:
|
|
1049
|
+
return line
|
|
1050
|
+
|
|
1051
|
+
func = self.functions[func_name]
|
|
1052
|
+
return_types = func.get_return_types()
|
|
1053
|
+
|
|
1054
|
+
# Tuple assignment
|
|
1055
|
+
if len(result_vars) > 1:
|
|
1056
|
+
if not func.is_tuple_return():
|
|
1057
|
+
return line
|
|
1058
|
+
|
|
1059
|
+
if len(result_vars) != len(return_types):
|
|
1060
|
+
raise self.make_error(
|
|
1061
|
+
f"Function {func_name} returns {len(return_types)} values, "
|
|
1062
|
+
f"but {len(result_vars)} variables provided"
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
if len(args) != len(func.params):
|
|
1066
|
+
raise self.make_error(
|
|
1067
|
+
f"Function {func_name} expects {len(func.params)} arguments, got {len(args)}"
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
result_lines = []
|
|
1071
|
+
for (param_name, param_type), arg in zip(func.params, args):
|
|
1072
|
+
global_name = func.get_param_global(param_name)
|
|
1073
|
+
result_lines.extend(self._param_to_global(prefix, global_name, arg, param_type))
|
|
1074
|
+
|
|
1075
|
+
result_lines.append(f"{prefix}call {func_name};")
|
|
1076
|
+
|
|
1077
|
+
for idx, (result_var, ret_type) in enumerate(zip(result_vars, return_types)):
|
|
1078
|
+
return_global = func.get_return_global(idx)
|
|
1079
|
+
result_lines.extend(
|
|
1080
|
+
self._global_to_var(prefix, result_var, return_global, ret_type)
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
if suffix:
|
|
1084
|
+
result_lines[-1] = f"{result_lines[-1]}{suffix}"
|
|
1085
|
+
|
|
1086
|
+
return "\n".join(result_lines)
|
|
1087
|
+
|
|
1088
|
+
# Single assignment
|
|
1089
|
+
if func.is_tuple_return():
|
|
1090
|
+
return line
|
|
1091
|
+
|
|
1092
|
+
if len(args) != len(func.params):
|
|
1093
|
+
raise self.make_error(
|
|
1094
|
+
f"Function {func_name} expects {len(func.params)} arguments, got {len(args)}"
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
result_lines = []
|
|
1098
|
+
for (param_name, param_type), arg in zip(func.params, args):
|
|
1099
|
+
global_name = func.get_param_global(param_name)
|
|
1100
|
+
result_lines.extend(self._param_to_global(prefix, global_name, arg, param_type))
|
|
1101
|
+
|
|
1102
|
+
result_lines.append(f"{prefix}call {func_name};")
|
|
1103
|
+
|
|
1104
|
+
return_global = func.get_return_global()
|
|
1105
|
+
result_lines.extend(
|
|
1106
|
+
self._global_to_var(prefix, result_vars[0], return_global, return_types[0])
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
if suffix:
|
|
1110
|
+
result_lines[-1] = f"{result_lines[-1]}{suffix}"
|
|
1111
|
+
|
|
1112
|
+
return "\n".join(result_lines)
|
|
1113
|
+
|
|
1114
|
+
def _expand_macros(self, line: str) -> str:
|
|
1115
|
+
"""Expand all macro calls in a line."""
|
|
1116
|
+
# Keep expanding until no more macros found (handle nested macros)
|
|
1117
|
+
max_iterations = 10 # Prevent infinite loops
|
|
1118
|
+
iteration = 0
|
|
1119
|
+
|
|
1120
|
+
while iteration < max_iterations:
|
|
1121
|
+
new_line = self._expand_macros_once(line)
|
|
1122
|
+
if new_line == line:
|
|
1123
|
+
break # No more macros to expand
|
|
1124
|
+
line = new_line
|
|
1125
|
+
iteration += 1
|
|
1126
|
+
|
|
1127
|
+
if iteration >= max_iterations:
|
|
1128
|
+
raise self.make_error("Too many macro expansion iterations (possible recursive macros)")
|
|
1129
|
+
|
|
1130
|
+
return line
|
|
1131
|
+
|
|
1132
|
+
def _expand_macros_once(self, line: str) -> str:
|
|
1133
|
+
"""Expand macros in a line once (one pass). Expand leftmost macro first."""
|
|
1134
|
+
# Find potential macro calls by looking for identifier followed by (
|
|
1135
|
+
pattern = r"\b(\w+)\s*\("
|
|
1136
|
+
|
|
1137
|
+
for match in re.finditer(pattern, line):
|
|
1138
|
+
macro_name = match.group(1)
|
|
1139
|
+
|
|
1140
|
+
# Check if this is a macro
|
|
1141
|
+
if macro_name not in self.macros:
|
|
1142
|
+
continue
|
|
1143
|
+
|
|
1144
|
+
# Find the matching closing parenthesis (ignoring parens inside strings)
|
|
1145
|
+
start_pos = match.end() # Position after the opening (
|
|
1146
|
+
paren_depth = 1
|
|
1147
|
+
pos = start_pos
|
|
1148
|
+
in_string = False
|
|
1149
|
+
string_char = None
|
|
1150
|
+
|
|
1151
|
+
while pos < len(line) and paren_depth > 0:
|
|
1152
|
+
char = line[pos]
|
|
1153
|
+
if in_string:
|
|
1154
|
+
if char == "\\" and pos + 1 < len(line):
|
|
1155
|
+
# Skip escaped character
|
|
1156
|
+
pos += 2
|
|
1157
|
+
continue
|
|
1158
|
+
elif char == string_char:
|
|
1159
|
+
in_string = False
|
|
1160
|
+
else:
|
|
1161
|
+
if char in ('"', "'"):
|
|
1162
|
+
in_string = True
|
|
1163
|
+
string_char = char
|
|
1164
|
+
elif char == "(":
|
|
1165
|
+
paren_depth += 1
|
|
1166
|
+
elif char == ")":
|
|
1167
|
+
paren_depth -= 1
|
|
1168
|
+
pos += 1
|
|
1169
|
+
|
|
1170
|
+
if paren_depth != 0:
|
|
1171
|
+
# Unmatched parentheses - skip this match
|
|
1172
|
+
continue
|
|
1173
|
+
|
|
1174
|
+
# Extract arguments string (between parentheses)
|
|
1175
|
+
args_str = line[start_pos : pos - 1]
|
|
1176
|
+
|
|
1177
|
+
# Parse arguments
|
|
1178
|
+
args = []
|
|
1179
|
+
if args_str.strip():
|
|
1180
|
+
args = self._parse_macro_args(args_str)
|
|
1181
|
+
|
|
1182
|
+
# Expand the macro
|
|
1183
|
+
macro = self.macros[macro_name]
|
|
1184
|
+
try:
|
|
1185
|
+
expanded = macro.expand(args)
|
|
1186
|
+
self.log_debug(f"Expanded macro {macro_name}({args_str}) -> {expanded}", indent=3)
|
|
1187
|
+
except ValueError as e:
|
|
1188
|
+
raise self.make_error(str(e))
|
|
1189
|
+
|
|
1190
|
+
# Build result with the macro replaced
|
|
1191
|
+
result = line[: match.start()] + expanded + line[pos:]
|
|
1192
|
+
return result
|
|
1193
|
+
|
|
1194
|
+
# No macros found
|
|
1195
|
+
return line
|
|
1196
|
+
|
|
1197
|
+
def _parse_macro_args(self, args_str: str) -> list[str]:
|
|
1198
|
+
"""Parse macro arguments, handling nested parentheses and strings."""
|
|
1199
|
+
args = []
|
|
1200
|
+
current_arg = []
|
|
1201
|
+
depth = 0
|
|
1202
|
+
in_string = False
|
|
1203
|
+
string_char = None
|
|
1204
|
+
i = 0
|
|
1205
|
+
|
|
1206
|
+
while i < len(args_str):
|
|
1207
|
+
char = args_str[i]
|
|
1208
|
+
|
|
1209
|
+
if in_string:
|
|
1210
|
+
current_arg.append(char)
|
|
1211
|
+
if char == "\\" and i + 1 < len(args_str):
|
|
1212
|
+
# Include escaped character
|
|
1213
|
+
current_arg.append(args_str[i + 1])
|
|
1214
|
+
i += 2
|
|
1215
|
+
continue
|
|
1216
|
+
elif char == string_char:
|
|
1217
|
+
in_string = False
|
|
1218
|
+
else:
|
|
1219
|
+
if char in ('"', "'"):
|
|
1220
|
+
in_string = True
|
|
1221
|
+
string_char = char
|
|
1222
|
+
current_arg.append(char)
|
|
1223
|
+
elif char == "(":
|
|
1224
|
+
depth += 1
|
|
1225
|
+
current_arg.append(char)
|
|
1226
|
+
elif char == ")":
|
|
1227
|
+
depth -= 1
|
|
1228
|
+
current_arg.append(char)
|
|
1229
|
+
elif char == "," and depth == 0:
|
|
1230
|
+
# End of current argument
|
|
1231
|
+
args.append("".join(current_arg).strip())
|
|
1232
|
+
current_arg = []
|
|
1233
|
+
else:
|
|
1234
|
+
current_arg.append(char)
|
|
1235
|
+
i += 1
|
|
1236
|
+
|
|
1237
|
+
# Add last argument
|
|
1238
|
+
if current_arg:
|
|
1239
|
+
args.append("".join(current_arg).strip())
|
|
1240
|
+
|
|
1241
|
+
return args
|
|
1242
|
+
|
|
1243
|
+
def _param_to_global(
|
|
1244
|
+
self, prefix: str, global_name: str, arg: str, param_type: str
|
|
1245
|
+
) -> list[str]:
|
|
1246
|
+
"""Convert parameter to global with type conversion."""
|
|
1247
|
+
lines = []
|
|
1248
|
+
if param_type == "INTEGER":
|
|
1249
|
+
lines.append(f"{prefix}set {global_name} = std.itoa({arg});")
|
|
1250
|
+
elif param_type == "FLOAT":
|
|
1251
|
+
lines.append(f'{prefix}set {global_name} = "" + {arg};')
|
|
1252
|
+
elif param_type == "BOOL":
|
|
1253
|
+
lines.append(f"{prefix}if ({arg}) {{")
|
|
1254
|
+
lines.append(f'{prefix} set {global_name} = "true";')
|
|
1255
|
+
lines.append(f"{prefix}}} else {{")
|
|
1256
|
+
lines.append(f'{prefix} set {global_name} = "false";')
|
|
1257
|
+
lines.append(f"{prefix}}}")
|
|
1258
|
+
else:
|
|
1259
|
+
lines.append(f"{prefix}set {global_name} = {arg};")
|
|
1260
|
+
return lines
|
|
1261
|
+
|
|
1262
|
+
def _global_to_var(
|
|
1263
|
+
self, prefix: str, result_var: str, return_global: str, ret_type: str
|
|
1264
|
+
) -> list[str]:
|
|
1265
|
+
"""Convert global to variable with type conversion."""
|
|
1266
|
+
lines = []
|
|
1267
|
+
if ret_type == "INTEGER":
|
|
1268
|
+
lines.append(f"{prefix}set {result_var} = std.atoi({return_global});")
|
|
1269
|
+
elif ret_type == "FLOAT":
|
|
1270
|
+
lines.append(f"{prefix}set {result_var} = std.atof({return_global});")
|
|
1271
|
+
elif ret_type == "BOOL":
|
|
1272
|
+
lines.append(f'{prefix}set {result_var} = ({return_global} == "true");')
|
|
1273
|
+
else:
|
|
1274
|
+
lines.append(f"{prefix}set {result_var} = {return_global};")
|
|
1275
|
+
return lines
|
|
1276
|
+
|
|
1277
|
+
def _process_for_loop(
|
|
1278
|
+
self, lines: list[str], start: int, end: int, context: dict[str, Any]
|
|
1279
|
+
) -> int:
|
|
1280
|
+
"""Process a #for loop with optional tuple unpacking."""
|
|
1281
|
+
line = lines[start].strip()
|
|
1282
|
+
|
|
1283
|
+
match = re.match(r"#for\s+(\w+(?:\s*,\s*\w+)*)\s+in\s+(.+)", line)
|
|
1284
|
+
if not match:
|
|
1285
|
+
raise self.make_error(f"Invalid #for syntax: {line}")
|
|
1286
|
+
|
|
1287
|
+
vars_str = match.group(1).strip()
|
|
1288
|
+
iterable_expr = match.group(2)
|
|
1289
|
+
|
|
1290
|
+
var_names = [v.strip() for v in vars_str.split(",")] if "," in vars_str else [vars_str]
|
|
1291
|
+
|
|
1292
|
+
for var_name in var_names:
|
|
1293
|
+
if not re.match(r"^\w+$", var_name):
|
|
1294
|
+
raise self.make_error(f"Invalid variable name in #for: '{var_name}'")
|
|
1295
|
+
|
|
1296
|
+
try:
|
|
1297
|
+
iterable = self._evaluate_expression(iterable_expr, context)
|
|
1298
|
+
except Exception as e:
|
|
1299
|
+
raise self.make_error(f"Error evaluating loop expression '{iterable_expr}': {e}")
|
|
1300
|
+
|
|
1301
|
+
try:
|
|
1302
|
+
loop_end = self._find_matching_end(lines, start, end, "#for", "#endfor")
|
|
1303
|
+
except SyntaxError as e:
|
|
1304
|
+
raise self.make_error(str(e))
|
|
1305
|
+
|
|
1306
|
+
iterable = list(iterable)
|
|
1307
|
+
self.log_debug(f"Loop iterating {len(iterable)} times", indent=2)
|
|
1308
|
+
|
|
1309
|
+
for idx, value in enumerate(iterable):
|
|
1310
|
+
loop_context = context.copy()
|
|
1311
|
+
|
|
1312
|
+
if len(var_names) == 1:
|
|
1313
|
+
loop_context[var_names[0]] = value
|
|
1314
|
+
self.log_debug(f"Iteration {idx}: {var_names[0]} = {value}", indent=3)
|
|
1315
|
+
else:
|
|
1316
|
+
try:
|
|
1317
|
+
values = tuple(value)
|
|
1318
|
+
except TypeError:
|
|
1319
|
+
raise self.make_error(
|
|
1320
|
+
f"Cannot unpack non-iterable value '{value}' into {len(var_names)} variables"
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
if len(values) != len(var_names):
|
|
1324
|
+
raise self.make_error(
|
|
1325
|
+
f"Cannot unpack {len(values)} values into {len(var_names)} variables "
|
|
1326
|
+
f"({', '.join(var_names)})"
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
for var_name, val in zip(var_names, values):
|
|
1330
|
+
loop_context[var_name] = val
|
|
1331
|
+
|
|
1332
|
+
self.log_debug(
|
|
1333
|
+
f"Iteration {idx}: {', '.join(f'{n}={v}' for n, v in zip(var_names, values))}",
|
|
1334
|
+
indent=3,
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
self._process_lines(lines, start + 1, loop_end, loop_context)
|
|
1338
|
+
|
|
1339
|
+
return loop_end + 1
|
|
1340
|
+
|
|
1341
|
+
def _process_if(self, lines: list[str], start: int, end: int, context: dict[str, Any]) -> int:
|
|
1342
|
+
"""Process a #if conditional."""
|
|
1343
|
+
line = lines[start].strip()
|
|
1344
|
+
|
|
1345
|
+
match = re.match(r"#if\s+(.+)", line)
|
|
1346
|
+
if not match:
|
|
1347
|
+
raise self.make_error(f"Invalid #if syntax: {line}")
|
|
1348
|
+
|
|
1349
|
+
condition = match.group(1)
|
|
1350
|
+
|
|
1351
|
+
try:
|
|
1352
|
+
result = self._evaluate_expression(condition, context)
|
|
1353
|
+
except Exception as e:
|
|
1354
|
+
raise self.make_error(f"Error evaluating condition '{condition}': {e}")
|
|
1355
|
+
|
|
1356
|
+
self.log_debug(f"Condition '{condition}' evaluated to {result}", indent=2)
|
|
1357
|
+
|
|
1358
|
+
else_idx = None
|
|
1359
|
+
try:
|
|
1360
|
+
endif_idx = self._find_matching_end(lines, start, end, "#if", "#endif")
|
|
1361
|
+
except SyntaxError as e:
|
|
1362
|
+
raise self.make_error(str(e))
|
|
1363
|
+
|
|
1364
|
+
depth = 0
|
|
1365
|
+
for i in range(start, endif_idx):
|
|
1366
|
+
stripped = lines[i].strip()
|
|
1367
|
+
if stripped.startswith("#if"):
|
|
1368
|
+
depth += 1
|
|
1369
|
+
elif stripped == "#endif":
|
|
1370
|
+
depth -= 1
|
|
1371
|
+
elif stripped == "#else" and depth == 1:
|
|
1372
|
+
else_idx = i
|
|
1373
|
+
break
|
|
1374
|
+
|
|
1375
|
+
if result:
|
|
1376
|
+
branch_end = else_idx if else_idx else endif_idx
|
|
1377
|
+
self.log_debug("Taking if branch", indent=2)
|
|
1378
|
+
self._process_lines(lines, start + 1, branch_end, context)
|
|
1379
|
+
else:
|
|
1380
|
+
if else_idx:
|
|
1381
|
+
self.log_debug("Taking else branch", indent=2)
|
|
1382
|
+
self._process_lines(lines, else_idx + 1, endif_idx, context)
|
|
1383
|
+
else:
|
|
1384
|
+
self.log_debug("Skipping if block", indent=2)
|
|
1385
|
+
|
|
1386
|
+
return endif_idx + 1
|
|
1387
|
+
|
|
1388
|
+
def _find_matching_end(
|
|
1389
|
+
self, lines: list[str], start: int, end: int, open_keyword: str, close_keyword: str
|
|
1390
|
+
) -> int:
|
|
1391
|
+
"""Find the matching closing keyword for a block."""
|
|
1392
|
+
depth = 0
|
|
1393
|
+
for i in range(start, end):
|
|
1394
|
+
stripped = lines[i].strip()
|
|
1395
|
+
# Check for open keyword with word boundary (followed by space or end of line)
|
|
1396
|
+
if stripped == open_keyword or stripped.startswith(open_keyword + " "):
|
|
1397
|
+
depth += 1
|
|
1398
|
+
elif stripped == close_keyword:
|
|
1399
|
+
depth -= 1
|
|
1400
|
+
if depth == 0:
|
|
1401
|
+
return i
|
|
1402
|
+
|
|
1403
|
+
raise SyntaxError(f"No matching {close_keyword} for {open_keyword} at line {start + 1}")
|
|
1404
|
+
|
|
1405
|
+
def _substitute_expressions(self, line: str, context: dict[str, Any]) -> str:
|
|
1406
|
+
"""Substitute {{expression}} in a line."""
|
|
1407
|
+
|
|
1408
|
+
def replace_expr(match):
|
|
1409
|
+
expr = match.group(1)
|
|
1410
|
+
try:
|
|
1411
|
+
value = self._evaluate_expression(expr, context)
|
|
1412
|
+
except Exception as e:
|
|
1413
|
+
raise self.make_error(f"Error evaluating expression '{expr}': {e}")
|
|
1414
|
+
return str(value)
|
|
1415
|
+
|
|
1416
|
+
return re.sub(r"\{\{(.+?)\}\}", replace_expr, line)
|
|
1417
|
+
|
|
1418
|
+
def _evaluate_expression(self, expr: str, context: dict[str, Any]) -> Any:
|
|
1419
|
+
"""Safely evaluate an expression in the given context."""
|
|
1420
|
+
try:
|
|
1421
|
+
safe_globals = {
|
|
1422
|
+
"range": range,
|
|
1423
|
+
"len": len,
|
|
1424
|
+
"str": str,
|
|
1425
|
+
"int": int,
|
|
1426
|
+
"hex": hex,
|
|
1427
|
+
"format": format,
|
|
1428
|
+
"abs": abs,
|
|
1429
|
+
"min": min,
|
|
1430
|
+
"max": max,
|
|
1431
|
+
"enumerate": enumerate,
|
|
1432
|
+
# Boolean literals (both Python and C-style)
|
|
1433
|
+
"True": True,
|
|
1434
|
+
"False": False,
|
|
1435
|
+
"true": True,
|
|
1436
|
+
"false": False,
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
# Merge constants into context
|
|
1440
|
+
eval_env = {**safe_globals, **self.constants, **context}
|
|
1441
|
+
result = eval(expr, {"__builtins__": {}}, eval_env)
|
|
1442
|
+
|
|
1443
|
+
return result
|
|
1444
|
+
except NameError as e:
|
|
1445
|
+
# Provide helpful suggestions
|
|
1446
|
+
var_name = str(e).split("'")[1] if "'" in str(e) else ""
|
|
1447
|
+
available_names = (
|
|
1448
|
+
list(safe_globals.keys()) + list(self.constants.keys()) + list(context.keys())
|
|
1449
|
+
)
|
|
1450
|
+
suggestions = get_close_matches(var_name, available_names, n=3, cutoff=0.6)
|
|
1451
|
+
|
|
1452
|
+
error_msg = f"Name '{var_name}' is not defined"
|
|
1453
|
+
if suggestions:
|
|
1454
|
+
error_msg += f"\n Did you mean: {', '.join(suggestions)}?"
|
|
1455
|
+
error_msg += f"\n Available: {', '.join(sorted(available_names))}"
|
|
1456
|
+
|
|
1457
|
+
raise NameError(error_msg)
|
|
1458
|
+
except Exception as e:
|
|
1459
|
+
raise ValueError(f"Error evaluating expression '{expr}': {e}")
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def main():
|
|
1463
|
+
"""Main entry point."""
|
|
1464
|
+
parser = argparse.ArgumentParser(
|
|
1465
|
+
description="xvcl - Extended VCL compiler with metaprogramming features",
|
|
1466
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1467
|
+
epilog="""
|
|
1468
|
+
Features:
|
|
1469
|
+
- For loops: #for var in range(n)
|
|
1470
|
+
- Conditionals: #if condition
|
|
1471
|
+
- Templates: {{expression}}
|
|
1472
|
+
- Constants: #const NAME TYPE = value
|
|
1473
|
+
- Includes: #include "path/to/file.xvcl"
|
|
1474
|
+
- Inline macros: #inline name(params) ... #endinline
|
|
1475
|
+
- Functions: #def name(params) -> TYPE
|
|
1476
|
+
- Variables: #let name TYPE = expression;
|
|
1477
|
+
|
|
1478
|
+
Example:
|
|
1479
|
+
xvcl input.xvcl -o output.vcl
|
|
1480
|
+
xvcl input.xvcl -o output.vcl --debug
|
|
1481
|
+
xvcl input.xvcl -o output.vcl -I /path/to/includes
|
|
1482
|
+
""",
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
parser.add_argument("input", help="Input XVCL file")
|
|
1486
|
+
parser.add_argument("-o", "--output", help="Output VCL file (default: removes .xvcl extension)")
|
|
1487
|
+
parser.add_argument(
|
|
1488
|
+
"-I",
|
|
1489
|
+
"--include",
|
|
1490
|
+
dest="include_paths",
|
|
1491
|
+
action="append",
|
|
1492
|
+
help="Add include search path (can be specified multiple times)",
|
|
1493
|
+
)
|
|
1494
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug mode (verbose output)")
|
|
1495
|
+
parser.add_argument(
|
|
1496
|
+
"--source-maps", action="store_true", help="Add source map comments to generated code"
|
|
1497
|
+
)
|
|
1498
|
+
parser.add_argument(
|
|
1499
|
+
"-v", "--verbose", action="store_true", help="Verbose output (alias for --debug)"
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
args = parser.parse_args()
|
|
1503
|
+
|
|
1504
|
+
# Determine output path
|
|
1505
|
+
if args.output:
|
|
1506
|
+
output_path = args.output
|
|
1507
|
+
elif args.input.endswith(".xvcl"):
|
|
1508
|
+
output_path = args.input.replace(".xvcl", ".vcl")
|
|
1509
|
+
else:
|
|
1510
|
+
output_path = args.input + ".vcl"
|
|
1511
|
+
|
|
1512
|
+
# Set up include paths
|
|
1513
|
+
include_paths = args.include_paths or ["."]
|
|
1514
|
+
|
|
1515
|
+
# Enable debug if verbose flag is used
|
|
1516
|
+
debug = args.debug or args.verbose
|
|
1517
|
+
|
|
1518
|
+
try:
|
|
1519
|
+
compiler = XVCLCompiler(
|
|
1520
|
+
include_paths=include_paths, debug=debug, source_maps=args.source_maps
|
|
1521
|
+
)
|
|
1522
|
+
compiler.process_file(args.input, output_path)
|
|
1523
|
+
|
|
1524
|
+
print(f"{Colors.GREEN}{Colors.BOLD}✓ Compilation complete{Colors.RESET}")
|
|
1525
|
+
|
|
1526
|
+
except PreprocessorError as e:
|
|
1527
|
+
print(e.format_error(use_colors=True), file=sys.stderr)
|
|
1528
|
+
sys.exit(1)
|
|
1529
|
+
except FileNotFoundError as e:
|
|
1530
|
+
print(f"{Colors.RED}Error:{Colors.RESET} {e}", file=sys.stderr)
|
|
1531
|
+
sys.exit(1)
|
|
1532
|
+
except Exception as e:
|
|
1533
|
+
print(f"{Colors.RED}Unexpected error:{Colors.RESET} {e}", file=sys.stderr)
|
|
1534
|
+
import traceback
|
|
1535
|
+
|
|
1536
|
+
traceback.print_exc()
|
|
1537
|
+
sys.exit(1)
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
if __name__ == "__main__":
|
|
1541
|
+
main()
|