Esempio n. 1
0
    def get_sprite_number(self):
        # Layout of sprite number
        # bit  0 - 13: Sprite number
        # bit 14 - 15: Recolour mode (normal/transparent/remap)
        # bit 16 - 29: Palette sprite number
        # bit 30: Always draw sprite, even in transparent mode
        # bit 31: This is a custom sprite (from action1), not a TTD sprite
        if not self.is_set("sprite"):
            raise generic.ScriptError(
                "'sprite' must be set for this layout sprite", self.pos)

        # Make sure that recolouring is set correctly
        if self.get_param("recolour_mode") == 0 and self.is_set("palette"):
            raise generic.ScriptError(
                "'palette' may not be set when 'recolour_mode' is RECOLOUR_NONE."
            )
        elif self.get_param("recolour_mode") != 0 and not self.is_set(
                "palette"):
            raise generic.ScriptError(
                "'palette' must be set when 'recolour_mode' is not set to RECOLOUR_NONE."
            )

        # Add the constant terms first
        sprite_num = self.get_param("recolour_mode") << 14
        if self.get_param("always_draw"):
            sprite_num |= 1 << 30
        if self.sprite_from_action1:
            sprite_num |= 1 << 31

        # Add the sprite
        expr = nmlop.ADD(self.get_param("sprite"), sprite_num, self.pos)
        # Add the palette
        expr = nmlop.ADD(
            nmlop.SHIFT_LEFT(self.get_param("palette"), 16, self.pos), expr)
        return expr.reduce()
Esempio n. 2
0
def builtin_date(name, args, pos):
    """
    date(year, month, day) builtin function.

    @return Days since 1 jan 1 of the given date.
    """
    days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    if len(args) != 3:
        raise generic.ScriptError("date() requires exactly 3 arguments", pos)
    identifier.ignore_all_invalid_ids = True
    year = args[0].reduce(global_constants.const_list)
    identifier.ignore_all_invalid_ids = False
    try:
        month = args[1].reduce_constant().value
        day = args[2].reduce_constant().value
    except generic.ConstError:
        raise generic.ScriptError(
            "Month and day parameters of date() should be compile-time constants",
            pos)
    generic.check_range(month, 1, 12, "month", args[1].pos)
    generic.check_range(day, 1, days_in_month[month - 1], "day", args[2].pos)

    if not isinstance(year, ConstantNumeric):
        if month != 1 or day != 1:
            raise generic.ScriptError(
                "when the year parameter of date() is not a compile time constant month and day should be 1",
                pos)
        # num_days = year*365 + year/4 - year/100 + year/400
        part1 = nmlop.MUL(year, 365)
        part2 = nmlop.DIV(year, 4)
        part3 = nmlop.DIV(year, 100)
        part4 = nmlop.DIV(year, 400)
        res = nmlop.ADD(part1, part2)
        res = nmlop.SUB(res, part3)
        res = nmlop.ADD(res, part4)
        return res

    generic.check_range(year.value, 0, 5000000, "year", year.pos)
    day_in_year = 0
    for i in range(month - 1):
        day_in_year += days_in_month[i]
    day_in_year += day
    if month >= 3 and (year.value % 4 == 0) and ((not year.value % 100 == 0) or
                                                 (year.value % 400 == 0)):
        day_in_year += 1
    return ConstantNumeric(
        year.value * 365 + calendar.leapdays(0, year.value) + day_in_year - 1,
        pos)
Esempio n. 3
0
def builtin_visual_effect_and_powered(name, args, pos):
    """
    Builtin function, used in two forms:
    visual_effect_and_powered(effect, offset, powered)
    visual_effect(effect, offset)
    Use this to set the vehicle property visual_effect[_and_powered]
    and for the callback VEH_CB_VISUAL_EFFECT[_AND_POWERED]

    """
    arg_len = 2 if name == "visual_effect" else 3
    if len(args) != arg_len:
        raise generic.ScriptError(name + "() must have {:d} parameters".format(arg_len), pos)
    effect = args[0].reduce_constant(global_constants.const_list).value
    offset = nmlop.ADD(args[1], 8).reduce_constant().value
    generic.check_range(offset, 0, 0x0F, "offset in function " + name, pos)
    if arg_len == 3:
        powered = args[2].reduce_constant(global_constants.const_list).value
        if powered != 0 and powered != 0x80:
            raise generic.ScriptError(
                "3rd argument to visual_effect_and_powered (powered) must be"
                " either ENABLE_WAGON_POWER or DISABLE_WAGON_POWER",
                pos,
            )
    else:
        powered = 0
    return ConstantNumeric(effect | offset | powered)
