xvcl 2.5.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.

Potentially problematic release.


This version of xvcl might be problematic. Click here for more details.

xvcl/compiler.py ADDED
@@ -0,0 +1,1381 @@
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
+ self.output.append("")
655
+ self.output.append(
656
+ "// ============================================================================"
657
+ )
658
+ self.output.append("// GENERATED FUNCTION SUBROUTINES")
659
+ self.output.append(
660
+ "// ============================================================================"
661
+ )
662
+ self.output.append("")
663
+
664
+ for func in self.functions.values():
665
+ self._generate_function_subroutine(func)
666
+
667
+ def _generate_function_subroutine(self, func: Function) -> None:
668
+ """Generate a VCL subroutine for a function."""
669
+ if self.source_maps and func.location:
670
+ self.output.append(f"// Generated from {func.location}")
671
+
672
+ self.output.append(f"// Function: {func.name}")
673
+ self.output.append("//@recv, hash, hit, miss, pass, fetch, error, deliver, log")
674
+ self.output.append(f"sub {func.name} {{")
675
+ self.output.append("")
676
+
677
+ # Declare local variables for parameters
678
+ for param_name, param_type in func.params:
679
+ self.output.append(f" declare local var.{param_name} {param_type};")
680
+
681
+ if func.params:
682
+ self.output.append("")
683
+
684
+ # Read parameters from globals with type conversion
685
+ for param_name, param_type in func.params:
686
+ global_name = func.get_param_global(param_name)
687
+ if param_type == "INTEGER":
688
+ self.output.append(f" set var.{param_name} = std.atoi({global_name});")
689
+ elif param_type == "FLOAT":
690
+ self.output.append(f" set var.{param_name} = std.atof({global_name});")
691
+ elif param_type == "BOOL":
692
+ self.output.append(f' set var.{param_name} = ({global_name} == "true");')
693
+ else:
694
+ # STRING and others
695
+ self.output.append(f" set var.{param_name} = {global_name};")
696
+
697
+ if func.params:
698
+ self.output.append("")
699
+
700
+ # Declare return variable(s)
701
+ return_types = func.get_return_types()
702
+ if func.is_tuple_return():
703
+ # Multiple return values
704
+ for idx, ret_type in enumerate(return_types):
705
+ self.output.append(f" declare local var.return_value{idx} {ret_type};")
706
+ else:
707
+ # Single return value
708
+ self.output.append(f" declare local var.return_value {return_types[0]};")
709
+ self.output.append("")
710
+
711
+ # Process function body
712
+ param_substituted_body = []
713
+ for line in func.body:
714
+ processed_line = line
715
+ for param_name, _ in func.params:
716
+ # Replace standalone parameter references
717
+ processed_line = re.sub(rf"\b{param_name}\b", f"var.{param_name}", processed_line)
718
+ param_substituted_body.append(processed_line)
719
+
720
+ # Save current output and process body
721
+ saved_output = self.output
722
+ self.output = []
723
+
724
+ self._process_lines(param_substituted_body, 0, len(param_substituted_body), {})
725
+
726
+ body_output = self.output
727
+ self.output = saved_output
728
+
729
+ # Post-process the body output to handle return statements
730
+ for line in body_output:
731
+ line.strip()
732
+
733
+ # Replace "return expr1, expr2;" with multiple assignments
734
+ if re.match(r"\s*return\s+", line):
735
+ if func.is_tuple_return():
736
+ # Parse: return expr1, expr2, expr3;
737
+ return_match = re.search(r"\breturn\s+(.+);", line)
738
+ if return_match:
739
+ exprs_str = return_match.group(1)
740
+ exprs = [e.strip() for e in exprs_str.split(",")]
741
+
742
+ if len(exprs) != len(return_types):
743
+ raise ValueError(
744
+ f"Function {func.name} expects {len(return_types)} return values, got {len(exprs)}"
745
+ )
746
+
747
+ match_indent = re.match(r"(\s*)", line)
748
+ indent = match_indent.group(1) if match_indent else ""
749
+ for idx, expr in enumerate(exprs):
750
+ self.output.append(f"{indent}set var.return_value{idx} = {expr};")
751
+ continue
752
+ else:
753
+ # Single return
754
+ line = re.sub(r"\breturn\s+(.+);", r"set var.return_value = \1;", line)
755
+
756
+ self.output.append(line)
757
+
758
+ # Write return value(s) to global(s)
759
+ self.output.append("")
760
+ if func.is_tuple_return():
761
+ for idx, ret_type in enumerate(return_types):
762
+ return_global = func.get_return_global(idx)
763
+ self._write_return_conversion(return_global, f"var.return_value{idx}", ret_type)
764
+ else:
765
+ return_global = func.get_return_global()
766
+ self._write_return_conversion(return_global, "var.return_value", return_types[0])
767
+
768
+ self.output.append("}")
769
+ self.output.append("")
770
+
771
+ def _write_return_conversion(self, global_var: str, local_var: str, var_type: str) -> None:
772
+ """Helper to write type conversion for return value."""
773
+ if var_type == "INTEGER":
774
+ self.output.append(f" set {global_var} = std.itoa({local_var});")
775
+ elif var_type == "FLOAT":
776
+ self.output.append(f' set {global_var} = "" + {local_var};')
777
+ elif var_type == "BOOL":
778
+ self.output.append(f" if ({local_var}) {{")
779
+ self.output.append(f' set {global_var} = "true";')
780
+ self.output.append(" } else {")
781
+ self.output.append(f' set {global_var} = "false";')
782
+ self.output.append(" }")
783
+ else:
784
+ # STRING and others
785
+ self.output.append(f" set {global_var} = {local_var};")
786
+
787
+ def _process_lines(
788
+ self, lines: list[str], start: int, end: int, context: dict[str, Any]
789
+ ) -> int:
790
+ """
791
+ Process lines from start to end with given context.
792
+ Returns the index of the last processed line.
793
+ """
794
+ i = start
795
+ while i < end:
796
+ line = lines[i]
797
+ stripped = line.strip()
798
+ self.current_line = i + 1
799
+
800
+ # Handle #for loops
801
+ if stripped.startswith("#for "):
802
+ self.log_debug(f"Processing #for at line {self.current_line}", indent=1)
803
+ i = self._process_for_loop(lines, i, end, context)
804
+
805
+ # Handle #if conditionals
806
+ elif stripped.startswith("#if "):
807
+ self.log_debug(f"Processing #if at line {self.current_line}", indent=1)
808
+ i = self._process_if(lines, i, end, context)
809
+
810
+ # Handle #let (declare + initialize)
811
+ elif stripped.startswith("#let "):
812
+ self.log_debug(f"Processing #let at line {self.current_line}", indent=1)
813
+ self._process_let(line)
814
+ i += 1
815
+
816
+ # Skip control flow keywords
817
+ elif stripped in ("#else", "#endif", "#endfor", "#enddef", "#endinline"):
818
+ return i
819
+
820
+ # Regular line - process function calls and template substitutions
821
+ else:
822
+ processed_line = self._process_function_calls(line)
823
+ processed_line = self._substitute_expressions(processed_line, context)
824
+ self.output.append(processed_line)
825
+ i += 1
826
+
827
+ return i
828
+
829
+ def _process_let(self, line: str) -> None:
830
+ """
831
+ Process #let directive (declare + initialize).
832
+ Format: #let name TYPE = expression;
833
+ Generates: declare local var.name TYPE;
834
+ set var.name = expression;
835
+ """
836
+ # Match: #let name TYPE = expression;
837
+ match = re.match(r"(\s*)#let\s+(\w+)\s+(\w+)\s*=\s*(.+);", line)
838
+ if not match:
839
+ raise self.make_error(f"Invalid #let syntax: {line}")
840
+
841
+ indent = match.group(1)
842
+ var_name = match.group(2)
843
+ var_type = match.group(3)
844
+ expression = match.group(4)
845
+
846
+ self.log_debug(f"Declaring variable: var.{var_name} {var_type} = {expression}", indent=2)
847
+
848
+ # Generate declare statement
849
+ self.output.append(f"{indent}declare local var.{var_name} {var_type};")
850
+
851
+ # Generate set statement and process any function calls in the expression
852
+ set_statement = f"{indent}set var.{var_name} = {expression};"
853
+ processed_set = self._process_function_calls(set_statement)
854
+
855
+ # The processed_set might be multi-line if it contains function calls
856
+ if "\n" in processed_set:
857
+ self.output.extend(processed_set.split("\n"))
858
+ else:
859
+ self.output.append(processed_set)
860
+
861
+ def _join_multiline_function_calls(self, lines: list[str]) -> list[str]:
862
+ """
863
+ Join multi-line function calls into single lines.
864
+ Transforms:
865
+ set var.x = func(
866
+ arg1,
867
+ arg2
868
+ );
869
+ Into:
870
+ set var.x = func(arg1, arg2);
871
+ """
872
+ result = []
873
+ i = 0
874
+
875
+ while i < len(lines):
876
+ line = lines[i]
877
+
878
+ # Check if this line contains an opening parenthesis
879
+ if "(" not in line:
880
+ result.append(line)
881
+ i += 1
882
+ continue
883
+
884
+ # Count parentheses to see if they balance on this line
885
+ paren_depth = line.count("(") - line.count(")")
886
+
887
+ if paren_depth == 0:
888
+ # Balanced on this line, no joining needed
889
+ result.append(line)
890
+ i += 1
891
+ continue
892
+
893
+ # Unbalanced - need to join with following lines
894
+ accumulated = [line]
895
+ i += 1
896
+
897
+ while i < len(lines) and paren_depth > 0:
898
+ next_line = lines[i]
899
+ accumulated.append(next_line)
900
+ paren_depth += next_line.count("(") - next_line.count(")")
901
+ i += 1
902
+
903
+ # Join the accumulated lines
904
+ # Preserve the indentation of the first line
905
+ leading_ws = len(line) - len(line.lstrip())
906
+ indent = line[:leading_ws]
907
+
908
+ # Join all lines, removing leading/trailing whitespace from each
909
+ joined_parts = []
910
+ for part in accumulated:
911
+ stripped = part.strip()
912
+ if stripped:
913
+ joined_parts.append(stripped)
914
+
915
+ joined = " ".join(joined_parts)
916
+
917
+ # Normalize multiple spaces to single spaces
918
+ joined = re.sub(r"\s+", " ", joined)
919
+
920
+ # Add back the original indentation
921
+ result.append(indent + joined)
922
+
923
+ return result
924
+
925
+ def _process_function_calls(self, line: str) -> str:
926
+ """Replace function calls with VCL subroutine calls using globals."""
927
+ # First, expand any macros in the line (NEW in v2.4)
928
+ line = self._expand_macros(line)
929
+
930
+ # Try tuple unpacking first
931
+ tuple_pattern = (
932
+ r"(.*?)\bset\s+((?:\w+(?:\.\w+)*\s*,\s*)+\w+(?:\.\w+)*)\s*=\s*(\w+)\s*\((.*?)\)\s*;"
933
+ )
934
+
935
+ def replace_tuple_call(match):
936
+ prefix = match.group(1)
937
+ result_vars_str = match.group(2)
938
+ func_name = match.group(3)
939
+ args_str = match.group(4).strip()
940
+
941
+ if func_name not in self.functions:
942
+ return match.group(0)
943
+
944
+ func = self.functions[func_name]
945
+ if not func.is_tuple_return():
946
+ return match.group(0)
947
+
948
+ result_vars = [v.strip() for v in result_vars_str.split(",")]
949
+ return_types = func.get_return_types()
950
+
951
+ if len(result_vars) != len(return_types):
952
+ raise self.make_error(
953
+ f"Function {func_name} returns {len(return_types)} values, "
954
+ f"but {len(result_vars)} variables provided"
955
+ )
956
+
957
+ args = [arg.strip() for arg in args_str.split(",") if arg.strip()]
958
+ if len(args) != len(func.params):
959
+ raise self.make_error(
960
+ f"Function {func_name} expects {len(func.params)} arguments, got {len(args)}"
961
+ )
962
+
963
+ result_lines = []
964
+ for (param_name, param_type), arg in zip(func.params, args):
965
+ global_name = func.get_param_global(param_name)
966
+ result_lines.extend(self._param_to_global(prefix, global_name, arg, param_type))
967
+
968
+ result_lines.append(f"{prefix}call {func_name};")
969
+
970
+ for idx, (result_var, ret_type) in enumerate(zip(result_vars, return_types)):
971
+ return_global = func.get_return_global(idx)
972
+ result_lines.extend(
973
+ self._global_to_var(prefix, result_var, return_global, ret_type)
974
+ )
975
+
976
+ return "\n".join(result_lines)
977
+
978
+ # Try single value
979
+ single_pattern = r"(.*?)\bset\s+(\w+(?:\.\w+)*)\s*=\s*(\w+)\s*\((.*?)\)\s*;"
980
+
981
+ def replace_single_call(match):
982
+ prefix = match.group(1)
983
+ result_var = match.group(2)
984
+ func_name = match.group(3)
985
+ args_str = match.group(4).strip()
986
+
987
+ if func_name not in self.functions:
988
+ return match.group(0)
989
+
990
+ func = self.functions[func_name]
991
+ if func.is_tuple_return():
992
+ return match.group(0)
993
+
994
+ args = [arg.strip() for arg in args_str.split(",") if arg.strip()]
995
+ if len(args) != len(func.params):
996
+ raise self.make_error(
997
+ f"Function {func_name} expects {len(func.params)} arguments, got {len(args)}"
998
+ )
999
+
1000
+ result_lines = []
1001
+ for (param_name, param_type), arg in zip(func.params, args):
1002
+ global_name = func.get_param_global(param_name)
1003
+ result_lines.extend(self._param_to_global(prefix, global_name, arg, param_type))
1004
+
1005
+ result_lines.append(f"{prefix}call {func_name};")
1006
+
1007
+ return_global = func.get_return_global()
1008
+ return_types = func.get_return_types()
1009
+ result_lines.extend(
1010
+ self._global_to_var(prefix, result_var, return_global, return_types[0])
1011
+ )
1012
+
1013
+ return "\n".join(result_lines)
1014
+
1015
+ result = re.sub(tuple_pattern, replace_tuple_call, line)
1016
+ if result != line:
1017
+ return result
1018
+ return re.sub(single_pattern, replace_single_call, line)
1019
+
1020
+ def _expand_macros(self, line: str) -> str:
1021
+ """Expand all macro calls in a line."""
1022
+ # Keep expanding until no more macros found (handle nested macros)
1023
+ max_iterations = 10 # Prevent infinite loops
1024
+ iteration = 0
1025
+
1026
+ while iteration < max_iterations:
1027
+ new_line = self._expand_macros_once(line)
1028
+ if new_line == line:
1029
+ break # No more macros to expand
1030
+ line = new_line
1031
+ iteration += 1
1032
+
1033
+ if iteration >= max_iterations:
1034
+ raise self.make_error("Too many macro expansion iterations (possible recursive macros)")
1035
+
1036
+ return line
1037
+
1038
+ def _expand_macros_once(self, line: str) -> str:
1039
+ """Expand macros in a line once (one pass). Expand leftmost macro first."""
1040
+ # Find potential macro calls by looking for identifier followed by (
1041
+ pattern = r"\b(\w+)\s*\("
1042
+
1043
+ for match in re.finditer(pattern, line):
1044
+ macro_name = match.group(1)
1045
+
1046
+ # Check if this is a macro
1047
+ if macro_name not in self.macros:
1048
+ continue
1049
+
1050
+ # Find the matching closing parenthesis
1051
+ start_pos = match.end() # Position after the opening (
1052
+ paren_depth = 1
1053
+ pos = start_pos
1054
+
1055
+ while pos < len(line) and paren_depth > 0:
1056
+ if line[pos] == "(":
1057
+ paren_depth += 1
1058
+ elif line[pos] == ")":
1059
+ paren_depth -= 1
1060
+ pos += 1
1061
+
1062
+ if paren_depth != 0:
1063
+ # Unmatched parentheses - skip this match
1064
+ continue
1065
+
1066
+ # Extract arguments string (between parentheses)
1067
+ args_str = line[start_pos : pos - 1]
1068
+
1069
+ # Parse arguments
1070
+ args = []
1071
+ if args_str.strip():
1072
+ args = self._parse_macro_args(args_str)
1073
+
1074
+ # Expand the macro
1075
+ macro = self.macros[macro_name]
1076
+ try:
1077
+ expanded = macro.expand(args)
1078
+ self.log_debug(f"Expanded macro {macro_name}({args_str}) -> {expanded}", indent=3)
1079
+ except ValueError as e:
1080
+ raise self.make_error(str(e))
1081
+
1082
+ # Build result with the macro replaced
1083
+ result = line[: match.start()] + expanded + line[pos:]
1084
+ return result
1085
+
1086
+ # No macros found
1087
+ return line
1088
+
1089
+ def _parse_macro_args(self, args_str: str) -> list[str]:
1090
+ """Parse macro arguments, handling nested parentheses."""
1091
+ args = []
1092
+ current_arg = []
1093
+ depth = 0
1094
+
1095
+ for char in args_str:
1096
+ if char == "(":
1097
+ depth += 1
1098
+ current_arg.append(char)
1099
+ elif char == ")":
1100
+ depth -= 1
1101
+ current_arg.append(char)
1102
+ elif char == "," and depth == 0:
1103
+ # End of current argument
1104
+ args.append("".join(current_arg).strip())
1105
+ current_arg = []
1106
+ else:
1107
+ current_arg.append(char)
1108
+
1109
+ # Add last argument
1110
+ if current_arg:
1111
+ args.append("".join(current_arg).strip())
1112
+
1113
+ return args
1114
+
1115
+ def _param_to_global(
1116
+ self, prefix: str, global_name: str, arg: str, param_type: str
1117
+ ) -> list[str]:
1118
+ """Convert parameter to global with type conversion."""
1119
+ lines = []
1120
+ if param_type == "INTEGER":
1121
+ lines.append(f"{prefix}set {global_name} = std.itoa({arg});")
1122
+ elif param_type == "FLOAT":
1123
+ lines.append(f'{prefix}set {global_name} = "" + {arg};')
1124
+ elif param_type == "BOOL":
1125
+ lines.append(f"{prefix}if ({arg}) {{")
1126
+ lines.append(f'{prefix} set {global_name} = "true";')
1127
+ lines.append(f"{prefix}}} else {{")
1128
+ lines.append(f'{prefix} set {global_name} = "false";')
1129
+ lines.append(f"{prefix}}}")
1130
+ else:
1131
+ lines.append(f"{prefix}set {global_name} = {arg};")
1132
+ return lines
1133
+
1134
+ def _global_to_var(
1135
+ self, prefix: str, result_var: str, return_global: str, ret_type: str
1136
+ ) -> list[str]:
1137
+ """Convert global to variable with type conversion."""
1138
+ lines = []
1139
+ if ret_type == "INTEGER":
1140
+ lines.append(f"{prefix}set {result_var} = std.atoi({return_global});")
1141
+ elif ret_type == "FLOAT":
1142
+ lines.append(f"{prefix}set {result_var} = std.atof({return_global});")
1143
+ elif ret_type == "BOOL":
1144
+ lines.append(f'{prefix}set {result_var} = ({return_global} == "true");')
1145
+ else:
1146
+ lines.append(f"{prefix}set {result_var} = {return_global};")
1147
+ return lines
1148
+
1149
+ def _process_for_loop(
1150
+ self, lines: list[str], start: int, end: int, context: dict[str, Any]
1151
+ ) -> int:
1152
+ """Process a #for loop."""
1153
+ line = lines[start].strip()
1154
+
1155
+ match = re.match(r"#for\s+(\w+)\s+in\s+(.+)", line)
1156
+ if not match:
1157
+ raise self.make_error(f"Invalid #for syntax: {line}")
1158
+
1159
+ var_name = match.group(1)
1160
+ iterable_expr = match.group(2)
1161
+
1162
+ try:
1163
+ iterable = self._evaluate_expression(iterable_expr, context)
1164
+ except Exception as e:
1165
+ raise self.make_error(f"Error evaluating loop expression '{iterable_expr}': {e}")
1166
+
1167
+ try:
1168
+ loop_end = self._find_matching_end(lines, start, end, "#for", "#endfor")
1169
+ except SyntaxError as e:
1170
+ raise self.make_error(str(e))
1171
+
1172
+ self.log_debug(f"Loop iterating {len(list(iterable))} times", indent=2)
1173
+
1174
+ for idx, value in enumerate(iterable):
1175
+ self.log_debug(f"Iteration {idx}: {var_name} = {value}", indent=3)
1176
+ loop_context = context.copy()
1177
+ loop_context[var_name] = value
1178
+ self._process_lines(lines, start + 1, loop_end, loop_context)
1179
+
1180
+ return loop_end + 1
1181
+
1182
+ def _process_if(self, lines: list[str], start: int, end: int, context: dict[str, Any]) -> int:
1183
+ """Process a #if conditional."""
1184
+ line = lines[start].strip()
1185
+
1186
+ match = re.match(r"#if\s+(.+)", line)
1187
+ if not match:
1188
+ raise self.make_error(f"Invalid #if syntax: {line}")
1189
+
1190
+ condition = match.group(1)
1191
+
1192
+ try:
1193
+ result = self._evaluate_expression(condition, context)
1194
+ except Exception as e:
1195
+ raise self.make_error(f"Error evaluating condition '{condition}': {e}")
1196
+
1197
+ self.log_debug(f"Condition '{condition}' evaluated to {result}", indent=2)
1198
+
1199
+ else_idx = None
1200
+ try:
1201
+ endif_idx = self._find_matching_end(lines, start, end, "#if", "#endif")
1202
+ except SyntaxError as e:
1203
+ raise self.make_error(str(e))
1204
+
1205
+ depth = 0
1206
+ for i in range(start, endif_idx):
1207
+ stripped = lines[i].strip()
1208
+ if stripped.startswith("#if"):
1209
+ depth += 1
1210
+ elif stripped == "#endif":
1211
+ depth -= 1
1212
+ elif stripped == "#else" and depth == 1:
1213
+ else_idx = i
1214
+ break
1215
+
1216
+ if result:
1217
+ branch_end = else_idx if else_idx else endif_idx
1218
+ self.log_debug("Taking if branch", indent=2)
1219
+ self._process_lines(lines, start + 1, branch_end, context)
1220
+ else:
1221
+ if else_idx:
1222
+ self.log_debug("Taking else branch", indent=2)
1223
+ self._process_lines(lines, else_idx + 1, endif_idx, context)
1224
+ else:
1225
+ self.log_debug("Skipping if block", indent=2)
1226
+
1227
+ return endif_idx + 1
1228
+
1229
+ def _find_matching_end(
1230
+ self, lines: list[str], start: int, end: int, open_keyword: str, close_keyword: str
1231
+ ) -> int:
1232
+ """Find the matching closing keyword for a block."""
1233
+ depth = 0
1234
+ for i in range(start, end):
1235
+ stripped = lines[i].strip()
1236
+ if stripped.startswith(open_keyword):
1237
+ depth += 1
1238
+ elif stripped == close_keyword:
1239
+ depth -= 1
1240
+ if depth == 0:
1241
+ return i
1242
+
1243
+ raise SyntaxError(f"No matching {close_keyword} for {open_keyword} at line {start + 1}")
1244
+
1245
+ def _substitute_expressions(self, line: str, context: dict[str, Any]) -> str:
1246
+ """Substitute {{expression}} in a line."""
1247
+
1248
+ def replace_expr(match):
1249
+ expr = match.group(1)
1250
+ try:
1251
+ value = self._evaluate_expression(expr, context)
1252
+ except Exception as e:
1253
+ raise self.make_error(f"Error evaluating expression '{expr}': {e}")
1254
+ return str(value)
1255
+
1256
+ return re.sub(r"\{\{(.+?)\}\}", replace_expr, line)
1257
+
1258
+ def _evaluate_expression(self, expr: str, context: dict[str, Any]) -> Any:
1259
+ """Safely evaluate an expression in the given context."""
1260
+ try:
1261
+ safe_globals = {
1262
+ "range": range,
1263
+ "len": len,
1264
+ "str": str,
1265
+ "int": int,
1266
+ "hex": hex,
1267
+ "format": format,
1268
+ "abs": abs,
1269
+ "min": min,
1270
+ "max": max,
1271
+ "enumerate": enumerate,
1272
+ # Boolean literals (both Python and C-style)
1273
+ "True": True,
1274
+ "False": False,
1275
+ "true": True,
1276
+ "false": False,
1277
+ }
1278
+
1279
+ # Merge constants into context
1280
+ eval_env = {**safe_globals, **self.constants, **context}
1281
+ result = eval(expr, {"__builtins__": {}}, eval_env)
1282
+
1283
+ return result
1284
+ except NameError as e:
1285
+ # Provide helpful suggestions
1286
+ var_name = str(e).split("'")[1] if "'" in str(e) else ""
1287
+ available_names = (
1288
+ list(safe_globals.keys()) + list(self.constants.keys()) + list(context.keys())
1289
+ )
1290
+ suggestions = get_close_matches(var_name, available_names, n=3, cutoff=0.6)
1291
+
1292
+ error_msg = f"Name '{var_name}' is not defined"
1293
+ if suggestions:
1294
+ error_msg += f"\n Did you mean: {', '.join(suggestions)}?"
1295
+ error_msg += f"\n Available: {', '.join(sorted(available_names))}"
1296
+
1297
+ raise NameError(error_msg)
1298
+ except Exception as e:
1299
+ raise ValueError(f"Error evaluating expression '{expr}': {e}")
1300
+
1301
+
1302
+ def main():
1303
+ """Main entry point."""
1304
+ parser = argparse.ArgumentParser(
1305
+ description="xvcl - Extended VCL compiler with metaprogramming features",
1306
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1307
+ epilog="""
1308
+ Features:
1309
+ - For loops: #for var in range(n)
1310
+ - Conditionals: #if condition
1311
+ - Templates: {{expression}}
1312
+ - Constants: #const NAME TYPE = value
1313
+ - Includes: #include "path/to/file.xvcl"
1314
+ - Inline macros: #inline name(params) ... #endinline
1315
+ - Functions: #def name(params) -> TYPE
1316
+ - Variables: #let name TYPE = expression;
1317
+
1318
+ Example:
1319
+ xvcl input.xvcl -o output.vcl
1320
+ xvcl input.xvcl -o output.vcl --debug
1321
+ xvcl input.xvcl -o output.vcl -I /path/to/includes
1322
+ """,
1323
+ )
1324
+
1325
+ parser.add_argument("input", help="Input XVCL file")
1326
+ parser.add_argument("-o", "--output", help="Output VCL file (default: removes .xvcl extension)")
1327
+ parser.add_argument(
1328
+ "-I",
1329
+ "--include",
1330
+ dest="include_paths",
1331
+ action="append",
1332
+ help="Add include search path (can be specified multiple times)",
1333
+ )
1334
+ parser.add_argument("--debug", action="store_true", help="Enable debug mode (verbose output)")
1335
+ parser.add_argument(
1336
+ "--source-maps", action="store_true", help="Add source map comments to generated code"
1337
+ )
1338
+ parser.add_argument(
1339
+ "-v", "--verbose", action="store_true", help="Verbose output (alias for --debug)"
1340
+ )
1341
+
1342
+ args = parser.parse_args()
1343
+
1344
+ # Determine output path
1345
+ if args.output:
1346
+ output_path = args.output
1347
+ elif args.input.endswith(".xvcl"):
1348
+ output_path = args.input.replace(".xvcl", ".vcl")
1349
+ else:
1350
+ output_path = args.input + ".vcl"
1351
+
1352
+ # Set up include paths
1353
+ include_paths = args.include_paths or ["."]
1354
+
1355
+ # Enable debug if verbose flag is used
1356
+ debug = args.debug or args.verbose
1357
+
1358
+ try:
1359
+ compiler = XVCLCompiler(
1360
+ include_paths=include_paths, debug=debug, source_maps=args.source_maps
1361
+ )
1362
+ compiler.process_file(args.input, output_path)
1363
+
1364
+ print(f"{Colors.GREEN}{Colors.BOLD}✓ Compilation complete{Colors.RESET}")
1365
+
1366
+ except PreprocessorError as e:
1367
+ print(e.format_error(use_colors=True), file=sys.stderr)
1368
+ sys.exit(1)
1369
+ except FileNotFoundError as e:
1370
+ print(f"{Colors.RED}Error:{Colors.RESET} {e}", file=sys.stderr)
1371
+ sys.exit(1)
1372
+ except Exception as e:
1373
+ print(f"{Colors.RED}Unexpected error:{Colors.RESET} {e}", file=sys.stderr)
1374
+ import traceback
1375
+
1376
+ traceback.print_exc()
1377
+ sys.exit(1)
1378
+
1379
+
1380
+ if __name__ == "__main__":
1381
+ main()