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']}').")))
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
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
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)
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]) ]
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
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
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]