Exemple #1
0
    def collect(self) -> SsbLabelJumpBlueprint:
        self.ctx: ExplorerScriptParser.If_headerContext
        is_positive = self._is_positive

        self.ctx: ExplorerScriptParser.If_headerContext
        # Complex branches
        if self._header_cmplx_handler:
            if isinstance(self._header_cmplx_handler, OperationCompileHandler):
                # An operation as condition
                op = self._header_cmplx_handler.collect()
                if len(op) != 1:
                    raise SsbCompilerError(
                        _("Invalid content for an if-header"))
                op = op[0]
                if op.op_code.name not in OPS_BRANCH.keys():
                    raise SsbCompilerError(
                        f(
                            _("Invalid operation for if condition: {op.op_code.name} (line {self.ctx.start.line})"
                              )))
                jmp = SsbLabelJumpBlueprint(self.compiler_ctx, self.ctx,
                                            op.op_code.name, op.params)
                jmp.set_jump_is_positive(is_positive)
                return jmp
            else:
                # A regular complex if condition
                tmpl: SsbLabelJumpBlueprint = self._header_cmplx_handler.collect(
                )
                tmpl.set_jump_is_positive(is_positive)
                return tmpl

        raise SsbCompilerError(
            f(_("Unknown if operation in line {self.ctx.start.line}).")))
 def _check_cycles(self):
     for v in self._dependency_graph.vs:
         if any(self._has_path(out_e.target, v) for out_e in v.out_edges()):
             raise SsbCompilerError(f(_("Dependency cycle detected while trying to resolve macros"
                                        " (for macro '{v['name']}').")))
         # Check direct cycles
         if any(v.index == e.target for e in v.out_edges()):
             raise SsbCompilerError(f(_("Dependency cycle detected while trying to resolve macros"
                                        " (for macro '{v['name']}').")))
Exemple #3
0
    def collect(self) -> List[SsbOperation]:
        ops = []
        self.ctx: ExplorerScriptParser.Ctx_blockContext
        if self._for_id is None:
            raise SsbCompilerError(_("No target ID set for with(){} block."))
        if not self._sub_stmt:
            raise SsbCompilerError(
                _("A with(){} block needs exactly one statement."))
        for_type = str(self.ctx.ctx_header().CTX_TYPE())

        if for_type == 'actor':
            ops.append(self._generate_operation(OPS_CTX_LIVES, [self._for_id]))
        elif for_type == 'object':
            ops.append(self._generate_operation(OPS_CTX_OBJECT,
                                                [self._for_id]))
        elif for_type == 'performer':
            ops.append(
                self._generate_operation(OPS_CTX_PERFORMER, [self._for_id]))
        else:
            raise SsbCompilerError(
                f(_("Invalid with(){{}} target type '{for_type}'.")))

        sub_ops = self._sub_stmt.collect()
        if len(sub_ops) != 1:
            raise SsbCompilerError(
                _("A with(){} block needs exactly one binary operation. "
                  "The handler for it generated multiple operations."))
        ops += sub_ops

        return ops
