|
@@ -0,0 +1,92 @@
|
|
|
|
+# today we're gonna write a preprocessor for TIC-80 shit
|
|
|
|
+# support: #include, #define, overloaded ops (+=, -=, *=, /=)
|
|
|
|
+# license: MIT
|
|
|
|
+import os
|
|
|
|
+import re
|
|
|
|
+from typing import Dict, Set
|
|
|
|
+
|
|
|
|
+class LuaPreprocessor:
|
|
|
|
+ def __init__(self):
|
|
|
|
+ self.defines: Dict[str, str] = {}
|
|
|
|
+ self.included_files: Set[str] = set()
|
|
|
|
+
|
|
|
|
+ def process_file(self, filename: str) -> str:
|
|
|
|
+ if filename in self.included_files:
|
|
|
|
+ raise RecursionError(f"Circular reference detected: {filename}")
|
|
|
|
+
|
|
|
|
+ self.included_files.add(filename)
|
|
|
|
+
|
|
|
|
+ try:
|
|
|
|
+ with open(filename, 'r') as f:
|
|
|
|
+ content = f.read()
|
|
|
|
+ except FileNotFoundError:
|
|
|
|
+ raise FileNotFoundError(f"Include file not found: {filename}")
|
|
|
|
+
|
|
|
|
+ base_dir = os.path.dirname(os.path.abspath(filename))
|
|
|
|
+ # Process lines
|
|
|
|
+ lines = content.split('\n')
|
|
|
|
+ processed_lines = []
|
|
|
|
+
|
|
|
|
+ for line in lines:
|
|
|
|
+ # Strip comments except TIC metadata
|
|
|
|
+ tic_comment = re.match(r'-- \S+:\s+.+', line)
|
|
|
|
+ if not tic_comment:
|
|
|
|
+ if '--' in line:
|
|
|
|
+ line = line[:line.index('--')]
|
|
|
|
+
|
|
|
|
+ # Handle #defines and store for later macro replacement
|
|
|
|
+ define_match = re.match(r'^\s*#define\s+(\w+)\s+(.+)$', line)
|
|
|
|
+ if define_match:
|
|
|
|
+ macro_name, macro_value = define_match.groups()
|
|
|
|
+ self.defines[macro_name] = macro_value.strip()
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ # Handle #includes
|
|
|
|
+ include_match = re.match(r'^\s*#include\s+"([^"]+)"$', line)
|
|
|
|
+ if include_match:
|
|
|
|
+ include_path = include_match.group(1)
|
|
|
|
+ abs_path = os.path.join(base_dir, include_path)
|
|
|
|
+ processed_lines.append(self.process_file(abs_path))
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ # Do the later macro replacement
|
|
|
|
+ for macro_name, macro_value in self.defines.items():
|
|
|
|
+ line = re.sub(r'\b' + macro_name + r'\b', macro_value, line)
|
|
|
|
+
|
|
|
|
+ # Handle overload operators that don't exist in Lua
|
|
|
|
+ operator_match = re.match(r'(\S+)\s?(\+|-)=\s?(\S+)', line)
|
|
|
|
+ if operator_match:
|
|
|
|
+ op_var, op_type, op_inc = operator_match.groups()
|
|
|
|
+ replace_str = op_var + "=" + op_var + op_type + op_inc
|
|
|
|
+ processed_lines.append(line.replace(operator_match[0], replace_str))
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ if line.strip() == "":
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ processed_lines.append(line)
|
|
|
|
+
|
|
|
|
+ return '\n'.join(processed_lines)
|
|
|
|
+
|
|
|
|
+def preprocess_lua(source_file: str) -> str:
|
|
|
|
+ """
|
|
|
|
+ Parse a C-style Lua file #define and #include directives into an actual Lua file that doesn't barf.
|
|
|
|
+ Also parses basic assigment operators, (+=, -=, *=, /=)
|
|
|
|
+
|
|
|
|
+ Args:
|
|
|
|
+ source_file (str): Path to the root Lua file
|
|
|
|
+
|
|
|
|
+ Returns:
|
|
|
|
+ str: Parsed Lua
|
|
|
|
+
|
|
|
|
+ Raises:
|
|
|
|
+ FileNotFoundError: If source file or included file is not found
|
|
|
|
+ RecursionError: If you're dumb and make a circular reference
|
|
|
|
+ """
|
|
|
|
+ preprocessor = LuaPreprocessor()
|
|
|
|
+ return preprocessor.process_file(source_file)
|
|
|
|
+
|
|
|
|
+lambda: os.system('cls')
|
|
|
|
+parseoutput = preprocess_lua("pre-tests.lua")
|
|
|
|
+print("Output:\n%s" % parseoutput)
|
|
|
|
+
|