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
Вспомогательные модули:
├── 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
STRING_METHODS = {
# methods/string.py
class StringMethods:
...
"new_method": MethodDef(
"new_method",
min_args=1,
max_args=1,
new_method = Method(
name="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 автоматически подхватят изменения.
......@@ -366,7 +366,12 @@ bootstrap/ # Bootstrap compiler (Python)
├── ast_nodes.py # AST node classes
├── errors.py # Error handling
├── 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
├── codegen.py # Main Bash code generator (mixin coordinator)
├── expr_codegen.py # Expression generation (mixin)
......
......@@ -366,7 +366,12 @@ bootstrap/ # Bootstrap-компилятор (Python)
├── ast_nodes.py # Классы узлов AST
├── errors.py # Обработка ошибок
├── 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 # Устранение мёртвого кода
├── codegen.py # Основной генератор Bash-кода (координатор миксинов)
├── expr_codegen.py # Генерация выражений (миксин)
......
......@@ -8,33 +8,8 @@ from .ast_nodes import (
BoolLiteral, NilLiteral, ArrayLiteral, DictLiteral, BinaryOp,
UnaryOp, CallExpr, IndexAccess, MemberAccess
)
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 "",
}
from .methods import get_awk_builtin, generate_awk, MATH_METHODS
from .constants import RET_VAR
class AwkCodegenMixin:
......@@ -93,8 +68,13 @@ class AwkCodegenMixin:
"""Generate a function that runs as inline AWK instead of Bash."""
name = func.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
for dec in func.decorators:
if dec.name == "validate":
......@@ -154,7 +134,7 @@ class AwkCodegenMixin:
for stmt in after_stmts:
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:
self._awk_helper_func (nf)
......@@ -186,7 +166,7 @@ class AwkCodegenMixin:
for stmt in main_stmts:
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:
self._awk_helper_func (nf)
......@@ -197,11 +177,8 @@ class AwkCodegenMixin:
self.emit ("}')")
self.emit ('local __awk_rc=$?')
self.emit ('echo "$__CT_RET"')
self.emit (f'echo "${{{RET_VAR}}}"')
self.emit ('return $__awk_rc')
self.indent_level -= 1
self.emit ("}")
self.emit ()
def _awk_helper_func (self, func: FunctionDecl):
"""Generate AWK helper function definition."""
......@@ -500,99 +477,27 @@ class AwkCodegenMixin:
method = expr.callee.member
args = expr.arguments
if ns == "math" and method in AWK_MATH_FUNCS:
awk_args = [self._awk_expr(a) for a in args]
return AWK_MATH_FUNCS[method](awk_args)
if ns == "math" and method in MATH_METHODS:
math_method = MATH_METHODS[method]
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_type = var_types.get (ns, "string")
if var_type == "array":
if method == "len":
return f"length({ns})"
if method == "push" and len (args) >= 1:
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})"
type_name = {"array": "array", "dict": "dict"}.get(var_type, "string")
awk_args = [self._awk_expr(a) for a in args]
awk_code = generate_awk(type_name, method, ns, awk_args)
if awk_code:
return awk_code
if isinstance (expr.callee, Identifier):
func_name = expr.callee.name
args = [self._awk_expr(a) for a in expr.arguments]
if func_name in AWK_BUILTIN_FUNCS:
return AWK_BUILTIN_FUNCS[func_name](args)
awk_code = get_awk_builtin(func_name, args)
if awk_code:
return awk_code
return f"{func_name}({', '.join(args)})"
......
......@@ -89,81 +89,78 @@ class ClassMixin:
def _generate_class_constructor(self, cls: ClassDecl):
"""Generate class factory function."""
self.emit(f"{cls.name} () {{")
self.indent_level += 1
# Save instance immediately as nested constructors may overwrite __ct_last_instance
self.emit('local __ct_this_instance="__ct_inst_$RANDOM$RANDOM"')
self.emit('__ct_obj_class["$__ct_this_instance"]="{}"'.format(cls.name))
for field_name, default_value in cls.fields:
if isinstance(default_value, ArrayLiteral):
elements = [self.generate_expr(e) for e in default_value.elements]
if elements:
arr_content = " ".join([f'"{e}"' for e in elements])
self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=({arr_content})"')
with self.indented():
# Save instance immediately as nested constructors may overwrite __ct_last_instance
self.emit('local __ct_this_instance="__ct_inst_$RANDOM$RANDOM"')
self.emit('__ct_obj_class["$__ct_this_instance"]="{}"'.format(cls.name))
for field_name, default_value in cls.fields:
if isinstance(default_value, ArrayLiteral):
elements = [self.generate_expr(e) for e in default_value.elements]
if elements:
arr_content = " ".join([f'"{e}"' for e in elements])
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:
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:
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]=""')
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]=""')
if cls.parent:
self.emit(f'# Inherit from {cls.parent}')
if cls.parent:
self.emit(f'# Inherit from {cls.parent}')
if cls.constructor:
self.emit("# Call constructor")
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}')
# Restore __ct_last_instance to this instance (after nested constructors may have changed it)
self.emit('__ct_last_instance="$__ct_this_instance"')
if cls.constructor:
self.emit("# Call constructor")
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.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()
def _generate_construct_method(self, cls: ClassDecl):
"""Generate construct method."""
self.emit(f"__ct_class_{cls.name}_construct () {{")
self.indent_level += 1
self.emit('local this="$1"')
self.emit('shift')
with self.indented():
self.emit('local this="$1"')
self.emit('shift')
self.in_class_method = True
old_in_function = self.in_function
old_local_vars = self.local_vars.copy()
old_param_positions = self.current_param_positions.copy()
self.in_function = True
self.local_vars = set()
self.current_param_positions = {}
self.in_class_method = True
old_in_function = self.in_function
old_local_vars = self.local_vars.copy()
old_param_positions = self.current_param_positions.copy()
self.in_function = True
self.local_vars = set()
self.current_param_positions = {}
for i, param in enumerate(cls.constructor.params):
self.current_param_positions[param.name] = i + 1
for i, param in enumerate(cls.constructor.params):
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):
if param.name in array_field_params:
continue
if param.default is not None:
default_val = self.generate_expr(param.default)
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
else:
self.emit(f'local {param.name}="${{{i + 1}}}"')
self.local_vars.add(param.name)
for i, param in enumerate(cls.constructor.params):
if param.name in array_field_params:
continue
if param.default is not None:
default_val = self.generate_expr(param.default)
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
else:
self.emit(f'local {param.name}="${{{i + 1}}}"')
self.local_vars.add(param.name)
for stmt in cls.constructor.body.statements:
self.generate_statement(stmt)
for stmt in cls.constructor.body.statements:
self.generate_statement(stmt)
self.in_class_method = False
self.in_function = old_in_function
self.local_vars = old_local_vars
self.current_param_positions = old_param_positions
self.indent_level -= 1
self.in_class_method = False
self.in_function = old_in_function
self.local_vars = old_local_vars
self.current_param_positions = old_param_positions
self.emit("}")
self.emit()
......@@ -184,48 +181,46 @@ class ClassMixin:
def _generate_plain_method(self, cls: ClassDecl, method: FunctionDecl):
"""Generate a plain class method."""
self.emit(f"__ct_class_{cls.name}_{method.name} () {{")
self.indent_level += 1
self.emit('local this="$1"')
self.emit('shift')
self.in_class_method = True
old_in_function = self.in_function
old_local_vars = self.local_vars.copy()
old_object_vars = self.object_vars.copy()
self.in_function = True
self.local_vars = set()
param_types = self._analyze_param_types(method)
for i, param in enumerate(method.params):
if param.is_variadic:
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}}}"')
with self.indented():
self.emit('local this="$1"')
self.emit('shift')
self.in_class_method = True
old_in_function = self.in_function
old_local_vars = self.local_vars.copy()
old_object_vars = self.object_vars.copy()
self.in_function = True
self.local_vars = set()
param_types = self._analyze_param_types(method)
for i, param in enumerate(method.params):
if param.is_variadic:
self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")')
else:
self.emit(f'local {param.name}="${{{i + 1}}}"')
self.local_vars.add(param.name)
if param_types.get(param.name) == "object":
self.object_vars.add(param.name)
for stmt in method.body.statements:
self.generate_statement(stmt)
self.in_class_method = False
self.in_function = old_in_function
self.local_vars = old_local_vars
self.object_vars = old_object_vars
self.indent_level -= 1
if param.default is not None:
default_val = self.generate_expr(param.default)
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
else:
self.emit(f'local {param.name}="${{{i + 1}}}"')
self.local_vars.add(param.name)
if param_types.get(param.name) == "object":
self.object_vars.add(param.name)
for stmt in method.body.statements:
self.generate_statement(stmt)
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()
def _generate_inherited_method(self, child_cls: ClassDecl, parent_cls: ClassDecl, method: FunctionDecl):
"""Generate proxy method for inherited method."""
self.emit(f"__ct_class_{child_cls.name}_{method.name} () {{")
self.indent_level += 1
self.emit(f'__ct_class_{parent_cls.name}_{method.name} "$@"')
self.indent_level -= 1
with self.indented():
self.emit(f'__ct_class_{parent_cls.name}_{method.name} "$@"')
self.emit("}")
self.emit()
......@@ -235,39 +230,38 @@ class ClassMixin:
original_name = f"{base_name}_orig"
self.emit(f"{original_name} () {{")
self.indent_level += 1
self.emit('local this="$1"')
self.emit('shift')
self.in_class_method = True
old_in_function = self.in_function
old_local_vars = self.local_vars.copy()
old_object_vars = self.object_vars.copy()
self.in_function = True
self.local_vars = set()
param_types = self._analyze_param_types(method)
for i, param in enumerate(method.params):
if param.is_variadic:
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}}}"')
with self.indented():
self.emit('local this="$1"')
self.emit('shift')
self.in_class_method = True
old_in_function = self.in_function
old_local_vars = self.local_vars.copy()
old_object_vars = self.object_vars.copy()
self.in_function = True
self.local_vars = set()
param_types = self._analyze_param_types(method)
for i, param in enumerate(method.params):
if param.is_variadic:
self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")')
else:
self.emit(f'local {param.name}="${{{i + 1}}}"')
self.local_vars.add(param.name)
if param_types.get(param.name) == "object":
self.object_vars.add(param.name)
for stmt in method.body.statements:
self.generate_statement(stmt)
self.in_class_method = False
self.in_function = old_in_function
self.local_vars = old_local_vars
self.object_vars = old_object_vars
self.indent_level -= 1
if param.default is not None:
default_val = self.generate_expr(param.default)
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
else:
self.emit(f'local {param.name}="${{{i + 1}}}"')
self.local_vars.add(param.name)
if param_types.get(param.name) == "object":
self.object_vars.add(param.name)
for stmt in method.body.statements:
self.generate_statement(stmt)
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()
......@@ -278,10 +272,9 @@ class ClassMixin:
current_name = wrapper_name
self.emit(f"{base_name} () {{")
self.indent_level += 1
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(method.params) + 1)])
self.emit(f'{current_name} {params_str}')
self.indent_level -= 1
with self.indented():
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(method.params) + 1)])
self.emit(f'{current_name} {params_str}')
self.emit("}")
self.emit()
......@@ -301,11 +294,10 @@ class ClassMixin:
self.emit(f"# @awk method - 'this' is ignored, calls AWK function")
self.emit(f"{method_name} () {{")
self.indent_level += 1
self.emit('shift # ignore this')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(method.params))])
self.emit(f'{awk_func_name} {params_str}')
self.indent_level -= 1
with self.indented():
self.emit('shift # ignore this')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(method.params))])
self.emit(f'{awk_func_name} {params_str}')
self.emit("}")
self.emit()
......@@ -460,55 +452,52 @@ class ClassMixin:
name = f"__ct_class_{self.current_class}_{func.name}"
self.emit(f"{name} () {{")
self.indent_level += 1
param_types = self._analyze_param_types(func)
old_param_name_map = getattr(self, 'param_name_map', {})
self.param_name_map = {}
for i, param in enumerate(func.params):
ptype = param_types.get(param.name, "scalar")
if param.is_variadic:
self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")')
elif ptype in ("array", "dict"):
nameref_name = f"__ct_{func.name}_{param.name}"
self.emit(f'local -n {nameref_name}="${{{i + 1}}}"')
self.param_name_map[param.name] = nameref_name
if ptype == "array":
self.array_vars.add(nameref_name)
with self.indented():
param_types = self._analyze_param_types(func)
old_param_name_map = getattr(self, 'param_name_map', {})
self.param_name_map = {}
for i, param in enumerate(func.params):
ptype = param_types.get(param.name, "scalar")
if param.is_variadic:
self.emit(f'local -a {param.name}=("${{@:{i + 1}}}")')
elif ptype in ("array", "dict"):
nameref_name = f"__ct_{func.name}_{param.name}"
self.emit(f'local -n {nameref_name}="${{{i + 1}}}"')
self.param_name_map[param.name] = nameref_name
if ptype == "array":
self.array_vars.add(nameref_name)
else:
self.dict_vars.add(nameref_name)
else:
self.dict_vars.add(nameref_name)
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:
self.emit(f'local {param.name}="${{{i + 1}}}"')
if param.default is not None:
default_val = self.generate_expr(param.default)
self.emit(f'local {param.name}="${{{i + 1}:-{default_val}}}"')
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_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)
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.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.emit("}")
self.emit()
......@@ -530,10 +519,9 @@ class ClassMixin:
current_name = wrapper_name
self.emit(f"{func.name} () {{")
self.indent_level += 1
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(func.params))])
self.emit(f'{current_name} {params_str}')
self.indent_level -= 1
with self.indented():
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(func.params))])
self.emit(f'{current_name} {params_str}')
self.emit("}")
self.emit()
......
......@@ -189,31 +189,27 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.emit('echo')
self.emit('__ct_run_tests () {')
self.indent_level += 1
self.emit('local __test_failed=0')
for func_name, description in self.test_functions:
escaped_desc = description.replace('"', '\\"')
self.emit(f'__ct_test_start "{escaped_desc}"')
self.emit('local __prev_failed=$__ct_test_failed')
self.emit(f'if {func_name}; then')
self.indent_level += 1
self.emit('__ct_test_pass')
self.indent_level -= 1
self.emit('else')
self.indent_level += 1
self.emit('if [[ $__ct_test_failed -eq $__prev_failed ]]; then')
self.indent_level += 1
self.emit('__ct_test_fail ""')
self.indent_level -= 1
self.emit('fi')
self.emit('__test_failed=1')
self.indent_level -= 1
self.emit('fi')
self.emit()
self.emit('__ct_test_summary')
self.indent_level -= 1
with self.indented():
self.emit('local __test_failed=0')
for func_name, description in self.test_functions:
escaped_desc = description.replace('"', '\\"')
self.emit(f'__ct_test_start "{escaped_desc}"')
self.emit('local __prev_failed=$__ct_test_failed')
self.emit(f'if {func_name}; then')
with self.indented():
self.emit('__ct_test_pass')
self.emit('else')
with self.indented():
self.emit('if [[ $__ct_test_failed -eq $__prev_failed ]]; then')
with self.indented():
self.emit('__ct_test_fail ""')
self.emit('fi')
self.emit('__test_failed=1')
self.emit('fi')
self.emit()
self.emit('__ct_test_summary')
self.emit('}')
self.emit()
self.emit('__ct_run_tests')
"""Constants for bash code generation."""
RET_VAR = "__CT_RET"
RET_ARR = "__CT_RET_ARR"
TMP_PREFIX = "__ct_tmp_"
CLASS_FUNC_PREFIX = "__ct_class_"
LAMBDA_PREFIX = "__ct_lambda_"
......
......@@ -3,6 +3,7 @@ from .ast_nodes import (
Expression, CallExpr, MemberAccess, ThisExpr, Identifier,
BinaryOp, UnaryOp, BoolLiteral
)
from .constants import RET_VAR
class NodeIdMap:
......@@ -76,7 +77,7 @@ class CseMixin:
if key not in seen:
temp = self.new_temp()
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(assign_line)
seen[key] = temp
......@@ -105,7 +106,7 @@ class CseMixin:
if key not in seen:
temp = self.new_temp()
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(assign_line)
seen[key] = temp
......@@ -140,7 +141,7 @@ class CseMixin:
if key not in seen:
temp = self.new_temp()
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(assign_line)
seen[key] = temp
......
import re
from typing import List
from .ast_nodes import Decorator, Parameter
from .constants import RET_VAR
class DecoratorMixin:
......@@ -48,57 +49,50 @@ class DecoratorMixin:
delay = self.generate_expr(arg_val)
self.emit(f"{wrapper_name} () {{")
self.indent_level += 1
if is_method:
self.emit('local this="$1"')
self.emit('shift')
self.emit(f"local __attempts={attempts}")
self.emit(f"local __delay={delay}")
self.emit("local __i")
self.emit("for __i in $(seq 1 $__attempts); do")
self.indent_level += 1
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'if {wrapped_name} "$this" {params_str}; then')
else:
self.emit(f'if {wrapped_name} {params_str}; then')
self.indent_level += 1
self.emit("return 0")
self.indent_level -= 1
self.emit("fi")
self.emit('sleep "$__delay"')
self.indent_level -= 1
self.emit("done")
self.emit("return 1")
self.indent_level -= 1
with self.indented():
if is_method:
self.emit('local this="$1"')
self.emit('shift')
self.emit(f"local __attempts={attempts}")
self.emit(f"local __delay={delay}")
self.emit("local __i")
self.emit("for __i in $(seq 1 $__attempts); do")
with self.indented():
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'if {wrapped_name} "$this" {params_str}; then')
else:
self.emit(f'if {wrapped_name} {params_str}; then')
with self.indented():
self.emit("return 0")
self.emit("fi")
self.emit('sleep "$__delay"')
self.emit("done")
self.emit("return 1")
self.emit("}")
self.emit()
def _generate_log_wrapper(self, wrapped_name: str, wrapper_name: str,
params: List[Parameter], is_method: bool):
self.emit(f"{wrapper_name} () {{")
self.indent_level += 1
if is_method:
self.emit('local this="$1"')
self.emit('shift')
self.emit(f'echo "[LOG] Calling {wrapped_name}" >&2')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'{wrapped_name} "$this" {params_str}')
else:
self.emit(f'{wrapped_name} {params_str}')
self.emit('local __ret=$?')
self.emit(f'echo "[LOG] {wrapped_name} returned $__ret" >&2')
self.emit('return $__ret')
self.indent_level -= 1
with self.indented():
if is_method:
self.emit('local this="$1"')
self.emit('shift')
self.emit(f'echo "[LOG] Calling {wrapped_name}" >&2')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'{wrapped_name} "$this" {params_str}')
else:
self.emit(f'{wrapped_name} {params_str}')
self.emit('local __ret=$?')
self.emit(f'echo "[LOG] {wrapped_name} returned $__ret" >&2')
self.emit('return $__ret')
self.emit("}")
self.emit()
......@@ -113,55 +107,49 @@ class DecoratorMixin:
self.emit(f"declare -g __ct_cache_time_{wrapper_name}=0")
self.emit()
self.emit(f"{wrapper_name} () {{")
self.indent_level += 1
if is_method:
self.emit('local this="$1"')
self.emit('shift')
self.emit(f'local __key="$this:$*"')
else:
self.emit(f'local __key="$*"')
self.emit(f'local __now=$(date +%s)')
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.indent_level += 1
self.emit(f'__CT_RET="${{__ct_cache_{wrapper_name}[$__key]}}"')
self.emit('echo "$__CT_RET"')
self.emit("return 0")
self.indent_level -= 1
self.emit("fi")
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'local __result=$({wrapped_name} "$this" {params_str})')
else:
self.emit(f'local __result=$({wrapped_name} {params_str})')
self.emit(f'__CT_RET="$__result"')
self.emit(f'__ct_cache_{wrapper_name}["$__key"]="$__result"')
self.emit(f'__ct_cache_time_{wrapper_name}=$__now')
self.emit('echo "$__result"')
self.indent_level -= 1
with self.indented():
if is_method:
self.emit('local this="$1"')
self.emit('shift')
self.emit(f'local __key="$this:$*"')
else:
self.emit(f'local __key="$*"')
self.emit(f'local __now=$(date +%s)')
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')
with self.indented():
self.emit(f'{RET_VAR}="${{__ct_cache_{wrapper_name}[$__key]}}"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
self.emit("fi")
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'local __result=$({wrapped_name} "$this" {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_cache_time_{wrapper_name}=$__now')
self.emit('echo "$__result"')
self.emit("}")
self.emit()
def _generate_passthrough_wrapper(self, wrapped_name: str, wrapper_name: str,
params: List[Parameter], is_method: bool):
self.emit(f"{wrapper_name} () {{")
self.indent_level += 1
if is_method:
self.emit('local this="$1"')
self.emit('shift')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'{wrapped_name} "$this" {params_str}')
else:
self.emit(f'{wrapped_name} {params_str}')
self.indent_level -= 1
with self.indented():
if is_method:
self.emit('local this="$1"')
self.emit('shift')
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'{wrapped_name} "$this" {params_str}')
else:
self.emit(f'{wrapped_name} {params_str}')
self.emit("}")
self.emit()
......@@ -173,24 +161,21 @@ class DecoratorMixin:
validations[arg_name] = arg_val.value
self.emit(f"{wrapper_name} () {{")
self.indent_level += 1
if is_method:
self.emit('local this="$1"')
self.emit('shift')
for i, param in enumerate(params):
rule = validations.get(param.name)
if rule:
self._emit_validation_check(param.name, rule, i + 1)
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'{wrapped_name} "$this" {params_str}')
else:
self.emit(f'{wrapped_name} {params_str}')
self.indent_level -= 1
with self.indented():
if is_method:
self.emit('local this="$1"')
self.emit('shift')
for i, param in enumerate(params):
rule = validations.get(param.name)
if rule:
self._emit_validation_check(param.name, rule, i + 1)
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'{wrapped_name} "$this" {params_str}')
else:
self.emit(f'{wrapped_name} {params_str}')
self.emit("}")
self.emit()
......@@ -199,27 +184,24 @@ class DecoratorMixin:
if "int" in rule:
self.emit(f'if ! [[ "${param_name}" =~ ^-?[0-9]+$ ]]; then')
self.indent_level += 1
self.emit(f'echo "Validation error: {param_name} must be integer" >&2')
self.emit('return 1')
self.indent_level -= 1
with self.indented():
self.emit(f'echo "Validation error: {param_name} must be integer" >&2')
self.emit('return 1')
self.emit('fi')
for match in re.finditer(r'(>=|<=|>|<|==|!=)\s*(-?\d+)', rule):
op, val = match.groups()
bash_op = {'>': '-gt', '<': '-lt', '>=': '-ge', '<=': '-le', '==': '-eq', '!=': '-ne'}[op]
self.emit(f'if ! [[ ${param_name} {bash_op} {val} ]]; then')
self.indent_level += 1
self.emit(f'echo "Validation error: {param_name} must be {op} {val}" >&2')
self.emit('return 1')
self.indent_level -= 1
with self.indented():
self.emit(f'echo "Validation error: {param_name} must be {op} {val}" >&2')
self.emit('return 1')
self.emit('fi')
elif "string" in rule:
if "nonempty" in rule or "required" in rule:
self.emit(f'if [[ -z "${param_name}" ]]; then')
self.indent_level += 1
self.emit(f'echo "Validation error: {param_name} cannot be empty" >&2')
self.emit('return 1')
self.indent_level -= 1
with self.indented():
self.emit(f'echo "Validation error: {param_name} cannot be empty" >&2')
self.emit('return 1')
self.emit('fi')
......@@ -7,6 +7,7 @@ from .methods import (
STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS,
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()}
STR_METHODS = {name: m.bash_func for name, m in STRING_METHODS.items()}
......@@ -50,12 +51,12 @@ class DispatchMixin:
return True
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:
self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET[@]}}")')
self.emit(f'local -a {target}=("${{{RET_VAR}[@]}}")')
else:
self.emit(f'{target}=("${{__CT_RET[@]}}")')
self.emit(f'{target}=("${{{RET_VAR}[@]}}")')
def _is_method_call_with_side_effects(self, expr) -> bool:
"""Check if expression is a method call that may have side effects."""
......@@ -134,7 +135,7 @@ class DispatchMixin:
if isinstance(callee.object, Identifier) and callee.object.name == "fs" and callee.member == "open":
args_str = self._generate_call_args_str(stmt.value.arguments)
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)
return
if isinstance(callee.object, Identifier) and callee.object.name == "json" and callee.member == "parse":
......@@ -153,7 +154,7 @@ class DispatchMixin:
if func_name not in BUILTIN_FUNCS and func_name not in self.classes:
args_str = self._generate_call_args_str(stmt.value.arguments)
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
if isinstance(stmt.value, ArrayLiteral):
......@@ -285,7 +286,7 @@ class DispatchMixin:
if isinstance(callee.object, ThisExpr) and self.current_class:
method = callee.member
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
if isinstance(callee.object, MemberAccess) and isinstance(callee.object.object, ThisExpr):
......@@ -297,7 +298,7 @@ class DispatchMixin:
if method in ARR_METHODS:
arr_name = f'"${{this}}_{field_name}"'
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
else:
self._validate_type_method("array", method, location)
......@@ -305,14 +306,14 @@ class DispatchMixin:
if method in DICT_METHODS:
dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"'
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
else:
self._validate_type_method("dict", method, location)
if field_type == "object":
obj_ref = f'${{__CT_OBJ["$this.{field_name}"]}}'
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 False
......@@ -324,7 +325,7 @@ class DispatchMixin:
field_ref = f'${{__CT_OBJ["${{{var_name}}}.{field_name}"]:-}}'
if method in STR_METHODS:
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
if isinstance(callee.object, Identifier):
......@@ -344,7 +345,7 @@ class DispatchMixin:
arr_ref = mapped_name
if method == "push" and len(args) == 1:
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:
first_arg = stmt.value.arguments[0]
if isinstance(first_arg, Lambda):
......@@ -358,14 +359,14 @@ class DispatchMixin:
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:
self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET_ARR[@]}}")')
self.emit(f'local -a {target}=("${{{RET_ARR}[@]}}")')
else:
self.emit(f'{target}=("${{__CT_RET_ARR[@]}}")')
self.emit(f'{target}=("${{{RET_ARR}[@]}}")')
self.array_vars.add(target)
return True
else:
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
else:
self._validate_type_method("array", method, location)
......@@ -376,16 +377,16 @@ class DispatchMixin:
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:
self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET_ARR[@]}}")')
self.emit(f'local -a {target}=("${{{RET_ARR}[@]}}")')
else:
self.emit(f'{target}=("${{__CT_RET_ARR[@]}}")')
self.emit(f'{target}=("${{{RET_ARR}[@]}}")')
self.array_vars.add(target)
return True
elif method in DICT_METHODS:
func_name = DICT_METHODS_MAP[method]
dict_ref = mapped_name
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
else:
self._validate_type_method("dict", method, location)
......@@ -394,7 +395,7 @@ class DispatchMixin:
if method in FILE_HANDLE_METHODS:
func_name = FILE_HANDLE_METHODS_MAP[method]
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
else:
self._validate_type_method("file_handle", method, location)
......@@ -404,18 +405,18 @@ class DispatchMixin:
obj = self.generate_expr(callee.object)
self.emit(f'__ct_call_method "{obj}" "{method}" {args_str} >/dev/null')
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.nameref_vars.add(target)
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.nameref_vars.add(target)
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)
else:
self._emit_assign_with_op(target, '$__CT_RET', stmt.operator)
self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True
if method in STR_METHODS:
......@@ -425,20 +426,20 @@ class DispatchMixin:
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:
self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET_ARR[@]}}")')
self.emit(f'local -a {target}=("${{{RET_ARR}[@]}}")')
else:
self.emit(f'{target}=("${{__CT_RET_ARR[@]}}")')
self.emit(f'{target}=("${{{RET_ARR}[@]}}")')
self.array_vars.add(target)
return True
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
if method in DICT_METHODS:
obj = self.generate_expr(callee.object)
func_name = DICT_METHODS_MAP[method]
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
ret_type = self._get_method_return_type(obj_name, method)
......@@ -446,18 +447,18 @@ class DispatchMixin:
self.emit(f'__ct_call_method "{obj}" "{method}" {args_str} >/dev/null')
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.nameref_vars.add(target)
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.nameref_vars.add(target)
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)
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 False
......@@ -568,7 +569,7 @@ class DispatchMixin:
call_code = self.generate_call_statement(first)
self.emit(f'{call_code} >/dev/null')
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):
current_var = first.name
else:
......@@ -590,12 +591,12 @@ class DispatchMixin:
else:
self.emit(f'{func_name} "${{{current_var}}}" >/dev/null')
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
elif isinstance(elem, Identifier):
self.emit(f'{elem.name} "${{{current_var}}}" >/dev/null')
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
self.emit_var_assign(target, f'${{{current_var}}}')
......
......@@ -593,23 +593,20 @@ class ExprMixin:
def generate_lambda_as_function(self, expr: Lambda, name: str):
"""Generate lambda as a named function."""
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):
self.emit(f'local {param}="${{{i + 1}}}"')
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"')
if isinstance(expr.body, Block):
for stmt in expr.body.statements:
self.generate_statement(stmt)
else:
result = self.generate_expr(expr.body)
self.emit(f'echo "{result}"')
self.indent_level -= 1
if self._is_boolean_expr(expr.body):
cond = self.generate_condition(expr.body)
self.emit(f'{cond} && echo "true" || echo "false"')
else:
result = self.generate_expr(expr.body)
self.emit(f'echo "{result}"')
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.
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
# Bash implementations for string methods
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"',
}
from .methods import (
STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS,
HTTP_METHODS, FS_METHODS, JSON_METHODS, LOGGER_METHODS,
REGEX_METHODS, MATH_METHODS, TIME_METHODS, ARGS_METHODS, CORE_FUNCTIONS
)
class StdlibMixin:
......@@ -95,7 +59,7 @@ class StdlibMixin:
self._emit_args()
else:
self._emit_utils_minimal()
if 'awk' in used_categories:
if 'awk' in used_categories or 'math' in used_categories:
self._emit_awk_wrapper()
if 'math' in used_categories:
self._emit_math()
......@@ -114,11 +78,7 @@ class StdlibMixin:
self.emit("exec 3>&1 # Save stdout to FD3 for print()")
self.emit()
self.emit("__ct_print () {")
with self.indented():
self.emit('local msg="$1"')
self.emit('echo -e "$msg" >&3')
self.emit("}")
self.emit(f"{CORE_FUNCTIONS['print'].bash_func} () {{ {CORE_FUNCTIONS['print'].bash_impl}; }}")
self.emit()
self.emit("__ct_range () {")
......@@ -134,55 +94,22 @@ class StdlibMixin:
self.emit("}")
self.emit()
self.emit("__ct_len () {")
with self.indented():
self.emit('local -n arr=$1')
self.emit('echo "${#arr[@]}"')
self.emit("}")
self.emit(f"{CORE_FUNCTIONS['len'].bash_func} () {{ {CORE_FUNCTIONS['len'].bash_impl}; }}")
self.emit()
def _emit_http(self):
"""HTTP functions: get, post, put, delete."""
http_funcs = [
("get", ['url', 'timeout="${2:-30}"'],
'curl -sS --fail --show-error --max-time "$timeout" "$url"'),
("post", ['url', 'data', 'timeout="${3:-30}"'],
'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()
"""HTTP functions from HTTP_METHODS."""
for method_name, method_def in HTTP_METHODS.items():
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
def _emit_fs(self):
"""Filesystem functions."""
fs_simple = [
("read", 'cat "$1"'),
("write", 'echo -n "$2" > "$1"'),
("append", 'echo -n "$2" >> "$1"'),
("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()
"""Filesystem functions from FS_METHODS."""
for method_name, method_def in FS_METHODS.items():
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
self._emit_file_handles()
def _emit_file_handles(self):
......@@ -206,48 +133,10 @@ class StdlibMixin:
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():
if method_name in fh_impls:
self.emit(f"{method_def.bash_func} () {{")
with self.indented():
for line in fh_impls[method_name]:
self.emit(line)
self.emit("}")
self.emit()
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
self.emit("# Context manager support")
self.emit('__ct_fh___enter__ () { echo "$1"; }')
......@@ -332,32 +221,26 @@ class StdlibMixin:
self.emit()
def _emit_logger(self):
"""Logger functions."""
levels = [("info", "INFO", ""), ("warn", "WARN", " >&2"),
("error", "ERROR", " >&2"), ("debug", "DEBUG", "")]
for name, label, redir in levels:
self.emit(f'__ct_logger_{name} () {{ echo "[{label}] $1"{redir}; }}')
"""Logger functions from LOGGER_METHODS."""
for method_name, method_def in LOGGER_METHODS.items():
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
def _emit_string(self):
"""String functions from STRING_METHODS."""
self.emit("# String functions")
for method_name, method_def in STRING_METHODS.items():
if method_name in STRING_IMPLS:
self.emit(f"{method_def.bash_func} () {{ {STRING_IMPLS[method_name]}; }}")
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\"; }")
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
def _emit_array(self):
"""Array functions from ARRAY_METHODS."""
self.emit("# Array functions")
for method_name, method_def in ARRAY_METHODS.items():
if method_name in ARRAY_IMPLS:
self.emit(f"{method_def.bash_func} () {{ {ARRAY_IMPLS[method_name]}; }}")
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
self.emit("# Array map/filter with lambda functions")
......@@ -390,34 +273,33 @@ class StdlibMixin:
self.emit()
def _emit_regex(self):
"""Regex functions."""
"""Regex functions from REGEX_METHODS."""
self.emit("# Regex functions")
self.emit("__ct_ngrep () { echo \"$2\" | grep -n \"$1\" || true; }")
self.emit('__ct_regex_match () { [[ "$1" =~ $2 ]] && echo true || echo false; }')
self.emit("__ct_regex_extract () {")
with self.indented():
self.emit('[[ "$1" =~ $2 ]] && echo "${BASH_REMATCH[0]}"')
self.emit("}")
for method_name, method_def in REGEX_METHODS.items():
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit(f"{CORE_FUNCTIONS['ngrep'].bash_func} () {{ {CORE_FUNCTIONS['ngrep'].bash_impl}; }}")
self.emit()
def _emit_utils(self):
"""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('__ct_is_number () { [[ "$1" =~ ^-?[0-9]+$ ]] && echo true || echo false; }')
self.emit('__ct_is_empty () { [[ -z "$1" ]] && echo true || echo false; }')
self.emit(f"{CORE_FUNCTIONS['is_number'].bash_func} () {{ {CORE_FUNCTIONS['is_number'].bash_impl}; }}")
self.emit(f"{CORE_FUNCTIONS['is_empty'].bash_func} () {{ {CORE_FUNCTIONS['is_empty'].bash_impl}; }}")
self.emit()
self.emit('__ct_args=("$@")')
self.emit('__ct_args_count () { echo ${#__ct_args[@]}; }')
self.emit("__ct_args_get () { printf '%s\\n' \"${__ct_args[$1]}\"; }")
for method_name, method_def in ARGS_METHODS.items():
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
def _emit_utils_minimal(self):
"""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('__ct_is_number () { [[ "$1" =~ ^-?[0-9]+$ ]] && echo true || echo false; }')
self.emit('__ct_is_empty () { [[ -z "$1" ]] && echo true || echo false; }')
self.emit(f"{CORE_FUNCTIONS['is_number'].bash_func} () {{ {CORE_FUNCTIONS['is_number'].bash_impl}; }}")
self.emit(f"{CORE_FUNCTIONS['is_empty'].bash_func} () {{ {CORE_FUNCTIONS['is_empty'].bash_impl}; }}")
self.emit()
def _emit_args(self):
......@@ -437,26 +319,19 @@ class StdlibMixin:
self.emit()
def _emit_math(self):
"""Math functions."""
"""Math functions from MATH_METHODS."""
self.emit("# Math functions")
math_ops = [
("add", "+"), ("sub", "-"), ("mul", "*"), ("div", "/"), ("mod", "%")
]
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#-}; }")
for method_name, method_def in MATH_METHODS.items():
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
def _emit_dict(self):
"""Dict functions from DICT_METHODS."""
self.emit("# Dict functions")
for method_name, method_def in DICT_METHODS.items():
if method_name in DICT_IMPLS:
self.emit(f"{method_def.bash_func} () {{ {DICT_IMPLS[method_name]}; }}")
# len is not in DICT_METHODS, add separately
self.emit(f"__ct_dict_len () {{ {DICT_IMPLS['len']}; }}")
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
def _emit_misc(self):
......@@ -466,12 +341,13 @@ class StdlibMixin:
self.emit("__ct_hex_to_byte () { printf '%d' \"0x$1\"; }")
self.emit()
self.emit("# Time functions")
self.emit("__ct_time_now () { date +%s; }")
self.emit("__ct_time_ms () { date +%s%3N; }")
for method_name, method_def in TIME_METHODS.items():
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit()
self.emit("# Random")
self.emit("__ct_random () { echo $RANDOM; }")
self.emit("__ct_random_range () { echo $(($1 + RANDOM % ($2 - $1 + 1))); }")
self.emit(f"{CORE_FUNCTIONS['random'].bash_func} () {{ {CORE_FUNCTIONS['random'].bash_impl}; }}")
self.emit(f"{CORE_FUNCTIONS['random_range'].bash_func} () {{ {CORE_FUNCTIONS['random_range'].bash_impl}; }}")
self.emit()
self.emit("__CT_NL=$'\\n'")
self.emit()
......
......@@ -5,6 +5,7 @@ from .ast_nodes import (
CallExpr, Identifier, MemberAccess, ThisExpr, StringLiteral, NewExpr,
BinaryOp, DictLiteral, ArrayLiteral, WhenBranch
)
from .constants import RET_VAR, RET_ARR
class StmtMixin:
......@@ -61,13 +62,11 @@ class StmtMixin:
else:
self.emit(f'# import "{path}"')
self.emit(f'if [[ -f "$HOME/.content/libs/{path}.sh" ]]; then')
self.indent_level += 1
self.emit(f'source "$HOME/.content/libs/{path}.sh"')
self.indent_level -= 1
with self.indented():
self.emit(f'source "$HOME/.content/libs/{path}.sh"')
self.emit(f'elif [[ -f "/usr/lib/content/{path}.sh" ]]; then')
self.indent_level += 1
self.emit(f'source "/usr/lib/content/{path}.sh"')
self.indent_level -= 1
with self.indented():
self.emit(f'source "/usr/lib/content/{path}.sh"')
self.emit("fi")
self.emit()
......@@ -77,7 +76,7 @@ class StmtMixin:
args = [self.generate_expr(a) for a in stmt.condition.arguments]
args_str = " ".join([f'"{a}"' for a in args])
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:
mapping, _ = self.precompute_all_calls(stmt.condition)
if mapping:
......@@ -86,25 +85,22 @@ class StmtMixin:
cond = self.generate_condition(stmt.condition)
self.emit(f"if {cond}; then")
self.indent_level += 1
for s in stmt.then_branch.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in stmt.then_branch.statements:
self.generate_statement(s)
for elif_cond, elif_block in stmt.elif_branches:
cond = self.generate_condition(elif_cond)
self.emit(f"elif {cond}; then")
self.indent_level += 1
for s in elif_block.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in elif_block.statements:
self.generate_statement(s)
if stmt.else_branch:
self.emit("else")
self.indent_level += 1
for s in stmt.else_branch.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in stmt.else_branch.statements:
self.generate_statement(s)
self.emit("fi")
......@@ -121,25 +117,23 @@ class StmtMixin:
def _generate_when_case(self, stmt: WhenStmt, value_expr: str):
self.emit(f'case "{value_expr}" in')
self.indent_level += 1
for branch in stmt.branches:
if branch.is_else:
self.emit("*)")
else:
patterns = []
for p in branch.patterns:
val = self.generate_expr(p)
if isinstance(p, StringLiteral):
patterns.append(f'"{val}"' if val else '""')
else:
patterns.append(val)
self.emit(f'{"|".join(patterns)})')
self.indent_level += 1
for s in branch.body.statements:
self.generate_statement(s)
self.emit(";;")
self.indent_level -= 1
self.indent_level -= 1
with self.indented():
for branch in stmt.branches:
if branch.is_else:
self.emit("*)")
else:
patterns = []
for p in branch.patterns:
val = self.generate_expr(p)
if isinstance(p, StringLiteral):
patterns.append(f'"{val}"' if val else '""')
else:
patterns.append(val)
self.emit(f'{"|".join(patterns)})')
with self.indented():
for s in branch.body.statements:
self.generate_statement(s)
self.emit(";;")
self.emit("esac")
def _generate_when_if_chain(self, stmt: WhenStmt, value_expr: str):
......@@ -164,10 +158,9 @@ class StmtMixin:
first = False
else:
self.emit(f"elif {cond_str}; then")
self.indent_level += 1
for s in branch.body.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in branch.body.statements:
self.generate_statement(s)
self.emit("fi")
def generate_while(self, stmt: WhileStmt):
......@@ -175,16 +168,15 @@ class StmtMixin:
if mapping:
cond = self.generate_condition_with_precompute(stmt.condition, mapping)
self.emit(f"while {cond}; do")
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
if regen_code:
self.emit("# CSE: re-compute condition vars")
for call_line, assign_line in regen_code:
self.emit(call_line)
if assign_line:
self.emit(assign_line)
self.indent_level -= 1
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
if regen_code:
self.emit("# CSE: re-compute condition vars")
for call_line, assign_line in regen_code:
self.emit(call_line)
if assign_line:
self.emit(assign_line)
self.emit("done")
return
......@@ -193,21 +185,19 @@ class StmtMixin:
args = [self.generate_expr(a) for a in stmt.condition.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit("while true; do")
self.indent_level += 1
self.emit(f'{func_name} {args_str} >/dev/null')
self.emit(f'if [[ "$__CT_RET" != "true" ]]; then break; fi')
for s in stmt.body.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
self.emit(f'{func_name} {args_str} >/dev/null')
self.emit(f'if [[ "${{{RET_VAR}}}" != "true" ]]; then break; fi')
for s in stmt.body.statements:
self.generate_statement(s)
self.emit("done")
return
cond = self.generate_condition(stmt.condition)
self.emit(f"while {cond}; do")
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
self.emit("done")
def generate_for(self, stmt: ForStmt):
......@@ -221,19 +211,17 @@ class StmtMixin:
self.emit(f"for {var} in $(seq {args[0]} $(({args[1]} - 1))); do")
else:
self.emit(f"for {var} in $(seq {args[0]} {args[2]} $(({args[1]} - 1))); do")
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
self.emit("done")
return
iterable = self.generate_expr(stmt.iterable)
self.emit(f'for {var} in {iterable}; do')
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
self.emit("done")
def generate_foreach(self, stmt: ForeachStmt):
......@@ -247,10 +235,9 @@ class StmtMixin:
self.emit(f"for {var} in $(seq {args[0]} $(({args[1]} - 1))); do")
else:
self.emit(f"for {var} in $(seq {args[0]} {args[2]} $(({args[1]} - 1))); do")
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
self.emit("done")
return
......@@ -266,12 +253,11 @@ class StmtMixin:
val_var = stmt.variables[1]
self.emit(f'{idx_var}=0')
self.emit(f'for {val_var} in "${{{arr_name}[@]}}"; do')
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))')
self.indent_level -= 1
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))')
self.emit("done")
return
......@@ -292,12 +278,11 @@ class StmtMixin:
val_var = stmt.variables[1]
self.emit(f'{idx_var}=0')
self.emit(f'for {val_var} in "${{__ct_split_arr[@]}}"; do')
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))')
self.indent_level -= 1
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))')
self.emit("done")
return
......@@ -307,28 +292,26 @@ class StmtMixin:
var = stmt.variables[0]
self.emit(f'__ct_str_split "{str_expr}" "{delim_arg}"')
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:
idx_var = stmt.variables[0]
val_var = stmt.variables[1]
self.emit(f'{idx_var}=0')
self.emit(f'for {val_var} in "${{__CT_RET_ARR[@]}}"; do')
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))')
self.indent_level -= 1
self.emit(f'for {val_var} in "${{{RET_ARR}[@]}}"; do')
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
if len(stmt.variables) == 2:
self.emit(f'((++{stmt.variables[0]}))')
self.emit("done")
return
iterable = self.generate_expr(stmt.iterable)
var = stmt.variables[0]
self.emit(f'for {var} in {iterable}; do')
self.indent_level += 1
for s in stmt.body.statements:
self.generate_statement(s)
self.indent_level -= 1
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
self.emit("done")
def generate_with(self, stmt: WithStmt):
......@@ -343,7 +326,7 @@ class StmtMixin:
args = [self.generate_expr(a) for a in resource.arguments]
args_str = " ".join([f'"{a}"' for a in args])
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}"')
continue
......@@ -366,45 +349,45 @@ class StmtMixin:
self.emit("__ct_try_failed=0")
self.emit()
self.emit("while true; do")
self.indent_level += 1
for s in stmt.try_block.statements:
self.generate_statement(s)
self.emit('if [[ $? -ne 0 ]]; then')
self.indent_level += 1
self.emit('__ct_try_failed=1')
self.emit('__ct_exception_type="Error"')
self.emit('__ct_exception="Command failed"')
self.emit('break')
self.indent_level -= 1
self.emit('fi')
self.emit("break")
self.indent_level -= 1
with self.indented():
for s in stmt.try_block.statements:
self.generate_statement(s)
self.emit('if [[ $? -ne 0 ]]; then')
with self.indented():
self.emit('__ct_try_failed=1')
self.emit('__ct_exception_type="Error"')
self.emit('__ct_exception="Command failed"')
self.emit('break')
self.emit('fi')
self.emit("break")
self.emit("done")
self.emit()
self.emit("set -e")
self.emit()
self.emit('if [[ "$__ct_try_failed" == "1" ]]; then')
self.indent_level += 1
for i, (exc_type, exc_var, exc_block) in enumerate(stmt.except_clauses):
if i == 0:
if exc_type:
self.emit(f'if [[ "$__ct_exception_type" == "{exc_type}" ]]; then')
else:
if exc_type:
self.emit(f'elif [[ "$__ct_exception_type" == "{exc_type}" ]]; then')
with self.indented():
for i, (exc_type, exc_var, exc_block) in enumerate(stmt.except_clauses):
if i == 0:
if exc_type:
self.emit(f'if [[ "$__ct_exception_type" == "{exc_type}" ]]; then')
else:
self.emit("else")
if exc_type or i > 0:
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 exc_type or i > 0:
self.indent_level -= 1
if stmt.except_clauses and stmt.except_clauses[0][0] is not None:
self.emit("fi")
self.indent_level -= 1
if exc_type:
self.emit(f'elif [[ "$__ct_exception_type" == "{exc_type}" ]]; then')
else:
self.emit("else")
if exc_type or i > 0:
with self.indented():
if exc_var:
self.emit(f'{exc_var}="$__ct_exception"')
for s in exc_block.statements:
self.generate_statement(s)
else:
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")
if stmt.finally_block:
self.emit()
......@@ -453,15 +436,15 @@ class StmtMixin:
args = [self.generate_expr(arg) for arg in stmt.value.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{stmt.value.class_name} {args_str}')
self.emit('__CT_RET="$__ct_last_instance"')
self.emit('echo "$__CT_RET"')
self.emit('{RET_VAR}="$__ct_last_instance"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return
if isinstance(stmt.value, BinaryOp) and stmt.value.operator in ("==", "!=", "<", ">", "<=", ">=", "&&", "||"):
cond = self.generate_condition(stmt.value)
self.emit(f'{cond} && __CT_RET=true || __CT_RET=false')
self.emit('echo "$__CT_RET"')
self.emit(f'{cond} && {RET_VAR}=true || {RET_VAR}=false')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return
......@@ -472,8 +455,8 @@ class StmtMixin:
key = self.generate_expr(k)
val = self.generate_expr(v)
self.emit(f'eval "$__ct_ret_dict[{key}]=\\"{val}\\""')
self.emit('__CT_RET="$__ct_ret_dict"')
self.emit('echo "$__CT_RET"')
self.emit('{RET_VAR}="$__ct_ret_dict"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return
......@@ -483,14 +466,14 @@ class StmtMixin:
elements = [self.generate_expr(e) for e in stmt.value.elements]
for i, elem in enumerate(elements):
self.emit(f'eval "$__ct_ret_arr[{i}]=\\"{elem}\\""')
self.emit('__CT_RET="$__ct_ret_arr"')
self.emit('echo "$__CT_RET"')
self.emit('{RET_VAR}="$__ct_ret_arr"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return
value = self.generate_expr(stmt.value)
self.emit(f'__CT_RET="{value}"')
self.emit('echo "$__CT_RET"')
self.emit(f'{RET_VAR}="{value}"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
else:
self.emit("return 0")
......@@ -501,22 +484,22 @@ class StmtMixin:
if method == "contains" and len(expr.arguments) == 2:
haystack = self.generate_expr(expr.arguments[0])
needle = self.generate_expr(expr.arguments[1])
self.emit(f'[[ "{haystack}" == *"{needle}"* ]] && __CT_RET=true || __CT_RET=false')
self.emit('echo "$__CT_RET"')
self.emit(f'[[ "{haystack}" == *"{needle}"* ]] && {RET_VAR}=true || {RET_VAR}=false')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
elif method == "starts" and len(expr.arguments) == 2:
s = self.generate_expr(expr.arguments[0])
prefix = self.generate_expr(expr.arguments[1])
self.emit(f'[[ "{s}" == "{prefix}"* ]] && __CT_RET=true || __CT_RET=false')
self.emit('echo "$__CT_RET"')
self.emit(f'[[ "{s}" == "{prefix}"* ]] && {RET_VAR}=true || {RET_VAR}=false')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
elif method == "ends" and len(expr.arguments) == 2:
s = self.generate_expr(expr.arguments[0])
suffix = self.generate_expr(expr.arguments[1])
self.emit(f'[[ "{s}" == *"{suffix}" ]] && __CT_RET=true || __CT_RET=false')
self.emit('echo "$__CT_RET"')
self.emit(f'[[ "{s}" == *"{suffix}" ]] && {RET_VAR}=true || {RET_VAR}=false')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
return False
......@@ -538,7 +521,7 @@ class StmtMixin:
if method in str_methods:
func_name = str_methods[method]
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")
return True
if isinstance(expr.callee.object, ThisExpr) and self.current_class:
......@@ -546,7 +529,7 @@ class StmtMixin:
args = [self.generate_expr(arg) for arg in expr.arguments]
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('echo "$__CT_RET"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
elif isinstance(expr.callee.object, Identifier):
......@@ -564,15 +547,15 @@ class StmtMixin:
args = [self._generate_call_arg(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{func_name} {args_str}')
self.emit('__CT_RET="$__ct_last_instance"')
self.emit('echo "$__CT_RET"')
self.emit('{RET_VAR}="$__ct_last_instance"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
if func_name in self.functions:
args = [self._generate_call_arg(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{func_name} {args_str} >/dev/null')
self.emit('echo "$__CT_RET"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
return False
......@@ -604,23 +587,23 @@ class StmtMixin:
if mapped_name in self.array_vars and method in arr_methods:
func_name = arr_methods[method]
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")
return True
elif mapped_name in self.dict_vars and method in dict_methods:
func_name = dict_methods[method]
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")
return True
elif method in str_methods:
func_name = str_methods[method]
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")
return True
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")
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