Esempio n. 4
0
def builtin_slope_to_sprite_offset(name, args, pos):
    """
    builtin function slope_to_sprite_offset(slope)

    @return sprite offset to use
    """
    if len(args) != 1:
        raise generic.ScriptError(name + "() must have 1 parameter", pos)

    if isinstance(args[0], ConstantNumeric):
        generic.check_range(args[0].value, 0, 15,
                            "Argument of '{}'".format(name), args[0].pos)

    # step 1: ((slope >= 0) & (slope <= 14)) * slope
    # This handles all non-steep slopes
    expr = nmlop.AND(nmlop.CMP_LE(args[0], 14, pos),
                     nmlop.CMP_GE(args[0], 0, pos))
    expr = nmlop.MUL(expr, args[0])
    # Now handle the steep slopes separately
    # So add (slope == SLOPE_XX) * offset_of_SLOPE_XX for each steep slope
    steep_slopes = [(23, 16), (27, 17), (29, 15), (30, 18)]
    for slope, offset in steep_slopes:
        to_add = nmlop.MUL(nmlop.CMP_EQ(args[0], slope, pos), offset)
        expr = nmlop.ADD(expr, to_add)
    return expr
Esempio n. 5
0
 def preprocess_ternaryop(self, expr):
     assert isinstance(expr, expression.TernaryOp)
     guard = expression.Boolean(expr.guard).reduce()
     self.parse(guard)
     if isinstance(expr.expr1, expression.ConstantNumeric) and isinstance(expr.expr2, expression.ConstantNumeric):
         # This can be done more efficiently as (guard)*(expr1-expr2) + expr2
         self.var_list.append(nmlop.MUL)
         diff_var = VarAction2Var(0x1A, 0, expr.expr1.value - expr.expr2.value)
         diff_var.comment = "expr1 - expr2"
         self.var_list.append(diff_var)
         self.var_list.append(nmlop.ADD)
         # Add var sizes, +2 for the operators
         self.var_list_size += 2 + diff_var.get_size()
         return expr.expr2
     else:
         guard_var = VarAction2StoreTempVar()
         guard_var.comment = "guard"
         inverted_guard_var = VarAction2StoreTempVar()
         inverted_guard_var.comment = "!guard"
         self.var_list.append(nmlop.STO_TMP)
         self.var_list.append(guard_var)
         self.var_list.append(nmlop.XOR)
         var = VarAction2Var(0x1A, 0, 1)
         self.var_list.append(var)
         self.var_list.append(nmlop.STO_TMP)
         self.var_list.append(inverted_guard_var)
         self.var_list.append(nmlop.VAL2)
         # the +4 is for the 4 operators added above (STO_TMP, XOR, STO_TMP, VAL2)
         self.var_list_size += 4 + guard_var.get_size() + inverted_guard_var.get_size() + var.get_size()
         expr1 = nmlop.MUL(expr.expr1, VarAction2LoadTempVar(guard_var))
         expr2 = nmlop.MUL(expr.expr2, VarAction2LoadTempVar(inverted_guard_var))
         return nmlop.ADD(expr1, expr2)
Esempio n. 6
0
def builtin_palette_2cc(name, args, pos):
    """
    palette_2cc(colour1, colour2) builtin function.

    @return Recolour sprite to use
    """
    if len(args) != 2:
        raise generic.ScriptError(name + "() must have 2 parameters", pos)

    for i in range(0, 2):
        if isinstance(args[i], ConstantNumeric):
            generic.check_range(args[i].value, 0, 15, "Argument of '{}'".format(name), args[i].pos)

    col2 = nmlop.MUL(args[1], 16, pos)
    col12 = nmlop.ADD(col2, args[0])
    # Base sprite is not a constant
    base = global_constants.patch_variable("base_sprite_2cc", global_constants.patch_variables["base_sprite_2cc"], pos)

    return nmlop.ADD(col12, base)
Esempio n. 7
0
def builtin_palette_1cc(name, args, pos):
    """
    palette_1cc(colour) builtin function.

    @return Recolour sprite to use
    """
    if len(args) != 1:
        raise generic.ScriptError(name + "() must have 1 parameter", pos)

    if isinstance(args[0], ConstantNumeric):
        generic.check_range(args[0].value, 0, 15, "Argument of '{}'".format(name), args[0].pos)

    return nmlop.ADD(args[0], 775, pos)
