Skip to content

y5gfunc.expr.postfix2infix

postfix2infix

Functions:

Name Description
postfix2infix

Convert postfix expr to infix code

postfix2infix

postfix2infix(expr: str, check_mode: bool = False) -> str

Convert postfix expr to infix code If check_mode is True, it only checks for expression validity without building the result.

Parameters:

Name Type Description Default

expr

str

Input postfix expr.

required

check_mode

bool

If True, only perform validation.

False

Returns:

Type Description
str

Converted infix code. Or an empty string if check_mode is True and expr is valid.

Raises:

Type Description
ValueError

If an error was found in the input expr.

Source code in y5gfunc/expr/postfix2infix.py
def postfix2infix(expr: str, check_mode: bool = False) -> str:
    """
    Convert postfix expr to infix code
    If check_mode is True, it only checks for expression validity without building the result.

    Args:
        expr: Input postfix expr.
        check_mode: If True, only perform validation.

    Returns:
        Converted infix code. Or an empty string if check_mode is True and expr is valid.

    Raises:
        ValueError: If an error was found in the input expr.
    """
    # Preprocessing
    expr = expr.strip()
    tokens = tokenize_expr(expr)

    stack = []
    output_lines = []
    defined_vars = set()

    i = 0
    while i < len(tokens):

        def pop(n=1):
            try:
                if n == 1:
                    return stack.pop()
                r = stack[-n:]
                del stack[-n:]
                return r
            except IndexError:
                raise ValueError(
                    f"postfix2infix: Stack Underflow at token at {i}th token {token}."
                )

        def push(item):
            if not check_mode:
                stack.append(item)
            else:
                stack.append(None)

        token = tokens[i]

        # Constants
        if is_constant_postfix(token):
            push(f"${ensure_akarin_clip_name(token)}")
            i += 1
            continue

        # Numbers
        if any(pattern.match(token) for pattern in _NUMBER_PATTERNS):
            push(token)
            i += 1
            continue

        # Frame property
        if _FRAME_PROP_PATTERN.match(token):
            clip_name = token.split(".")[0]
            clip_name_akarin = ensure_akarin_clip_name(clip_name)
            push(f"{clip_name_akarin}.{token.split(".")[1]}")
            i += 1
            continue

        # Dynamic pixel access
        if token.endswith("[]"):
            clip_identifier = token[:-2]
            absY = pop()
            absX = pop()

            if not is_clip_postfix(clip_identifier):
                raise ValueError(
                    f"postfix2infix: {i}th token {token} expected a clip identifier, but got {clip_identifier}."
                )

            # Add $ prefix for clip identifiers
            clip_name = f"${clip_identifier}"
            push(f"dyn({clip_name},{absX},{absY})")
            i += 1
            continue

        # Static relative pixel access
        m = _STATIC_PIXEL_PATTERN.match(token)
        if m:
            clip_identifier = m.group(1)
            statX = int(m.group(2))
            statY = int(m.group(3))
            boundary_suffix = m.group(4)
            if boundary_suffix not in [None, ":c", ":m"]:
                raise ValueError(
                    f"postfix2infix: unknown boundary_suffix {boundary_suffix} at {i}th token {token}"
                )
            if not is_clip_postfix(clip_identifier):
                raise ValueError(
                    f"postfix2infix: {i}th token {token} expected a clip identifier, but got {clip_identifier}."
                )
            # Add $ prefix for clip identifiers
            clip_name = f"${clip_identifier}"
            push(f"{clip_name}[{statX},{statY}]{boundary_suffix or ""}")
            i += 1
            continue

        # Variable operations
        var_store_match = _VAR_STORE_PATTERN.match(token)
        var_load_match = _VAR_LOAD_PATTERN.match(token)
        if var_store_match:
            var_name = var_store_match.group(1)
            val = pop()
            if not check_mode:
                output_lines.append(f"{var_name} = {val}")
            defined_vars.add(var_name)
            i += 1
            continue
        elif var_load_match:
            var_name = var_load_match.group(1)
            if var_name not in defined_vars:
                raise ValueError(
                    f"postfix2infix: {i}th token {token} loads an undefined variable {var_name}."
                )
            push(var_name)
            i += 1
            continue

        # Drop operations
        drop_match = _DROP_PATTERN.match(token)
        if drop_match:
            num = int(drop_match.group(1)) if drop_match.group(1) else 1
            pop(num)
            i += 1
            continue

        # Sort operations
        sort_match = _SORT_PATTERN.match(token)
        if sort_match:
            num = int(sort_match.group(1))
            items = pop(num)
            if not check_mode:
                sorted_items_expr = f"nth_{{}}({', '.join(items)})"
            else:
                sorted_items_expr = f"nth_{{}}({', '* num})"
            for idx in range(len(items)):
                push(sorted_items_expr.format(len(items) - idx))
            i += 1
            continue

        # Duplicate operations
        dup_match = _DUP_PATTERN.match(token)
        if dup_match:
            n = int(dup_match.group(1)) if dup_match.group(1) else 0
            if len(stack) <= n:
                raise ValueError(
                    f"postfix2infix: {i}th token {token} needs at least {n+1} values, while only {len(stack)} in stack."
                )
            else:
                push(stack[-1 - n])
            i += 1
            continue

        # Swap operations
        swap_match = _SWAP_PATTERN.match(token)
        if swap_match:
            n = int(swap_match.group(1)) if swap_match.group(1) else 1
            if len(stack) <= n:
                raise ValueError(
                    f"postfix2infix: {i}th token {token} needs at least {n+1} values, while only {len(stack)} in stack."
                )
            else:
                stack[-1], stack[-1 - n] = stack[-1 - n], stack[-1]
            i += 1
            continue

        # Unary operators
        if token in (
            "sin",
            "cos",
            "round",
            "trunc",
            "floor",
            "bitnot",
            "abs",
            "sqrt",
            "not",
            "log",
            "exp",
        ):
            a = pop()
            if token == "not":
                push(f"(!({a}))")
            elif token == "bitnot":
                push(f"(~round({a}))")
            else:
                push(f"{token}({a})")
            i += 1
            continue

        # Binary operators
        if token in ("%", "**", "pow", "bitand", "bitor", "bitxor"):
            b = pop()
            a = pop()
            if token == "%":
                push(f"({a} % {b})")
            elif token in ("**", "pow"):
                push(f"({a} ** {b})")
            elif token == "bitand":
                push(f"(round({a}) & round({b}))")
            elif token == "bitor":
                push(f"(round({a}) | round({b}))")
            elif token == "bitxor":
                push(f"(round({a}) ^ round({b}))")
            i += 1
            continue

        # Basic arithmetic, comparison and logical operators
        if token in (
            "+",
            "-",
            "*",
            "/",
            "max",
            "min",
            ">",
            "<",
            ">=",
            "<=",
            "=",
            "and",
            "or",
            "xor",
        ):
            b = pop()
            a = pop()
            if token in ("max", "min"):
                push(f"{token}({a}, {b})")
            elif token == "and":
                push(f"({a} && {b})")
            elif token == "or":
                push(f"({a} || {b})")
            elif token == "xor":
                # (a || b) && !(a && b)
                # (a && !b) || (!a && b)
                push(f"(({a} && !{b}) || (!{a} && {b}))")
            elif token == "=":
                push(f"({a} == {b})")
            else:
                push(f"({a} {token} {b})")
            i += 1
            continue

        # Ternary operator
        if token == "?":
            false_val = pop()
            true_val = pop()
            cond = pop()
            push(f"({cond} ? {true_val} : {false_val})")
            i += 1
            continue
        if token == "clip" or token == "clamp":
            max = pop()
            min = pop()
            value = pop()
            push(f"(clamp({value}, {min}, {max}))")
            i += 1
            continue

        # Unknown tokens
        if not check_mode:
            output_lines.append(f"# [Unknown token]: {token}  (Push as-is)")
        push(token)
        i += 1

    # Handle remaining stack items
    if len(stack) == 1:
        if check_mode:
            return ""
        output_lines.append(f"RESULT = {stack[0]}")
        ret = "\n".join(output_lines)
    else:
        if check_mode:
            msg = f"postfix2infix: Invalid expression: the stack contains {len(stack)} value(s), but should contain exactly one."
        else:
            for idx, item in enumerate(stack):
                output_lines.append(f"# stack[{idx}]: {item}")
            ret = "\n".join(output_lines)
            msg = f"postfix2infix: Invalid expression: the stack contains not exactly one value after evaluation. \n {ret}"
        raise ValueError(msg)
    return ret