Exemple #4
0
    def collect(self) -> SsbLabelJumpBlueprint:
        if self.scn_var_target is None:
            raise SsbCompilerError(_("No variable for assignment."))
        if self.operator not in [
                SsbOperator.EQ, SsbOperator.LE, SsbOperator.LT, SsbOperator.GE,
                SsbOperator.GT
        ]:
            raise SsbCompilerError(
                f(
                    _("The only supported operators for scn if "
                      "conditions are ==,<,<=,>,>= (line {self.ctx.start.line})"
                      )))

        scn_value = exps_int(str(self.ctx.INTEGER(0)))
        level_value = exps_int(str(self.ctx.INTEGER(1)))

        if self.operator == SsbOperator.LE:
            return SsbLabelJumpBlueprint(
                self.compiler_ctx, self.ctx, OP_BRANCH_SCENARIO_NOW_BEFORE,
                [self.scn_var_target, scn_value, level_value])
        if self.operator == SsbOperator.LT:
            return SsbLabelJumpBlueprint(
                self.compiler_ctx, self.ctx, OP_BRANCH_SCENARIO_BEFORE,
                [self.scn_var_target, scn_value, level_value])
        if self.operator == SsbOperator.GE:
            return SsbLabelJumpBlueprint(
                self.compiler_ctx, self.ctx, OP_BRANCH_SCENARIO_NOW_AFTER,
                [self.scn_var_target, scn_value, level_value])
        if self.operator == SsbOperator.GT:
            return SsbLabelJumpBlueprint(
                self.compiler_ctx, self.ctx, OP_BRANCH_SCENARIO_AFTER,
                [self.scn_var_target, scn_value, level_value])
        return SsbLabelJumpBlueprint(
            self.compiler_ctx, self.ctx, OP_BRANCH_SCENARIO_NOW,
            [self.scn_var_target, scn_value, level_value])
    def _resolve_imported_file(self, dir_name):
        """
        Returns the full paths to all imports specified in self.imports.
        If any file can not be found, raises an SsbCompilerError.
        """
        fs = []
        for import_file in self.imports:
            if import_file.startswith('.') or import_file.startswith('/'):
                # Relative or absolute import
                abs_path = os.path.realpath(
                    str(
                        PurePath(
                            PurePosixPath(dir_name).joinpath(
                                PurePosixPath(import_file)))))
                if not os.path.exists(abs_path):
                    raise SsbCompilerError(
                        f(
                            _("The file to import ('{import_file}') was not found."
                              )))
            else:
                # Relative to one of the lookup paths
                abs_path = None
                path_parts = import_file.split('/')
                if '.' in path_parts or '..' in path_parts:
                    raise SsbCompilerError(
                        f(
                            _("Invalid import: '{import_file}'. Non absolute/relative "
                              "imports must not contain relative paths.")))
                for lp in self.lookup_paths:
                    abs_path_c = os.path.realpath(
                        str(
                            PurePath(dir_name).joinpath(
                                PurePosixPath(lp).joinpath(import_file))))
                    if os.path.exists(abs_path_c):
                        abs_path = abs_path_c
                        break
                if abs_path is None:
                    raise SsbCompilerError(
                        f(
                            _("The file to import ('{import_file}') was not found."
                              )))
            fs.append(abs_path)

        return fs
Exemple #6
0
    def collect(self) -> SsbOperation:
        if self.scn_var_target is None:
            raise SsbCompilerError(_("No variable set for scn switch condition."))

        index = exps_int(str(self.ctx.INTEGER()))
        if index == 0:
            return self._generate_operation(OP_SWITCH_SCENARIO, [self.scn_var_target])
        elif index == 1:
            return self._generate_operation(OP_SWITCH_SCENARIO_LEVEL, [self.scn_var_target])
        raise SsbCompilerError(f(_("Index for scn() if condition must be 0 or 1 (line {self.ctx.start.line}).")))
    def collect(self) -> List[SsbOperation]:
        self.ctx: ExplorerScriptParser.Message_switch_blockContext
        if self.ctx.MESSAGE_SWITCH_MONOLOGUE():
            switch_op = self._generate_operation(
                OP_MESSAGE_SWITCH_MONOLOGUE,
                [self._switch_header_handler.collect()])
        elif self.ctx.MESSAGE_SWITCH_TALK():
            switch_op = self._generate_operation(
                OP_MESSAGE_SWITCH_TALK,
                [self._switch_header_handler.collect()])
        else:
            raise SsbCompilerError(_("Invalid message switch."))
        case_ops = []
        for h in self._case_handlers:
            if not h.is_message_case:
                raise SsbCompilerError(
                    f(
                        _("A message_ switch can only contain cases with strings "
                          "(line {self.ctx.start.line}).")))
            header_handler = h.collect_header_handler()
            if header_handler.get_header_handler_type(
            ) != IntegerLikeCompileHandler:
                raise SsbCompilerError(
                    f(
                        _("Invalid case type for message_ switch (line {self.ctx.start.line})."
                          )))
            string = h.get_text()
            value_blueprint = header_handler.collect()
            # We obviously don't want the bluprint
            value = value_blueprint.params[0]
            case_ops.append(
                self._generate_operation(OP_CASE_TEXT, [value, string]))
        if self._default_handler:
            if not self._default_handler.is_message_case:
                raise SsbCompilerError(
                    f(
                        _("A message_ switch can only contain cases with strings (line {self.ctx.start.line})."
                          )))
            case_ops.append(
                self._generate_operation(OP_DEFAULT_TEXT,
                                         [self._default_handler.get_text()]))

        return [switch_op] + case_ops
 def add(self, obj: any):
     if isinstance(obj, CaseBlockCompileHandler):
         self._case_handlers.append(obj)
         return
     if isinstance(obj, DefaultCaseBlockCompileHandler):
         if self._default_handler is not None:
             raise SsbCompilerError(
                 f(
                     _("A switch block can only have a single default case (line {self.ctx.start.line}"
                       )))
         self._default_handler = obj
         return
     if isinstance(obj, IntegerLikeCompileHandler):
         self._switch_header_handler = obj
         return
     self._raise_add_error(obj)
    def collect(self) -> List[SsbOperation]:
        self.ctx: ExplorerScriptParser.Macro_callContext
        name = str(self.ctx.MACRO_CALL())[1:]
        args: List[SsbOpParam] = []
        if self.arg_list_handler:
            args = self.arg_list_handler.collect()
        if name not in self.compiler_ctx.macros.keys():
            raise SsbCompilerError(f(_("Macro {name} not found.")))
        macro = self.compiler_ctx.macros[name]

        self.compiler_ctx.source_map_builder.next_macro_opcode_called_in(
            None, self.ctx.start.line - 1, self.ctx.start.column)
        return macro.build(self.compiler_ctx.counter_ops,
                           self.compiler_ctx.counter_labels,
                           dict(zip(macro.variables, args)),
                           self.compiler_ctx.source_map_builder)