Esempio n. 8
0
def tile_offset(name, args, pos, info, min, max):
    if len(args) != 2:
        raise generic.ScriptError("'{}'() requires 2 arguments, encountered {:d}".format(name, len(args)), pos)
    for arg in args:
        if isinstance(arg, expression.ConstantNumeric):
            generic.check_range(arg.value, min, max, "Argument of '{}'".format(name), arg.pos)

    x = nmlop.AND(args[0], 0xF)
    y = nmlop.AND(args[1], 0xF)
    # Shift y left by four
    y = nmlop.SHIFT_LEFT(y, 4)
    param = nmlop.ADD(x, y)
    #Make sure to reduce the result
    return ( param.reduce(), [] )
Esempio n. 9
0
def parse_graphics_block(graphics_block,
                         feature,
                         id,
                         size,
                         is_livery_override=False):
    """
    Parse a graphics block (or livery override) into a list of actions, mainly action3

    @param graphics_block: Graphics-block to parse
    @type graphics_block: L{GraphicsBlock}

    @param feature: Feature of the associated item
    @type feature: C{int}

    @param id: ID of the associated item
    @type id: L{Expression}

    @param size: Size of the associated item (relevant for houses only)
    @type size: L{ConstantNumeric} or C{None}

    @param is_livery_override: Whether this is a livery override instead of a normal graphics block
    @type is_livery_override: C{bool}

    @return: The resulting list of actions
    @rtype: L{BaseAction}
    """
    action_list = action2real.create_spriteset_actions(graphics_block)
    if feature == 0x07:
        # Multi-tile houses need more work
        size_bit = size.value if size is not None else 0
        for i, tile in enumerate(house_tiles[size_bit]):
            tile_id = id if i == 0 else nmlop.ADD(id, i).reduce()
            action_list.extend(
                parse_graphics_block_single_id(graphics_block, feature,
                                               tile_id, is_livery_override,
                                               tile, id))
    else:
        action_list.extend(
            parse_graphics_block_single_id(graphics_block, feature, id,
                                           is_livery_override))
    return action_list
Esempio n. 10
0
def parse_actionA(replaces):
    """
    Parse replace-block to ActionA.

    @param replaces: Replace-block to parse.
    @type  replaces: L{ReplaceSprite}
    """
    action_list = []
    action6.free_parameters.save()
    act6 = action6.Action6()

    real_sprite_list = real_sprite.parse_sprite_data(replaces)
    block_list = []
    total_sprites = len(real_sprite_list)
    offset = 2  # Skip 0A and <num-sets>
    sprite_offset = 0  # Number of sprites already covered by previous [<num-sprites> <first-sprite>]-pairs

    while total_sprites > 0:
        this_block = min(total_sprites, 255)  # number of sprites in this block
        total_sprites -= this_block
        offset += 1  # Skip <num-sprites>

        first_sprite = replaces.start_id  # number of first sprite
        if sprite_offset != 0:
            first_sprite = nmlop.ADD(first_sprite, sprite_offset).reduce()
        first_sprite, offset = actionD.write_action_value(
            first_sprite, action_list, act6, offset, 2)
        block_list.append((this_block, first_sprite.value))

        sprite_offset += this_block  # increase first-sprite for next block

    if len(act6.modifications) > 0:
        action_list.append(act6)
    action6.free_parameters.restore()

    action_list.append(ActionA(block_list))
    action_list.extend(real_sprite_list)

    return action_list
Esempio n. 11
0
    def pre_process(self):
        new_costs = []

        for cost in self.costs:
            cost.value = cost.value.reduce(global_constants.const_list)
            if isinstance(cost.value, expression.ConstantNumeric):
                generic.check_range(cost.value.value, -8, 16,
                                    "Base cost value", cost.value.pos)
            cost.value = nmlop.ADD(cost.value, 8).reduce()

            if isinstance(cost.name, expression.Identifier):
                if cost.name.value in base_cost_table:
                    cost.name = expression.ConstantNumeric(
                        base_cost_table[cost.name.value][0])
                    new_costs.append(cost)
                elif cost.name.value in generic_base_costs:
                    # create temporary list, so it can be sorted for efficiency
                    tmp_list = []
                    for num, type in base_cost_table.values():
                        if type == cost.name.value:
                            tmp_list.append(
                                assignment.Assignment(
                                    expression.ConstantNumeric(num),
                                    cost.value, cost.name.pos))
                    tmp_list.sort(key=lambda x: x.name.value)
                    new_costs.extend(tmp_list)
                else:
                    raise generic.ScriptError(
                        "Unrecognized base cost identifier '{}' encountered".
                        format(cost.name.value), cost.name.pos)
            else:
                cost.name = cost.name.reduce()
                if isinstance(cost.name, expression.ConstantNumeric):
                    generic.check_range(cost.name.value, 0,
                                        len(base_cost_table),
                                        "Base cost number", cost.name.pos)
                new_costs.append(cost)
        self.costs = new_costs
