Commit af265565 authored by Roman Alifanov's avatar Roman Alifanov

Optimize codegen: peephole tmp/RET elimination, static dispatch, metadata DCE

Peephole optimizations in _assign() eliminate __CT_RET and __ct_tmp round-trips by retargeting assignments directly to the target variable. Boolean condition peephole in _condition_bash() extracts test expressions directly instead of going through __CT_RET. Static dispatch for this method calls uses class_name to emit direct function calls instead of dynamic __ct_call_method. Class metadata (_fields/_types) only emitted when reflect/json categories are used. Stdlib methods (math, args, core) use direct __CT_RET= instead of echo to avoid subshell forks.
parent dd891640
......@@ -184,10 +184,11 @@ class BashBackend:
if getattr(ir, 'skipped_tests', []):
lines.append('')
# Classes
# Classes — only emit metadata when reflect/json.unmarshal is used
needs_metadata = bool({'reflect', 'json'} & used_categories)
all_classes = {cls.name: cls for cls in ir.classes}
for cls in ir.classes:
emit_class(cls, top_ctx, all_classes)
emit_class(cls, top_ctx, all_classes, emit_metadata=needs_metadata)
# Functions (non-method, non-awk)
for fn in ir.functions:
......
......@@ -20,10 +20,12 @@ if TYPE_CHECKING:
def emit_class(cls: IRClass, ctx: 'EmitContext',
all_classes: 'dict[str, IRClass] | None' = None) -> None:
all_classes: 'dict[str, IRClass] | None' = None,
emit_metadata: bool = True) -> None:
"""Emit all bash code for a class."""
_emit_constructor_func(cls, ctx)
_emit_class_metadata(cls, ctx)
if emit_metadata:
_emit_class_metadata(cls, ctx)
if cls.constructor:
_emit_construct_method(cls, ctx)
for method in cls.methods:
......
......@@ -696,6 +696,8 @@ def _method_stmt(node: IRMethodCall, ctx: 'EmitContext') -> None:
cls_hint = ''
if node.receiver.type.kind == 'class':
cls_hint = node.receiver.type.class_name or ''
if not cls_hint and isinstance(node.receiver, IRThis):
cls_hint = node.receiver.class_name
if cls_hint:
ctx.emit(f'__ct_class_{cls_hint}_{method} "${{{recv_name}}}" {args}')
else:
......@@ -756,11 +758,16 @@ def _try_inline_string_method(recv_name: str, method: str, args: list, ctx: 'Emi
if method == 'replace':
old_arg = _expr(args[0], ctx)
new_arg = _expr(args[1], ctx)
tmp_p = ctx.fresh_tmp()
tmp_r = ctx.fresh_tmp()
ctx.emit(f'{tmp_p}={old_arg}')
ctx.emit(f'{tmp_r}={new_arg}')
ctx.emit(f'{RET_VAR}="${{{recv_name}//"${{{tmp_p}}}"/"${{{tmp_r}}}"}}"')
is_old_lit = isinstance(args[0], IRString) and args[0].is_plain
is_new_lit = isinstance(args[1], IRString) and args[1].is_plain
if is_old_lit and is_new_lit:
ctx.emit(f'{RET_VAR}="${{{recv_name}//{old_arg}/{new_arg}}}"')
else:
tmp_p = ctx.fresh_tmp()
tmp_r = ctx.fresh_tmp()
ctx.emit(f'{tmp_p}={old_arg}')
ctx.emit(f'{tmp_r}={new_arg}')
ctx.emit(f'{RET_VAR}="${{{recv_name}//"${{{tmp_p}}}"/"${{{tmp_r}}}"}}"')
return f'"${{{RET_VAR}}}"'
if method == 'contains':
......@@ -1024,6 +1031,8 @@ def _instance_method_expr(node: IRMethodCall, ctx: 'EmitContext') -> str:
cls_hint = ''
if node.receiver.type.kind == 'class':
cls_hint = node.receiver.type.class_name or ''
if not cls_hint and isinstance(node.receiver, IRThis):
cls_hint = node.receiver.class_name
if cls_hint:
ctx.emit(f'__ct_class_{cls_hint}_{method} "${{{recv_name}}}" {args}')
else:
......
......@@ -18,6 +18,7 @@ from ...ir.nodes import (
)
from .constants import RET_VAR, RET_ARR, OBJ_STORE, COPROC_PREFIX
from ...constants import TMP_PREFIX
from .expr import emit_expr, emit_expr_as_stmt, _expr, _ct_args, _var_name
if TYPE_CHECKING:
......@@ -274,6 +275,49 @@ def _assign(node: IRAssign, ctx: 'EmitContext') -> None:
ctx.array_vars.add(target)
return
# Peephole: eliminate __CT_RET round-trip
# If _expr() set __CT_RET in the last emitted line and returns "${__CT_RET}",
# retarget that assignment directly to the target variable.
if val == f'"${{{RET_VAR}}}"' and ctx._output:
last_raw = ctx._output[-1]
last_stripped = last_raw.lstrip()
if last_stripped and f'{RET_VAR}=' in last_stripped:
indent = last_raw[:len(last_raw) - len(last_stripped)]
new_line = last_stripped.replace(f'{RET_VAR}=', f'{target}=')
needs_local = node.is_local and not ctx.is_declared(target)
if new_line.startswith(f'{target}='):
if needs_local:
ctx._output[-1] = f'{indent}local {new_line}'
ctx.declare_local(target)
else:
ctx._output[-1] = f'{indent}{new_line}'
else:
if needs_local:
ctx._output[-1] = f'{indent}local {target}=""'
ctx._output.append(f'{indent}{new_line}')
ctx.declare_local(target)
else:
ctx._output[-1] = f'{indent}{new_line}'
return
# Peephole: eliminate tmp round-trip
# If _expr() assigned to a tmp var and returns "${__ct_tmp_N}",
# retarget that assignment directly to the target variable.
if val.startswith(f'"${{{TMP_PREFIX}') and ctx._output:
tmp_name = val.strip('"').strip('${}')
last_raw = ctx._output[-1]
last_stripped = last_raw.lstrip()
if last_stripped.startswith(f'local {tmp_name}=') or last_stripped.startswith(f'{tmp_name}='):
indent = last_raw[:len(last_raw) - len(last_stripped)]
rhs = last_stripped.split('=', 1)[1]
needs_local = node.is_local and not ctx.is_declared(target)
if needs_local:
ctx._output[-1] = f'{indent}local {target}={rhs}'
ctx.declare_local(target)
else:
ctx._output[-1] = f'{indent}{target}={rhs}'
return
# Value already contains the result (emit() was called inside _expr)
if node.is_local:
if ctx.is_declared(target):
......@@ -431,6 +475,17 @@ def _condition_bash(cond, ctx: 'EmitContext') -> str:
# Variable or call that returns "true"/"false"
bash = expr_(cond, ctx)
# Peephole: if _expr set __CT_RET with boolean pattern, extract condition directly
if bash == f'"${{{RET_VAR}}}"' and ctx._output:
last_raw = ctx._output[-1]
last_stripped = last_raw.lstrip()
bool_tail = f' && {RET_VAR}=true || {RET_VAR}=false'
if last_stripped.endswith(bool_tail):
cond_str = last_stripped[:-len(bool_tail)]
ctx._output.pop()
return cond_str
if bash == '"true"':
return 'true'
if bash == '"false"':
......@@ -720,9 +775,17 @@ def _on_signal(node: IROnSignal, ctx: 'EmitContext') -> None:
def _when(node: IRWhen, ctx: 'EmitContext') -> None:
val = _expr(node.value, ctx) if node.value else '""'
tmp = ctx.fresh_tmp()
decl = 'local ' if ctx.in_function else ''
ctx.emit(f'{decl}{tmp}={val}')
is_simple_var = (isinstance(node.value, IRIdentifier)
or isinstance(node.value, IRFieldAccess))
if is_simple_var:
switch_var = val.strip('"').strip('${}')
switch_ref = f'"${{{switch_var}}}"'
else:
tmp = ctx.fresh_tmp()
decl = 'local ' if ctx.in_function else ''
ctx.emit(f'{decl}{tmp}={val}')
switch_var = tmp
switch_ref = f'"${{{tmp}}}"'
first = True
for branch in node.branches:
if branch.is_else:
......@@ -734,11 +797,11 @@ def _when(node: IRWhen, ctx: 'EmitContext') -> None:
parts = []
for p in branch.patterns:
pv = _expr(p, ctx)
parts.append(f'[[ "${{{tmp}}}" == {pv} ]]')
parts.append(f'[[ {switch_ref} == {pv} ]]')
for (lo, hi) in branch.ranges:
lv = _expr(lo, ctx).strip('"')
hv = _expr(hi, ctx).strip('"')
parts.append(f'[[ "${{{tmp}}}" -ge {lv} && "${{{tmp}}}" -le {hv} ]]')
parts.append(f'[[ {switch_ref} -ge {lv} && {switch_ref} -le {hv} ]]')
if not parts:
parts = ['false']
cond = ' || '.join(parts)
......
......@@ -2,5 +2,5 @@ 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)
count = Method(name="count", bash_func="__ct_args_count", bash_impl='__CT_RET=${#__ct_args[@]}')
get = Method(name="get", bash_func="__ct_args_get", bash_impl='__CT_RET="${__ct_args[$1]}"', min_args=1, max_args=1)
......@@ -32,27 +32,27 @@ class CoreFunctions:
is_number = Method(
name="is_number",
bash_func="__ct_is_number",
bash_impl='[[ "$1" =~ ^-?[0-9]+$ ]] && echo true || echo false',
bash_impl='[[ "$1" =~ ^-?[0-9]+$ ]] && __CT_RET=true || __CT_RET=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',
bash_impl='[[ -z "$1" ]] && __CT_RET=true || __CT_RET=false',
awk_builtin=lambda a: f"(length({a[0]}) == 0)",
min_args=1, max_args=1,
)
random = Method(
name="random",
bash_func="__ct_random",
bash_impl='echo $RANDOM',
bash_impl='__CT_RET=$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)))',
bash_impl='__CT_RET=$(($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,
)
......
......@@ -2,19 +2,19 @@ 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)
add = Method(name="add", bash_func="__ct_math_add", bash_impl='__CT_RET=$(($1 + $2))', min_args=2, max_args=2)
sub = Method(name="sub", bash_func="__ct_math_sub", bash_impl='__CT_RET=$(($1 - $2))', min_args=2, max_args=2)
mul = Method(name="mul", bash_func="__ct_math_mul", bash_impl='__CT_RET=$(($1 * $2))', min_args=2, max_args=2)
div = Method(name="div", bash_func="__ct_math_div", bash_impl='__CT_RET=$(($1 / $2))', min_args=2, max_args=2)
mod = Method(name="mod", bash_func="__ct_math_mod", bash_impl='__CT_RET=$(($1 % $2))', min_args=2, max_args=2)
min = Method(name="min", bash_func="__ct_math_min", bash_impl='(($1 < $2)) && __CT_RET=$1 || __CT_RET=$2', min_args=2, max_args=2)
max = Method(name="max", bash_func="__ct_math_max", bash_impl='(($1 > $2)) && __CT_RET=$1 || __CT_RET=$2', min_args=2, max_args=2)
abs = Method(name="abs", bash_func="__ct_math_abs", bash_impl='local n=$1; __CT_RET=${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)
int_ = Method(name="int", bash_func="__ct_math_int", bash_impl='__CT_RET="${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)
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