Exemple #10
0
    def collect(self) -> List[SsbOperation]:
        if self.var_target is None:
            raise SsbCompilerError(_("No variable for assignment."))
        if self.operator is None:
            raise SsbCompilerError(_("No operator set for assignment."))
        if self.value is None:
            raise SsbCompilerError(_("No value set for assignment."))

        if self.ctx.INTEGER():
            index = exps_int(str(self.ctx.INTEGER()))
            # CalcBit / SetPerformance
            if self.value_is_a_variable:
                raise SsbCompilerError(
                    f(
                        _("value(X) can not be used with index based assignments "
                          "(line {self.ctx.start.line}).")))
            if str(self.var_target
                   ) == self.compiler_ctx.performance_progress_list_var_name:
                return [
                    self._generate_operation(OPS_FLAG__SET_PERFORMANCE,
                                             [index, self.value])
                ]
            return [
                self._generate_operation(OPS_FLAG__CALC_BIT,
                                         [self.var_target, index, self.value])
            ]

        # CalcValue / CalcVariable / Set
        if self.value_is_a_variable:
            return [
                self._generate_operation(
                    OPS_FLAG__CALC_VARIABLE,
                    [self.var_target, self.operator.value, self.value])
            ]
        if self.operator == SsbCalcOperator.ASSIGN:
            return [
                self._generate_operation(OPS_FLAG__SET,
                                         [self.var_target, self.value])
            ]
        return [
            self._generate_operation(
                OPS_FLAG__CALC_VALUE,
                [self.var_target, self.operator.value, self.value])
        ]