Esempio n. 12
0
File: action0.py Progetto: spnda/nml
def parse_property_value(prop_info, value, unit=None, size_bit=None):
    """
    Parse a single property value / unit
    To determine the value that is to be used in nfo

    @param prop_info: A dictionary with property information
    @type prop_info: C{dict}

    @param value: Value of the property
    @type value: L{Expression}

    @param unit: Unit of the property value (e.g. km/h)
    @type unit: L{Unit} or C{None}

    @param size_bit: Bit that indicates the size of a multitile house
                     Set iff the item is a house
    @type size_bit: C{int} or C{None}

    @return: List of values to actually use (in nfo) for the property
    @rtype: L{Expression}
    """
    # Change value to use, except when the 'nfo' unit is used
    if unit is None or unit.type != "nfo":
        # Save the original value to test conversion against it
        org_value = value

        # Multiply by property-specific conversion factor
        mul, div = 1, 1
        if "unit_conversion" in prop_info:
            mul = prop_info["unit_conversion"]
            if isinstance(mul, tuple):
                mul, div = mul

        # Divide by conversion factor specified by unit
        if unit is not None:
            if "unit_type" not in prop_info or unit.type != prop_info[
                    "unit_type"]:
                raise generic.ScriptError("Invalid unit for property",
                                          value.pos)
            unit_mul, unit_div = unit.convert, 1
            if isinstance(unit_mul, tuple):
                unit_mul, unit_div = unit_mul
            mul *= unit_div
            div *= unit_mul

        # Factor out common factors
        gcd = generic.greatest_common_divisor(mul, div)
        mul //= gcd
        div //= gcd

        if isinstance(value,
                      (expression.ConstantNumeric, expression.ConstantFloat)):
            # Even if mul == div == 1, we have to round floats and adjust value
            value = expression.ConstantNumeric(
                int(float(value.value) * mul / div + 0.5), value.pos)
            if unit is not None and "adjust_value" in prop_info:
                value = adjust_value(value, org_value, unit,
                                     prop_info["adjust_value"])
        elif mul != div:
            # Compute (value * mul + div/2) / div
            value = nmlop.MUL(value, mul)
            value = nmlop.ADD(value, int(div / 2))
            value = nmlop.DIV(value, div)

    elif isinstance(value, expression.ConstantFloat):
        # Round floats to ints
        value = expression.ConstantNumeric(int(value.value + 0.5), value.pos)

    # Apply value_function if it exists
    if "value_function" in prop_info:
        value = prop_info["value_function"](value)

    # Make multitile houses work
    if size_bit is not None:
        num_ids = house_sizes[size_bit]
        assert "multitile_function" in prop_info
        ret = prop_info["multitile_function"](value, num_ids, size_bit)
        assert len(ret) == num_ids
        return ret
    else:
        return [value]
Esempio n. 13
0
def value_add_constant(const):
    return lambda var, info: nmlop.ADD(var, const)
