Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- app.py +481 -20
- compiler.py +40 -6
app.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
import subprocess
|
| 2 |
import sys
|
| 3 |
import os
|
|
|
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
from fastapi import FastAPI, HTTPException, Request # type: ignore
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware # type: ignore
|
| 8 |
-
from pydantic import BaseModel # type: ignore
|
| 9 |
from slowapi import Limiter, _rate_limit_exceeded_handler # type: ignore
|
| 10 |
from slowapi.util import get_remote_address # type: ignore
|
| 11 |
from slowapi.errors import RateLimitExceeded # type: ignore
|
|
@@ -63,6 +65,313 @@ class PerformanceRequest(BaseModel):
|
|
| 63 |
inputs: list[int] = []
|
| 64 |
password: str # Secret password for authentication (required)
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
@app.get("/")
|
| 67 |
@limiter.limit("20/minute")
|
| 68 |
async def root(request: Request):
|
|
@@ -87,38 +396,136 @@ async def run_eraslang(request: Request, body: PerformanceRequest):
|
|
| 87 |
4. Returns results or errors
|
| 88 |
"""
|
| 89 |
|
| 90 |
-
# --- Step 0:
|
| 91 |
if not body.password:
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
if body.password != API_SECRET:
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
# --- Step 1: Transpilation ---
|
| 104 |
try:
|
| 105 |
# We ensure the compiler adds # ERAS_LINE_X comments for our worker
|
| 106 |
python_source = compiler.transpile(body.code)
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
# If inputs are provided, inject an input provider function
|
| 109 |
-
if body.inputs:
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
# Replace all int(input()) calls with our custom input function
|
| 112 |
python_source = re.sub(
|
| 113 |
r'int\s*\(\s*input\s*\(\s*\)\s*\)',
|
| 114 |
'int(_eras_input())',
|
| 115 |
python_source
|
| 116 |
)
|
|
|
|
| 117 |
# Prepend the input provider code
|
| 118 |
input_provider = f"""
|
| 119 |
# Input provider for prefilled inputs
|
| 120 |
_input_index = 0
|
| 121 |
-
_input_values = {body.inputs}
|
| 122 |
|
| 123 |
def _eras_input():
|
| 124 |
global _input_index
|
|
@@ -131,18 +538,32 @@ def _eras_input():
|
|
| 131 |
|
| 132 |
"""
|
| 133 |
python_source = input_provider + python_source
|
|
|
|
| 134 |
except Exception as e:
|
| 135 |
-
#
|
|
|
|
| 136 |
return {
|
| 137 |
"status": "transpilation_error",
|
| 138 |
"output": "",
|
| 139 |
-
"error":
|
| 140 |
}
|
| 141 |
|
| 142 |
# --- Step 2: Subprocess Execution ---
|
| 143 |
try:
|
| 144 |
# We call worker.py and pipe the python_source into its stdin
|
| 145 |
worker_path = os.path.join(os.path.dirname(__file__), "worker.py")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
process = subprocess.Popen(
|
| 147 |
[sys.executable, worker_path],
|
| 148 |
stdin=subprocess.PIPE,
|
|
@@ -159,10 +580,16 @@ def _eras_input():
|
|
| 159 |
)
|
| 160 |
except subprocess.TimeoutExpired:
|
| 161 |
process.kill()
|
|
|
|
| 162 |
return {
|
| 163 |
"status": "timeout",
|
| 164 |
-
"output": "
|
| 165 |
-
"error":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
# --- Step 3: Handle Results ---
|
|
@@ -174,14 +601,48 @@ def _eras_input():
|
|
| 174 |
}
|
| 175 |
else:
|
| 176 |
# The worker.py prints thematic errors to stdout on failure
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
return {
|
| 178 |
"status": "runtime_error",
|
| 179 |
-
"output": stdout_data,
|
| 180 |
-
"error":
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
except Exception as e:
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
if __name__ == "__main__":
|
| 187 |
import uvicorn # type: ignore
|
|
|
|
| 1 |
import subprocess
|
| 2 |
import sys
|
| 3 |
import os
|
| 4 |
+
import re
|
| 5 |
+
import traceback
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
from fastapi import FastAPI, HTTPException, Request # type: ignore
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware # type: ignore
|
| 10 |
+
from pydantic import BaseModel, ValidationError # type: ignore
|
| 11 |
from slowapi import Limiter, _rate_limit_exceeded_handler # type: ignore
|
| 12 |
from slowapi.util import get_remote_address # type: ignore
|
| 13 |
from slowapi.errors import RateLimitExceeded # type: ignore
|
|
|
|
| 65 |
inputs: list[int] = []
|
| 66 |
password: str # Secret password for authentication (required)
|
| 67 |
|
| 68 |
+
def parse_error_message(error_output: str) -> dict:
|
| 69 |
+
"""
|
| 70 |
+
Parse error messages from worker.py to extract structured information.
|
| 71 |
+
Returns a dictionary with error details.
|
| 72 |
+
"""
|
| 73 |
+
error_info = {
|
| 74 |
+
"message": error_output.strip(),
|
| 75 |
+
"type": "unknown",
|
| 76 |
+
"line": None,
|
| 77 |
+
"variable": None,
|
| 78 |
+
"suggestion": None
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# Parse EXILE ERROR (ZeroDivisionError)
|
| 82 |
+
if "EXILE ERROR" in error_output:
|
| 83 |
+
error_info["type"] = "division_by_zero"
|
| 84 |
+
error_info["message"] = "You tried to divide by zero. You're on your own, kid."
|
| 85 |
+
error_info["suggestion"] = "Check your math operations - make sure you're not dividing by zero."
|
| 86 |
+
|
| 87 |
+
# Parse VAULT ERROR (NameError)
|
| 88 |
+
elif "VAULT ERROR" in error_output:
|
| 89 |
+
error_info["type"] = "undefined_variable"
|
| 90 |
+
# Extract variable name and line number
|
| 91 |
+
var_match = re.search(r"'(.+?)' was never declared at Line (\d+)", error_output)
|
| 92 |
+
if var_match:
|
| 93 |
+
error_info["variable"] = var_match.group(1)
|
| 94 |
+
error_info["line"] = var_match.group(2)
|
| 95 |
+
error_info["message"] = f"Variable '{error_info['variable']}' was never declared at Line {error_info['line']}. You left a Blank Space."
|
| 96 |
+
error_info["suggestion"] = f"Use 'BLANK SPACE {error_info['variable']}' to declare the variable before using it."
|
| 97 |
+
else:
|
| 98 |
+
error_info["message"] = error_output.strip()
|
| 99 |
+
|
| 100 |
+
# Parse BAD BLOOD (SyntaxError)
|
| 101 |
+
elif "BAD BLOOD" in error_output:
|
| 102 |
+
error_info["type"] = "syntax_error"
|
| 103 |
+
line_match = re.search(r"Line (\d+)", error_output)
|
| 104 |
+
if line_match:
|
| 105 |
+
error_info["line"] = line_match.group(1)
|
| 106 |
+
error_info["message"] = f"The bridge is broken. Syntax error near Line {error_info['line']}."
|
| 107 |
+
else:
|
| 108 |
+
error_info["message"] = error_output.strip()
|
| 109 |
+
error_info["suggestion"] = "Check your ErasLang syntax - make sure all keywords are spelled correctly and properly formatted."
|
| 110 |
+
|
| 111 |
+
# Parse RUNTIME ERROR
|
| 112 |
+
elif "RUNTIME ERROR" in error_output:
|
| 113 |
+
error_info["type"] = "runtime_error"
|
| 114 |
+
line_match = re.search(r"Line (\d+):", error_output)
|
| 115 |
+
if line_match:
|
| 116 |
+
error_info["line"] = line_match.group(1)
|
| 117 |
+
# Extract the actual error message
|
| 118 |
+
msg_match = re.search(r"RUNTIME ERROR at Line \d+: (.+)", error_output)
|
| 119 |
+
if msg_match:
|
| 120 |
+
error_info["message"] = msg_match.group(1).strip()
|
| 121 |
+
else:
|
| 122 |
+
error_info["message"] = error_output.strip()
|
| 123 |
+
error_info["suggestion"] = "Check your code logic - there may be an issue with how variables are being used."
|
| 124 |
+
|
| 125 |
+
# Parse ValueError (not enough inputs)
|
| 126 |
+
elif "Not enough inputs provided" in error_output:
|
| 127 |
+
error_info["type"] = "insufficient_inputs"
|
| 128 |
+
error_info["message"] = "Not enough inputs provided. Expected more QUESTION...? calls."
|
| 129 |
+
error_info["suggestion"] = "Make sure you provide enough input values in the inputs array for all QUESTION...? calls in your code."
|
| 130 |
+
|
| 131 |
+
# Default: use the error output as-is
|
| 132 |
+
else:
|
| 133 |
+
error_info["message"] = error_output.strip()
|
| 134 |
+
if not error_info["message"]:
|
| 135 |
+
error_info["message"] = "An unknown error occurred during execution."
|
| 136 |
+
error_info["suggestion"] = "Please check your code and try again."
|
| 137 |
+
|
| 138 |
+
return error_info
|
| 139 |
+
|
| 140 |
+
def format_transpilation_error(error: Exception) -> dict:
|
| 141 |
+
"""
|
| 142 |
+
Format transpilation errors with helpful context.
|
| 143 |
+
"""
|
| 144 |
+
error_info = {
|
| 145 |
+
"message": str(error),
|
| 146 |
+
"type": "transpilation_error",
|
| 147 |
+
"line": None,
|
| 148 |
+
"suggestion": None
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
error_str = str(error)
|
| 152 |
+
|
| 153 |
+
# Try to extract line number from error message
|
| 154 |
+
line_match = re.search(r'line (\d+)', error_str, re.IGNORECASE)
|
| 155 |
+
if line_match:
|
| 156 |
+
error_info["line"] = line_match.group(1)
|
| 157 |
+
|
| 158 |
+
# Provide specific suggestions based on error type
|
| 159 |
+
if "not defined" in error_str.lower() or "name" in error_str.lower():
|
| 160 |
+
error_info["suggestion"] = "Make sure all variables are declared with 'BLANK SPACE' before use."
|
| 161 |
+
elif "syntax" in error_str.lower():
|
| 162 |
+
error_info["suggestion"] = "Check your ErasLang syntax. Make sure all keywords are spelled correctly."
|
| 163 |
+
elif "unexpected" in error_str.lower():
|
| 164 |
+
error_info["suggestion"] = "There's an unexpected token in your code. Check for typos or missing keywords."
|
| 165 |
+
else:
|
| 166 |
+
error_info["suggestion"] = "Review your ErasLang code for syntax errors or unrecognized keywords."
|
| 167 |
+
|
| 168 |
+
return error_info
|
| 169 |
+
|
| 170 |
+
def validate_code_structure(code: str) -> dict:
|
| 171 |
+
"""
|
| 172 |
+
Validate ErasLang code structure before transpilation.
|
| 173 |
+
Returns None if valid, or error dict if invalid.
|
| 174 |
+
"""
|
| 175 |
+
lines = code.splitlines()
|
| 176 |
+
non_empty_lines = [line.strip() for line in lines if line.strip() and not line.strip().startswith("DEAR JOHN")]
|
| 177 |
+
|
| 178 |
+
if not non_empty_lines:
|
| 179 |
+
return {
|
| 180 |
+
"message": "Code contains no valid ErasLang instructions. Only comments or empty lines found.",
|
| 181 |
+
"type": "empty_code",
|
| 182 |
+
"suggestion": "Add ErasLang code starting with 'ARE YOU READY FOR IT?' and ending with 'LONG LIVE'."
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
# Check for BEGIN_MAIN
|
| 186 |
+
has_begin_main = any("ARE YOU READY FOR IT?" in line for line in lines)
|
| 187 |
+
if not has_begin_main:
|
| 188 |
+
return {
|
| 189 |
+
"message": "Code is missing 'ARE YOU READY FOR IT?' - every ErasLang program must start with this.",
|
| 190 |
+
"type": "missing_begin_main",
|
| 191 |
+
"suggestion": "Start your code with 'ARE YOU READY FOR IT?' to begin the main function."
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
# Check for END_MAIN
|
| 195 |
+
has_end_main = any("LONG LIVE" in line for line in lines)
|
| 196 |
+
if not has_end_main:
|
| 197 |
+
return {
|
| 198 |
+
"message": "Code is missing 'LONG LIVE' - every ErasLang program must end with this.",
|
| 199 |
+
"type": "missing_end_main",
|
| 200 |
+
"suggestion": "End your code with 'LONG LIVE' to terminate the program."
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
# Check for unmatched blocks
|
| 204 |
+
begin_main_count = sum(1 for line in lines if "ARE YOU READY FOR IT?" in line)
|
| 205 |
+
end_main_count = sum(1 for line in lines if "LONG LIVE" in line)
|
| 206 |
+
|
| 207 |
+
if begin_main_count > end_main_count:
|
| 208 |
+
return {
|
| 209 |
+
"message": f"Found {begin_main_count} 'ARE YOU READY FOR IT?' but only {end_main_count} 'LONG LIVE'. Blocks are not properly closed.",
|
| 210 |
+
"type": "unmatched_blocks",
|
| 211 |
+
"suggestion": "Make sure every 'ARE YOU READY FOR IT?' has a matching 'LONG LIVE'."
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
if end_main_count > begin_main_count:
|
| 215 |
+
return {
|
| 216 |
+
"message": f"Found {end_main_count} 'LONG LIVE' but only {begin_main_count} 'ARE YOU READY FOR IT?'. Too many closing statements.",
|
| 217 |
+
"type": "unmatched_blocks",
|
| 218 |
+
"suggestion": "Make sure you have the correct number of 'LONG LIVE' statements matching your 'ARE YOU READY FOR IT?' statements."
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
# Check for unmatched IF blocks
|
| 222 |
+
if_count = sum(1 for line in lines if "I KNEW YOU WERE TROUBLE" in line)
|
| 223 |
+
end_if_count = sum(1 for line in lines if line.strip() == "EXILE")
|
| 224 |
+
|
| 225 |
+
if if_count != end_if_count:
|
| 226 |
+
return {
|
| 227 |
+
"message": f"Found {if_count} 'I KNEW YOU WERE TROUBLE' but {end_if_count} 'EXILE'. IF blocks are not properly closed.",
|
| 228 |
+
"type": "unmatched_if_blocks",
|
| 229 |
+
"suggestion": "Make sure every 'I KNEW YOU WERE TROUBLE' has a matching 'EXILE' to close the IF block."
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
# Check for unmatched WHILE blocks
|
| 233 |
+
while_count = sum(1 for line in lines if "IS IT OVER NOW?" in line)
|
| 234 |
+
end_while_count = sum(1 for line in lines if "OUT OF THE WOODS" in line)
|
| 235 |
+
|
| 236 |
+
if while_count != end_while_count:
|
| 237 |
+
return {
|
| 238 |
+
"message": f"Found {while_count} 'IS IT OVER NOW?' but {end_while_count} 'OUT OF THE WOODS'. WHILE loops are not properly closed.",
|
| 239 |
+
"type": "unmatched_while_blocks",
|
| 240 |
+
"suggestion": "Make sure every 'IS IT OVER NOW?' has a matching 'OUT OF THE WOODS' to close the WHILE loop."
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
# Check for unmatched MATH blocks
|
| 244 |
+
begin_math_count = sum(1 for line in lines if "SHAKE IT OFF" in line)
|
| 245 |
+
end_math_count = sum(1 for line in lines if "CALL IT WHAT YOU WANT" in line)
|
| 246 |
+
|
| 247 |
+
if begin_math_count != end_math_count:
|
| 248 |
+
return {
|
| 249 |
+
"message": f"Found {begin_math_count} 'SHAKE IT OFF' but {end_math_count} 'CALL IT WHAT YOU WANT'. Math blocks are not properly closed.",
|
| 250 |
+
"type": "unmatched_math_blocks",
|
| 251 |
+
"suggestion": "Make sure every 'SHAKE IT OFF' has a matching 'CALL IT WHAT YOU WANT' to close the math block."
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
# Check for unmatched FUNCTION blocks
|
| 255 |
+
begin_func_count = sum(1 for line in lines if "FROM THE VAULT" in line)
|
| 256 |
+
end_func_count = sum(1 for line in lines if "CLEAN" in line)
|
| 257 |
+
|
| 258 |
+
if begin_func_count != end_func_count:
|
| 259 |
+
return {
|
| 260 |
+
"message": f"Found {begin_func_count} 'FROM THE VAULT' but {end_func_count} 'CLEAN'. Function blocks are not properly closed.",
|
| 261 |
+
"type": "unmatched_function_blocks",
|
| 262 |
+
"suggestion": "Make sure every 'FROM THE VAULT' has a matching 'CLEAN' to close the function definition."
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
return None # Code structure is valid
|
| 266 |
+
|
| 267 |
+
def count_input_calls(code: str) -> int:
|
| 268 |
+
"""
|
| 269 |
+
Count the number of QUESTION...? calls in the code.
|
| 270 |
+
"""
|
| 271 |
+
# Count standalone QUESTION...? (INPUT token)
|
| 272 |
+
standalone_count = sum(1 for line in code.splitlines() if line.strip() == "QUESTION...?")
|
| 273 |
+
|
| 274 |
+
# Count QUESTION...? in "YOU BELONG WITH ME QUESTION...?"
|
| 275 |
+
inline_count = sum(1 for line in code.splitlines() if "YOU BELONG WITH ME QUESTION...?" in line)
|
| 276 |
+
|
| 277 |
+
return standalone_count + inline_count
|
| 278 |
+
|
| 279 |
+
def validate_inputs(code: str, inputs: list[int]) -> dict:
|
| 280 |
+
"""
|
| 281 |
+
Validate that the inputs array matches the number of QUESTION...? calls.
|
| 282 |
+
Returns None if valid, or error dict if invalid.
|
| 283 |
+
"""
|
| 284 |
+
input_calls = count_input_calls(code)
|
| 285 |
+
|
| 286 |
+
if input_calls == 0:
|
| 287 |
+
# No inputs needed, but if inputs are provided, warn
|
| 288 |
+
if inputs and len(inputs) > 0:
|
| 289 |
+
return {
|
| 290 |
+
"message": f"Your code has no QUESTION...? calls, but you provided {len(inputs)} input value(s). These will be ignored.",
|
| 291 |
+
"type": "unused_inputs",
|
| 292 |
+
"suggestion": "Remove the inputs array or add QUESTION...? calls to your code if you need user input."
|
| 293 |
+
}
|
| 294 |
+
return None # No inputs needed, none provided - valid
|
| 295 |
+
|
| 296 |
+
# Code requires inputs
|
| 297 |
+
if not inputs or len(inputs) == 0:
|
| 298 |
+
return {
|
| 299 |
+
"message": f"Your code has {input_calls} QUESTION...? call(s) but no input values were provided.",
|
| 300 |
+
"type": "missing_inputs",
|
| 301 |
+
"suggestion": f"Provide {input_calls} integer value(s) in the inputs array to match your QUESTION...? calls."
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
if len(inputs) < input_calls:
|
| 305 |
+
return {
|
| 306 |
+
"message": f"Your code has {input_calls} QUESTION...? call(s) but only {len(inputs)} input value(s) were provided.",
|
| 307 |
+
"type": "insufficient_inputs",
|
| 308 |
+
"suggestion": f"Provide {input_calls} integer value(s) in the inputs array. You need {input_calls - len(inputs)} more value(s)."
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
if len(inputs) > input_calls:
|
| 312 |
+
return {
|
| 313 |
+
"message": f"Your code has {input_calls} QUESTION...? call(s) but {len(inputs)} input value(s) were provided. Extra inputs will be ignored.",
|
| 314 |
+
"type": "excess_inputs",
|
| 315 |
+
"suggestion": f"Provide exactly {input_calls} integer value(s) in the inputs array, or remove the extra {len(inputs) - input_calls} value(s)."
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
return None # Inputs match perfectly
|
| 319 |
+
|
| 320 |
+
def validate_syntax(code: str) -> dict:
|
| 321 |
+
"""
|
| 322 |
+
Perform basic syntax validation before transpilation.
|
| 323 |
+
Returns None if valid, or error dict if issues found.
|
| 324 |
+
"""
|
| 325 |
+
lines = code.splitlines()
|
| 326 |
+
|
| 327 |
+
# Check for common syntax issues
|
| 328 |
+
for i, line in enumerate(lines, 1):
|
| 329 |
+
stripped = line.strip()
|
| 330 |
+
if not stripped or stripped.startswith("DEAR JOHN"):
|
| 331 |
+
continue
|
| 332 |
+
|
| 333 |
+
# Check for invalid variable names in BLANK SPACE
|
| 334 |
+
if stripped.startswith("BLANK SPACE"):
|
| 335 |
+
var_name = stripped.replace("BLANK SPACE", "").strip()
|
| 336 |
+
if not var_name:
|
| 337 |
+
return {
|
| 338 |
+
"message": f"Line {i}: 'BLANK SPACE' requires a variable name.",
|
| 339 |
+
"type": "syntax_error",
|
| 340 |
+
"line": str(i),
|
| 341 |
+
"suggestion": "Use 'BLANK SPACE [variable_name]' to declare a variable. Example: 'BLANK SPACE myVar'"
|
| 342 |
+
}
|
| 343 |
+
# Check for invalid characters in variable name
|
| 344 |
+
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_name):
|
| 345 |
+
return {
|
| 346 |
+
"message": f"Line {i}: Invalid variable name '{var_name}'. Variable names must start with a letter or underscore and contain only letters, numbers, and underscores.",
|
| 347 |
+
"type": "syntax_error",
|
| 348 |
+
"line": str(i),
|
| 349 |
+
"suggestion": f"Use a valid variable name like 'myVar', 'x', or '_temp'. Invalid: '{var_name}'"
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
# Check for invalid usage of QUESTION...? without BLANK SPACE
|
| 353 |
+
if stripped == "QUESTION...?":
|
| 354 |
+
# This is valid as standalone, but check context
|
| 355 |
+
# Look backwards for BLANK SPACE
|
| 356 |
+
found_declaration = False
|
| 357 |
+
for j in range(i - 1, max(0, i - 10), -1):
|
| 358 |
+
prev_line = lines[j].strip()
|
| 359 |
+
if prev_line.startswith("BLANK SPACE"):
|
| 360 |
+
found_declaration = True
|
| 361 |
+
break
|
| 362 |
+
if prev_line and not prev_line.startswith("DEAR JOHN"):
|
| 363 |
+
break
|
| 364 |
+
|
| 365 |
+
if not found_declaration:
|
| 366 |
+
return {
|
| 367 |
+
"message": f"Line {i}: 'QUESTION...?' is used without a preceding variable declaration.",
|
| 368 |
+
"type": "syntax_error",
|
| 369 |
+
"line": str(i),
|
| 370 |
+
"suggestion": "Declare a variable with 'BLANK SPACE [name]' before using 'QUESTION...?', or use 'YOU BELONG WITH ME QUESTION...?' to assign input to the last declared variable."
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
return None # Syntax appears valid
|
| 374 |
+
|
| 375 |
@app.get("/")
|
| 376 |
@limiter.limit("20/minute")
|
| 377 |
async def root(request: Request):
|
|
|
|
| 396 |
4. Returns results or errors
|
| 397 |
"""
|
| 398 |
|
| 399 |
+
# --- Step 0: Input Validation ---
|
| 400 |
if not body.password:
|
| 401 |
+
return {
|
| 402 |
+
"status": "authentication_error",
|
| 403 |
+
"output": "",
|
| 404 |
+
"error": {
|
| 405 |
+
"message": "Password is required for authentication.",
|
| 406 |
+
"type": "missing_password",
|
| 407 |
+
"suggestion": "Please provide a valid password in your request."
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
|
| 411 |
if body.password != API_SECRET:
|
| 412 |
+
return {
|
| 413 |
+
"status": "authentication_error",
|
| 414 |
+
"output": "",
|
| 415 |
+
"error": {
|
| 416 |
+
"message": "Invalid password. Access denied.",
|
| 417 |
+
"type": "invalid_password",
|
| 418 |
+
"suggestion": "Please check your password and try again."
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
# Validate code is not empty
|
| 423 |
+
if not body.code or not body.code.strip():
|
| 424 |
+
return {
|
| 425 |
+
"status": "validation_error",
|
| 426 |
+
"output": "",
|
| 427 |
+
"error": {
|
| 428 |
+
"message": "Code cannot be empty. Please provide ErasLang code to execute.",
|
| 429 |
+
"type": "empty_code",
|
| 430 |
+
"suggestion": "Write some ErasLang code starting with 'ARE YOU READY FOR IT?' and ending with 'LONG LIVE'."
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
# --- Step 0.5: Pre-transpilation Validation ---
|
| 435 |
+
# Validate code structure
|
| 436 |
+
structure_error = validate_code_structure(body.code)
|
| 437 |
+
if structure_error:
|
| 438 |
+
return {
|
| 439 |
+
"status": "validation_error",
|
| 440 |
+
"output": "",
|
| 441 |
+
"error": structure_error
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
# Validate syntax
|
| 445 |
+
syntax_error = validate_syntax(body.code)
|
| 446 |
+
if syntax_error:
|
| 447 |
+
return {
|
| 448 |
+
"status": "validation_error",
|
| 449 |
+
"output": "",
|
| 450 |
+
"error": syntax_error
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
# Validate inputs match QUESTION...? calls
|
| 454 |
+
input_error = validate_inputs(body.code, body.inputs)
|
| 455 |
+
if input_error:
|
| 456 |
+
# For unused/excess inputs, we can still proceed but warn
|
| 457 |
+
# For missing/insufficient inputs, we should error
|
| 458 |
+
if input_error["type"] in ["missing_inputs", "insufficient_inputs"]:
|
| 459 |
+
return {
|
| 460 |
+
"status": "validation_error",
|
| 461 |
+
"output": "",
|
| 462 |
+
"error": input_error
|
| 463 |
+
}
|
| 464 |
+
# For unused/excess, we'll proceed but the error will be in the response
|
| 465 |
+
# (Actually, let's proceed for now and handle at runtime)
|
| 466 |
|
| 467 |
# --- Step 1: Transpilation ---
|
| 468 |
try:
|
| 469 |
# We ensure the compiler adds # ERAS_LINE_X comments for our worker
|
| 470 |
python_source = compiler.transpile(body.code)
|
| 471 |
|
| 472 |
+
# Validate that transpilation produced valid code
|
| 473 |
+
if not python_source or not python_source.strip():
|
| 474 |
+
return {
|
| 475 |
+
"status": "transpilation_error",
|
| 476 |
+
"output": "",
|
| 477 |
+
"error": {
|
| 478 |
+
"message": "Transpilation produced empty code. Your ErasLang code may be missing required elements.",
|
| 479 |
+
"type": "empty_transpilation",
|
| 480 |
+
"suggestion": "Make sure your code includes 'ARE YOU READY FOR IT?' to start and 'LONG LIVE' to end."
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
# Validate Python syntax of transpiled code
|
| 485 |
+
try:
|
| 486 |
+
compile(python_source, '<string>', 'exec')
|
| 487 |
+
except SyntaxError as syn_err:
|
| 488 |
+
return {
|
| 489 |
+
"status": "transpilation_error",
|
| 490 |
+
"output": "",
|
| 491 |
+
"error": {
|
| 492 |
+
"message": f"Transpiled code has a syntax error at line {syn_err.lineno}: {syn_err.msg}",
|
| 493 |
+
"type": "transpilation_syntax_error",
|
| 494 |
+
"line": str(syn_err.lineno) if syn_err.lineno else None,
|
| 495 |
+
"suggestion": "This usually indicates an issue with your ErasLang code structure. Check for unmatched blocks or invalid syntax."
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
# Count how many input() calls we'll need to replace
|
| 500 |
+
input_call_count = len(re.findall(r'int\s*\(\s*input\s*\(\s*\)\s*\)', python_source))
|
| 501 |
+
|
| 502 |
# If inputs are provided, inject an input provider function
|
| 503 |
+
if body.inputs or input_call_count > 0:
|
| 504 |
+
# Validate inputs match input calls
|
| 505 |
+
if input_call_count > 0:
|
| 506 |
+
if not body.inputs or len(body.inputs) < input_call_count:
|
| 507 |
+
return {
|
| 508 |
+
"status": "validation_error",
|
| 509 |
+
"output": "",
|
| 510 |
+
"error": {
|
| 511 |
+
"message": f"Your code requires {input_call_count} input value(s) but only {len(body.inputs) if body.inputs else 0} were provided.",
|
| 512 |
+
"type": "insufficient_inputs",
|
| 513 |
+
"suggestion": f"Provide {input_call_count} integer value(s) in the inputs array. You need {input_call_count - (len(body.inputs) if body.inputs else 0)} more value(s)."
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
# Replace all int(input()) calls with our custom input function
|
| 518 |
python_source = re.sub(
|
| 519 |
r'int\s*\(\s*input\s*\(\s*\)\s*\)',
|
| 520 |
'int(_eras_input())',
|
| 521 |
python_source
|
| 522 |
)
|
| 523 |
+
|
| 524 |
# Prepend the input provider code
|
| 525 |
input_provider = f"""
|
| 526 |
# Input provider for prefilled inputs
|
| 527 |
_input_index = 0
|
| 528 |
+
_input_values = {body.inputs if body.inputs else []}
|
| 529 |
|
| 530 |
def _eras_input():
|
| 531 |
global _input_index
|
|
|
|
| 538 |
|
| 539 |
"""
|
| 540 |
python_source = input_provider + python_source
|
| 541 |
+
|
| 542 |
except Exception as e:
|
| 543 |
+
# Format transpilation errors with helpful context
|
| 544 |
+
error_info = format_transpilation_error(e)
|
| 545 |
return {
|
| 546 |
"status": "transpilation_error",
|
| 547 |
"output": "",
|
| 548 |
+
"error": error_info
|
| 549 |
}
|
| 550 |
|
| 551 |
# --- Step 2: Subprocess Execution ---
|
| 552 |
try:
|
| 553 |
# We call worker.py and pipe the python_source into its stdin
|
| 554 |
worker_path = os.path.join(os.path.dirname(__file__), "worker.py")
|
| 555 |
+
|
| 556 |
+
if not os.path.exists(worker_path):
|
| 557 |
+
return {
|
| 558 |
+
"status": "system_error",
|
| 559 |
+
"output": "",
|
| 560 |
+
"error": {
|
| 561 |
+
"message": "Worker script not found. System configuration error.",
|
| 562 |
+
"type": "missing_worker",
|
| 563 |
+
"suggestion": "Please contact support - this is a system configuration issue."
|
| 564 |
+
}
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
process = subprocess.Popen(
|
| 568 |
[sys.executable, worker_path],
|
| 569 |
stdin=subprocess.PIPE,
|
|
|
|
| 580 |
)
|
| 581 |
except subprocess.TimeoutExpired:
|
| 582 |
process.kill()
|
| 583 |
+
process.wait() # Ensure process is terminated
|
| 584 |
return {
|
| 585 |
"status": "timeout",
|
| 586 |
+
"output": "",
|
| 587 |
+
"error": {
|
| 588 |
+
"message": "Performance Timed Out. Did you get stuck in an 'Is it Over Now?' loop?",
|
| 589 |
+
"type": "execution_timeout",
|
| 590 |
+
"line": None,
|
| 591 |
+
"suggestion": "Your code took longer than 10 seconds to execute. Check for infinite loops or very long-running operations. Make sure your 'IS IT OVER NOW?' loops have proper exit conditions."
|
| 592 |
+
}
|
| 593 |
}
|
| 594 |
|
| 595 |
# --- Step 3: Handle Results ---
|
|
|
|
| 601 |
}
|
| 602 |
else:
|
| 603 |
# The worker.py prints thematic errors to stdout on failure
|
| 604 |
+
# Parse the error message for better structure
|
| 605 |
+
error_output = stdout_data if stdout_data.strip() else stderr_data
|
| 606 |
+
error_info = parse_error_message(error_output)
|
| 607 |
+
|
| 608 |
return {
|
| 609 |
"status": "runtime_error",
|
| 610 |
+
"output": stdout_data if process.returncode != 0 else "",
|
| 611 |
+
"error": error_info
|
| 612 |
}
|
| 613 |
|
| 614 |
+
except FileNotFoundError:
|
| 615 |
+
return {
|
| 616 |
+
"status": "system_error",
|
| 617 |
+
"output": "",
|
| 618 |
+
"error": {
|
| 619 |
+
"message": "Python interpreter not found. System configuration error.",
|
| 620 |
+
"type": "missing_python",
|
| 621 |
+
"suggestion": "Please contact support - this is a system configuration issue."
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
except PermissionError:
|
| 625 |
+
return {
|
| 626 |
+
"status": "system_error",
|
| 627 |
+
"output": "",
|
| 628 |
+
"error": {
|
| 629 |
+
"message": "Permission denied when trying to execute code. System configuration error.",
|
| 630 |
+
"type": "permission_error",
|
| 631 |
+
"suggestion": "Please contact support - this is a system configuration issue."
|
| 632 |
+
}
|
| 633 |
+
}
|
| 634 |
except Exception as e:
|
| 635 |
+
# Log the full error for debugging (in production, use proper logging)
|
| 636 |
+
error_trace = traceback.format_exc()
|
| 637 |
+
return {
|
| 638 |
+
"status": "system_error",
|
| 639 |
+
"output": "",
|
| 640 |
+
"error": {
|
| 641 |
+
"message": f"An unexpected system error occurred: {str(e)}",
|
| 642 |
+
"type": "unexpected_error",
|
| 643 |
+
"suggestion": "Please try again. If the problem persists, contact support with details about what you were trying to do."
|
| 644 |
+
}
|
| 645 |
+
}
|
| 646 |
|
| 647 |
if __name__ == "__main__":
|
| 648 |
import uvicorn # type: ignore
|
compiler.py
CHANGED
|
@@ -34,6 +34,8 @@ class ErasCompiler:
|
|
| 34 |
indent_level = 0
|
| 35 |
math_target = None
|
| 36 |
last_var = None
|
|
|
|
|
|
|
| 37 |
|
| 38 |
for line in lines:
|
| 39 |
raw = line # Save original line for counting
|
|
@@ -52,6 +54,18 @@ class ErasCompiler:
|
|
| 52 |
if not token:
|
| 53 |
continue
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
# Replace comparison operators in payload
|
| 56 |
payload = payload.replace("STAY STAY STAY", "==")
|
| 57 |
payload = payload.replace("LET'S GO BATTLE", ">")
|
|
@@ -80,8 +94,13 @@ class ErasCompiler:
|
|
| 80 |
python_output.append(f"{current_indent}{last_var} = {val}\n")
|
| 81 |
elif token == "BEGIN_MATH":
|
| 82 |
math_target = payload
|
|
|
|
|
|
|
| 83 |
elif token == "END_MATH":
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
| 85 |
math_target = None
|
| 86 |
elif token == "ADD":
|
| 87 |
python_output.append(f"{current_indent}_acc += {payload}\n")
|
|
@@ -126,13 +145,28 @@ class ErasCompiler:
|
|
| 126 |
elif token == "INPUT":
|
| 127 |
python_output.append(f"{current_indent}{last_var} = int(input())\n")
|
| 128 |
elif token == "BEGIN_FUNC":
|
| 129 |
-
python_output.append(f"def {payload}")
|
|
|
|
|
|
|
| 130 |
elif token == "FUNC_ARG":
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
elif token == "END_FUNC":
|
| 134 |
-
#
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
indent_level -= 1
|
| 137 |
elif token == "RETURN":
|
| 138 |
python_output.append(f"{current_indent}return {payload}\n")
|
|
|
|
| 34 |
indent_level = 0
|
| 35 |
math_target = None
|
| 36 |
last_var = None
|
| 37 |
+
in_function_def = False # Track if we're building a function definition
|
| 38 |
+
func_arg_count = 0 # Count function arguments
|
| 39 |
|
| 40 |
for line in lines:
|
| 41 |
raw = line # Save original line for counting
|
|
|
|
| 54 |
if not token:
|
| 55 |
continue
|
| 56 |
|
| 57 |
+
# If we're in a function definition and encounter a non-FUNC_ARG/END_FUNC token,
|
| 58 |
+
# close the function definition first
|
| 59 |
+
if in_function_def and token not in ["FUNC_ARG", "END_FUNC"]:
|
| 60 |
+
if python_output and not python_output[-1].endswith(":\n"):
|
| 61 |
+
if python_output[-1].endswith("("):
|
| 62 |
+
python_output[-1] += "):\n"
|
| 63 |
+
else:
|
| 64 |
+
python_output[-1] += "):\n"
|
| 65 |
+
indent_level += 1 # Increment for function body
|
| 66 |
+
in_function_def = False
|
| 67 |
+
func_arg_count = 0
|
| 68 |
+
|
| 69 |
# Replace comparison operators in payload
|
| 70 |
payload = payload.replace("STAY STAY STAY", "==")
|
| 71 |
payload = payload.replace("LET'S GO BATTLE", ">")
|
|
|
|
| 94 |
python_output.append(f"{current_indent}{last_var} = {val}\n")
|
| 95 |
elif token == "BEGIN_MATH":
|
| 96 |
math_target = payload
|
| 97 |
+
# Initialize accumulator with the target variable
|
| 98 |
+
python_output.append(f"{current_indent}_acc = {payload}\n")
|
| 99 |
elif token == "END_MATH":
|
| 100 |
+
# Use payload if provided, otherwise use math_target
|
| 101 |
+
target_var = payload if payload else math_target
|
| 102 |
+
if target_var:
|
| 103 |
+
python_output.append(f"{current_indent}{target_var} = _acc\n")
|
| 104 |
math_target = None
|
| 105 |
elif token == "ADD":
|
| 106 |
python_output.append(f"{current_indent}_acc += {payload}\n")
|
|
|
|
| 145 |
elif token == "INPUT":
|
| 146 |
python_output.append(f"{current_indent}{last_var} = int(input())\n")
|
| 147 |
elif token == "BEGIN_FUNC":
|
| 148 |
+
python_output.append(f"{current_indent}def {payload}(")
|
| 149 |
+
in_function_def = True
|
| 150 |
+
func_arg_count = 0
|
| 151 |
elif token == "FUNC_ARG":
|
| 152 |
+
func_arg_count += 1
|
| 153 |
+
# Check if this is the first argument (last line ends with "(")
|
| 154 |
+
if python_output and python_output[-1].endswith("("):
|
| 155 |
+
python_output[-1] += payload
|
| 156 |
+
else:
|
| 157 |
+
# Add comma and argument
|
| 158 |
+
python_output[-1] += f", {payload}"
|
| 159 |
elif token == "END_FUNC":
|
| 160 |
+
# Close the function definition if it's still open
|
| 161 |
+
if in_function_def and python_output and not python_output[-1].endswith(":\n"):
|
| 162 |
+
if python_output[-1].endswith("("):
|
| 163 |
+
# No arguments, close with ):
|
| 164 |
+
python_output[-1] += "):\n"
|
| 165 |
+
else:
|
| 166 |
+
# Has arguments, close with ):
|
| 167 |
+
python_output[-1] += "):\n"
|
| 168 |
+
in_function_def = False
|
| 169 |
+
func_arg_count = 0
|
| 170 |
indent_level -= 1
|
| 171 |
elif token == "RETURN":
|
| 172 |
python_output.append(f"{current_indent}return {payload}\n")
|