Exemple #11
0
    def collect(self) -> SsbLabelJumpBlueprint:
        self.ctx: ExplorerScriptParser.If_h_bitContext
        if self.var_target is None:
            raise SsbCompilerError(_("No variable in if condition."))

        var_target_name = None
        if hasattr(self.var_target, 'name'):
            var_target_name = self.var_target.name

        index = exps_int(str(self.ctx.INTEGER()))
        is_simple_positive = self.ctx.NOT() is None

        if var_target_name == self.compiler_ctx.performance_progress_list_var_name:
            return SsbLabelJumpBlueprint(
                self.compiler_ctx, self.ctx, OP_BRANCH_PERFORMANCE,
                [index, 1 if is_simple_positive else 0])
        elif not is_simple_positive:
            raise SsbCompilerError(
                f(
                    _("The variable {var_target_name} can not be used with 'not' "
                      "(line {self.ctx.start.line}).")))
        return SsbLabelJumpBlueprint(self.compiler_ctx, self.ctx,
                                     OP_BRANCH_BIT, [self.var_target, index])
 def __str__(self):
     return f(_("line {self.line}:{self.column}: {self.msg}"))
    def compile(self,
                explorerscript_src: str,
                file_name: str,
                macros_only=False,
                original_base_file=None):
        """
        After compiling, the components are present in this object's attributes.

        file_name is the full path to the file that is being compiled.
        original_base_file is the full path to the file that originally started an import chain. If not given, file_name
        is used.
        If macros_only is True, then an exception is raised, if the script files contains any routines.

        :raises: ParseError: On parsing errors
        :raises: SsbCompilerError: On logical compiling errors
        :raises: ValueError: On misc. unexpected compilation errors
        """
        logger.debug(
            "<%d> Compiling ExplorerScript (-> %s)... - Macros only:%d, base:%s",
            id(self), file_name, macros_only, original_base_file)
        self.routine_infos = None
        self.routine_ops = None
        self.named_coroutines = None
        self.source_map = None
        self.imports = []
        self.macros = {}
        if original_base_file is None:
            original_base_file = file_name

        reader = ExplorerScriptReader(explorerscript_src)
        tree = reader.read()
        parser = reader.get_parser()

        # Collect imports
        logger.debug("<%d> Collecting imports...", id(self))
        self.imports = ImportVisitor().visit(tree)

        # Resolve imports and load macros in the imported files
        for subfile_path in self._resolve_imported_file(
                os.path.dirname(file_name)):
            logger.debug("<%d> Compiling sub-file %s...", id(self),
                         subfile_path)
            if subfile_path in self.recursion_check:
                raise SsbCompilerError(
                    f(
                        _("Infinite recursion detected while trying to load "
                          "an ExplorerScript file from {subfile_path}.\n"
                          "Tried loading from: {file_name}.")))
            subfile_compiler = self.__class__(
                self.performance_progress_list_var_name,
                self.lookup_paths,
                recursion_check=self.recursion_check + [file_name])
            with open_utf8(subfile_path, 'r') as file:
                subfile_compiler.compile(file.read(),
                                         subfile_path,
                                         macros_only=True,
                                         original_base_file=original_base_file)
            self.macros.update(
                self._macros_add_filenames(subfile_compiler.macros,
                                           original_base_file, subfile_path))

        # Sort the list of macros by how they are used
        logger.debug("<%d> Building macro resolution order...", id(self))
        self.macro_resolution_order = MacroResolutionOrderVisitor(
            self.macros).visit(tree)

        # Loads and compiles modules in base file
        # (we write our absolute path there only for now, if this is an inclusion, the outer compiler will update it).
        logger.debug("<%d> Compiling macros...", id(self))
        self.macros.update(
            self._macros_add_filenames(
                MacroVisitor(self.performance_progress_list_var_name,
                             self.macros,
                             self.macro_resolution_order).visit(tree), None,
                file_name))

        # Check if macros_only
        if macros_only:
            # Check if the file contains any routines
            if HasRoutinesVisitor().visit(parser.start()):
                # noinspection PyUnusedLocal
                fn = os.path.basename(file_name)
                raise SsbCompilerError(
                    f(_("{fn}: Macro scripts must not contain any routines.")))
            return self

        # Start Compiling
        try:
            try:
                logger.debug("<%d> Compiling routines...", id(self))
                compiler_visitor = RoutineVisitor(
                    self.performance_progress_list_var_name, self.macros)
                compiler_visitor.visit(tree)
            except Exception as ex:
                # due to the stack nature of the decompile visitor, we get many stack exceptions after raising
                # the first. Raise the last exception in the context chain.
                while ex.__context__ is not None:
                    ex = ex.__context__
                raise ex
        except AssertionError as e:
            raise ValueError(str(e)) from e

        assert routine_op_offsets_are_ordered(compiler_visitor.routine_ops)

        # Copy from listener / remove labels and label jumps
        label_finalizer = LabelFinalizer(
            strip_last_label(compiler_visitor.routine_ops))

        self.routine_ops = OpsLabelJumpToRemover(
            label_finalizer.routines, label_finalizer.label_offsets).routines
        self.routine_infos = compiler_visitor.routine_infos
        self.named_coroutines = compiler_visitor.named_coroutines
        self.source_map = compiler_visitor.source_map_builder.build()

        # Done!
        return self
Exemple #14
0
    def build(self, op_idx_counter: Counter, lbl_idx_counter: Counter, parameters: Dict[str, SsbOpParam],
              smb: SourceMapBuilder) -> List[SsbOperation]:
        """
        Returns new built opcodes for this macro. SsbOpConstants in the blueprints, that have the names of variables,
        are replaced with the values from the parameter dict. The keys for the dict are the names of the variables,
        all must be defined.
        Also updates the source map with macro opcode / position mark entries.
        """
        # Check:
        for var_name in self.variables:
            if var_name not in parameters.keys():
                raise ValueError(f(_("Value for macro variable {var_name} not provided.")))

        # Macro callstack: Push our outer call
        len_real_ops_in_blueprints = len([o for o in self.blueprints if not isinstance(o, SsbLabel)]) + 1
        parameter_mapping = self._create_parameter_mapping(parameters)
        smb.macro_context__push(op_idx_counter.count + len_real_ops_in_blueprints, parameter_mapping)

        # Add the macro start label if we are processed later as a sub-macro, so that the parent macro can push
        # our ops to the callstack.
        out_ops: List[SsbOperation] = [MacroStartSsbLabel(
            lbl_idx_counter(), -1, len_real_ops_in_blueprints, parameter_mapping, f"Macro call {self.name} start label."
        )]

        # End label, also for sub-macros and when we are processing a return statement
        end_label = MacroEndSsbLabel(
            lbl_idx_counter(), -1, f"Macro call {self.name} end label."
        )

        # Maps blueprint label ids to new actual labels
        new_labels: Dict[int, SsbLabel] = {}

        for blueprint_op in self.blueprints:
            # If this a start / end of a macro, update the macro callstack
            if isinstance(blueprint_op, MacroStartSsbLabel):
                smb.macro_context__push(op_idx_counter.count + blueprint_op.length_of_macro,
                                        self._replace_in_param_mapping(blueprint_op.parameter_mapping, parameters))
            elif isinstance(blueprint_op, MacroEndSsbLabel):
                smb.macro_context__pop()

            if isinstance(blueprint_op, SsbLabel):
                if blueprint_op.id not in new_labels.keys():
                    # Copy the label with a new proper index
                    new_labels[blueprint_op.id] = self._copy_blueprint_label(
                        lbl_idx_counter, blueprint_op
                    )
                    new_labels[blueprint_op.id].markers = blueprint_op.markers.copy()
                out_ops.append(new_labels[blueprint_op.id])
            elif isinstance(blueprint_op, SsbLabelJump):
                if blueprint_op.label.id not in new_labels.keys():
                    # Copy the label with a new proper index
                    new_labels[blueprint_op.label.id] = self._copy_blueprint_label(
                        lbl_idx_counter, blueprint_op.label
                    )
                    new_labels[blueprint_op.label.id].markers = blueprint_op.label.markers.copy()
                new_root = self._build_op(op_idx_counter, blueprint_op.root, smb, parameters)
                new_jumps = SsbLabelJump(new_root, new_labels[blueprint_op.label.id])
                new_jumps.markers = blueprint_op.markers.copy()
                out_ops.append(new_jumps)
            elif blueprint_op.op_code.name == OP_RETURN:
                # Process return: Exit the macro instead
                replacement_blueprint_op = SsbOperation(blueprint_op.offset, SsbOpCode(-1, OP_JUMP), [])
                out_ops.append(SsbLabelJump(
                    self._build_op(
                        op_idx_counter, replacement_blueprint_op, smb, parameters
                    ), end_label
                ))
            else:
                out_ops.append(self._build_op(op_idx_counter, blueprint_op, smb, parameters))
        for pos_mark in self.source_map.get_position_marks__direct():
            smb.add_macro_position_mark(self.included__relative_path, self.name, pos_mark)
        # Also add the sub-macro position marks to the map
        for m in self.source_map.get_position_marks__macros():
            smb.add_macro_position_mark(*m)

        out_ops.append(end_label)

        # Macro callstack: Pop our return address
        smb.macro_context__pop()

        return out_ops
Exemple #15
0
    def collect(self) -> List[SsbOperation]:
        self.ctx: ExplorerScriptParser.Switch_blockContext
        # 0. Prepare labels to insert
        default_start_label = SsbLabel(self.compiler_ctx.counter_labels(), -1,
                                       'switch default start label')
        end_label = SsbLabel(self.compiler_ctx.counter_labels(), -1,
                             'entire switch-block end label')
        default_jmp_to_case_block: Optional[SsbLabelJumpBlueprint] = None
        case_ops: List[SsbOperation] = []
        default_ops: List[SsbOperation]

        # 0b. Switch op
        switch_op = self._switch_header_handler.collect()

        # If there is no default and also no cases... we really don't need anything.
        if self._default_handler is None and len(self._case_handlers) == 0:
            return [switch_op]

        # 1. For each case: Generate and allocate case header op templates
        for h in self._case_handlers:
            h.set_end_label(end_label)
            if h.is_message_case:
                raise SsbCompilerError(
                    f(
                        _("A switch case must contain a list of statements "
                          "(line {self.ctx.start.line}).")))
            jmp_blueprint = h.get_header_jump_template()
            first = self.compiler_ctx.counter_ops.allocate(1)
            jmp_blueprint.set_index_number(first)
            # A little special case: If the switch header op is SwitchScenario and the case is CaseValue, change it to
            # CaseScenario, just to be more consistent with how the game odes it.
            if switch_op.op_code.name == OP_SWITCH_SCENARIO and jmp_blueprint.op_code_name == OP_CASE_VALUE:
                jmp_blueprint.op_code_name = OP_CASE_SCENARIO

        # 2. Default block (first because default is after no case op branched)
        if self._default_handler:
            self._default_handler.set_end_label(end_label)
            default_ops = []
            # Insert a jump blueprint for now, note it, and if it comes up later during 3b,
            # process it like explained there.
            default_jmp_to_case_block = SsbLabelJumpBlueprint(
                self.compiler_ctx, self.ctx, OP_JUMP, [])
            self._case_handlers.insert(self._default_handler_index,
                                       self._default_handler)
        else:
            # 2c. If no default: Create a default block with just one jump to end label
            default_ops = [
                self._generate_jump_operation(OP_JUMP, [], end_label)
            ]
        # 3. For each case:
        cases_waiting_for_a_block = []
        for i, h in enumerate(self._case_handlers):
            if not h.has_sub_block_handlers():
                # 3b. Else: Jump to the block of the next case which has a block.
                cases_waiting_for_a_block.append(h)
            else:
                # 3a. If the case has operations: Collect case sub-block ops
                ops = h.collect()
                if isinstance(h, DefaultCaseBlockCompileHandler):
                    default_ops = [
                        default_jmp_to_case_block.build_for(
                            h.get_start_label())
                    ]
                for h_waiting in cases_waiting_for_a_block:
                    if isinstance(h_waiting, DefaultCaseBlockCompileHandler):
                        default_ops = [
                            default_jmp_to_case_block.build_for(
                                h.get_start_label())
                        ]
                    else:
                        h_waiting.set_processed_header_jumps([
                            h_waiting.get_header_jump_template().build_for(
                                h.get_start_label())
                        ])
                cases_waiting_for_a_block = []
                case_ops += ops
        # 3c. Edge case: We expected a next case with ops, but got end of switch instead. Invalid!
        if len(cases_waiting_for_a_block) > 0:
            raise SsbCompilerError(
                f(_("Unexpected switch end (line {self.ctx.start.line})")))
        # 4. Build ops list
        header_ops = [switch_op]
        for h in self._case_handlers:
            header_ops += h.get_processed_header_jumps()
        return header_ops + [default_start_label
                             ] + default_ops + case_ops + [end_label]