Esempio n. 14
0
def compute_table(snowline):
    """
    Compute the table with snowline height for each day of the year.

    @param snowline: Snowline definition.
    @type  snowline: L{Snowline}

    @return: Table of 12*32 entries with snowline heights.
    @rtype:  C{str}
    """
    day_table = [None] * 365  # Height at each day, starting at day 0
    for dh in snowline.date_heights:
        doy = dh.name.reduce()
        if not isinstance(doy, expression.ConstantNumeric):
            raise generic.ScriptError(
                "Day of year is not a compile-time constant", doy.pos)
        if doy.value < 1 or doy.value > 365:
            raise generic.ScriptError(
                "Day of the year must be between 1 and 365", doy.pos)

        height = dh.value.reduce()
        if isinstance(height, expression.ConstantNumeric) and height.value < 0:
            raise generic.ScriptError("Height must be at least 0", height.pos)
        if dh.unit is None:
            if isinstance(height,
                          expression.ConstantNumeric) and height.value > 255:
                raise generic.ScriptError("Height must be at most 255",
                                          height.pos)
        else:
            unit = dh.unit
            if unit.type != "snowline":
                raise generic.ScriptError(
                    'Expected a snowline percentage ("snow%")', height.pos)

            if isinstance(height,
                          expression.ConstantNumeric) and height.value > 100:
                raise generic.ScriptError("Height must be at most 100 snow%",
                                          height.pos)

            mul, div = unit.convert, 1
            if isinstance(mul, tuple):
                mul, div = mul

            # Factor out common factors
            gcd = generic.greatest_common_divisor(mul, div)
            mul //= gcd
            div //= gcd

            if isinstance(
                    height,
                (expression.ConstantNumeric, expression.ConstantFloat)):
                # Even if mul == div == 1, we have to round floats and adjust value
                height = expression.ConstantNumeric(
                    int(float(height.value) * mul / div + 0.5), height.pos)
            elif mul != div:
                # Compute (value * mul + div/2) / div
                height = nmlop.MUL(height, mul)
                height = nmlop.ADD(height, int(div / 2))
                height = nmlop.DIV(height, div)

        # For 'linear' snow-line, only accept integer constants.
        if snowline.type != "equal" and not isinstance(
                height, expression.ConstantNumeric):
            raise generic.ScriptError("Height is not a compile-time constant",
                                      height.pos)

        day_table[doy.value - 1] = height

    # Find first specified point.
    start = 0
    while start < 365 and day_table[start] is None:
        start = start + 1
    if start == 365:
        raise generic.ScriptError("No heights given for the snowline table",
                                  snowline.pos)

    first_point = start
    while True:
        # Find second point from start
        end = start + 1
        if end == 365:
            end = 0

        while end != first_point and day_table[end] is None:
            end = end + 1
            if end == 365:
                end = 0

        # Fill the days between start and end (exclusive both border values)
        startvalue = day_table[start]
        endvalue = day_table[end]
        unwrapped_end = end
        if end < start:
            unwrapped_end += 365

        if snowline.type == "equal":
            for day in range(start + 1, unwrapped_end):
                if day >= 365:
                    day -= 365
                day_table[day] = startvalue
        else:
            assert snowline.type == "linear"

            if start != end:
                dhd = float(endvalue.value -
                            startvalue.value) / float(unwrapped_end - start)
            else:
                assert startvalue.value == endvalue.value
                dhd = 0

            for day in range(start + 1, unwrapped_end):
                uday = day
                if uday >= 365:
                    uday -= 365
                height = startvalue.value + int(round(dhd * (day - start)))
                day_table[uday] = expression.ConstantNumeric(height)

        if end == first_point:  # All days done
            break

        start = end

    table = [None] * (12 * 32)
    for dy in range(365):
        today = datetime.date.fromordinal(dy + 1)
        if day_table[dy]:
            expr = day_table[dy].reduce()
        else:
            expr = None
        table[(today.month - 1) * 32 + today.day - 1] = expr

    for idx, d in enumerate(table):
        if d is None:
            table[idx] = table[idx - 1]
    # Second loop is needed because we need make sure the first item is also set.
    for idx, d in enumerate(table):
        if d is None:
            table[idx] = table[idx - 1]

    return table
Esempio n. 15
0
    def reduce(self, id_dicts=None, unknown_id_fatal=True):
        # Reducing a BinOp expression is done in several phases:
        # - Reduce both subexpressions.
        # - If both subexpressions are constant, compute the result and return it.
        # - If the operator allows it and the second expression is more complex than
        #   the first one swap them.
        # - If the operation is a no-op, delete it.
        # - Variables (as used in action2var) can have some computations attached to
        #   them, do that if possible.
        # - Try to merge multiple additions/subtractions with constant numbers

        # - Reduce both subexpressions.
        expr1 = self.expr1.reduce(id_dicts)
        expr2 = self.expr2.reduce(id_dicts)

        # Make sure the combination of operands / operator is valid
        if self.op.validate_func is not None:
            self.op.validate_func(expr1, expr2, self.pos)

        # - If both subexpressions are constant, compute the result and return it.
        if (
            isinstance(expr1, ConstantNumeric)
            and isinstance(expr2, ConstantNumeric)
            and self.op.compiletime_func is not None
        ):
            return ConstantNumeric(self.op.compiletime_func(expr1.value, expr2.value), self.pos)

        if isinstance(expr1, StringLiteral) and isinstance(expr2, StringLiteral):
            assert self.op == nmlop.ADD
            return StringLiteral(expr1.value + expr2.value, expr1.pos)

        if (
            isinstance(expr1, (ConstantNumeric, ConstantFloat))
            and isinstance(expr2, (ConstantNumeric, ConstantFloat))
            and self.op.compiletime_func is not None
        ):
            return ConstantFloat(self.op.compiletime_func(expr1.value, expr2.value), self.pos)

        # - If the operator allows it and the second expression is more complex than
        #   the first one swap them.
        op = self.op
        if op.commutative or op in (nmlop.CMP_LT, nmlop.CMP_GT):
            prio1 = self.get_priority(expr1)
            prio2 = self.get_priority(expr2)
            if prio2 < prio1:
                expr1, expr2 = expr2, expr1
                if op == nmlop.CMP_LT:
                    op = nmlop.CMP_GT
                elif op == nmlop.CMP_GT:
                    op = nmlop.CMP_LT

        # - If the operation is a no-op, delete it.
        if op == nmlop.AND and isinstance(expr2, ConstantNumeric) and (expr2.value == -1 or expr2.value == 0xFFFFFFFF):
            return expr1

        if op in (nmlop.DIV, nmlop.DIVU, nmlop.MUL) and isinstance(expr2, ConstantNumeric) and expr2.value == 1:
            return expr1

        if op in (nmlop.ADD, nmlop.SUB) and isinstance(expr2, ConstantNumeric) and expr2.value == 0:
            return expr1

        # - Variables (as used in action2var) can have some computations attached to
        #   them, do that if possible.
        if isinstance(expr1, Variable) and expr2.supported_by_actionD(False):
            # An action2 Variable has some special fields (mask, add, div and mod) that can be used
            # to perform some operations on the value. These operations are faster than a normal
            # advanced varaction2 operator so we try to use them whenever we can.
            if op == nmlop.AND and expr1.add is None:
                expr1.mask = nmlop.AND(expr1.mask, expr2, self.pos).reduce(id_dicts)
                return expr1
            if op == nmlop.ADD and expr1.div is None and expr1.mod is None:
                if expr1.add is None:
                    expr1.add = expr2
                else:
                    expr1.add = nmlop.ADD(expr1.add, expr2, self.pos).reduce(id_dicts)
                return expr1
            if op == nmlop.SUB and expr1.div is None and expr1.mod is None:
                if expr1.add is None:
                    expr1.add = ConstantNumeric(0)
                expr1.add = nmlop.SUB(expr1.add, expr2, self.pos).reduce(id_dicts)
                return expr1
            # The div and mod fields cannot be used at the same time. Also whenever either of those
            # two are used the add field has to be set, so we change it to zero when it's not yet set.
            if op == nmlop.DIV and expr1.div is None and expr1.mod is None:
                if expr1.add is None:
                    expr1.add = ConstantNumeric(0)
                expr1.div = expr2
                return expr1
            if op == nmlop.MOD and expr1.div is None and expr1.mod is None:
                if expr1.add is None:
                    expr1.add = ConstantNumeric(0)
                expr1.mod = expr2
                return expr1
            # Since we have a lot of nml-variables that are in fact only the high bits of an nfo
            # variable it can happen that we want to shift back the variable to the left.
            # Don't use any extra opcodes but just reduce the shift-right in that case.
            if (
                op == nmlop.SHIFT_LEFT
                and isinstance(expr2, ConstantNumeric)
                and expr1.add is None
                and expr2.value < expr1.shift.value
            ):
                expr1.shift.value -= expr2.value
                expr1.mask = nmlop.SHIFT_LEFT(expr1.mask, expr2).reduce()
                return expr1

        # - Try to merge multiple additions/subtractions with constant numbers
        if (
            op in (nmlop.ADD, nmlop.SUB)
            and isinstance(expr2, ConstantNumeric)
            and isinstance(expr1, BinOp)
            and expr1.op in (nmlop.ADD, nmlop.SUB)
            and isinstance(expr1.expr2, ConstantNumeric)
        ):
            val = expr2.value if op == nmlop.ADD else -expr2.value
            if expr1.op == nmlop.ADD:
                return nmlop.ADD(expr1.expr1, (expr1.expr2.value + val), self.pos).reduce()
            if expr1.op == nmlop.SUB:
                return nmlop.SUB(expr1.expr1, (expr1.expr2.value - val), self.pos).reduce()

        if op == nmlop.OR and isinstance(expr1, Boolean) and isinstance(expr2, Boolean):
            return Boolean(nmlop.OR(expr1.expr, expr2.expr, self.pos)).reduce(id_dicts)

        return BinOp(op, expr1, expr2, self.pos)