Commit d68d4f71 authored by Roman Alifanov's avatar Roman Alifanov

Refactor: unified method registry and codegen cleanup

- Split methods.py into methods/ directory with separate modules - Add awk_builtin field to Method for unified AWK generation - Replace hardcoded method dispatch in awk_codegen with generate_awk() - Add RET_VAR/RET_ARR constants, replace hardcoded __CT_RET - Migrate all codegen files to use indented() context manager
parent c3bf25cc
...@@ -1021,24 +1021,29 @@ CodeGenerator ...@@ -1021,24 +1021,29 @@ CodeGenerator
Вспомогательные модули: Вспомогательные модули:
├── constants.py # Константы (RET_VAR, TMP_PREFIX, CLASS_FUNC_PREFIX, etc.) ├── constants.py # Константы (RET_VAR, TMP_PREFIX, CLASS_FUNC_PREFIX, etc.)
└── methods.py # Единый реестр методов для bash/awk синхронизации └── methods/ # Единый реестр методов (bash_impl + awk_gen)
├── base.py # Method dataclass
├── string.py # StringMethods
├── array.py # ArrayMethods
├── dict.py # DictMethods
└── ... # http, fs, json, logger, math, time, etc.
``` ```
### Добавление новых методов ### Добавление новых методов
Для добавления нового метода достаточно обновить `methods.py`: Для добавления нового метода достаточно обновить соответствующий файл в `methods/`:
```python ```python
STRING_METHODS = { # methods/string.py
class StringMethods:
... ...
"new_method": MethodDef( new_method = Method(
"new_method", name="new_method",
min_args=1,
max_args=1,
bash_func="__ct_str_new_method", bash_func="__ct_str_new_method",
awk_gen=lambda obj, args: f"awk_impl({obj}, {args[0]})" bash_impl='__CT_RET="${1}..."; echo "$__CT_RET"',
), awk_gen=lambda obj, args: f"awk_impl({obj}, {args[0]})",
} min_args=1, max_args=1,
)
``` ```
Bash и AWK codegen автоматически подхватят изменения. Bash и AWK codegen автоматически подхватят изменения.
...@@ -366,7 +366,12 @@ bootstrap/ # Bootstrap compiler (Python) ...@@ -366,7 +366,12 @@ bootstrap/ # Bootstrap compiler (Python)
├── ast_nodes.py # AST node classes ├── ast_nodes.py # AST node classes
├── errors.py # Error handling ├── errors.py # Error handling
├── constants.py # Codegen constants (RET_VAR, TMP_PREFIX, etc.) ├── constants.py # Codegen constants (RET_VAR, TMP_PREFIX, etc.)
├── methods.py # Unified method registry for bash/awk sync ├── methods/ # Unified method registry (bash + awk)
│ ├── base.py # Method dataclass
│ ├── string.py # String methods
│ ├── array.py # Array methods
│ ├── dict.py # Dict methods
│ └── ... # http, fs, json, logger, math, time, etc.
├── dce.py # Dead code elimination ├── dce.py # Dead code elimination
├── codegen.py # Main Bash code generator (mixin coordinator) ├── codegen.py # Main Bash code generator (mixin coordinator)
├── expr_codegen.py # Expression generation (mixin) ├── expr_codegen.py # Expression generation (mixin)
......
...@@ -366,7 +366,12 @@ bootstrap/ # Bootstrap-компилятор (Python) ...@@ -366,7 +366,12 @@ bootstrap/ # Bootstrap-компилятор (Python)
├── ast_nodes.py # Классы узлов AST ├── ast_nodes.py # Классы узлов AST
├── errors.py # Обработка ошибок ├── errors.py # Обработка ошибок
├── constants.py # Константы кодогенерации (RET_VAR, TMP_PREFIX, etc.) ├── constants.py # Константы кодогенерации (RET_VAR, TMP_PREFIX, etc.)
├── methods.py # Единый реестр методов для bash/awk синхронизации ├── methods/ # Единый реестр методов (bash + awk)
│ ├── base.py # Method dataclass
│ ├── string.py # Строковые методы
│ ├── array.py # Методы массивов
│ ├── dict.py # Методы словарей
│ └── ... # http, fs, json, logger, math, time, etc.
├── dce.py # Устранение мёртвого кода ├── dce.py # Устранение мёртвого кода
├── codegen.py # Основной генератор Bash-кода (координатор миксинов) ├── codegen.py # Основной генератор Bash-кода (координатор миксинов)
├── expr_codegen.py # Генерация выражений (миксин) ├── expr_codegen.py # Генерация выражений (миксин)
......
...@@ -8,33 +8,8 @@ from .ast_nodes import ( ...@@ -8,33 +8,8 @@ from .ast_nodes import (
BoolLiteral, NilLiteral, ArrayLiteral, DictLiteral, BinaryOp, BoolLiteral, NilLiteral, ArrayLiteral, DictLiteral, BinaryOp,
UnaryOp, CallExpr, IndexAccess, MemberAccess UnaryOp, CallExpr, IndexAccess, MemberAccess
) )
from .methods import get_awk_builtin, generate_awk, MATH_METHODS
from .constants import RET_VAR
AWK_MATH_FUNCS = {
"sin": lambda a: f"sin({a[0]})",
"cos": lambda a: f"cos({a[0]})",
"sqrt": lambda a: f"sqrt({a[0]})",
"log": lambda a: f"log({a[0]})",
"exp": lambda a: f"exp({a[0]})",
"int": lambda a: f"int({a[0]})",
"rand": lambda a: "rand()",
"atan2": lambda a: f"atan2({a[0]}, {a[1]})",
}
AWK_BUILTIN_FUNCS = {
"print": lambda a: f"print {', '.join(a)}" if a else "print",
"printf": lambda a: f"printf {', '.join(a)}",
"sprintf": lambda a: f"sprintf({', '.join(a)})",
"length": lambda a: f"length({a[0]})" if a else "length()",
"substr": lambda a: f"substr({', '.join(a)})",
"split": lambda a: f"split({', '.join(a)})",
"sub": lambda a: f"sub({', '.join(a)})",
"gsub": lambda a: f"gsub({', '.join(a)})",
"match": lambda a: f"match({', '.join(a)})",
"tolower": lambda a: f"tolower({a[0]})" if a else "",
"toupper": lambda a: f"toupper({a[0]})" if a else "",
"int": lambda a: f"int({a[0]})" if a else "",
}
class AwkCodegenMixin: class AwkCodegenMixin:
...@@ -93,8 +68,13 @@ class AwkCodegenMixin: ...@@ -93,8 +68,13 @@ class AwkCodegenMixin:
"""Generate a function that runs as inline AWK instead of Bash.""" """Generate a function that runs as inline AWK instead of Bash."""
name = func.name name = func.name
self.emit (f"{name} () {{") self.emit (f"{name} () {{")
self.indent_level += 1 with self.indented():
self._generate_awk_function_body(func)
self.emit ("}")
self.emit ()
def _generate_awk_function_body (self, func: FunctionDecl):
"""Generate the body of an AWK function."""
validate_decorator = None validate_decorator = None
for dec in func.decorators: for dec in func.decorators:
if dec.name == "validate": if dec.name == "validate":
...@@ -154,7 +134,7 @@ class AwkCodegenMixin: ...@@ -154,7 +134,7 @@ class AwkCodegenMixin:
for stmt in after_stmts: for stmt in after_stmts:
self._awk_stmt (stmt, end_emit, end_inc, end_dec) self._awk_stmt (stmt, end_emit, end_inc, end_dec)
self.emit (f"__CT_RET=$({awk_cmd} '") self.emit (f"{RET_VAR}=$({awk_cmd} '")
for nf in nested_funcs: for nf in nested_funcs:
self._awk_helper_func (nf) self._awk_helper_func (nf)
...@@ -186,7 +166,7 @@ class AwkCodegenMixin: ...@@ -186,7 +166,7 @@ class AwkCodegenMixin:
for stmt in main_stmts: for stmt in main_stmts:
self._awk_stmt (stmt, awk_emit, awk_inc, awk_dec) self._awk_stmt (stmt, awk_emit, awk_inc, awk_dec)
self.emit (f"__CT_RET=$({awk_cmd} '") self.emit (f"{RET_VAR}=$({awk_cmd} '")
for nf in nested_funcs: for nf in nested_funcs:
self._awk_helper_func (nf) self._awk_helper_func (nf)
...@@ -197,11 +177,8 @@ class AwkCodegenMixin: ...@@ -197,11 +177,8 @@ class AwkCodegenMixin:
self.emit ("}')") self.emit ("}')")
self.emit ('local __awk_rc=$?') self.emit ('local __awk_rc=$?')
self.emit ('echo "$__CT_RET"') self.emit (f'echo "${{{RET_VAR}}}"')
self.emit ('return $__awk_rc') self.emit ('return $__awk_rc')
self.indent_level -= 1
self.emit ("}")
self.emit ()
def _awk_helper_func (self, func: FunctionDecl): def _awk_helper_func (self, func: FunctionDecl):
"""Generate AWK helper function definition.""" """Generate AWK helper function definition."""
...@@ -500,99 +477,27 @@ class AwkCodegenMixin: ...@@ -500,99 +477,27 @@ class AwkCodegenMixin:
method = expr.callee.member method = expr.callee.member
args = expr.arguments args = expr.arguments
if ns == "math" and method in AWK_MATH_FUNCS: if ns == "math" and method in MATH_METHODS:
awk_args = [self._awk_expr(a) for a in args] math_method = MATH_METHODS[method]
return AWK_MATH_FUNCS[method](awk_args) if math_method.awk_builtin:
awk_args = [self._awk_expr(a) for a in args]
return math_method.awk_builtin(awk_args)
var_types = getattr (self, '_awk_var_types', {}) var_types = getattr (self, '_awk_var_types', {})
var_type = var_types.get (ns, "string") var_type = var_types.get (ns, "string")
type_name = {"array": "array", "dict": "dict"}.get(var_type, "string")
if var_type == "array": awk_args = [self._awk_expr(a) for a in args]
if method == "len": awk_code = generate_awk(type_name, method, ns, awk_args)
return f"length({ns})" if awk_code:
if method == "push" and len (args) >= 1: return awk_code
val = self._awk_expr (args[0])
return f"{ns}[length({ns}) + 1] = {val}"
if method == "pop":
return f"delete {ns}[length({ns})]"
if method == "shift":
return f"delete {ns}[1]"
if method == "get" and len (args) >= 1:
idx = self._awk_expr (args[0])
return f"{ns}[{idx}]"
if method == "set" and len (args) >= 2:
idx = self._awk_expr (args[0])
val = self._awk_expr (args[1])
return f"{ns}[{idx}] = {val}"
if method == "has" and len (args) >= 1:
key = self._awk_expr (args[0])
return f"({key} in {ns})"
if method == "del" and len (args) >= 1:
key = self._awk_expr (args[0])
return f"delete {ns}[{key}]"
if method == "join" and len(args) >= 1:
sep = self._awk_expr(args[0])
return f"__ct_awk_join({ns}, {sep})"
elif var_type == "dict":
if method == "get" and len (args) >= 1:
key = self._awk_expr (args[0])
return f"{ns}[{key}]"
if method == "set" and len (args) >= 2:
key = self._awk_expr (args[0])
val = self._awk_expr (args[1])
return f"{ns}[{key}] = {val}"
if method == "has" and len (args) >= 1:
key = self._awk_expr (args[0])
return f"({key} in {ns})"
if method == "del" and len (args) >= 1:
key = self._awk_expr (args[0])
return f"delete {ns}[{key}]"
if method == "keys":
return f"{ns}"
else:
if method == "len":
return f"length({ns})"
if method == "upper":
return f"toupper({ns})"
if method == "lower":
return f"tolower({ns})"
if method == "contains" and len (args) >= 1:
needle = self._awk_expr (args[0])
return f"(index({ns}, {needle}) > 0)"
if method == "index" and len (args) >= 1:
needle = self._awk_expr (args[0])
return f"(index({ns}, {needle}) - 1)"
if method == "substr" and len (args) >= 2:
start = self._awk_expr (args[0])
length = self._awk_expr (args[1])
return f"substr({ns}, {start} + 1, {length})"
if method == "charAt" and len (args) >= 1:
pos = self._awk_expr (args[0])
return f"substr({ns}, {pos} + 1, 1)"
if method == "trim":
return f"(gsub(/^[ \\t]+|[ \\t]+$/, \"\", {ns}) ? {ns} : {ns})"
if method == "replace" and len (args) >= 2:
old = self._awk_expr (args[0])
new = self._awk_expr (args[1])
return f"(gsub({old}, {new}, {ns}) ? {ns} : {ns})"
if method == "split" and len (args) >= 1:
delim = self._awk_expr (args[0])
return f"split({ns}, __split_arr, {delim})"
if method == "starts" and len (args) >= 1:
prefix = self._awk_expr (args[0])
return f"(substr({ns}, 1, length({prefix})) == {prefix})"
if method == "ends" and len (args) >= 1:
suffix = self._awk_expr (args[0])
return f"(substr({ns}, length({ns}) - length({suffix}) + 1) == {suffix})"
if isinstance (expr.callee, Identifier): if isinstance (expr.callee, Identifier):
func_name = expr.callee.name func_name = expr.callee.name
args = [self._awk_expr(a) for a in expr.arguments] args = [self._awk_expr(a) for a in expr.arguments]
if func_name in AWK_BUILTIN_FUNCS: awk_code = get_awk_builtin(func_name, args)
return AWK_BUILTIN_FUNCS[func_name](args) if awk_code:
return awk_code
return f"{func_name}({', '.join(args)})" return f"{func_name}({', '.join(args)})"
......
...@@ -89,81 +89,78 @@ class ClassMixin: ...@@ -89,81 +89,78 @@ class ClassMixin:
def _generate_class_constructor(self, cls: ClassDecl): def _generate_class_constructor(self, cls: ClassDecl):
"""Generate class factory function.""" """Generate class factory function."""
self.emit(f"{cls.name} () {{") self.emit(f"{cls.name} () {{")
self.indent_level += 1 with self.indented():
# Save instance immediately as nested constructors may overwrite __ct_last_instance # Save instance immediately as nested constructors may overwrite __ct_last_instance
self.emit('local __ct_this_instance="__ct_inst_$RANDOM$RANDOM"') self.emit('local __ct_this_instance="__ct_inst_$RANDOM$RANDOM"')
self.emit('__ct_obj_class["$__ct_this_instance"]="{}"'.format(cls.name)) self.emit('__ct_obj_class["$__ct_this_instance"]="{}"'.format(cls.name))
for field_name, default_value in cls.fields: for field_name, default_value in cls.fields:
if isinstance(default_value, ArrayLiteral): if isinstance(default_value, ArrayLiteral):
elements = [self.generate_expr(e) for e in default_value.elements] elements = [self.generate_expr(e) for e in default_value.elements]
if elements: if elements:
arr_content = " ".join([f'"{e}"' for e in elements]) arr_content = " ".join([f'"{e}"' for e in elements])
self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=({arr_content})"') self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=({arr_content})"')
else:
self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=()"')
elif isinstance(default_value, DictLiteral):
self.emit(f'eval "declare -gA ${{__ct_this_instance}}_{field_name}=()"')
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]="${{__ct_this_instance}}_{field_name}"')
elif default_value:
val = self.generate_expr(default_value)
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]="{val}"')
else: else:
self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=()"') self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]=""')
elif isinstance(default_value, DictLiteral):
self.emit(f'eval "declare -gA ${{__ct_this_instance}}_{field_name}=()"')
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]="${{__ct_this_instance}}_{field_name}"')
elif default_value:
val = self.generate_expr(default_value)
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]="{val}"')
else:
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]=""')
if cls.parent: if cls.parent:
self.emit(f'# Inherit from {cls.parent}') self.emit(f'# Inherit from {cls.parent}')
if cls.constructor: if cls.constructor:
self.emit("# Call constructor") self.emit("# Call constructor")
params_list = " ".join([f'"${{{i + 1}}}"' for i in range(len(cls.constructor.params))]) params_list = " ".join([f'"${{{i + 1}}}"' for i in range(len(cls.constructor.params))])
self.emit(f'__ct_class_{cls.name}_construct "$__ct_this_instance" {params_list}') self.emit(f'__ct_class_{cls.name}_construct "$__ct_this_instance" {params_list}')
# Restore __ct_last_instance to this instance (after nested constructors may have changed it)
self.emit('__ct_last_instance="$__ct_this_instance"')
self.indent_level -= 1 # Restore __ct_last_instance to this instance (after nested constructors may have changed it)
self.emit('__ct_last_instance="$__ct_this_instance"')
self.emit("}") self.emit("}")
self.emit() self.emit()
def _generate_construct_method(self, cls: ClassDecl): def _generate_construct_method(self, cls: ClassDecl):
"""Generate construct method.""" """Generate construct method."""
self.emit(f"__ct_class_{cls.name}_construct () {{") self.emit(f"__ct_class_{cls.name}_construct () {{")
self.indent_level += 1 with self.indented():
self.emit('local this="$1"') self.emit('local this="$1"')
self.emit('shift') self.emit('shift')
self.in_class_method = True self.in_class_method = True
old_in_function = self.in_function old_in_function = self.in_function
old_local_vars = self.local_vars.copy() old_local_vars = self.local_vars.copy()
old_param_positions = self.current_param_positions.copy() old_param_positions = self.current_param_positions.copy()
self.in_function = True self.in_function = True
self.local_vars = set() self.local_vars = set()
self.current_param_positions = {} self.current_param_positions = {}
for i, param in enumerate(cls.constructor.params): for i, param in enumerate(cls.constructor.params):
self.current_param_positions[param.name] = i + 1 self.current_param_positions[param.name] = i + 1
array_field_params = self._find_array_field_params(cls) array_field_params = self._find_array_field_params(cls)
for i, param in enumerate(cls.constructor.params): for i, param in enumerate(cls.constructor.params):
if param.name in array_field_params: if param.name in array_field_params:
continue continue
if param.default is not None: if param.default is not None:
default_val = self.generate_expr(param.default) default_val = self.generate_expr(param.default)
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"') self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
else: else:
self.emit(f'local {param.name}="${{{i + 1}}}"') self.emit(f'local {param.name}="${{{i + 1}}}"')
self.local_vars.add(param.name) self.local_vars.add(param.name)
for stmt in cls.constructor.body.statements: for stmt in cls.constructor.body.statements:
self.generate_statement(stmt) self.generate_statement(stmt)
self.in_class_method = False self.in_class_method = False
self.in_function = old_in_function self.in_function = old_in_function
self.local_vars = old_local_vars self.local_vars = old_local_vars
self.current_param_positions = old_param_positions self.current_param_positions = old_param_positions
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -184,48 +181,46 @@ class ClassMixin: ...@@ -184,48 +181,46 @@ class ClassMixin:
def _generate_plain_method(self, cls: ClassDecl, method: FunctionDecl): def _generate_plain_method(self, cls: ClassDecl, method: FunctionDecl):
"""Generate a plain class method.""" """Generate a plain class method."""
self.emit(f"__ct_class_{cls.name}_{method.name} () {{") self.emit(f"__ct_class_{cls.name}_{method.name} () {{")
self.indent_level += 1 with self.indented():
self.emit('local this="$1"') self.emit('local this="$1"')
self.emit('shift') self.emit('shift')
self.in_class_method = True self.in_class_method = True
old_in_function = self.in_function old_in_function = self.in_function
old_local_vars = self.local_vars.copy() old_local_vars = self.local_vars.copy()
old_object_vars = self.object_vars.copy() old_object_vars = self.object_vars.copy()
self.in_function = True self.in_function = True
self.local_vars = set() self.local_vars = set()
param_types = self._analyze_param_types(method) param_types = self._analyze_param_types(method)
for i, param in enumerate(method.params): for i, param in enumerate(method.params):
if param.is_variadic: if param.is_variadic:
self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")') self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")')
else:
if param.default is not None:
default_val = self.generate_expr(param.default)
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
else: else:
self.emit(f'local {param.name}="${{{i + 1}}}"') if param.default is not None:
self.local_vars.add(param.name) default_val = self.generate_expr(param.default)
if param_types.get(param.name) == "object": self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
self.object_vars.add(param.name) else:
self.emit(f'local {param.name}="${{{i + 1}}}"')
for stmt in method.body.statements: self.local_vars.add(param.name)
self.generate_statement(stmt) if param_types.get(param.name) == "object":
self.object_vars.add(param.name)
self.in_class_method = False
self.in_function = old_in_function for stmt in method.body.statements:
self.local_vars = old_local_vars self.generate_statement(stmt)
self.object_vars = old_object_vars
self.indent_level -= 1 self.in_class_method = False
self.in_function = old_in_function
self.local_vars = old_local_vars
self.object_vars = old_object_vars
self.emit("}") self.emit("}")
self.emit() self.emit()
def _generate_inherited_method(self, child_cls: ClassDecl, parent_cls: ClassDecl, method: FunctionDecl): def _generate_inherited_method(self, child_cls: ClassDecl, parent_cls: ClassDecl, method: FunctionDecl):
"""Generate proxy method for inherited method.""" """Generate proxy method for inherited method."""
self.emit(f"__ct_class_{child_cls.name}_{method.name} () {{") self.emit(f"__ct_class_{child_cls.name}_{method.name} () {{")
self.indent_level += 1 with self.indented():
self.emit(f'__ct_class_{parent_cls.name}_{method.name} "$@"') self.emit(f'__ct_class_{parent_cls.name}_{method.name} "$@"')
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -235,39 +230,38 @@ class ClassMixin: ...@@ -235,39 +230,38 @@ class ClassMixin:
original_name = f"{base_name}_orig" original_name = f"{base_name}_orig"
self.emit(f"{original_name} () {{") self.emit(f"{original_name} () {{")
self.indent_level += 1 with self.indented():
self.emit('local this="$1"') self.emit('local this="$1"')
self.emit('shift') self.emit('shift')
self.in_class_method = True self.in_class_method = True
old_in_function = self.in_function old_in_function = self.in_function
old_local_vars = self.local_vars.copy() old_local_vars = self.local_vars.copy()
old_object_vars = self.object_vars.copy() old_object_vars = self.object_vars.copy()
self.in_function = True self.in_function = True
self.local_vars = set() self.local_vars = set()
param_types = self._analyze_param_types(method) param_types = self._analyze_param_types(method)
for i, param in enumerate(method.params): for i, param in enumerate(method.params):
if param.is_variadic: if param.is_variadic:
self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")') self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")')
else:
if param.default is not None:
default_val = self.generate_expr(param.default)
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
else: else:
self.emit(f'local {param.name}="${{{i + 1}}}"') if param.default is not None:
self.local_vars.add(param.name) default_val = self.generate_expr(param.default)
if param_types.get(param.name) == "object": self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
self.object_vars.add(param.name) else:
self.emit(f'local {param.name}="${{{i + 1}}}"')
for stmt in method.body.statements: self.local_vars.add(param.name)
self.generate_statement(stmt) if param_types.get(param.name) == "object":
self.object_vars.add(param.name)
self.in_class_method = False
self.in_function = old_in_function for stmt in method.body.statements:
self.local_vars = old_local_vars self.generate_statement(stmt)
self.object_vars = old_object_vars
self.indent_level -= 1 self.in_class_method = False
self.in_function = old_in_function
self.local_vars = old_local_vars
self.object_vars = old_object_vars
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -278,10 +272,9 @@ class ClassMixin: ...@@ -278,10 +272,9 @@ class ClassMixin:
current_name = wrapper_name current_name = wrapper_name
self.emit(f"{base_name} () {{") self.emit(f"{base_name} () {{")
self.indent_level += 1 with self.indented():
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(method.params) + 1)]) params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(method.params) + 1)])
self.emit(f'{current_name} {params_str}') self.emit(f'{current_name} {params_str}')
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -301,11 +294,10 @@ class ClassMixin: ...@@ -301,11 +294,10 @@ class ClassMixin:
self.emit(f"# @awk method - 'this' is ignored, calls AWK function") self.emit(f"# @awk method - 'this' is ignored, calls AWK function")
self.emit(f"{method_name} () {{") self.emit(f"{method_name} () {{")
self.indent_level += 1 with self.indented():
self.emit('shift # ignore this') self.emit('shift # ignore this')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(method.params))]) params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(method.params))])
self.emit(f'{awk_func_name} {params_str}') self.emit(f'{awk_func_name} {params_str}')
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -460,55 +452,52 @@ class ClassMixin: ...@@ -460,55 +452,52 @@ class ClassMixin:
name = f"__ct_class_{self.current_class}_{func.name}" name = f"__ct_class_{self.current_class}_{func.name}"
self.emit(f"{name} () {{") self.emit(f"{name} () {{")
self.indent_level += 1 with self.indented():
param_types = self._analyze_param_types(func)
param_types = self._analyze_param_types(func)
old_param_name_map = getattr(self, 'param_name_map', {})
old_param_name_map = getattr(self, 'param_name_map', {}) self.param_name_map = {}
self.param_name_map = {}
for i, param in enumerate(func.params):
for i, param in enumerate(func.params): ptype = param_types.get(param.name, "scalar")
ptype = param_types.get(param.name, "scalar") if param.is_variadic:
if param.is_variadic: self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")')
self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")') elif ptype in ("array", "dict"):
elif ptype in ("array", "dict"): nameref_name = f"__ct_{func.name}_{param.name}"
nameref_name = f"__ct_{func.name}_{param.name}" self.emit(f'local -n {nameref_name}="${{{i + 1}}}"')
self.emit(f'local -n {nameref_name}="${{{i + 1}}}"') self.param_name_map[param.name] = nameref_name
self.param_name_map[param.name] = nameref_name if ptype == "array":
if ptype == "array": self.array_vars.add(nameref_name)
self.array_vars.add(nameref_name) else:
self.dict_vars.add(nameref_name)
else: else:
self.dict_vars.add(nameref_name) if param.default is not None:
else: default_val = self.generate_expr(param.default)
if param.default is not None: self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
default_val = self.generate_expr(param.default) else:
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"') self.emit(f'local {param.name}="${{{i + 1}}}"')
else:
self.emit(f'local {param.name}="${{{i + 1}}}"') old_deferred = self.deferred_calls
self.deferred_calls = []
old_in_function = self.in_function
self.in_function = True
old_local_vars = self.local_vars
self.local_vars = set()
for param in func.params:
self.local_vars.add(param.name)
for stmt in func.body.statements:
self.generate_statement(stmt)
if self.deferred_calls:
self.emit("# Deferred calls")
for call in reversed(self.deferred_calls):
self.emit(call)
old_deferred = self.deferred_calls self.deferred_calls = old_deferred
self.deferred_calls = [] self.in_function = old_in_function
old_in_function = self.in_function self.local_vars = old_local_vars
self.in_function = True self.param_name_map = old_param_name_map
old_local_vars = self.local_vars
self.local_vars = set()
for param in func.params:
self.local_vars.add(param.name)
for stmt in func.body.statements:
self.generate_statement(stmt)
if self.deferred_calls:
self.emit("# Deferred calls")
for call in reversed(self.deferred_calls):
self.emit(call)
self.deferred_calls = old_deferred
self.in_function = old_in_function
self.local_vars = old_local_vars
self.param_name_map = old_param_name_map
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -530,10 +519,9 @@ class ClassMixin: ...@@ -530,10 +519,9 @@ class ClassMixin:
current_name = wrapper_name current_name = wrapper_name
self.emit(f"{func.name} () {{") self.emit(f"{func.name} () {{")
self.indent_level += 1 with self.indented():
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(func.params))]) params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(func.params))])
self.emit(f'{current_name} {params_str}') self.emit(f'{current_name} {params_str}')
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
......
...@@ -189,31 +189,27 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin, ...@@ -189,31 +189,27 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.emit('echo') self.emit('echo')
self.emit('__ct_run_tests () {') self.emit('__ct_run_tests () {')
self.indent_level += 1 with self.indented():
self.emit('local __test_failed=0') self.emit('local __test_failed=0')
for func_name, description in self.test_functions: for func_name, description in self.test_functions:
escaped_desc = description.replace('"', '\\"') escaped_desc = description.replace('"', '\\"')
self.emit(f'__ct_test_start "{escaped_desc}"') self.emit(f'__ct_test_start "{escaped_desc}"')
self.emit('local __prev_failed=$__ct_test_failed') self.emit('local __prev_failed=$__ct_test_failed')
self.emit(f'if {func_name}; then') self.emit(f'if {func_name}; then')
self.indent_level += 1 with self.indented():
self.emit('__ct_test_pass') self.emit('__ct_test_pass')
self.indent_level -= 1 self.emit('else')
self.emit('else') with self.indented():
self.indent_level += 1 self.emit('if [[ $__ct_test_failed -eq $__prev_failed ]]; then')
self.emit('if [[ $__ct_test_failed -eq $__prev_failed ]]; then') with self.indented():
self.indent_level += 1 self.emit('__ct_test_fail ""')
self.emit('__ct_test_fail ""') self.emit('fi')
self.indent_level -= 1 self.emit('__test_failed=1')
self.emit('fi') self.emit('fi')
self.emit('__test_failed=1') self.emit()
self.indent_level -= 1
self.emit('fi') self.emit('__ct_test_summary')
self.emit()
self.emit('__ct_test_summary')
self.indent_level -= 1
self.emit('}') self.emit('}')
self.emit() self.emit()
self.emit('__ct_run_tests') self.emit('__ct_run_tests')
"""Constants for bash code generation.""" """Constants for bash code generation."""
RET_VAR = "__CT_RET" RET_VAR = "__CT_RET"
RET_ARR = "__CT_RET_ARR"
TMP_PREFIX = "__ct_tmp_" TMP_PREFIX = "__ct_tmp_"
CLASS_FUNC_PREFIX = "__ct_class_" CLASS_FUNC_PREFIX = "__ct_class_"
LAMBDA_PREFIX = "__ct_lambda_" LAMBDA_PREFIX = "__ct_lambda_"
......
...@@ -3,6 +3,7 @@ from .ast_nodes import ( ...@@ -3,6 +3,7 @@ from .ast_nodes import (
Expression, CallExpr, MemberAccess, ThisExpr, Identifier, Expression, CallExpr, MemberAccess, ThisExpr, Identifier,
BinaryOp, UnaryOp, BoolLiteral BinaryOp, UnaryOp, BoolLiteral
) )
from .constants import RET_VAR
class NodeIdMap: class NodeIdMap:
...@@ -76,7 +77,7 @@ class CseMixin: ...@@ -76,7 +77,7 @@ class CseMixin:
if key not in seen: if key not in seen:
temp = self.new_temp() temp = self.new_temp()
call_line = f'__ct_class_{self.current_class}_{method} "$this" {args_str} >/dev/null' call_line = f'__ct_class_{self.current_class}_{method} "$this" {args_str} >/dev/null'
assign_line = f'{temp}="$__CT_RET"' assign_line = f'{temp}="${{{RET_VAR}}}"'
self.emit(call_line) self.emit(call_line)
self.emit(assign_line) self.emit(assign_line)
seen[key] = temp seen[key] = temp
...@@ -105,7 +106,7 @@ class CseMixin: ...@@ -105,7 +106,7 @@ class CseMixin:
if key not in seen: if key not in seen:
temp = self.new_temp() temp = self.new_temp()
call_line = f'__ct_class_{self.current_class}_{method} "$this" {args_str} >/dev/null' call_line = f'__ct_class_{self.current_class}_{method} "$this" {args_str} >/dev/null'
assign_line = f'{temp}="$__CT_RET"' assign_line = f'{temp}="${{{RET_VAR}}}"'
self.emit(call_line) self.emit(call_line)
self.emit(assign_line) self.emit(assign_line)
seen[key] = temp seen[key] = temp
...@@ -140,7 +141,7 @@ class CseMixin: ...@@ -140,7 +141,7 @@ class CseMixin:
if key not in seen: if key not in seen:
temp = self.new_temp() temp = self.new_temp()
call_line = f'{func_name} {args_str} >/dev/null' call_line = f'{func_name} {args_str} >/dev/null'
assign_line = f'{temp}="$__CT_RET"' assign_line = f'{temp}="${{{RET_VAR}}}"'
self.emit(call_line) self.emit(call_line)
self.emit(assign_line) self.emit(assign_line)
seen[key] = temp seen[key] = temp
......
import re import re
from typing import List from typing import List
from .ast_nodes import Decorator, Parameter from .ast_nodes import Decorator, Parameter
from .constants import RET_VAR
class DecoratorMixin: class DecoratorMixin:
...@@ -48,57 +49,50 @@ class DecoratorMixin: ...@@ -48,57 +49,50 @@ class DecoratorMixin:
delay = self.generate_expr(arg_val) delay = self.generate_expr(arg_val)
self.emit(f"{wrapper_name} () {{") self.emit(f"{wrapper_name} () {{")
self.indent_level += 1 with self.indented():
if is_method:
if is_method: self.emit('local this="$1"')
self.emit('local this="$1"') self.emit('shift')
self.emit('shift')
self.emit(f"local __attempts={attempts}")
self.emit(f"local __attempts={attempts}") self.emit(f"local __delay={delay}")
self.emit(f"local __delay={delay}") self.emit("local __i")
self.emit("local __i") self.emit("for __i in $(seq 1 $__attempts); do")
self.emit("for __i in $(seq 1 $__attempts); do") with self.indented():
self.indent_level += 1 params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))]) self.emit(f'if {wrapped_name} "$this" {params_str}; then')
if is_method: else:
self.emit(f'if {wrapped_name} "$this" {params_str}; then') self.emit(f'if {wrapped_name} {params_str}; then')
else:
self.emit(f'if {wrapped_name} {params_str}; then') with self.indented():
self.emit("return 0")
self.indent_level += 1 self.emit("fi")
self.emit("return 0") self.emit('sleep "$__delay"')
self.indent_level -= 1 self.emit("done")
self.emit("fi") self.emit("return 1")
self.emit('sleep "$__delay"')
self.indent_level -= 1
self.emit("done")
self.emit("return 1")
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
def _generate_log_wrapper(self, wrapped_name: str, wrapper_name: str, def _generate_log_wrapper(self, wrapped_name: str, wrapper_name: str,
params: List[Parameter], is_method: bool): params: List[Parameter], is_method: bool):
self.emit(f"{wrapper_name} () {{") self.emit(f"{wrapper_name} () {{")
self.indent_level += 1 with self.indented():
if is_method:
if is_method: self.emit('local this="$1"')
self.emit('local this="$1"') self.emit('shift')
self.emit('shift')
self.emit(f'echo "[LOG] Calling {wrapped_name}" >&2')
self.emit(f'echo "[LOG] Calling {wrapped_name}" >&2')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))]) if is_method:
if is_method: self.emit(f'{wrapped_name} "$this" {params_str}')
self.emit(f'{wrapped_name} "$this" {params_str}') else:
else: self.emit(f'{wrapped_name} {params_str}')
self.emit(f'{wrapped_name} {params_str}')
self.emit('local __ret=$?')
self.emit('local __ret=$?') self.emit(f'echo "[LOG] {wrapped_name} returned $__ret" >&2')
self.emit(f'echo "[LOG] {wrapped_name} returned $__ret" >&2') self.emit('return $__ret')
self.emit('return $__ret')
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -113,55 +107,49 @@ class DecoratorMixin: ...@@ -113,55 +107,49 @@ class DecoratorMixin:
self.emit(f"declare -g __ct_cache_time_{wrapper_name}=0") self.emit(f"declare -g __ct_cache_time_{wrapper_name}=0")
self.emit() self.emit()
self.emit(f"{wrapper_name} () {{") self.emit(f"{wrapper_name} () {{")
self.indent_level += 1 with self.indented():
if is_method:
if is_method: self.emit('local this="$1"')
self.emit('local this="$1"') self.emit('shift')
self.emit('shift') self.emit(f'local __key="$this:$*"')
self.emit(f'local __key="$this:$*"') else:
else: self.emit(f'local __key="$*"')
self.emit(f'local __key="$*"')
self.emit(f'local __now=$(date +%s)')
self.emit(f'local __now=$(date +%s)') self.emit(f'local __cache_age=$((__now - __ct_cache_time_{wrapper_name}))')
self.emit(f'local __cache_age=$((__now - __ct_cache_time_{wrapper_name}))') self.emit(f'if [[ $__cache_age -lt {ttl} ]] && [[ -n "${{__ct_cache_{wrapper_name}[$__key]:-}}" ]]; then')
self.emit(f'if [[ $__cache_age -lt {ttl} ]] && [[ -n "${{__ct_cache_{wrapper_name}[$__key]:-}}" ]]; then') with self.indented():
self.indent_level += 1 self.emit(f'{RET_VAR}="${{__ct_cache_{wrapper_name}[$__key]}}"')
self.emit(f'__CT_RET="${{__ct_cache_{wrapper_name}[$__key]}}"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit('echo "$__CT_RET"') self.emit("return 0")
self.emit("return 0") self.emit("fi")
self.indent_level -= 1
self.emit("fi") params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))]) self.emit(f'local __result=$({wrapped_name} "$this" {params_str})')
if is_method: else:
self.emit(f'local __result=$({wrapped_name} "$this" {params_str})') self.emit(f'local __result=$({wrapped_name} {params_str})')
else:
self.emit(f'local __result=$({wrapped_name} {params_str})') self.emit(f'{RET_VAR}="$__result"')
self.emit(f'__ct_cache_{wrapper_name}["$__key"]="$__result"')
self.emit(f'__CT_RET="$__result"') self.emit(f'__ct_cache_time_{wrapper_name}=$__now')
self.emit(f'__ct_cache_{wrapper_name}["$__key"]="$__result"') self.emit('echo "$__result"')
self.emit(f'__ct_cache_time_{wrapper_name}=$__now')
self.emit('echo "$__result"')
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
def _generate_passthrough_wrapper(self, wrapped_name: str, wrapper_name: str, def _generate_passthrough_wrapper(self, wrapped_name: str, wrapper_name: str,
params: List[Parameter], is_method: bool): params: List[Parameter], is_method: bool):
self.emit(f"{wrapper_name} () {{") self.emit(f"{wrapper_name} () {{")
self.indent_level += 1 with self.indented():
if is_method:
if is_method: self.emit('local this="$1"')
self.emit('local this="$1"') self.emit('shift')
self.emit('shift')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))]) if is_method:
if is_method: self.emit(f'{wrapped_name} "$this" {params_str}')
self.emit(f'{wrapped_name} "$this" {params_str}') else:
else: self.emit(f'{wrapped_name} {params_str}')
self.emit(f'{wrapped_name} {params_str}')
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -173,24 +161,21 @@ class DecoratorMixin: ...@@ -173,24 +161,21 @@ class DecoratorMixin:
validations[arg_name] = arg_val.value validations[arg_name] = arg_val.value
self.emit(f"{wrapper_name} () {{") self.emit(f"{wrapper_name} () {{")
self.indent_level += 1 with self.indented():
if is_method:
if is_method: self.emit('local this="$1"')
self.emit('local this="$1"') self.emit('shift')
self.emit('shift')
for i, param in enumerate(params):
for i, param in enumerate(params): rule = validations.get(param.name)
rule = validations.get(param.name) if rule:
if rule: self._emit_validation_check(param.name, rule, i + 1)
self._emit_validation_check(param.name, rule, i + 1)
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))]) if is_method:
if is_method: self.emit(f'{wrapped_name} "$this" {params_str}')
self.emit(f'{wrapped_name} "$this" {params_str}') else:
else: self.emit(f'{wrapped_name} {params_str}')
self.emit(f'{wrapped_name} {params_str}')
self.indent_level -= 1
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -199,27 +184,24 @@ class DecoratorMixin: ...@@ -199,27 +184,24 @@ class DecoratorMixin:
if "int" in rule: if "int" in rule:
self.emit(f'if ! [[ "${param_name}" =~ ^-?[0-9]+$ ]]; then') self.emit(f'if ! [[ "${param_name}" =~ ^-?[0-9]+$ ]]; then')
self.indent_level += 1 with self.indented():
self.emit(f'echo "Validation error: {param_name} must be integer" >&2') self.emit(f'echo "Validation error: {param_name} must be integer" >&2')
self.emit('return 1') self.emit('return 1')
self.indent_level -= 1
self.emit('fi') self.emit('fi')
for match in re.finditer(r'(>=|<=|>|<|==|!=)\s*(-?\d+)', rule): for match in re.finditer(r'(>=|<=|>|<|==|!=)\s*(-?\d+)', rule):
op, val = match.groups() op, val = match.groups()
bash_op = {'>': '-gt', '<': '-lt', '>=': '-ge', '<=': '-le', '==': '-eq', '!=': '-ne'}[op] bash_op = {'>': '-gt', '<': '-lt', '>=': '-ge', '<=': '-le', '==': '-eq', '!=': '-ne'}[op]
self.emit(f'if ! [[ ${param_name} {bash_op} {val} ]]; then') self.emit(f'if ! [[ ${param_name} {bash_op} {val} ]]; then')
self.indent_level += 1 with self.indented():
self.emit(f'echo "Validation error: {param_name} must be {op} {val}" >&2') self.emit(f'echo "Validation error: {param_name} must be {op} {val}" >&2')
self.emit('return 1') self.emit('return 1')
self.indent_level -= 1
self.emit('fi') self.emit('fi')
elif "string" in rule: elif "string" in rule:
if "nonempty" in rule or "required" in rule: if "nonempty" in rule or "required" in rule:
self.emit(f'if [[ -z "${param_name}" ]]; then') self.emit(f'if [[ -z "${param_name}" ]]; then')
self.indent_level += 1 with self.indented():
self.emit(f'echo "Validation error: {param_name} cannot be empty" >&2') self.emit(f'echo "Validation error: {param_name} cannot be empty" >&2')
self.emit('return 1') self.emit('return 1')
self.indent_level -= 1
self.emit('fi') self.emit('fi')
...@@ -7,6 +7,7 @@ from .methods import ( ...@@ -7,6 +7,7 @@ from .methods import (
STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS, STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS,
NAMESPACE_METHODS, BUILTIN_NAMESPACES, BUILTIN_FUNCS, get_method_names NAMESPACE_METHODS, BUILTIN_NAMESPACES, BUILTIN_FUNCS, get_method_names
) )
from .constants import RET_VAR, RET_ARR
ARR_METHODS = {name: m.bash_func for name, m in ARRAY_METHODS.items()} ARR_METHODS = {name: m.bash_func for name, m in ARRAY_METHODS.items()}
STR_METHODS = {name: m.bash_func for name, m in STRING_METHODS.items()} STR_METHODS = {name: m.bash_func for name, m in STRING_METHODS.items()}
...@@ -50,12 +51,12 @@ class DispatchMixin: ...@@ -50,12 +51,12 @@ class DispatchMixin:
return True return True
def _emit_array_assign(self, target: str): def _emit_array_assign(self, target: str):
"""Emit array assignment from __CT_RET.""" """Emit array assignment from RET_VAR."""
if self.in_function and target not in self.local_vars and target not in self.global_vars: if self.in_function and target not in self.local_vars and target not in self.global_vars:
self.local_vars.add(target) self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET[@]}}")') self.emit(f'local -a {target}=("${{{RET_VAR}[@]}}")')
else: else:
self.emit(f'{target}=("${{__CT_RET[@]}}")') self.emit(f'{target}=("${{{RET_VAR}[@]}}")')
def _is_method_call_with_side_effects(self, expr) -> bool: def _is_method_call_with_side_effects(self, expr) -> bool:
"""Check if expression is a method call that may have side effects.""" """Check if expression is a method call that may have side effects."""
...@@ -134,7 +135,7 @@ class DispatchMixin: ...@@ -134,7 +135,7 @@ class DispatchMixin:
if isinstance(callee.object, Identifier) and callee.object.name == "fs" and callee.member == "open": if isinstance(callee.object, Identifier) and callee.object.name == "fs" and callee.member == "open":
args_str = self._generate_call_args_str(stmt.value.arguments) args_str = self._generate_call_args_str(stmt.value.arguments)
self.emit(f'__ct_fs_open {args_str} >/dev/null') self.emit(f'__ct_fs_open {args_str} >/dev/null')
self.emit_var_assign(target, '$__CT_RET') self.emit_var_assign(target, f'${RET_VAR}')
self.file_handle_vars.add(target) self.file_handle_vars.add(target)
return return
if isinstance(callee.object, Identifier) and callee.object.name == "json" and callee.member == "parse": if isinstance(callee.object, Identifier) and callee.object.name == "json" and callee.member == "parse":
...@@ -153,7 +154,7 @@ class DispatchMixin: ...@@ -153,7 +154,7 @@ class DispatchMixin:
if func_name not in BUILTIN_FUNCS and func_name not in self.classes: if func_name not in BUILTIN_FUNCS and func_name not in self.classes:
args_str = self._generate_call_args_str(stmt.value.arguments) args_str = self._generate_call_args_str(stmt.value.arguments)
self.emit(f'{func_name} {args_str} >/dev/null') self.emit(f'{func_name} {args_str} >/dev/null')
self.emit_var_assign(target, '$__CT_RET') self.emit_var_assign(target, f'${RET_VAR}')
return return
if isinstance(stmt.value, ArrayLiteral): if isinstance(stmt.value, ArrayLiteral):
...@@ -285,7 +286,7 @@ class DispatchMixin: ...@@ -285,7 +286,7 @@ class DispatchMixin:
if isinstance(callee.object, ThisExpr) and self.current_class: if isinstance(callee.object, ThisExpr) and self.current_class:
method = callee.member method = callee.member
self.emit(f'__ct_class_{self.current_class}_{method} "$this" {args_str} >/dev/null') self.emit(f'__ct_class_{self.current_class}_{method} "$this" {args_str} >/dev/null')
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
if isinstance(callee.object, MemberAccess) and isinstance(callee.object.object, ThisExpr): if isinstance(callee.object, MemberAccess) and isinstance(callee.object.object, ThisExpr):
...@@ -297,7 +298,7 @@ class DispatchMixin: ...@@ -297,7 +298,7 @@ class DispatchMixin:
if method in ARR_METHODS: if method in ARR_METHODS:
arr_name = f'"${{this}}_{field_name}"' arr_name = f'"${{this}}_{field_name}"'
self.emit(f'{ARR_METHODS[method]} {arr_name} {args_str} >/dev/null'.strip()) self.emit(f'{ARR_METHODS[method]} {arr_name} {args_str} >/dev/null'.strip())
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
else: else:
self._validate_type_method("array", method, location) self._validate_type_method("array", method, location)
...@@ -305,14 +306,14 @@ class DispatchMixin: ...@@ -305,14 +306,14 @@ class DispatchMixin:
if method in DICT_METHODS: if method in DICT_METHODS:
dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"' dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"'
self.emit(f'{DICT_METHODS_MAP[method]} {dict_ref} {args_str} >/dev/null'.strip()) self.emit(f'{DICT_METHODS_MAP[method]} {dict_ref} {args_str} >/dev/null'.strip())
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
else: else:
self._validate_type_method("dict", method, location) self._validate_type_method("dict", method, location)
if field_type == "object": if field_type == "object":
obj_ref = f'${{__CT_OBJ["$this.{field_name}"]}}' obj_ref = f'${{__CT_OBJ["$this.{field_name}"]}}'
self.emit(f'__ct_call_method "{obj_ref}" "{method}" {args_str} >/dev/null') self.emit(f'__ct_call_method "{obj_ref}" "{method}" {args_str} >/dev/null')
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
return False return False
...@@ -324,7 +325,7 @@ class DispatchMixin: ...@@ -324,7 +325,7 @@ class DispatchMixin:
field_ref = f'${{__CT_OBJ["${{{var_name}}}.{field_name}"]:-}}' field_ref = f'${{__CT_OBJ["${{{var_name}}}.{field_name}"]:-}}'
if method in STR_METHODS: if method in STR_METHODS:
self.emit(f'{STR_METHODS[method]} "{field_ref}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{STR_METHODS[method]} "{field_ref}" {args_str} >/dev/null'.replace(' ', ' '))
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
if isinstance(callee.object, Identifier): if isinstance(callee.object, Identifier):
...@@ -344,7 +345,7 @@ class DispatchMixin: ...@@ -344,7 +345,7 @@ class DispatchMixin:
arr_ref = mapped_name arr_ref = mapped_name
if method == "push" and len(args) == 1: if method == "push" and len(args) == 1:
self.emit(f'{mapped_name}+=("{args[0]}")') self.emit(f'{mapped_name}+=("{args[0]}")')
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
elif method in ("map", "filter") and len(stmt.value.arguments) >= 1: elif method in ("map", "filter") and len(stmt.value.arguments) >= 1:
first_arg = stmt.value.arguments[0] first_arg = stmt.value.arguments[0]
if isinstance(first_arg, Lambda): if isinstance(first_arg, Lambda):
...@@ -358,14 +359,14 @@ class DispatchMixin: ...@@ -358,14 +359,14 @@ class DispatchMixin:
self.emit(f'{func_name} "{arr_ref}" {args_str}') self.emit(f'{func_name} "{arr_ref}" {args_str}')
if self.in_function and target not in self.local_vars and target not in self.global_vars: if self.in_function and target not in self.local_vars and target not in self.global_vars:
self.local_vars.add(target) self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET_ARR[@]}}")') self.emit(f'local -a {target}=("${{{RET_ARR}[@]}}")')
else: else:
self.emit(f'{target}=("${{__CT_RET_ARR[@]}}")') self.emit(f'{target}=("${{{RET_ARR}[@]}}")')
self.array_vars.add(target) self.array_vars.add(target)
return True return True
else: else:
self.emit(f'{func_name} "{arr_ref}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "{arr_ref}" {args_str} >/dev/null'.replace(' ', ' '))
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
else: else:
self._validate_type_method("array", method, location) self._validate_type_method("array", method, location)
...@@ -376,16 +377,16 @@ class DispatchMixin: ...@@ -376,16 +377,16 @@ class DispatchMixin:
self.emit(f'__ct_dict_keys "{dict_ref}"') self.emit(f'__ct_dict_keys "{dict_ref}"')
if self.in_function and target not in self.local_vars and target not in self.global_vars: if self.in_function and target not in self.local_vars and target not in self.global_vars:
self.local_vars.add(target) self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET_ARR[@]}}")') self.emit(f'local -a {target}=("${{{RET_ARR}[@]}}")')
else: else:
self.emit(f'{target}=("${{__CT_RET_ARR[@]}}")') self.emit(f'{target}=("${{{RET_ARR}[@]}}")')
self.array_vars.add(target) self.array_vars.add(target)
return True return True
elif method in DICT_METHODS: elif method in DICT_METHODS:
func_name = DICT_METHODS_MAP[method] func_name = DICT_METHODS_MAP[method]
dict_ref = mapped_name dict_ref = mapped_name
self.emit(f'{func_name} "{dict_ref}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "{dict_ref}" {args_str} >/dev/null'.replace(' ', ' '))
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
else: else:
self._validate_type_method("dict", method, location) self._validate_type_method("dict", method, location)
...@@ -394,7 +395,7 @@ class DispatchMixin: ...@@ -394,7 +395,7 @@ class DispatchMixin:
if method in FILE_HANDLE_METHODS: if method in FILE_HANDLE_METHODS:
func_name = FILE_HANDLE_METHODS_MAP[method] func_name = FILE_HANDLE_METHODS_MAP[method]
self.emit(f'{func_name} "${obj_name}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "${obj_name}" {args_str} >/dev/null'.replace(' ', ' '))
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
else: else:
self._validate_type_method("file_handle", method, location) self._validate_type_method("file_handle", method, location)
...@@ -404,18 +405,18 @@ class DispatchMixin: ...@@ -404,18 +405,18 @@ class DispatchMixin:
obj = self.generate_expr(callee.object) obj = self.generate_expr(callee.object)
self.emit(f'__ct_call_method "{obj}" "{method}" {args_str} >/dev/null') self.emit(f'__ct_call_method "{obj}" "{method}" {args_str} >/dev/null')
if ret_type == "array": if ret_type == "array":
self.emit_var_assign(target, '$__CT_RET') self.emit_var_assign(target, f'${RET_VAR}')
self.array_vars.add(target) self.array_vars.add(target)
self.nameref_vars.add(target) self.nameref_vars.add(target)
elif ret_type == "dict": elif ret_type == "dict":
self.emit_var_assign(target, '$__CT_RET') self.emit_var_assign(target, f'${RET_VAR}')
self.dict_vars.add(target) self.dict_vars.add(target)
self.nameref_vars.add(target) self.nameref_vars.add(target)
elif ret_type == "object": elif ret_type == "object":
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
self.object_vars.add(target) self.object_vars.add(target)
else: else:
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
if method in STR_METHODS: if method in STR_METHODS:
...@@ -425,20 +426,20 @@ class DispatchMixin: ...@@ -425,20 +426,20 @@ class DispatchMixin:
self.emit(f'{func_name} "{obj}" {args_str}') self.emit(f'{func_name} "{obj}" {args_str}')
if self.in_function and target not in self.local_vars and target not in self.global_vars: if self.in_function and target not in self.local_vars and target not in self.global_vars:
self.local_vars.add(target) self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET_ARR[@]}}")') self.emit(f'local -a {target}=("${{{RET_ARR}[@]}}")')
else: else:
self.emit(f'{target}=("${{__CT_RET_ARR[@]}}")') self.emit(f'{target}=("${{{RET_ARR}[@]}}")')
self.array_vars.add(target) self.array_vars.add(target)
return True return True
self.emit(f'{func_name} "{obj}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "{obj}" {args_str} >/dev/null'.replace(' ', ' '))
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
if method in DICT_METHODS: if method in DICT_METHODS:
obj = self.generate_expr(callee.object) obj = self.generate_expr(callee.object)
func_name = DICT_METHODS_MAP[method] func_name = DICT_METHODS_MAP[method]
self.emit(f'{func_name} "{obj}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "{obj}" {args_str} >/dev/null'.replace(' ', ' '))
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
ret_type = self._get_method_return_type(obj_name, method) ret_type = self._get_method_return_type(obj_name, method)
...@@ -446,18 +447,18 @@ class DispatchMixin: ...@@ -446,18 +447,18 @@ class DispatchMixin:
self.emit(f'__ct_call_method "{obj}" "{method}" {args_str} >/dev/null') self.emit(f'__ct_call_method "{obj}" "{method}" {args_str} >/dev/null')
if ret_type == "array": if ret_type == "array":
self.emit_var_assign(target, '$__CT_RET') self.emit_var_assign(target, f'${RET_VAR}')
self.array_vars.add(target) self.array_vars.add(target)
self.nameref_vars.add(target) self.nameref_vars.add(target)
elif ret_type == "dict": elif ret_type == "dict":
self.emit_var_assign(target, '$__CT_RET') self.emit_var_assign(target, f'${RET_VAR}')
self.dict_vars.add(target) self.dict_vars.add(target)
self.nameref_vars.add(target) self.nameref_vars.add(target)
elif ret_type == "object": elif ret_type == "object":
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
self.object_vars.add(target) self.object_vars.add(target)
else: else:
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator) self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True return True
return False return False
...@@ -568,7 +569,7 @@ class DispatchMixin: ...@@ -568,7 +569,7 @@ class DispatchMixin:
call_code = self.generate_call_statement(first) call_code = self.generate_call_statement(first)
self.emit(f'{call_code} >/dev/null') self.emit(f'{call_code} >/dev/null')
current_var = self.new_temp() current_var = self.new_temp()
self.emit_var_assign(current_var, '$__CT_RET') self.emit_var_assign(current_var, f'${RET_VAR}')
elif isinstance(first, Identifier): elif isinstance(first, Identifier):
current_var = first.name current_var = first.name
else: else:
...@@ -590,12 +591,12 @@ class DispatchMixin: ...@@ -590,12 +591,12 @@ class DispatchMixin:
else: else:
self.emit(f'{func_name} "${{{current_var}}}" >/dev/null') self.emit(f'{func_name} "${{{current_var}}}" >/dev/null')
new_var = self.new_temp() new_var = self.new_temp()
self.emit_var_assign(new_var, '$__CT_RET') self.emit_var_assign(new_var, f'${RET_VAR}')
current_var = new_var current_var = new_var
elif isinstance(elem, Identifier): elif isinstance(elem, Identifier):
self.emit(f'{elem.name} "${{{current_var}}}" >/dev/null') self.emit(f'{elem.name} "${{{current_var}}}" >/dev/null')
new_var = self.new_temp() new_var = self.new_temp()
self.emit_var_assign(new_var, '$__CT_RET') self.emit_var_assign(new_var, f'${RET_VAR}')
current_var = new_var current_var = new_var
self.emit_var_assign(target, f'${{{current_var}}}') self.emit_var_assign(target, f'${{{current_var}}}')
......
...@@ -593,23 +593,20 @@ class ExprMixin: ...@@ -593,23 +593,20 @@ class ExprMixin:
def generate_lambda_as_function(self, expr: Lambda, name: str): def generate_lambda_as_function(self, expr: Lambda, name: str):
"""Generate lambda as a named function.""" """Generate lambda as a named function."""
self.emit(f"{name} () {{") self.emit(f"{name} () {{")
self.indent_level += 1 with self.indented():
for i, param in enumerate(expr.params):
self.emit(f'local {param}="${{{i + 1}}}"')
for i, param in enumerate(expr.params): if isinstance(expr.body, Block):
self.emit(f'local {param}="${{{i + 1}}}"') for stmt in expr.body.statements:
self.generate_statement(stmt)
if isinstance(expr.body, Block):
for stmt in expr.body.statements:
self.generate_statement(stmt)
else:
if self._is_boolean_expr(expr.body):
cond = self.generate_condition(expr.body)
self.emit(f'{cond} && echo "true" || echo "false"')
else: else:
result = self.generate_expr(expr.body) if self._is_boolean_expr(expr.body):
self.emit(f'echo "{result}"') cond = self.generate_condition(expr.body)
self.emit(f'{cond} && echo "true" || echo "false"')
self.indent_level -= 1 else:
result = self.generate_expr(expr.body)
self.emit(f'echo "{result}"')
self.emit("}") self.emit("}")
self.emit() self.emit()
......
"""Unified method registry for bash and AWK code generation.
This module provides a single source of truth for all builtin methods,
ensuring consistency between bash and AWK code generators.
"""
from dataclasses import dataclass
from typing import Optional, Callable, List
@dataclass
class MethodDef:
"""Definition of a builtin method."""
name: str
min_args: int = 0
max_args: Optional[int] = None
bash_func: Optional[str] = None
awk_gen: Optional[Callable[[str, List[str]], str]] = None
returns_array: bool = False
STRING_METHODS = {
"len": MethodDef("len", 0, 0, "__ct_str_len",
lambda obj, args: f"length({obj})"),
"upper": MethodDef("upper", 0, 0, "__ct_str_upper",
lambda obj, args: f"toupper({obj})"),
"lower": MethodDef("lower", 0, 0, "__ct_str_lower",
lambda obj, args: f"tolower({obj})"),
"trim": MethodDef("trim", 0, 0, "__ct_str_trim",
lambda obj, args: f'(gsub(/^[ \\t]+|[ \\t]+$/, "", {obj}) ? {obj} : {obj})'),
"contains": MethodDef("contains", 1, 1, "__ct_str_contains",
lambda obj, args: f"(index({obj}, {args[0]}) > 0)"),
"starts": MethodDef("starts", 1, 1, "__ct_str_starts",
lambda obj, args: f"(substr({obj}, 1, length({args[0]})) == {args[0]})"),
"ends": MethodDef("ends", 1, 1, "__ct_str_ends",
lambda obj, args: f"(substr({obj}, length({obj}) - length({args[0]}) + 1) == {args[0]})"),
"index": MethodDef("index", 1, 1, "__ct_str_index",
lambda obj, args: f"(index({obj}, {args[0]}) - 1)"),
"replace": MethodDef("replace", 2, 2, "__ct_str_replace",
lambda obj, args: f"(gsub({args[0]}, {args[1]}, {obj}) ? {obj} : {obj})"),
"substr": MethodDef("substr", 2, 2, "__ct_str_substr",
lambda obj, args: f"substr({obj}, {args[0]} + 1, {args[1]})"),
"split": MethodDef("split", 1, 1, "__ct_str_split",
lambda obj, args: f"split({obj}, __split_arr, {args[0]})",
returns_array=True),
"charAt": MethodDef("charAt", 1, 1, "__ct_str_char_at",
lambda obj, args: f"substr({obj}, {args[0]} + 1, 1)"),
"urlencode": MethodDef("urlencode", 0, 0, "__ct_str_urlencode", None),
}
ARRAY_METHODS = {
"len": MethodDef("len", 0, 0, "__ct_arr_len",
lambda obj, args: f"length({obj})"),
"push": MethodDef("push", 1, 1, "__ct_arr_push",
lambda obj, args: f"{obj}[length({obj}) + 1] = {args[0]}"),
"pop": MethodDef("pop", 0, 0, "__ct_arr_pop",
lambda obj, args: f"delete {obj}[length({obj})]"),
"shift": MethodDef("shift", 0, 0, "__ct_arr_shift",
lambda obj, args: f"delete {obj}[1]"),
"join": MethodDef("join", 1, 1, "__ct_arr_join",
lambda obj, args: f"__ct_awk_join({obj}, {args[0]})"),
"get": MethodDef("get", 1, 1, "__ct_arr_get",
lambda obj, args: f"{obj}[{args[0]}]"),
"set": MethodDef("set", 2, 2, "__ct_arr_set",
lambda obj, args: f"{obj}[{args[0]}] = {args[1]}"),
"slice": MethodDef("slice", 2, 2, "__ct_arr_slice", None, returns_array=True),
"map": MethodDef("map", 1, 1, "__ct_arr_map", None, returns_array=True),
"filter": MethodDef("filter", 1, 1, "__ct_arr_filter", None, returns_array=True),
}
DICT_METHODS = {
"get": MethodDef("get", 1, 1, "__ct_dict_get",
lambda obj, args: f"{obj}[{args[0]}]"),
"set": MethodDef("set", 2, 2, "__ct_dict_set",
lambda obj, args: f"{obj}[{args[0]}] = {args[1]}"),
"has": MethodDef("has", 1, 1, "__ct_dict_has",
lambda obj, args: f"({args[0]} in {obj})"),
"del": MethodDef("del", 1, 1, "__ct_dict_del",
lambda obj, args: f"delete {obj}[{args[0]}]"),
"keys": MethodDef("keys", 0, 0, "__ct_dict_keys", None, returns_array=True),
}
FILE_HANDLE_METHODS = {
"read": MethodDef("read", 0, 0, "__ct_fh_read", None),
"readline": MethodDef("readline", 0, 0, "__ct_fh_readline", None),
"write": MethodDef("write", 1, 1, "__ct_fh_write", None),
"writeln": MethodDef("writeln", 1, 1, "__ct_fh_writeln", None),
"close": MethodDef("close", 0, 0, "__ct_fh_close", None),
}
NAMESPACE_METHODS = {
"fs": {"read", "write", "append", "exists", "remove", "mkdir", "list", "open"},
"http": {"get", "post", "put", "delete"},
"json": {"parse", "stringify", "get"},
"logger": {"info", "warn", "error", "debug"},
"regex": {"match", "extract"},
"args": {"count", "get"},
"shell": {"exec", "capture", "source"},
"time": {"now", "ms"},
"math": {"add", "sub", "mul", "div", "mod", "min", "max", "abs"},
}
BUILTIN_NAMESPACES = set(NAMESPACE_METHODS.keys())
BUILTIN_FUNCS = {"print", "exit", "len", "range", "ngrep", "is_number",
"is_empty", "chr", "ord", "assert", "assert_eq", "random", "random_range"}
def get_method_names(type_name: str) -> set:
"""Get all available method names for a type."""
if type_name == "string":
return set(STRING_METHODS.keys())
elif type_name == "array":
return set(ARRAY_METHODS.keys())
elif type_name == "dict":
return set(DICT_METHODS.keys())
elif type_name == "file_handle":
return set(FILE_HANDLE_METHODS.keys())
return set()
def get_method_def(type_name: str, method_name: str) -> Optional[MethodDef]:
"""Get method definition by type and name."""
methods = {
"string": STRING_METHODS,
"array": ARRAY_METHODS,
"dict": DICT_METHODS,
"file_handle": FILE_HANDLE_METHODS,
}
return methods.get(type_name, {}).get(method_name)
def get_bash_func(type_name: str, method_name: str) -> Optional[str]:
"""Get bash function name for a method."""
method = get_method_def(type_name, method_name)
return method.bash_func if method else None
def generate_awk(type_name: str, method_name: str, obj: str, args: List[str]) -> Optional[str]:
"""Generate AWK code for a method call."""
method = get_method_def(type_name, method_name)
if method and method.awk_gen:
return method.awk_gen(obj, args)
return None
from .base import Method, collect_methods
from .string import StringMethods
from .array import ArrayMethods
from .dict import DictMethods
from .file_handle import FileHandleMethods
from .http import HttpMethods
from .fs import FsMethods
from .json import JsonMethods
from .logger import LoggerMethods
from .regex import RegexMethods
from .math import MathMethods
from .time import TimeMethods
from .args import ArgsMethods
from .core import CoreFunctions, AwkBuiltinFunctions
STRING_METHODS = collect_methods(StringMethods)
ARRAY_METHODS = collect_methods(ArrayMethods)
DICT_METHODS = collect_methods(DictMethods)
FILE_HANDLE_METHODS = collect_methods(FileHandleMethods)
HTTP_METHODS = collect_methods(HttpMethods)
FS_METHODS = collect_methods(FsMethods)
JSON_METHODS = collect_methods(JsonMethods)
LOGGER_METHODS = collect_methods(LoggerMethods)
REGEX_METHODS = collect_methods(RegexMethods)
MATH_METHODS = collect_methods(MathMethods)
TIME_METHODS = collect_methods(TimeMethods)
ARGS_METHODS = collect_methods(ArgsMethods)
CORE_FUNCTIONS = collect_methods(CoreFunctions)
AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions)
NAMESPACE_REGISTRY = {
"fs": FS_METHODS,
"http": HTTP_METHODS,
"json": JSON_METHODS,
"logger": LOGGER_METHODS,
"regex": REGEX_METHODS,
"args": ARGS_METHODS,
"time": TIME_METHODS,
"math": MATH_METHODS,
"shell": {"exec", "capture", "source"},
}
NAMESPACE_METHODS = {ns: set(methods.keys()) if hasattr(methods, 'keys') else methods
for ns, methods in NAMESPACE_REGISTRY.items()}
BUILTIN_NAMESPACES = set(NAMESPACE_METHODS.keys())
BUILTIN_FUNCS = set(CORE_FUNCTIONS.keys()) | {"chr", "ord", "assert", "assert_eq"}
def get_method(type_name: str, method_name: str):
registry = {
"string": STRING_METHODS,
"array": ARRAY_METHODS,
"dict": DICT_METHODS,
"file_handle": FILE_HANDLE_METHODS,
}
return registry.get(type_name, {}).get(method_name)
def get_method_names(type_name: str) -> set:
registry = {
"string": STRING_METHODS,
"array": ARRAY_METHODS,
"dict": DICT_METHODS,
"file_handle": FILE_HANDLE_METHODS,
}
methods = registry.get(type_name, {})
return set(methods.keys())
def get_bash_func(type_name: str, method_name: str):
method = get_method(type_name, method_name)
return method.bash_func if method else None
def generate_awk(type_name: str, method_name: str, obj: str, args: list):
method = get_method(type_name, method_name)
if method and method.awk_gen:
return method.awk_gen(obj, args)
return None
def get_awk_builtin(func_name: str, args: list):
"""Get AWK code for a builtin function call."""
method = CORE_FUNCTIONS.get(func_name) or AWK_BUILTIN_FUNCTIONS.get(func_name)
if method and method.awk_builtin:
return method.awk_builtin(args)
return None
from .base import Method
class ArgsMethods:
count = Method(name="count", bash_func="__ct_args_count", bash_impl='echo ${#__ct_args[@]}')
get = Method(name="get", bash_func="__ct_args_get", bash_impl="printf '%s\\n' \"${__ct_args[$1]}\"", min_args=1, max_args=1)
from .base import Method
class ArrayMethods:
len = Method(
name="len",
bash_func="__ct_arr_len",
bash_impl='local -n __a=$1; __CT_RET=${#__a[@]}; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"length({obj})",
)
push = Method(
name="push",
bash_func="__ct_arr_push",
bash_impl='local -n __a=$1; shift; __a+=("$@")',
awk_gen=lambda obj, args: f"{obj}[length({obj}) + 1] = {args[0]}",
min_args=1, max_args=1,
)
pop = Method(
name="pop",
bash_func="__ct_arr_pop",
bash_impl="local -n __a=$1; unset '__a[-1]'",
awk_gen=lambda obj, args: f"delete {obj}[length({obj})]",
)
shift = Method(
name="shift",
bash_func="__ct_arr_shift",
bash_impl='local -n __a=$1; __CT_RET="${__a[0]}"; __a=("${__a[@]:1}"); echo "$__CT_RET"',
awk_gen=lambda obj, args: f"delete {obj}[1]",
)
join = Method(
name="join",
bash_func="__ct_arr_join",
bash_impl='local -n __a=$1; local sep; printf -v sep \'%b\' "$2"; local IFS="$sep"; __CT_RET="${__a[*]}"; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"__ct_awk_join({obj}, {args[0]})",
min_args=1, max_args=1,
)
get = Method(
name="get",
bash_func="__ct_arr_get",
bash_impl='local -n __a=$1; __CT_RET="${__a[$2]}"; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"{obj}[{args[0]}]",
min_args=1, max_args=1,
)
set = Method(
name="set",
bash_func="__ct_arr_set",
bash_impl='local -n __a=$1; __a[$2]="$3"',
awk_gen=lambda obj, args: f"{obj}[{args[0]}] = {args[1]}",
min_args=2, max_args=2,
)
slice = Method(
name="slice",
bash_func="__ct_arr_slice",
bash_impl='local -n __a=$1; __CT_RET_ARR=("${__a[@]:$2:$3}")',
min_args=2, max_args=2,
returns_array=True,
)
map = Method(
name="map",
bash_func="__ct_arr_map",
bash_impl=None,
min_args=1, max_args=1,
returns_array=True,
)
filter = Method(
name="filter",
bash_func="__ct_arr_filter",
bash_impl=None,
min_args=1, max_args=1,
returns_array=True,
)
from dataclasses import dataclass
from typing import Optional, Callable, List
@dataclass
class Method:
name: str
bash_func: str
bash_impl: str = ""
awk_gen: Optional[Callable[[str, List[str]], str]] = None
awk_builtin: Optional[Callable[[List[str]], str]] = None
min_args: int = 0
max_args: Optional[int] = None
returns_array: bool = False
def collect_methods(cls) -> dict:
return {
name: getattr(cls, name)
for name in dir(cls)
if isinstance(getattr(cls, name), Method)
}
from .base import Method
class CoreFunctions:
print = Method(
name="print",
bash_func="__ct_print",
bash_impl='local msg="$1"; echo -e "$msg" >&3',
awk_builtin=lambda a: f"print {', '.join(a)}" if a else "print",
min_args=1, max_args=1,
)
exit = Method(
name="exit",
bash_func="__ct_exit",
bash_impl='exit "${1:-0}"',
awk_builtin=lambda a: f"exit {a[0]}" if a else "exit",
max_args=1,
)
len = Method(
name="len",
bash_func="__ct_len",
bash_impl='local -n arr=$1; echo "${#arr[@]}"',
awk_builtin=lambda a: f"length({a[0]})" if a else "length()",
min_args=1, max_args=1,
)
range = Method(
name="range",
bash_func="__ct_range",
bash_impl=None,
min_args=1, max_args=3,
)
is_number = Method(
name="is_number",
bash_func="__ct_is_number",
bash_impl='[[ "$1" =~ ^-?[0-9]+$ ]] && echo true || echo false',
awk_builtin=lambda a: f"({a[0]} ~ /^-?[0-9]+$/)",
min_args=1, max_args=1,
)
is_empty = Method(
name="is_empty",
bash_func="__ct_is_empty",
bash_impl='[[ -z "$1" ]] && echo true || echo false',
awk_builtin=lambda a: f"(length({a[0]}) == 0)",
min_args=1, max_args=1,
)
ngrep = Method(
name="ngrep",
bash_func="__ct_ngrep",
bash_impl='echo "$2" | grep -n "$1" || true',
min_args=2, max_args=2,
)
random = Method(
name="random",
bash_func="__ct_random",
bash_impl='echo $RANDOM',
awk_builtin=lambda a: "int(rand() * 32768)",
)
random_range = Method(
name="random_range",
bash_func="__ct_random_range",
bash_impl='echo $(($1 + RANDOM % ($2 - $1 + 1)))',
awk_builtin=lambda a: f"int({a[0]} + rand() * ({a[1]} - {a[0]} + 1))",
min_args=2, max_args=2,
)
class AwkBuiltinFunctions:
printf = Method(
name="printf",
bash_func="",
awk_builtin=lambda a: f"printf {', '.join(a)}",
min_args=1,
)
sprintf = Method(
name="sprintf",
bash_func="",
awk_builtin=lambda a: f"sprintf({', '.join(a)})",
min_args=1,
)
substr = Method(
name="substr",
bash_func="",
awk_builtin=lambda a: f"substr({', '.join(a)})",
min_args=2, max_args=3,
)
split = Method(
name="split",
bash_func="",
awk_builtin=lambda a: f"split({', '.join(a)})",
min_args=2, max_args=3,
)
sub = Method(
name="sub",
bash_func="",
awk_builtin=lambda a: f"sub({', '.join(a)})",
min_args=2, max_args=3,
)
gsub = Method(
name="gsub",
bash_func="",
awk_builtin=lambda a: f"gsub({', '.join(a)})",
min_args=2, max_args=3,
)
match = Method(
name="match",
bash_func="",
awk_builtin=lambda a: f"match({', '.join(a)})",
min_args=2,
)
tolower = Method(
name="tolower",
bash_func="",
awk_builtin=lambda a: f"tolower({a[0]})" if a else "",
min_args=1, max_args=1,
)
toupper = Method(
name="toupper",
bash_func="",
awk_builtin=lambda a: f"toupper({a[0]})" if a else "",
min_args=1, max_args=1,
)
int_ = Method(
name="int",
bash_func="",
awk_builtin=lambda a: f"int({a[0]})" if a else "",
min_args=1, max_args=1,
)
from .base import Method
class DictMethods:
get = Method(
name="get",
bash_func="__ct_dict_get",
bash_impl='local -n __d="$1"; __CT_RET="${__d[$2]}"; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"{obj}[{args[0]}]",
min_args=1, max_args=1,
)
set = Method(
name="set",
bash_func="__ct_dict_set",
bash_impl='local -n __d="$1"; __d["$2"]="$3"',
awk_gen=lambda obj, args: f"{obj}[{args[0]}] = {args[1]}",
min_args=2, max_args=2,
)
has = Method(
name="has",
bash_func="__ct_dict_has",
bash_impl='local -n __d="$1"; [[ -v "__d[$2]" ]] && __CT_RET=true || __CT_RET=false; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"({args[0]} in {obj})",
min_args=1, max_args=1,
)
delete = Method(
name="del",
bash_func="__ct_dict_del",
bash_impl='local -n __d="$1"; unset "__d[$2]"',
awk_gen=lambda obj, args: f"delete {obj}[{args[0]}]",
min_args=1, max_args=1,
)
keys = Method(
name="keys",
bash_func="__ct_dict_keys",
bash_impl='local -n __d="$1"; __CT_RET_ARR=("${!__d[@]}")',
returns_array=True,
)
len = Method(
name="len",
bash_func="__ct_dict_len",
bash_impl='local -n __d="$1"; __CT_RET=${#__d[@]}; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"length({obj})",
)
from .base import Method
class FileHandleMethods:
read = Method(
name="read",
bash_func="__ct_fh_read",
bash_impl='local h="$1"; local path="${__ct_file_handles[${h}_path]}"; __CT_RET=$(cat "$path"); echo "$__CT_RET"',
)
readline = Method(
name="readline",
bash_func="__ct_fh_readline",
bash_impl='local h="$1"; local path="${__ct_file_handles[${h}_path]}"; local pos="${__ct_file_handles[${h}_pos]:-1}"; __CT_RET=$(sed -n "${pos}p" "$path"); __ct_file_handles[${h}_pos]=$((pos + 1)); echo "$__CT_RET"',
)
write = Method(
name="write",
bash_func="__ct_fh_write",
bash_impl='local h="$1" data="$2"; local path="${__ct_file_handles[${h}_path]}"; local mode="${__ct_file_handles[${h}_mode]}"; [[ "$mode" == "a" ]] && echo -n "$data" >> "$path" || echo -n "$data" > "$path"',
min_args=1, max_args=1,
)
writeln = Method(
name="writeln",
bash_func="__ct_fh_writeln",
bash_impl='local h="$1" data="$2"; local path="${__ct_file_handles[${h}_path]}"; local mode="${__ct_file_handles[${h}_mode]}"; [[ "$mode" == "a" ]] && echo "$data" >> "$path" || echo "$data" > "$path"',
min_args=1, max_args=1,
)
close = Method(
name="close",
bash_func="__ct_fh_close",
bash_impl='local h="$1"; for k in path fd mode pos; do unset "__ct_file_handles[${h}_$k]"; done',
)
from .base import Method
class FsMethods:
read = Method(
name="read",
bash_func="__ct_fs_read",
bash_impl='cat "$1"',
min_args=1, max_args=1,
)
write = Method(
name="write",
bash_func="__ct_fs_write",
bash_impl='echo -n "$2" > "$1"',
min_args=2, max_args=2,
)
append = Method(
name="append",
bash_func="__ct_fs_append",
bash_impl='echo -n "$2" >> "$1"',
min_args=2, max_args=2,
)
exists = Method(
name="exists",
bash_func="__ct_fs_exists",
bash_impl='[[ -e "$1" ]] && echo "true" || echo "false"',
min_args=1, max_args=1,
)
remove = Method(
name="remove",
bash_func="__ct_fs_remove",
bash_impl='rm -f "$1"',
min_args=1, max_args=1,
)
mkdir = Method(
name="mkdir",
bash_func="__ct_fs_mkdir",
bash_impl='mkdir -p "$1"',
min_args=1, max_args=1,
)
list = Method(
name="list",
bash_func="__ct_fs_list",
bash_impl='ls -1 "$1" 2>/dev/null || true',
min_args=1, max_args=1,
)
open = Method(
name="open",
bash_func="__ct_fs_open",
bash_impl=None,
min_args=1, max_args=2,
)
from .base import Method
class HttpMethods:
get = Method(
name="get",
bash_func="__ct_http_get",
bash_impl='local url="$1"; local timeout="${2:-30}"; curl -sS --fail --show-error --max-time "$timeout" "$url"',
min_args=1, max_args=2,
)
post = Method(
name="post",
bash_func="__ct_http_post",
bash_impl='local url="$1"; local data="$2"; local timeout="${3:-30}"; curl -sS --fail --show-error --max-time "$timeout" -X POST -H "Content-Type: application/json" -d "$data" "$url"',
min_args=2, max_args=3,
)
put = Method(
name="put",
bash_func="__ct_http_put",
bash_impl='local url="$1"; local data="$2"; local timeout="${3:-30}"; curl -sS --fail --show-error --max-time "$timeout" -X PUT -H "Content-Type: application/json" -d "$data" "$url"',
min_args=2, max_args=3,
)
delete = Method(
name="delete",
bash_func="__ct_http_delete",
bash_impl='local url="$1"; local timeout="${2:-30}"; curl -sS --fail --show-error --max-time "$timeout" -X DELETE "$url"',
min_args=1, max_args=2,
)
from .base import Method
class JsonMethods:
parse = Method(
name="parse",
bash_func="__ct_json_parse",
bash_impl=None,
min_args=1, max_args=2,
)
stringify = Method(
name="stringify",
bash_func="__ct_json_stringify",
bash_impl=None,
min_args=1, max_args=1,
)
get = Method(
name="get",
bash_func="__ct_json_get",
bash_impl='echo "$1" | jq -r "$2" 2>/dev/null',
min_args=2, max_args=2,
)
from .base import Method
class LoggerMethods:
info = Method(
name="info",
bash_func="__ct_logger_info",
bash_impl='echo "[INFO] $1"',
min_args=1, max_args=1,
)
warn = Method(
name="warn",
bash_func="__ct_logger_warn",
bash_impl='echo "[WARN] $1" >&2',
min_args=1, max_args=1,
)
error = Method(
name="error",
bash_func="__ct_logger_error",
bash_impl='echo "[ERROR] $1" >&2',
min_args=1, max_args=1,
)
debug = Method(
name="debug",
bash_func="__ct_logger_debug",
bash_impl='echo "[DEBUG] $1"',
min_args=1, max_args=1,
)
from .base import Method
class MathMethods:
add = Method(name="add", bash_func="__ct_math_add", bash_impl='echo $(($1 + $2))', min_args=2, max_args=2)
sub = Method(name="sub", bash_func="__ct_math_sub", bash_impl='echo $(($1 - $2))', min_args=2, max_args=2)
mul = Method(name="mul", bash_func="__ct_math_mul", bash_impl='echo $(($1 * $2))', min_args=2, max_args=2)
div = Method(name="div", bash_func="__ct_math_div", bash_impl='echo $(($1 / $2))', min_args=2, max_args=2)
mod = Method(name="mod", bash_func="__ct_math_mod", bash_impl='echo $(($1 % $2))', min_args=2, max_args=2)
min = Method(name="min", bash_func="__ct_math_min", bash_impl='(($1 < $2)) && echo $1 || echo $2', min_args=2, max_args=2)
max = Method(name="max", bash_func="__ct_math_max", bash_impl='(($1 > $2)) && echo $1 || echo $2', min_args=2, max_args=2)
abs = Method(name="abs", bash_func="__ct_math_abs", bash_impl='local n=$1; echo ${n#-}', min_args=1, max_args=1)
sin = Method(name="sin", bash_func="__ct_math_sin", bash_impl='__ct_awk "BEGIN{print sin($1)}"', awk_builtin=lambda a: f"sin({a[0]})", min_args=1, max_args=1)
cos = Method(name="cos", bash_func="__ct_math_cos", bash_impl='__ct_awk "BEGIN{print cos($1)}"', awk_builtin=lambda a: f"cos({a[0]})", min_args=1, max_args=1)
sqrt = Method(name="sqrt", bash_func="__ct_math_sqrt", bash_impl='__ct_awk "BEGIN{print sqrt($1)}"', awk_builtin=lambda a: f"sqrt({a[0]})", min_args=1, max_args=1)
log = Method(name="log", bash_func="__ct_math_log", bash_impl='__ct_awk "BEGIN{print log($1)}"', awk_builtin=lambda a: f"log({a[0]})", min_args=1, max_args=1)
exp = Method(name="exp", bash_func="__ct_math_exp", bash_impl='__ct_awk "BEGIN{print exp($1)}"', awk_builtin=lambda a: f"exp({a[0]})", min_args=1, max_args=1)
int_ = Method(name="int", bash_func="__ct_math_int", bash_impl='echo "${1%.*}"', awk_builtin=lambda a: f"int({a[0]})", min_args=1, max_args=1)
rand = Method(name="rand", bash_func="__ct_math_rand", bash_impl='__ct_awk "BEGIN{srand(); print rand()}"', awk_builtin=lambda a: "rand()")
atan2 = Method(name="atan2", bash_func="__ct_math_atan2", bash_impl='__ct_awk "BEGIN{print atan2($1, $2)}"', awk_builtin=lambda a: f"atan2({a[0]}, {a[1]})", min_args=2, max_args=2)
from .base import Method
class RegexMethods:
match = Method(
name="match",
bash_func="__ct_regex_match",
bash_impl='[[ "$1" =~ $2 ]] && echo true || echo false',
min_args=2, max_args=2,
)
extract = Method(
name="extract",
bash_func="__ct_regex_extract",
bash_impl='[[ "$1" =~ $2 ]] && echo "${BASH_REMATCH[0]}"',
min_args=2, max_args=2,
)
from .base import Method
class StringMethods:
len = Method(
name="len",
bash_func="__ct_str_len",
bash_impl='__CT_RET=${#1}; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"length({obj})",
)
upper = Method(
name="upper",
bash_func="__ct_str_upper",
bash_impl='__CT_RET="${1^^}"; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"toupper({obj})",
)
lower = Method(
name="lower",
bash_func="__ct_str_lower",
bash_impl='__CT_RET="${1,,}"; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"tolower({obj})",
)
trim = Method(
name="trim",
bash_func="__ct_str_trim",
bash_impl='local s="$1"; s="${s#"${s%%[![:space:]]*}"}"; __CT_RET="${s%"${s##*[![:space:]]}"}" ; echo "$__CT_RET"',
awk_gen=lambda obj, args: f'(gsub(/^[ \\t]+|[ \\t]+$/, "", {obj}) ? {obj} : {obj})',
)
contains = Method(
name="contains",
bash_func="__ct_str_contains",
bash_impl='[[ "$1" == *"$2"* ]] && __CT_RET=true || __CT_RET=false; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"(index({obj}, {args[0]}) > 0)",
min_args=1, max_args=1,
)
starts = Method(
name="starts",
bash_func="__ct_str_starts",
bash_impl='[[ "$1" == "$2"* ]] && __CT_RET=true || __CT_RET=false; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"(substr({obj}, 1, length({args[0]})) == {args[0]})",
min_args=1, max_args=1,
)
ends = Method(
name="ends",
bash_func="__ct_str_ends",
bash_impl='[[ "$1" == *"$2" ]] && __CT_RET=true || __CT_RET=false; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"(substr({obj}, length({obj}) - length({args[0]}) + 1) == {args[0]})",
min_args=1, max_args=1,
)
index = Method(
name="index",
bash_func="__ct_str_index",
bash_impl='local i="${1%%$2*}"; [[ "$i" == "$1" ]] && __CT_RET=-1 || __CT_RET=${#i}; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"(index({obj}, {args[0]}) - 1)",
min_args=1, max_args=1,
)
replace = Method(
name="replace",
bash_func="__ct_str_replace",
bash_impl='__CT_RET="${1//"$2"/"$3"}"; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"(gsub({args[0]}, {args[1]}, {obj}) ? {obj} : {obj})",
min_args=2, max_args=2,
)
substr = Method(
name="substr",
bash_func="__ct_str_substr",
bash_impl='__CT_RET="${1:$2:$3}"; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"substr({obj}, {args[0]} + 1, {args[1]})",
min_args=2, max_args=2,
)
split = Method(
name="split",
bash_func="__ct_str_split",
bash_impl='local IFS="$2"; read -ra __CT_RET_ARR <<< "$1"',
awk_gen=lambda obj, args: f"split({obj}, __split_arr, {args[0]})",
min_args=1, max_args=1,
returns_array=True,
)
charAt = Method(
name="charAt",
bash_func="__ct_str_char_at",
bash_impl='__CT_RET="${1:$2:1}"; printf \'%sX\' "$__CT_RET"',
awk_gen=lambda obj, args: f"substr({obj}, {args[0]} + 1, 1)",
min_args=1, max_args=1,
)
urlencode = Method(
name="urlencode",
bash_func="__ct_str_urlencode",
bash_impl='local s="$1" c i len=${#1}; __CT_RET=""; for ((i=0; i<len; i++)); do c="${s:i:1}"; case "$c" in [a-zA-Z0-9.~_-]) __CT_RET+="$c" ;; *) __CT_RET+=$(printf "%%%02X" "\'$c") ;; esac; done; echo "$__CT_RET"',
)
concat = Method(
name="concat",
bash_func="__ct_str_concat",
bash_impl='__CT_RET="$1$2"; echo "$__CT_RET"',
awk_gen=lambda obj, args: f"({obj} {args[0]})",
min_args=1, max_args=1,
)
ord = Method(
name="ord",
bash_func="__ct_str_ord",
bash_impl="__CT_RET=$(printf '%d' \"'$1\"); echo \"$__CT_RET\"",
)
chr = Method(
name="chr",
bash_func="__ct_str_chr",
bash_impl="printf -v __CT_RET '%b' \"\\\\x$(printf '%02x' \"$1\")\"; echo \"$__CT_RET\"",
)
from .base import Method
class TimeMethods:
now = Method(name="now", bash_func="__ct_time_now", bash_impl='date +%s')
ms = Method(name="ms", bash_func="__ct_time_ms", bash_impl='date +%s%3N')
"""Standard library code generation. """Standard library code generation.
Generates bash implementations for ContenT stdlib functions. Generates bash implementations for ContenT stdlib functions.
Uses method definitions from methods.py for consistency. Uses method definitions from methods.py as single source of truth.
""" """
from .methods import STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS from .methods import (
STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS,
HTTP_METHODS, FS_METHODS, JSON_METHODS, LOGGER_METHODS,
# Bash implementations for string methods REGEX_METHODS, MATH_METHODS, TIME_METHODS, ARGS_METHODS, CORE_FUNCTIONS
STRING_IMPLS = { )
"len": '__CT_RET=${#1}; echo "$__CT_RET"',
"substr": '__CT_RET="${1:$2:$3}"; echo "$__CT_RET"',
"index": 'local i="${1%%$2*}"; [[ "$i" == "$1" ]] && __CT_RET=-1 || __CT_RET=${#i}; echo "$__CT_RET"',
"contains": '[[ "$1" == *"$2"* ]] && __CT_RET=true || __CT_RET=false; echo "$__CT_RET"',
"starts": '[[ "$1" == "$2"* ]] && __CT_RET=true || __CT_RET=false; echo "$__CT_RET"',
"ends": '[[ "$1" == *"$2" ]] && __CT_RET=true || __CT_RET=false; echo "$__CT_RET"',
"replace": '__CT_RET="${1//"$2"/"$3"}"; echo "$__CT_RET"',
"split": 'local IFS="$2"; read -ra __CT_RET_ARR <<< "$1"',
"trim": 'local s="$1"; s="${s#"${s%%[![:space:]]*}"}"; __CT_RET="${s%"${s##*[![:space:]]}"}" ; echo "$__CT_RET"',
"upper": '__CT_RET="${1^^}"; echo "$__CT_RET"',
"lower": '__CT_RET="${1,,}"; echo "$__CT_RET"',
"charAt": '__CT_RET="${1:$2:1}"; printf \'%sX\' "$__CT_RET"',
"urlencode": "__CT_RET=$(printf '%s' \"$1\" | jq -sRr @uri); echo \"$__CT_RET\"",
}
# Bash implementations for array methods
ARRAY_IMPLS = {
"push": 'local -n __a=$1; shift; __a+=("$@")',
"pop": "local -n __a=$1; unset '__a[-1]'",
"shift": 'local -n __a=$1; __CT_RET="${__a[0]}"; __a=("${__a[@]:1}"); echo "$__CT_RET"',
"join": 'local -n __a=$1; local sep; printf -v sep \'%b\' "$2"; local IFS="$sep"; __CT_RET="${__a[*]}"; echo "$__CT_RET"',
"len": 'local -n __a=$1; __CT_RET=${#__a[@]}; echo "$__CT_RET"',
"get": 'local -n __a=$1; __CT_RET="${__a[$2]}"; echo "$__CT_RET"',
"set": 'local -n __a=$1; __a[$2]="$3"',
"slice": 'local -n __a=$1; __CT_RET_ARR=("${__a[@]:$2:$3}")',
}
# Bash implementations for dict methods
DICT_IMPLS = {
"set": 'local -n __d="$1"; __d["$2"]="$3"',
"get": 'local -n __d="$1"; __CT_RET="${__d[$2]}"; echo "$__CT_RET"',
"has": 'local -n __d="$1"; [[ -v "__d[$2]" ]] && __CT_RET=true || __CT_RET=false; echo "$__CT_RET"',
"del": 'local -n __d="$1"; unset "__d[$2]"',
"keys": 'local -n __d="$1"; __CT_RET_ARR=("${!__d[@]}")',
"len": 'local -n __d="$1"; __CT_RET=${#__d[@]}; echo "$__CT_RET"',
}
class StdlibMixin: class StdlibMixin:
...@@ -95,7 +59,7 @@ class StdlibMixin: ...@@ -95,7 +59,7 @@ class StdlibMixin:
self._emit_args() self._emit_args()
else: else:
self._emit_utils_minimal() self._emit_utils_minimal()
if 'awk' in used_categories: if 'awk' in used_categories or 'math' in used_categories:
self._emit_awk_wrapper() self._emit_awk_wrapper()
if 'math' in used_categories: if 'math' in used_categories:
self._emit_math() self._emit_math()
...@@ -114,11 +78,7 @@ class StdlibMixin: ...@@ -114,11 +78,7 @@ class StdlibMixin:
self.emit("exec 3>&1 # Save stdout to FD3 for print()") self.emit("exec 3>&1 # Save stdout to FD3 for print()")
self.emit() self.emit()
self.emit("__ct_print () {") self.emit(f"{CORE_FUNCTIONS['print'].bash_func} () {{ {CORE_FUNCTIONS['print'].bash_impl}; }}")
with self.indented():
self.emit('local msg="$1"')
self.emit('echo -e "$msg" >&3')
self.emit("}")
self.emit() self.emit()
self.emit("__ct_range () {") self.emit("__ct_range () {")
...@@ -134,55 +94,22 @@ class StdlibMixin: ...@@ -134,55 +94,22 @@ class StdlibMixin:
self.emit("}") self.emit("}")
self.emit() self.emit()
self.emit("__ct_len () {") self.emit(f"{CORE_FUNCTIONS['len'].bash_func} () {{ {CORE_FUNCTIONS['len'].bash_impl}; }}")
with self.indented():
self.emit('local -n arr=$1')
self.emit('echo "${#arr[@]}"')
self.emit("}")
self.emit() self.emit()
def _emit_http(self): def _emit_http(self):
"""HTTP functions: get, post, put, delete.""" """HTTP functions from HTTP_METHODS."""
http_funcs = [ for method_name, method_def in HTTP_METHODS.items():
("get", ['url', 'timeout="${2:-30}"'], if method_def.bash_impl:
'curl -sS --fail --show-error --max-time "$timeout" "$url"'), self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
("post", ['url', 'data', 'timeout="${3:-30}"'], self.emit()
'curl -sS --fail --show-error --max-time "$timeout" -X POST -H "Content-Type: application/json" -d "$data" "$url"'),
("put", ['url', 'data', 'timeout="${3:-30}"'],
'curl -sS --fail --show-error --max-time "$timeout" -X PUT -H "Content-Type: application/json" -d "$data" "$url"'),
("delete", ['url', 'timeout="${2:-30}"'],
'curl -sS --fail --show-error --max-time "$timeout" -X DELETE "$url"'),
]
for name, params, body in http_funcs:
self.emit(f"__ct_http_{name} () {{")
with self.indented():
for i, p in enumerate(params):
if '=' in p:
self.emit(f'local {p}')
else:
self.emit(f'local {p}="${{{i+1}}}"')
self.emit(body)
self.emit("}")
self.emit()
def _emit_fs(self): def _emit_fs(self):
"""Filesystem functions.""" """Filesystem functions from FS_METHODS."""
fs_simple = [ for method_name, method_def in FS_METHODS.items():
("read", 'cat "$1"'), if method_def.bash_impl:
("write", 'echo -n "$2" > "$1"'), self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
("append", 'echo -n "$2" >> "$1"'), self.emit()
("exists", '[[ -e "$1" ]] && echo "true" || echo "false"'),
("remove", 'rm -f "$1"'),
("mkdir", 'mkdir -p "$1"'),
("list", 'ls -1 "$1" 2>/dev/null || true'),
]
for name, body in fs_simple:
self.emit(f"__ct_fs_{name} () {{")
with self.indented():
self.emit(body)
self.emit("}")
self.emit()
self._emit_file_handles() self._emit_file_handles()
def _emit_file_handles(self): def _emit_file_handles(self):
...@@ -206,48 +133,10 @@ class StdlibMixin: ...@@ -206,48 +133,10 @@ class StdlibMixin:
self.emit("}") self.emit("}")
self.emit() self.emit()
# File handle methods - using FILE_HANDLE_METHODS for names
fh_impls = {
"read": [
'local h="$1"',
'local path="${__ct_file_handles[${h}_path]}"',
'__CT_RET=$(cat "$path")',
'echo "$__CT_RET"',
],
"readline": [
'local h="$1"',
'local path="${__ct_file_handles[${h}_path]}"',
'local pos="${__ct_file_handles[${h}_pos]:-1}"',
'__CT_RET=$(sed -n "${pos}p" "$path")',
'__ct_file_handles[${h}_pos]=$((pos + 1))',
'echo "$__CT_RET"',
],
"write": [
'local h="$1" data="$2"',
'local path="${__ct_file_handles[${h}_path]}"',
'local mode="${__ct_file_handles[${h}_mode]}"',
'[[ "$mode" == "a" ]] && echo -n "$data" >> "$path" || echo -n "$data" > "$path"',
],
"writeln": [
'local h="$1" data="$2"',
'local path="${__ct_file_handles[${h}_path]}"',
'local mode="${__ct_file_handles[${h}_mode]}"',
'[[ "$mode" == "a" ]] && echo "$data" >> "$path" || echo "$data" > "$path"',
],
"close": [
'local h="$1"',
'for k in path fd mode pos; do unset "__ct_file_handles[${h}_$k]"; done',
],
}
for method_name, method_def in FILE_HANDLE_METHODS.items(): for method_name, method_def in FILE_HANDLE_METHODS.items():
if method_name in fh_impls: if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{") self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
with self.indented(): self.emit()
for line in fh_impls[method_name]:
self.emit(line)
self.emit("}")
self.emit()
self.emit("# Context manager support") self.emit("# Context manager support")
self.emit('__ct_fh___enter__ () { echo "$1"; }') self.emit('__ct_fh___enter__ () { echo "$1"; }')
...@@ -332,32 +221,26 @@ class StdlibMixin: ...@@ -332,32 +221,26 @@ class StdlibMixin:
self.emit() self.emit()
def _emit_logger(self): def _emit_logger(self):
"""Logger functions.""" """Logger functions from LOGGER_METHODS."""
levels = [("info", "INFO", ""), ("warn", "WARN", " >&2"), for method_name, method_def in LOGGER_METHODS.items():
("error", "ERROR", " >&2"), ("debug", "DEBUG", "")] if method_def.bash_impl:
for name, label, redir in levels: self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit(f'__ct_logger_{name} () {{ echo "[{label}] $1"{redir}; }}')
self.emit() self.emit()
def _emit_string(self): def _emit_string(self):
"""String functions from STRING_METHODS.""" """String functions from STRING_METHODS."""
self.emit("# String functions") self.emit("# String functions")
for method_name, method_def in STRING_METHODS.items(): for method_name, method_def in STRING_METHODS.items():
if method_name in STRING_IMPLS: if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {STRING_IMPLS[method_name]}; }}") self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit("__ct_str_concat () { __CT_RET=\"$1$2\"; echo \"$__CT_RET\"; }")
self.emit()
self.emit("# Character code functions")
self.emit("__ct_str_ord () { __CT_RET=$(printf '%d' \"'$1\"); echo \"$__CT_RET\"; }")
self.emit("__ct_str_chr () { printf -v __CT_RET '%b' \"\\\\x$(printf '%02x' \"$1\")\"; echo \"$__CT_RET\"; }")
self.emit() self.emit()
def _emit_array(self): def _emit_array(self):
"""Array functions from ARRAY_METHODS.""" """Array functions from ARRAY_METHODS."""
self.emit("# Array functions") self.emit("# Array functions")
for method_name, method_def in ARRAY_METHODS.items(): for method_name, method_def in ARRAY_METHODS.items():
if method_name in ARRAY_IMPLS: if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {ARRAY_IMPLS[method_name]}; }}") self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit() self.emit()
self.emit("# Array map/filter with lambda functions") self.emit("# Array map/filter with lambda functions")
...@@ -390,34 +273,33 @@ class StdlibMixin: ...@@ -390,34 +273,33 @@ class StdlibMixin:
self.emit() self.emit()
def _emit_regex(self): def _emit_regex(self):
"""Regex functions.""" """Regex functions from REGEX_METHODS."""
self.emit("# Regex functions") self.emit("# Regex functions")
self.emit("__ct_ngrep () { echo \"$2\" | grep -n \"$1\" || true; }") for method_name, method_def in REGEX_METHODS.items():
self.emit('__ct_regex_match () { [[ "$1" =~ $2 ]] && echo true || echo false; }') if method_def.bash_impl:
self.emit("__ct_regex_extract () {") self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
with self.indented(): self.emit(f"{CORE_FUNCTIONS['ngrep'].bash_func} () {{ {CORE_FUNCTIONS['ngrep'].bash_impl}; }}")
self.emit('[[ "$1" =~ $2 ]] && echo "${BASH_REMATCH[0]}"')
self.emit("}")
self.emit() self.emit()
def _emit_utils(self): def _emit_utils(self):
"""Utility functions.""" """Utility functions."""
self.emit('__ct_exit () { exit "${1:-0}"; }') self.emit(f"{CORE_FUNCTIONS['exit'].bash_func} () {{ {CORE_FUNCTIONS['exit'].bash_impl}; }}")
self.emit() self.emit()
self.emit('__ct_is_number () { [[ "$1" =~ ^-?[0-9]+$ ]] && echo true || echo false; }') self.emit(f"{CORE_FUNCTIONS['is_number'].bash_func} () {{ {CORE_FUNCTIONS['is_number'].bash_impl}; }}")
self.emit('__ct_is_empty () { [[ -z "$1" ]] && echo true || echo false; }') self.emit(f"{CORE_FUNCTIONS['is_empty'].bash_func} () {{ {CORE_FUNCTIONS['is_empty'].bash_impl}; }}")
self.emit() self.emit()
self.emit('__ct_args=("$@")') self.emit('__ct_args=("$@")')
self.emit('__ct_args_count () { echo ${#__ct_args[@]}; }') for method_name, method_def in ARGS_METHODS.items():
self.emit("__ct_args_get () { printf '%s\\n' \"${__ct_args[$1]}\"; }") if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit() self.emit()
def _emit_utils_minimal(self): def _emit_utils_minimal(self):
"""Minimal utility functions (without args).""" """Minimal utility functions (without args)."""
self.emit('__ct_exit () { exit "${1:-0}"; }') self.emit(f"{CORE_FUNCTIONS['exit'].bash_func} () {{ {CORE_FUNCTIONS['exit'].bash_impl}; }}")
self.emit() self.emit()
self.emit('__ct_is_number () { [[ "$1" =~ ^-?[0-9]+$ ]] && echo true || echo false; }') self.emit(f"{CORE_FUNCTIONS['is_number'].bash_func} () {{ {CORE_FUNCTIONS['is_number'].bash_impl}; }}")
self.emit('__ct_is_empty () { [[ -z "$1" ]] && echo true || echo false; }') self.emit(f"{CORE_FUNCTIONS['is_empty'].bash_func} () {{ {CORE_FUNCTIONS['is_empty'].bash_impl}; }}")
self.emit() self.emit()
def _emit_args(self): def _emit_args(self):
...@@ -437,26 +319,19 @@ class StdlibMixin: ...@@ -437,26 +319,19 @@ class StdlibMixin:
self.emit() self.emit()
def _emit_math(self): def _emit_math(self):
"""Math functions.""" """Math functions from MATH_METHODS."""
self.emit("# Math functions") self.emit("# Math functions")
math_ops = [ for method_name, method_def in MATH_METHODS.items():
("add", "+"), ("sub", "-"), ("mul", "*"), ("div", "/"), ("mod", "%") if method_def.bash_impl:
] self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
for name, op in math_ops:
self.emit(f"__ct_math_{name} () {{ echo $(($1 {op} $2)); }}")
self.emit("__ct_math_min () { (($1 < $2)) && echo $1 || echo $2; }")
self.emit("__ct_math_max () { (($1 > $2)) && echo $1 || echo $2; }")
self.emit("__ct_math_abs () { local n=$1; echo ${n#-}; }")
self.emit() self.emit()
def _emit_dict(self): def _emit_dict(self):
"""Dict functions from DICT_METHODS.""" """Dict functions from DICT_METHODS."""
self.emit("# Dict functions") self.emit("# Dict functions")
for method_name, method_def in DICT_METHODS.items(): for method_name, method_def in DICT_METHODS.items():
if method_name in DICT_IMPLS: if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {DICT_IMPLS[method_name]}; }}") self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
# len is not in DICT_METHODS, add separately
self.emit(f"__ct_dict_len () {{ {DICT_IMPLS['len']}; }}")
self.emit() self.emit()
def _emit_misc(self): def _emit_misc(self):
...@@ -466,12 +341,13 @@ class StdlibMixin: ...@@ -466,12 +341,13 @@ class StdlibMixin:
self.emit("__ct_hex_to_byte () { printf '%d' \"0x$1\"; }") self.emit("__ct_hex_to_byte () { printf '%d' \"0x$1\"; }")
self.emit() self.emit()
self.emit("# Time functions") self.emit("# Time functions")
self.emit("__ct_time_now () { date +%s; }") for method_name, method_def in TIME_METHODS.items():
self.emit("__ct_time_ms () { date +%s%3N; }") if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit() self.emit()
self.emit("# Random") self.emit("# Random")
self.emit("__ct_random () { echo $RANDOM; }") self.emit(f"{CORE_FUNCTIONS['random'].bash_func} () {{ {CORE_FUNCTIONS['random'].bash_impl}; }}")
self.emit("__ct_random_range () { echo $(($1 + RANDOM % ($2 - $1 + 1))); }") self.emit(f"{CORE_FUNCTIONS['random_range'].bash_func} () {{ {CORE_FUNCTIONS['random_range'].bash_impl}; }}")
self.emit() self.emit()
self.emit("__CT_NL=$'\\n'") self.emit("__CT_NL=$'\\n'")
self.emit() self.emit()
......
...@@ -5,6 +5,7 @@ from .ast_nodes import ( ...@@ -5,6 +5,7 @@ from .ast_nodes import (
CallExpr, Identifier, MemberAccess, ThisExpr, StringLiteral, NewExpr, CallExpr, Identifier, MemberAccess, ThisExpr, StringLiteral, NewExpr,
BinaryOp, DictLiteral, ArrayLiteral, WhenBranch BinaryOp, DictLiteral, ArrayLiteral, WhenBranch
) )
from .constants import RET_VAR, RET_ARR
class StmtMixin: class StmtMixin:
...@@ -61,13 +62,11 @@ class StmtMixin: ...@@ -61,13 +62,11 @@ class StmtMixin:
else: else:
self.emit(f'# import "{path}"') self.emit(f'# import "{path}"')
self.emit(f'if [[ -f "$HOME/.content/libs/{path}.sh" ]]; then') self.emit(f'if [[ -f "$HOME/.content/libs/{path}.sh" ]]; then')
self.indent_level += 1 with self.indented():
self.emit(f'source "$HOME/.content/libs/{path}.sh"') self.emit(f'source "$HOME/.content/libs/{path}.sh"')
self.indent_level -= 1
self.emit(f'elif [[ -f "/usr/lib/content/{path}.sh" ]]; then') self.emit(f'elif [[ -f "/usr/lib/content/{path}.sh" ]]; then')
self.indent_level += 1 with self.indented():
self.emit(f'source "/usr/lib/content/{path}.sh"') self.emit(f'source "/usr/lib/content/{path}.sh"')
self.indent_level -= 1
self.emit("fi") self.emit("fi")
self.emit() self.emit()
...@@ -77,7 +76,7 @@ class StmtMixin: ...@@ -77,7 +76,7 @@ class StmtMixin:
args = [self.generate_expr(a) for a in stmt.condition.arguments] args = [self.generate_expr(a) for a in stmt.condition.arguments]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{func_name} {args_str} >/dev/null') self.emit(f'{func_name} {args_str} >/dev/null')
self.emit(f'if [[ "$__CT_RET" == "true" ]]; then') self.emit(f'if [[ "${{{RET_VAR}}}" == "true" ]]; then')
else: else:
mapping, _ = self.precompute_all_calls(stmt.condition) mapping, _ = self.precompute_all_calls(stmt.condition)
if mapping: if mapping:
...@@ -86,25 +85,22 @@ class StmtMixin: ...@@ -86,25 +85,22 @@ class StmtMixin:
cond = self.generate_condition(stmt.condition) cond = self.generate_condition(stmt.condition)
self.emit(f"if {cond}; then") self.emit(f"if {cond}; then")
self.indent_level += 1 with self.indented():
for s in stmt.then_branch.statements: for s in stmt.then_branch.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
for elif_cond, elif_block in stmt.elif_branches: for elif_cond, elif_block in stmt.elif_branches:
cond = self.generate_condition(elif_cond) cond = self.generate_condition(elif_cond)
self.emit(f"elif {cond}; then") self.emit(f"elif {cond}; then")
self.indent_level += 1 with self.indented():
for s in elif_block.statements: for s in elif_block.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
if stmt.else_branch: if stmt.else_branch:
self.emit("else") self.emit("else")
self.indent_level += 1 with self.indented():
for s in stmt.else_branch.statements: for s in stmt.else_branch.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
self.emit("fi") self.emit("fi")
...@@ -121,25 +117,23 @@ class StmtMixin: ...@@ -121,25 +117,23 @@ class StmtMixin:
def _generate_when_case(self, stmt: WhenStmt, value_expr: str): def _generate_when_case(self, stmt: WhenStmt, value_expr: str):
self.emit(f'case "{value_expr}" in') self.emit(f'case "{value_expr}" in')
self.indent_level += 1 with self.indented():
for branch in stmt.branches: for branch in stmt.branches:
if branch.is_else: if branch.is_else:
self.emit("*)") self.emit("*)")
else: else:
patterns = [] patterns = []
for p in branch.patterns: for p in branch.patterns:
val = self.generate_expr(p) val = self.generate_expr(p)
if isinstance(p, StringLiteral): if isinstance(p, StringLiteral):
patterns.append(f'"{val}"' if val else '""') patterns.append(f'"{val}"' if val else '""')
else: else:
patterns.append(val) patterns.append(val)
self.emit(f'{"|".join(patterns)})') self.emit(f'{"|".join(patterns)})')
self.indent_level += 1 with self.indented():
for s in branch.body.statements: for s in branch.body.statements:
self.generate_statement(s) self.generate_statement(s)
self.emit(";;") self.emit(";;")
self.indent_level -= 1
self.indent_level -= 1
self.emit("esac") self.emit("esac")
def _generate_when_if_chain(self, stmt: WhenStmt, value_expr: str): def _generate_when_if_chain(self, stmt: WhenStmt, value_expr: str):
...@@ -164,10 +158,9 @@ class StmtMixin: ...@@ -164,10 +158,9 @@ class StmtMixin:
first = False first = False
else: else:
self.emit(f"elif {cond_str}; then") self.emit(f"elif {cond_str}; then")
self.indent_level += 1 with self.indented():
for s in branch.body.statements: for s in branch.body.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
self.emit("fi") self.emit("fi")
def generate_while(self, stmt: WhileStmt): def generate_while(self, stmt: WhileStmt):
...@@ -175,16 +168,15 @@ class StmtMixin: ...@@ -175,16 +168,15 @@ class StmtMixin:
if mapping: if mapping:
cond = self.generate_condition_with_precompute(stmt.condition, mapping) cond = self.generate_condition_with_precompute(stmt.condition, mapping)
self.emit(f"while {cond}; do") self.emit(f"while {cond}; do")
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
if regen_code: if regen_code:
self.emit("# CSE: re-compute condition vars") self.emit("# CSE: re-compute condition vars")
for call_line, assign_line in regen_code: for call_line, assign_line in regen_code:
self.emit(call_line) self.emit(call_line)
if assign_line: if assign_line:
self.emit(assign_line) self.emit(assign_line)
self.indent_level -= 1
self.emit("done") self.emit("done")
return return
...@@ -193,21 +185,19 @@ class StmtMixin: ...@@ -193,21 +185,19 @@ class StmtMixin:
args = [self.generate_expr(a) for a in stmt.condition.arguments] args = [self.generate_expr(a) for a in stmt.condition.arguments]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
self.emit("while true; do") self.emit("while true; do")
self.indent_level += 1 with self.indented():
self.emit(f'{func_name} {args_str} >/dev/null') self.emit(f'{func_name} {args_str} >/dev/null')
self.emit(f'if [[ "$__CT_RET" != "true" ]]; then break; fi') self.emit(f'if [[ "${{{RET_VAR}}}" != "true" ]]; then break; fi')
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
self.emit("done") self.emit("done")
return return
cond = self.generate_condition(stmt.condition) cond = self.generate_condition(stmt.condition)
self.emit(f"while {cond}; do") self.emit(f"while {cond}; do")
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
self.emit("done") self.emit("done")
def generate_for(self, stmt: ForStmt): def generate_for(self, stmt: ForStmt):
...@@ -221,19 +211,17 @@ class StmtMixin: ...@@ -221,19 +211,17 @@ class StmtMixin:
self.emit(f"for {var} in $(seq {args[0]} $(({args[1]} - 1))); do") self.emit(f"for {var} in $(seq {args[0]} $(({args[1]} - 1))); do")
else: else:
self.emit(f"for {var} in $(seq {args[0]} {args[2]} $(({args[1]} - 1))); do") self.emit(f"for {var} in $(seq {args[0]} {args[2]} $(({args[1]} - 1))); do")
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
self.emit("done") self.emit("done")
return return
iterable = self.generate_expr(stmt.iterable) iterable = self.generate_expr(stmt.iterable)
self.emit(f'for {var} in {iterable}; do') self.emit(f'for {var} in {iterable}; do')
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
self.emit("done") self.emit("done")
def generate_foreach(self, stmt: ForeachStmt): def generate_foreach(self, stmt: ForeachStmt):
...@@ -247,10 +235,9 @@ class StmtMixin: ...@@ -247,10 +235,9 @@ class StmtMixin:
self.emit(f"for {var} in $(seq {args[0]} $(({args[1]} - 1))); do") self.emit(f"for {var} in $(seq {args[0]} $(({args[1]} - 1))); do")
else: else:
self.emit(f"for {var} in $(seq {args[0]} {args[2]} $(({args[1]} - 1))); do") self.emit(f"for {var} in $(seq {args[0]} {args[2]} $(({args[1]} - 1))); do")
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
self.emit("done") self.emit("done")
return return
...@@ -266,12 +253,11 @@ class StmtMixin: ...@@ -266,12 +253,11 @@ class StmtMixin:
val_var = stmt.variables[1] val_var = stmt.variables[1]
self.emit(f'{idx_var}=0') self.emit(f'{idx_var}=0')
self.emit(f'for {val_var} in "${{{arr_name}[@]}}"; do') self.emit(f'for {val_var} in "${{{arr_name}[@]}}"; do')
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
if len(stmt.variables) == 2: if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))') self.emit(f'((++{stmt.variables[0]}))')
self.indent_level -= 1
self.emit("done") self.emit("done")
return return
...@@ -292,12 +278,11 @@ class StmtMixin: ...@@ -292,12 +278,11 @@ class StmtMixin:
val_var = stmt.variables[1] val_var = stmt.variables[1]
self.emit(f'{idx_var}=0') self.emit(f'{idx_var}=0')
self.emit(f'for {val_var} in "${{__ct_split_arr[@]}}"; do') self.emit(f'for {val_var} in "${{__ct_split_arr[@]}}"; do')
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
if len(stmt.variables) == 2: if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))') self.emit(f'((++{stmt.variables[0]}))')
self.indent_level -= 1
self.emit("done") self.emit("done")
return return
...@@ -307,28 +292,26 @@ class StmtMixin: ...@@ -307,28 +292,26 @@ class StmtMixin:
var = stmt.variables[0] var = stmt.variables[0]
self.emit(f'__ct_str_split "{str_expr}" "{delim_arg}"') self.emit(f'__ct_str_split "{str_expr}" "{delim_arg}"')
if len(stmt.variables) == 1: if len(stmt.variables) == 1:
self.emit(f'for {var} in "${{__CT_RET_ARR[@]}}"; do') self.emit(f'for {var} in "${{{RET_ARR}[@]}}"; do')
else: else:
idx_var = stmt.variables[0] idx_var = stmt.variables[0]
val_var = stmt.variables[1] val_var = stmt.variables[1]
self.emit(f'{idx_var}=0') self.emit(f'{idx_var}=0')
self.emit(f'for {val_var} in "${{__CT_RET_ARR[@]}}"; do') self.emit(f'for {val_var} in "${{{RET_ARR}[@]}}"; do')
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
if len(stmt.variables) == 2: if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))') self.emit(f'((++{stmt.variables[0]}))')
self.indent_level -= 1
self.emit("done") self.emit("done")
return return
iterable = self.generate_expr(stmt.iterable) iterable = self.generate_expr(stmt.iterable)
var = stmt.variables[0] var = stmt.variables[0]
self.emit(f'for {var} in {iterable}; do') self.emit(f'for {var} in {iterable}; do')
self.indent_level += 1 with self.indented():
for s in stmt.body.statements: for s in stmt.body.statements:
self.generate_statement(s) self.generate_statement(s)
self.indent_level -= 1
self.emit("done") self.emit("done")
def generate_with(self, stmt: WithStmt): def generate_with(self, stmt: WithStmt):
...@@ -343,7 +326,7 @@ class StmtMixin: ...@@ -343,7 +326,7 @@ class StmtMixin:
args = [self.generate_expr(a) for a in resource.arguments] args = [self.generate_expr(a) for a in resource.arguments]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'__ct_fs_open {args_str} >/dev/null') self.emit(f'__ct_fs_open {args_str} >/dev/null')
self.emit(f'{local_kw}__ct_with_{i}="$__CT_RET"') self.emit(f'{local_kw}__ct_with_{i}="${{{RET_VAR}}}"')
self.emit(f'{local_kw}{var}="$__ct_with_{i}"') self.emit(f'{local_kw}{var}="$__ct_with_{i}"')
continue continue
...@@ -366,45 +349,45 @@ class StmtMixin: ...@@ -366,45 +349,45 @@ class StmtMixin:
self.emit("__ct_try_failed=0") self.emit("__ct_try_failed=0")
self.emit() self.emit()
self.emit("while true; do") self.emit("while true; do")
self.indent_level += 1 with self.indented():
for s in stmt.try_block.statements: for s in stmt.try_block.statements:
self.generate_statement(s) self.generate_statement(s)
self.emit('if [[ $? -ne 0 ]]; then') self.emit('if [[ $? -ne 0 ]]; then')
self.indent_level += 1 with self.indented():
self.emit('__ct_try_failed=1') self.emit('__ct_try_failed=1')
self.emit('__ct_exception_type="Error"') self.emit('__ct_exception_type="Error"')
self.emit('__ct_exception="Command failed"') self.emit('__ct_exception="Command failed"')
self.emit('break') self.emit('break')
self.indent_level -= 1 self.emit('fi')
self.emit('fi') self.emit("break")
self.emit("break")
self.indent_level -= 1
self.emit("done") self.emit("done")
self.emit() self.emit()
self.emit("set -e") self.emit("set -e")
self.emit() self.emit()
self.emit('if [[ "$__ct_try_failed" == "1" ]]; then') self.emit('if [[ "$__ct_try_failed" == "1" ]]; then')
self.indent_level += 1 with self.indented():
for i, (exc_type, exc_var, exc_block) in enumerate(stmt.except_clauses): for i, (exc_type, exc_var, exc_block) in enumerate(stmt.except_clauses):
if i == 0: if i == 0:
if exc_type: if exc_type:
self.emit(f'if [[ "$__ct_exception_type" == "{exc_type}" ]]; then') self.emit(f'if [[ "$__ct_exception_type" == "{exc_type}" ]]; then')
else:
if exc_type:
self.emit(f'elif [[ "$__ct_exception_type" == "{exc_type}" ]]; then')
else: else:
self.emit("else") if exc_type:
if exc_type or i > 0: self.emit(f'elif [[ "$__ct_exception_type" == "{exc_type}" ]]; then')
self.indent_level += 1 else:
if exc_var: self.emit("else")
self.emit(f'{exc_var}="$__ct_exception"') if exc_type or i > 0:
for s in exc_block.statements: with self.indented():
self.generate_statement(s) if exc_var:
if exc_type or i > 0: self.emit(f'{exc_var}="$__ct_exception"')
self.indent_level -= 1 for s in exc_block.statements:
if stmt.except_clauses and stmt.except_clauses[0][0] is not None: self.generate_statement(s)
self.emit("fi") else:
self.indent_level -= 1 if exc_var:
self.emit(f'{exc_var}="$__ct_exception"')
for s in exc_block.statements:
self.generate_statement(s)
if stmt.except_clauses and stmt.except_clauses[0][0] is not None:
self.emit("fi")
self.emit("fi") self.emit("fi")
if stmt.finally_block: if stmt.finally_block:
self.emit() self.emit()
...@@ -453,15 +436,15 @@ class StmtMixin: ...@@ -453,15 +436,15 @@ class StmtMixin:
args = [self.generate_expr(arg) for arg in stmt.value.arguments] args = [self.generate_expr(arg) for arg in stmt.value.arguments]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{stmt.value.class_name} {args_str}') self.emit(f'{stmt.value.class_name} {args_str}')
self.emit('__CT_RET="$__ct_last_instance"') self.emit('{RET_VAR}="$__ct_last_instance"')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return return
if isinstance(stmt.value, BinaryOp) and stmt.value.operator in ("==", "!=", "<", ">", "<=", ">=", "&&", "||"): if isinstance(stmt.value, BinaryOp) and stmt.value.operator in ("==", "!=", "<", ">", "<=", ">=", "&&", "||"):
cond = self.generate_condition(stmt.value) cond = self.generate_condition(stmt.value)
self.emit(f'{cond} && __CT_RET=true || __CT_RET=false') self.emit(f'{cond} && {RET_VAR}=true || {RET_VAR}=false')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return return
...@@ -472,8 +455,8 @@ class StmtMixin: ...@@ -472,8 +455,8 @@ class StmtMixin:
key = self.generate_expr(k) key = self.generate_expr(k)
val = self.generate_expr(v) val = self.generate_expr(v)
self.emit(f'eval "$__ct_ret_dict[{key}]=\\"{val}\\""') self.emit(f'eval "$__ct_ret_dict[{key}]=\\"{val}\\""')
self.emit('__CT_RET="$__ct_ret_dict"') self.emit('{RET_VAR}="$__ct_ret_dict"')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return return
...@@ -483,14 +466,14 @@ class StmtMixin: ...@@ -483,14 +466,14 @@ class StmtMixin:
elements = [self.generate_expr(e) for e in stmt.value.elements] elements = [self.generate_expr(e) for e in stmt.value.elements]
for i, elem in enumerate(elements): for i, elem in enumerate(elements):
self.emit(f'eval "$__ct_ret_arr[{i}]=\\"{elem}\\""') self.emit(f'eval "$__ct_ret_arr[{i}]=\\"{elem}\\""')
self.emit('__CT_RET="$__ct_ret_arr"') self.emit('{RET_VAR}="$__ct_ret_arr"')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return return
value = self.generate_expr(stmt.value) value = self.generate_expr(stmt.value)
self.emit(f'__CT_RET="{value}"') self.emit(f'{RET_VAR}="{value}"')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
else: else:
self.emit("return 0") self.emit("return 0")
...@@ -501,22 +484,22 @@ class StmtMixin: ...@@ -501,22 +484,22 @@ class StmtMixin:
if method == "contains" and len(expr.arguments) == 2: if method == "contains" and len(expr.arguments) == 2:
haystack = self.generate_expr(expr.arguments[0]) haystack = self.generate_expr(expr.arguments[0])
needle = self.generate_expr(expr.arguments[1]) needle = self.generate_expr(expr.arguments[1])
self.emit(f'[[ "{haystack}" == *"{needle}"* ]] && __CT_RET=true || __CT_RET=false') self.emit(f'[[ "{haystack}" == *"{needle}"* ]] && {RET_VAR}=true || {RET_VAR}=false')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
elif method == "starts" and len(expr.arguments) == 2: elif method == "starts" and len(expr.arguments) == 2:
s = self.generate_expr(expr.arguments[0]) s = self.generate_expr(expr.arguments[0])
prefix = self.generate_expr(expr.arguments[1]) prefix = self.generate_expr(expr.arguments[1])
self.emit(f'[[ "{s}" == "{prefix}"* ]] && __CT_RET=true || __CT_RET=false') self.emit(f'[[ "{s}" == "{prefix}"* ]] && {RET_VAR}=true || {RET_VAR}=false')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
elif method == "ends" and len(expr.arguments) == 2: elif method == "ends" and len(expr.arguments) == 2:
s = self.generate_expr(expr.arguments[0]) s = self.generate_expr(expr.arguments[0])
suffix = self.generate_expr(expr.arguments[1]) suffix = self.generate_expr(expr.arguments[1])
self.emit(f'[[ "{s}" == *"{suffix}" ]] && __CT_RET=true || __CT_RET=false') self.emit(f'[[ "{s}" == *"{suffix}" ]] && {RET_VAR}=true || {RET_VAR}=false')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
return False return False
...@@ -538,7 +521,7 @@ class StmtMixin: ...@@ -538,7 +521,7 @@ class StmtMixin:
if method in str_methods: if method in str_methods:
func_name = str_methods[method] func_name = str_methods[method]
self.emit(f'{func_name} "${{__CT_OBJ["$this.{field}"]}}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "${{__CT_OBJ["$this.{field}"]}}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
if isinstance(expr.callee.object, ThisExpr) and self.current_class: if isinstance(expr.callee.object, ThisExpr) and self.current_class:
...@@ -546,7 +529,7 @@ class StmtMixin: ...@@ -546,7 +529,7 @@ class StmtMixin:
args = [self.generate_expr(arg) for arg in expr.arguments] args = [self.generate_expr(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'__ct_class_{self.current_class}_{method} "$this" {args_str} >/dev/null') self.emit(f'__ct_class_{self.current_class}_{method} "$this" {args_str} >/dev/null')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
elif isinstance(expr.callee.object, Identifier): elif isinstance(expr.callee.object, Identifier):
...@@ -564,15 +547,15 @@ class StmtMixin: ...@@ -564,15 +547,15 @@ class StmtMixin:
args = [self._generate_call_arg(arg) for arg in expr.arguments] args = [self._generate_call_arg(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{func_name} {args_str}') self.emit(f'{func_name} {args_str}')
self.emit('__CT_RET="$__ct_last_instance"') self.emit('{RET_VAR}="$__ct_last_instance"')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
if func_name in self.functions: if func_name in self.functions:
args = [self._generate_call_arg(arg) for arg in expr.arguments] args = [self._generate_call_arg(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{func_name} {args_str} >/dev/null') self.emit(f'{func_name} {args_str} >/dev/null')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
return False return False
...@@ -604,23 +587,23 @@ class StmtMixin: ...@@ -604,23 +587,23 @@ class StmtMixin:
if mapped_name in self.array_vars and method in arr_methods: if mapped_name in self.array_vars and method in arr_methods:
func_name = arr_methods[method] func_name = arr_methods[method]
self.emit(f'{func_name} "{mapped_name}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "{mapped_name}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
elif mapped_name in self.dict_vars and method in dict_methods: elif mapped_name in self.dict_vars and method in dict_methods:
func_name = dict_methods[method] func_name = dict_methods[method]
self.emit(f'{func_name} "{mapped_name}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "{mapped_name}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
elif method in str_methods: elif method in str_methods:
func_name = str_methods[method] func_name = str_methods[method]
self.emit(f'{func_name} "${{{mapped_name}}}" {args_str} >/dev/null'.replace(' ', ' ')) self.emit(f'{func_name} "${{{mapped_name}}}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
self.emit(f'__ct_call_method "${{{mapped_name}}}" "{method}" {args_str} >/dev/null') self.emit(f'__ct_call_method "${{{mapped_name}}}" "{method}" {args_str} >/dev/null')
self.emit('echo "$__CT_RET"') self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0") self.emit("return 0")
return True return True
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment