diff --git a/src/preppipe/frontend/vnmodel/vncodegen.py b/src/preppipe/frontend/vnmodel/vncodegen.py index 21d4ffa..acdc708 100644 --- a/src/preppipe/frontend/vnmodel/vncodegen.py +++ b/src/preppipe/frontend/vnmodel/vncodegen.py @@ -886,14 +886,15 @@ def visit_default_handler(self, node : VNASTNodeBase): zh_hk="以下命令結點在一個已經結束的基本塊中,內容不會被處理: {node}。請將內容移至可能結束該基本塊的指令前(比如跳轉、選項等)。", ) - def check_blocklocal_cond(self, node : VNASTNodeBase) -> bool: + def check_blocklocal_cond(self, node : VNASTNodeBase, suppress_warning : bool = False) -> bool: # 检查该指令是否在正常的、未结束的块中 # 是的话返回 False, 不在正常情况下时生成错误并返回 True if self.cur_terminator is None: return False - msg = self._tr_unhandled_node_in_terminated_block.format(node=node.get_short_str(0)) - err = ErrorOp.create(error_code='vncodegen-unhandled-node-in-terminated-block', context=self.context, error_msg=StringLiteral.get(msg, self.context), loc=node.location) - err.insert_before(self.cur_terminator) + if not suppress_warning: + msg = self._tr_unhandled_node_in_terminated_block.format(node=node.get_short_str(0)) + err = ErrorOp.create(error_code='vncodegen-unhandled-node-in-terminated-block', context=self.context, error_msg=StringLiteral.get(msg, self.context), loc=node.location) + err.insert_before(self.cur_terminator) return True def check_block_or_function_local_cond(self, node : VNASTNodeBase) -> bool: @@ -1918,7 +1919,9 @@ def visitVNASTBreakNode(self, node : VNASTBreakNode) -> VNTerminatorInstBase | N return None def visitVNASTReturnNode(self, node : VNASTReturnNode) -> VNTerminatorInstBase | None: - if self.check_block_or_function_local_cond(node): + # 章节结束命令只能在函数内使用 + # 我们允许额外的章节结束命令(可能在转至章节命令后面)且不提供额外警告 + if self.check_blocklocal_cond(node, suppress_warning=True): return None ret = VNReturnInst.create(context=self.context, start_time=self.starttime, name=node.name, loc=node.location) self.destblock.push_back(ret) diff --git a/src/preppipe/irbase.py b/src/preppipe/irbase.py index a67a2df..e55790b 100644 --- a/src/preppipe/irbase.py +++ b/src/preppipe/irbase.py @@ -1812,6 +1812,12 @@ def dump(self) -> None: dump = writer.write_op(self) print(dump.decode('utf-8')) + def dump_html(self, index : int = 0, parentdir : str = '') -> None: + # for debugging + writer = IRWriter(self.context, True, None, None) + dump = writer.write_op(self) + _save_content_html_helper(dump, self.name, type(self).__name__, index, parentdir) + @IRObjectJsonTypeName("symbol_op") class Symbol(Operation): @@ -4094,12 +4100,17 @@ def write_block(self, b : Block) -> bytes: self._walk_block(b, 0) return self._output_body.getvalue() -def _view_content_helper(dump : bytes, name : str, typename : str): - name_portion = 'anon' +def _get_sanitized_name_for_dump(name : str) -> str | None: if len(name) > 0: sanitized_name = get_sanitized_filename(name) if len(sanitized_name) > 0: - name_portion = sanitized_name + return sanitized_name + return None + +def _view_content_helper(dump : bytes, name : str, typename : str): + name_portion = _get_sanitized_name_for_dump(name) + if name_portion is None: + name_portion = 'anon' file = tempfile.NamedTemporaryFile('w+b', suffix='_viewdump.html', prefix='preppipe_' + typename + '_' + name_portion + '_', delete=False) file.write(dump) file.close() @@ -4107,6 +4118,15 @@ def _view_content_helper(dump : bytes, name : str, typename : str): print('Opening HTML dump at ' + path) webbrowser.open_new_tab('file:///' + path) +def _save_content_html_helper(dump : bytes, name : str, typename : str, index : int = 0, parentdir : str = ''): + name_portion = _get_sanitized_name_for_dump(name) + if name_portion is None: + name_portion = f"anon_{index}" + filename = f"{name_portion}_{typename}.html" + path = os.path.join(parentdir, filename) if len(parentdir) > 0 else filename + with open(path, 'wb') as f: + f.write(dump) + # ------------------------------------------------------------------------------ # IR verification # ------------------------------------------------------------------------------ diff --git a/src/preppipe/pipeline.py b/src/preppipe/pipeline.py index c987364..23e0786 100644 --- a/src/preppipe/pipeline.py +++ b/src/preppipe/pipeline.py @@ -517,11 +517,32 @@ class _SaveIR(TransformBase): def run(self) -> None: raise PPNotImplementedError -@BackendDecl('dump', input_decl=Operation, output_decl=IODecl('', nargs=0)) -class _DumpIR(TransformBase): +@TransformArgumentGroup('debugdump', "Options for Debug Dump") +@BackendDecl('debugdump', input_decl=Operation, output_decl=IODecl('', nargs=0)) +class _DebugDump(TransformBase): + dumpdir : typing.ClassVar[str] = "dumps" + + @staticmethod + def install_arguments(argument_group : argparse._ArgumentGroup): + argument_group.add_argument("--debugdump-dir", required=False, type=str, nargs=1, default=_DebugDump.dumpdir, help="Directory to save the dumps") + + @staticmethod + def handle_arguments(args : argparse.Namespace): + if dumpdir := args.debugdump_dir: + assert isinstance(dumpdir, list) and len(dumpdir) == 1 + _DebugDump.dumpdir = dumpdir[0] + assert isinstance(_DebugDump.dumpdir, str) + if not os.path.exists(_DebugDump.dumpdir): + os.makedirs(_DebugDump.dumpdir, exist_ok=True) + elif not os.path.isdir(_DebugDump.dumpdir): + raise PPInternalError("Debug dump directory path exists but is not a directory") + def run(self) -> None: - for op in self.inputs: - op.dump() + for index, op in enumerate(self.inputs): + if isinstance(op, Operation): + op.dump_html(index, _DebugDump.dumpdir) + else: + raise PPInternalError("Debug dump input is not an operation") @BackendDecl('view', input_decl=Operation, output_decl=IODecl('', nargs=0)) class _ViewIR(TransformBase): diff --git a/src/preppipe_gui_pyside6/execution.py b/src/preppipe_gui_pyside6/execution.py index 7819d2e..7b40c6a 100644 --- a/src/preppipe_gui_pyside6/execution.py +++ b/src/preppipe_gui_pyside6/execution.py @@ -10,11 +10,14 @@ from preppipe.language import * from .settingsdict import * +TR_gui_execution = TranslationDomain("gui_execution") + @dataclasses.dataclass class SpecifiedOutputInfo: - # 有指定输出路径的输出项 + # 有指定输出路径的输出项,将在输出列表中出现。目前也包含未指定输出路径的项(因为最终它们也会有路径) field_name: Translatable | str argindex: int = -1 + auxiliary: bool = False # 是否为辅助输出(如调试输出等),是的话我们会把它们放在其他输出之后 @dataclasses.dataclass class UnspecifiedPathInfo: @@ -28,15 +31,16 @@ class ExecutionInfo: envs: dict[str, str] = dataclasses.field(default_factory=dict) unspecified_paths: dict[int, UnspecifiedPathInfo] = dataclasses.field(default_factory=dict) specified_outputs: list[SpecifiedOutputInfo] = dataclasses.field(default_factory=list) + enable_debug_dump: bool = False - def add_output_specified(self, field_name : Translatable | str, path : str): + def add_output_specified(self, field_name : Translatable | str, path : str, auxiliary : bool = False): argindex = len(self.args) - self.specified_outputs.append(SpecifiedOutputInfo(field_name, argindex)) + self.specified_outputs.append(SpecifiedOutputInfo(field_name, argindex, auxiliary)) self.args.append(path) - def add_output_unspecified(self, field_name : Translatable | str, default_name: Translatable | str, is_dir : bool = False): + def add_output_unspecified(self, field_name : Translatable | str, default_name: Translatable | str, is_dir : bool = False, auxiliary : bool = False): argindex = len(self.args) - self.specified_outputs.append(SpecifiedOutputInfo(field_name, argindex)) + self.specified_outputs.append(SpecifiedOutputInfo(field_name, argindex, auxiliary)) self.unspecified_paths[argindex] = UnspecifiedPathInfo(default_name, is_dir) self.args.append('') @@ -46,6 +50,11 @@ def add_output_unspecified(self, field_name : Translatable | str, default_name: "md": "--md", "txt": "--txt", } + _tr_debug_dump = TR_gui_execution.tr("output_debug_dump", + en="Debug dumps", + zh_cn="调试输出", + zh_hk="調試輸出", + ) @staticmethod def init_common(): @@ -71,6 +80,12 @@ def init_main_pipeline(inputs : list[str]): result.args.append("--searchpath") result.args.extend(searchpaths) + # 设置 IR 保存路径,如果需要的话 + result.enable_debug_dump = SettingsDict.instance().get("mainpipeline/debug", False) + if result.enable_debug_dump: + result.args.append("--debugdump-dir") + result.add_output_unspecified(ExecutionInfo._tr_debug_dump, "debug_dump", is_dir=True, auxiliary=True) + # 给输入文件选择合适的读取选项 last_input_flag = '' for i in inputs: @@ -83,16 +98,25 @@ def init_main_pipeline(inputs : list[str]): last_input_flag = flag result.args.append(i) + result.add_debug_dump() # 添加前端命令 result.args.extend([ "--cmdsyntax", "--vnparse", + ]) + result.add_debug_dump() + result.args.extend([ "--vncodegen", "--vn-blocksorting", "--vn-entryinference", ]) + result.add_debug_dump() return result + def add_debug_dump(self): + if self.enable_debug_dump: + self.args.append("--debugdump") + class ExecutionState(enum.Enum): INIT = 0 FAILED_TEMPDIR_CREATION = enum.auto() diff --git a/src/preppipe_gui_pyside6/forms/settingwidget.ui b/src/preppipe_gui_pyside6/forms/settingwidget.ui index b6cb983..7fd3271 100644 --- a/src/preppipe_gui_pyside6/forms/settingwidget.ui +++ b/src/preppipe_gui_pyside6/forms/settingwidget.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 576 + 435 @@ -46,6 +46,48 @@ + + + + Main Pipeline + + + + + + Generate Debug Outputs + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 10 + + + + Qt::Orientation::Horizontal + + + diff --git a/src/preppipe_gui_pyside6/toolwidgets/execute.py b/src/preppipe_gui_pyside6/toolwidgets/execute.py index c6e1269..5f1f6d7 100644 --- a/src/preppipe_gui_pyside6/toolwidgets/execute.py +++ b/src/preppipe_gui_pyside6/toolwidgets/execute.py @@ -103,7 +103,9 @@ def setData(self, execinfo : ExecutionInfo): self.appendPlainText('='*20 + '\n') self.exec.outputAvailable.connect(self.handleOutput) - for out in execinfo.specified_outputs: + auxiliary_outputs = [out for out in execinfo.specified_outputs if out.auxiliary] + main_outputs = [out for out in execinfo.specified_outputs if not out.auxiliary] + for out in main_outputs + auxiliary_outputs: value = self.exec.composed_args[out.argindex] w = OutputEntryWidget(self)#, out.field_name, value) w.setData(out.field_name, value) diff --git a/src/preppipe_gui_pyside6/toolwidgets/maininput.py b/src/preppipe_gui_pyside6/toolwidgets/maininput.py index d0bef79..83cf02b 100644 --- a/src/preppipe_gui_pyside6/toolwidgets/maininput.py +++ b/src/preppipe_gui_pyside6/toolwidgets/maininput.py @@ -121,6 +121,7 @@ def request_export_renpy(self): return info = ExecutionInfo.init_main_pipeline(filelist) info.args.append("--renpy-codegen") + info.add_debug_dump() info.args.append("--renpy-export") info.add_output_unspecified(self._tr_export_path, "game", is_dir=True) MainWindowInterface.getHandle(self).requestExecution(info) @@ -133,6 +134,7 @@ def request_export_webgal(self): return info = ExecutionInfo.init_main_pipeline(filelist) info.args.append("--webgal-codegen") + info.add_debug_dump() info.args.append("--webgal-export") info.add_output_unspecified(self._tr_export_path, "game", is_dir=True) MainWindowInterface.getHandle(self).requestExecution(info) diff --git a/src/preppipe_gui_pyside6/toolwidgets/setting.py b/src/preppipe_gui_pyside6/toolwidgets/setting.py index 38f8ca0..504bab7 100644 --- a/src/preppipe_gui_pyside6/toolwidgets/setting.py +++ b/src/preppipe_gui_pyside6/toolwidgets/setting.py @@ -21,15 +21,25 @@ class SettingWidget(QWidget, ToolWidgetInterface): zh_cn="语言", zh_hk="語言", ) + _tr_general_debug = TR_gui_setting.tr("general_debug", + en="Generate Debug Outputs", + zh_cn="生成调试输出", + zh_hk="生成調試輸出", + ) + _tr_general_debug_desc = TR_gui_setting.tr("general_debug_desc", + en="Enable debug mode to dump internal information (IRs, etc) to files. This makes execution slower.", + zh_cn="启用调试模式以将内部信息(IR等)保存到文件中。执行过程会变慢。", + zh_hk="啟用調試模式以將內部信息(IR等)保存到文件中。執行過程會變慢。", + ) _langs_dict = { "en": "English", "zh_cn": "中文(简体)", "zh_hk": "中文(繁體)", } _tr_desc = TR_gui_setting.tr("desc", - en="Edit settings here. Currently only language is supported.", - zh_cn="在这里编辑设置。目前仅支持语言设置。", - zh_hk="在這裡編輯設置。目前僅支持語言設置。", + en="Edit settings here. Currently only language and debug settings are supported.", + zh_cn="在这里编辑设置。目前仅支持语言与调试设置。", + zh_hk="在這裡編輯設置。目前僅支持語言與調試設置。", ) def __init__(self, parent : QWidget): @@ -38,16 +48,24 @@ def __init__(self, parent : QWidget): self.ui.setupUi(self) self.bind_text(lambda s : self.ui.tabWidget.setTabText(0, s), self._tr_tab_general) self.bind_text(self.ui.languageLabel.setText, self._tr_general_language) + self.bind_text(self.ui.mainPipelineGroupBox.setTitle, MainWindowInterface.tr_toolname_maininput) + self.bind_text(self.ui.debugModeCheckBox.setText, self._tr_general_debug) + self.bind_text(self.ui.debugModeCheckBox.setToolTip, self._tr_general_debug_desc) self.ui.languageComboBox.clear() for lang_code, lang_name in SettingsDict._langs_dict.items(): self.ui.languageComboBox.addItem(lang_name, lang_code) self.ui.languageComboBox.setCurrentIndex(self.ui.languageComboBox.findData(SettingsDict.get_current_language())) self.ui.languageComboBox.currentIndexChanged.connect(self.on_languageComboBox_currentIndexChanged) + self.ui.debugModeCheckBox.setChecked(True if SettingsDict.instance().get("mainpipeline/debug", False) else False) + self.ui.debugModeCheckBox.toggled.connect(self.on_debugModeCheckBox_toggled) def on_languageComboBox_currentIndexChanged(self, index): lang_code = self.ui.languageComboBox.currentData() self.language_updated(lang_code) + def on_debugModeCheckBox_toggled(self, checked): + SettingsDict.instance()["mainpipeline/debug"] = True if checked else False + @classmethod def getToolInfo(cls, **kwargs) -> ToolWidgetInfo: return ToolWidgetInfo( @@ -62,13 +80,6 @@ def initialize(): if lang := SettingsDict.instance().get("language"): SettingWidget.setLanguage(lang) - def get_initial_value(self, key : str): - match key: - case "language": - return SettingsDict.get_current_language() - case _: - raise RuntimeError("Unexpected key") - def language_updated(self, lang): if lang == SettingsDict.get_current_language(): return