Skip to content

Commit a49f6f0

Browse files
authored
Expose IR dump to users in GUI; fix duplicated return commands causing assertion failures (#1)
* [Core,GUI] Expose debug dump option to UI * [Core] Fix in VNCodeGen where duplicated VNReturn commands causes runtime aborts during later lowering
1 parent d650ffb commit a49f6f0

File tree

8 files changed

+155
-30
lines changed

8 files changed

+155
-30
lines changed

src/preppipe/frontend/vnmodel/vncodegen.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -886,14 +886,15 @@ def visit_default_handler(self, node : VNASTNodeBase):
886886
zh_hk="以下命令結點在一個已經結束的基本塊中,內容不會被處理: {node}。請將內容移至可能結束該基本塊的指令前(比如跳轉、選項等)。",
887887
)
888888

889-
def check_blocklocal_cond(self, node : VNASTNodeBase) -> bool:
889+
def check_blocklocal_cond(self, node : VNASTNodeBase, suppress_warning : bool = False) -> bool:
890890
# 检查该指令是否在正常的、未结束的块中
891891
# 是的话返回 False, 不在正常情况下时生成错误并返回 True
892892
if self.cur_terminator is None:
893893
return False
894-
msg = self._tr_unhandled_node_in_terminated_block.format(node=node.get_short_str(0))
895-
err = ErrorOp.create(error_code='vncodegen-unhandled-node-in-terminated-block', context=self.context, error_msg=StringLiteral.get(msg, self.context), loc=node.location)
896-
err.insert_before(self.cur_terminator)
894+
if not suppress_warning:
895+
msg = self._tr_unhandled_node_in_terminated_block.format(node=node.get_short_str(0))
896+
err = ErrorOp.create(error_code='vncodegen-unhandled-node-in-terminated-block', context=self.context, error_msg=StringLiteral.get(msg, self.context), loc=node.location)
897+
err.insert_before(self.cur_terminator)
897898
return True
898899

899900
def check_block_or_function_local_cond(self, node : VNASTNodeBase) -> bool:
@@ -1918,7 +1919,9 @@ def visitVNASTBreakNode(self, node : VNASTBreakNode) -> VNTerminatorInstBase | N
19181919
return None
19191920

19201921
def visitVNASTReturnNode(self, node : VNASTReturnNode) -> VNTerminatorInstBase | None:
1921-
if self.check_block_or_function_local_cond(node):
1922+
# 章节结束命令只能在函数内使用
1923+
# 我们允许额外的章节结束命令(可能在转至章节命令后面)且不提供额外警告
1924+
if self.check_blocklocal_cond(node, suppress_warning=True):
19221925
return None
19231926
ret = VNReturnInst.create(context=self.context, start_time=self.starttime, name=node.name, loc=node.location)
19241927
self.destblock.push_back(ret)

src/preppipe/irbase.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,12 @@ def dump(self) -> None:
18121812
dump = writer.write_op(self)
18131813
print(dump.decode('utf-8'))
18141814

1815+
def dump_html(self, index : int = 0, parentdir : str = '') -> None:
1816+
# for debugging
1817+
writer = IRWriter(self.context, True, None, None)
1818+
dump = writer.write_op(self)
1819+
_save_content_html_helper(dump, self.name, type(self).__name__, index, parentdir)
1820+
18151821
@IRObjectJsonTypeName("symbol_op")
18161822
class Symbol(Operation):
18171823

@@ -4094,19 +4100,33 @@ def write_block(self, b : Block) -> bytes:
40944100
self._walk_block(b, 0)
40954101
return self._output_body.getvalue()
40964102

4097-
def _view_content_helper(dump : bytes, name : str, typename : str):
4098-
name_portion = 'anon'
4103+
def _get_sanitized_name_for_dump(name : str) -> str | None:
40994104
if len(name) > 0:
41004105
sanitized_name = get_sanitized_filename(name)
41014106
if len(sanitized_name) > 0:
4102-
name_portion = sanitized_name
4107+
return sanitized_name
4108+
return None
4109+
4110+
def _view_content_helper(dump : bytes, name : str, typename : str):
4111+
name_portion = _get_sanitized_name_for_dump(name)
4112+
if name_portion is None:
4113+
name_portion = 'anon'
41034114
file = tempfile.NamedTemporaryFile('w+b', suffix='_viewdump.html', prefix='preppipe_' + typename + '_' + name_portion + '_', delete=False)
41044115
file.write(dump)
41054116
file.close()
41064117
path = os.path.abspath(file.name)
41074118
print('Opening HTML dump at ' + path)
41084119
webbrowser.open_new_tab('file:///' + path)
41094120

4121+
def _save_content_html_helper(dump : bytes, name : str, typename : str, index : int = 0, parentdir : str = ''):
4122+
name_portion = _get_sanitized_name_for_dump(name)
4123+
if name_portion is None:
4124+
name_portion = f"anon_{index}"
4125+
filename = f"{name_portion}_{typename}.html"
4126+
path = os.path.join(parentdir, filename) if len(parentdir) > 0 else filename
4127+
with open(path, 'wb') as f:
4128+
f.write(dump)
4129+
41104130
# ------------------------------------------------------------------------------
41114131
# IR verification
41124132
# ------------------------------------------------------------------------------

src/preppipe/pipeline.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,32 @@ class _SaveIR(TransformBase):
517517
def run(self) -> None:
518518
raise PPNotImplementedError
519519

520-
@BackendDecl('dump', input_decl=Operation, output_decl=IODecl('<No output>', nargs=0))
521-
class _DumpIR(TransformBase):
520+
@TransformArgumentGroup('debugdump', "Options for Debug Dump")
521+
@BackendDecl('debugdump', input_decl=Operation, output_decl=IODecl('<IR files>', nargs=0))
522+
class _DebugDump(TransformBase):
523+
dumpdir : typing.ClassVar[str] = "dumps"
524+
525+
@staticmethod
526+
def install_arguments(argument_group : argparse._ArgumentGroup):
527+
argument_group.add_argument("--debugdump-dir", required=False, type=str, nargs=1, default=_DebugDump.dumpdir, help="Directory to save the dumps")
528+
529+
@staticmethod
530+
def handle_arguments(args : argparse.Namespace):
531+
if dumpdir := args.debugdump_dir:
532+
assert isinstance(dumpdir, list) and len(dumpdir) == 1
533+
_DebugDump.dumpdir = dumpdir[0]
534+
assert isinstance(_DebugDump.dumpdir, str)
535+
if not os.path.exists(_DebugDump.dumpdir):
536+
os.makedirs(_DebugDump.dumpdir, exist_ok=True)
537+
elif not os.path.isdir(_DebugDump.dumpdir):
538+
raise PPInternalError("Debug dump directory path exists but is not a directory")
539+
522540
def run(self) -> None:
523-
for op in self.inputs:
524-
op.dump()
541+
for index, op in enumerate(self.inputs):
542+
if isinstance(op, Operation):
543+
op.dump_html(index, _DebugDump.dumpdir)
544+
else:
545+
raise PPInternalError("Debug dump input is not an operation")
525546

526547
@BackendDecl('view', input_decl=Operation, output_decl=IODecl('<No output>', nargs=0))
527548
class _ViewIR(TransformBase):

src/preppipe_gui_pyside6/execution.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
from preppipe.language import *
1111
from .settingsdict import *
1212

13+
TR_gui_execution = TranslationDomain("gui_execution")
14+
1315
@dataclasses.dataclass
1416
class SpecifiedOutputInfo:
15-
# 有指定输出路径的输出项
17+
# 有指定输出路径的输出项,将在输出列表中出现。目前也包含未指定输出路径的项(因为最终它们也会有路径)
1618
field_name: Translatable | str
1719
argindex: int = -1
20+
auxiliary: bool = False # 是否为辅助输出(如调试输出等),是的话我们会把它们放在其他输出之后
1821

1922
@dataclasses.dataclass
2023
class UnspecifiedPathInfo:
@@ -28,15 +31,16 @@ class ExecutionInfo:
2831
envs: dict[str, str] = dataclasses.field(default_factory=dict)
2932
unspecified_paths: dict[int, UnspecifiedPathInfo] = dataclasses.field(default_factory=dict)
3033
specified_outputs: list[SpecifiedOutputInfo] = dataclasses.field(default_factory=list)
34+
enable_debug_dump: bool = False
3135

32-
def add_output_specified(self, field_name : Translatable | str, path : str):
36+
def add_output_specified(self, field_name : Translatable | str, path : str, auxiliary : bool = False):
3337
argindex = len(self.args)
34-
self.specified_outputs.append(SpecifiedOutputInfo(field_name, argindex))
38+
self.specified_outputs.append(SpecifiedOutputInfo(field_name, argindex, auxiliary))
3539
self.args.append(path)
3640

37-
def add_output_unspecified(self, field_name : Translatable | str, default_name: Translatable | str, is_dir : bool = False):
41+
def add_output_unspecified(self, field_name : Translatable | str, default_name: Translatable | str, is_dir : bool = False, auxiliary : bool = False):
3842
argindex = len(self.args)
39-
self.specified_outputs.append(SpecifiedOutputInfo(field_name, argindex))
43+
self.specified_outputs.append(SpecifiedOutputInfo(field_name, argindex, auxiliary))
4044
self.unspecified_paths[argindex] = UnspecifiedPathInfo(default_name, is_dir)
4145
self.args.append('')
4246

@@ -46,6 +50,11 @@ def add_output_unspecified(self, field_name : Translatable | str, default_name:
4650
"md": "--md",
4751
"txt": "--txt",
4852
}
53+
_tr_debug_dump = TR_gui_execution.tr("output_debug_dump",
54+
en="Debug dumps",
55+
zh_cn="调试输出",
56+
zh_hk="調試輸出",
57+
)
4958

5059
@staticmethod
5160
def init_common():
@@ -71,6 +80,12 @@ def init_main_pipeline(inputs : list[str]):
7180
result.args.append("--searchpath")
7281
result.args.extend(searchpaths)
7382

83+
# 设置 IR 保存路径,如果需要的话
84+
result.enable_debug_dump = SettingsDict.instance().get("mainpipeline/debug", False)
85+
if result.enable_debug_dump:
86+
result.args.append("--debugdump-dir")
87+
result.add_output_unspecified(ExecutionInfo._tr_debug_dump, "debug_dump", is_dir=True, auxiliary=True)
88+
7489
# 给输入文件选择合适的读取选项
7590
last_input_flag = ''
7691
for i in inputs:
@@ -83,16 +98,25 @@ def init_main_pipeline(inputs : list[str]):
8398
last_input_flag = flag
8499
result.args.append(i)
85100

101+
result.add_debug_dump()
86102
# 添加前端命令
87103
result.args.extend([
88104
"--cmdsyntax",
89105
"--vnparse",
106+
])
107+
result.add_debug_dump()
108+
result.args.extend([
90109
"--vncodegen",
91110
"--vn-blocksorting",
92111
"--vn-entryinference",
93112
])
113+
result.add_debug_dump()
94114
return result
95115

116+
def add_debug_dump(self):
117+
if self.enable_debug_dump:
118+
self.args.append("--debugdump")
119+
96120
class ExecutionState(enum.Enum):
97121
INIT = 0
98122
FAILED_TEMPDIR_CREATION = enum.auto()

src/preppipe_gui_pyside6/forms/settingwidget.ui

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>400</width>
10-
<height>300</height>
9+
<width>576</width>
10+
<height>435</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
@@ -46,6 +46,48 @@
4646
<item row="0" column="1">
4747
<widget class="QComboBox" name="languageComboBox"/>
4848
</item>
49+
<item row="2" column="0" colspan="2">
50+
<widget class="QGroupBox" name="mainPipelineGroupBox">
51+
<property name="title">
52+
<string>Main Pipeline</string>
53+
</property>
54+
<layout class="QFormLayout" name="formLayout_2">
55+
<item row="0" column="0" colspan="2">
56+
<widget class="QCheckBox" name="debugModeCheckBox">
57+
<property name="text">
58+
<string>Generate Debug Outputs</string>
59+
</property>
60+
</widget>
61+
</item>
62+
</layout>
63+
</widget>
64+
</item>
65+
<item row="3" column="0">
66+
<spacer name="verticalSpacer">
67+
<property name="orientation">
68+
<enum>Qt::Orientation::Vertical</enum>
69+
</property>
70+
<property name="sizeHint" stdset="0">
71+
<size>
72+
<width>20</width>
73+
<height>40</height>
74+
</size>
75+
</property>
76+
</spacer>
77+
</item>
78+
<item row="1" column="0" colspan="2">
79+
<widget class="Line" name="line">
80+
<property name="minimumSize">
81+
<size>
82+
<width>0</width>
83+
<height>10</height>
84+
</size>
85+
</property>
86+
<property name="orientation">
87+
<enum>Qt::Orientation::Horizontal</enum>
88+
</property>
89+
</widget>
90+
</item>
4991
</layout>
5092
</widget>
5193
</widget>

src/preppipe_gui_pyside6/toolwidgets/execute.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ def setData(self, execinfo : ExecutionInfo):
103103
self.appendPlainText('='*20 + '\n')
104104
self.exec.outputAvailable.connect(self.handleOutput)
105105

106-
for out in execinfo.specified_outputs:
106+
auxiliary_outputs = [out for out in execinfo.specified_outputs if out.auxiliary]
107+
main_outputs = [out for out in execinfo.specified_outputs if not out.auxiliary]
108+
for out in main_outputs + auxiliary_outputs:
107109
value = self.exec.composed_args[out.argindex]
108110
w = OutputEntryWidget(self)#, out.field_name, value)
109111
w.setData(out.field_name, value)

src/preppipe_gui_pyside6/toolwidgets/maininput.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def request_export_renpy(self):
121121
return
122122
info = ExecutionInfo.init_main_pipeline(filelist)
123123
info.args.append("--renpy-codegen")
124+
info.add_debug_dump()
124125
info.args.append("--renpy-export")
125126
info.add_output_unspecified(self._tr_export_path, "game", is_dir=True)
126127
MainWindowInterface.getHandle(self).requestExecution(info)
@@ -133,6 +134,7 @@ def request_export_webgal(self):
133134
return
134135
info = ExecutionInfo.init_main_pipeline(filelist)
135136
info.args.append("--webgal-codegen")
137+
info.add_debug_dump()
136138
info.args.append("--webgal-export")
137139
info.add_output_unspecified(self._tr_export_path, "game", is_dir=True)
138140
MainWindowInterface.getHandle(self).requestExecution(info)

src/preppipe_gui_pyside6/toolwidgets/setting.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,25 @@ class SettingWidget(QWidget, ToolWidgetInterface):
2121
zh_cn="语言",
2222
zh_hk="語言",
2323
)
24+
_tr_general_debug = TR_gui_setting.tr("general_debug",
25+
en="Generate Debug Outputs",
26+
zh_cn="生成调试输出",
27+
zh_hk="生成調試輸出",
28+
)
29+
_tr_general_debug_desc = TR_gui_setting.tr("general_debug_desc",
30+
en="Enable debug mode to dump internal information (IRs, etc) to files. This makes execution slower.",
31+
zh_cn="启用调试模式以将内部信息(IR等)保存到文件中。执行过程会变慢。",
32+
zh_hk="啟用調試模式以將內部信息(IR等)保存到文件中。執行過程會變慢。",
33+
)
2434
_langs_dict = {
2535
"en": "English",
2636
"zh_cn": "中文(简体)",
2737
"zh_hk": "中文(繁體)",
2838
}
2939
_tr_desc = TR_gui_setting.tr("desc",
30-
en="Edit settings here. Currently only language is supported.",
31-
zh_cn="在这里编辑设置。目前仅支持语言设置。",
32-
zh_hk="在這裡編輯設置。目前僅支持語言設置。",
40+
en="Edit settings here. Currently only language and debug settings are supported.",
41+
zh_cn="在这里编辑设置。目前仅支持语言与调试设置。",
42+
zh_hk="在這裡編輯設置。目前僅支持語言與調試設置。",
3343
)
3444

3545
def __init__(self, parent : QWidget):
@@ -38,16 +48,24 @@ def __init__(self, parent : QWidget):
3848
self.ui.setupUi(self)
3949
self.bind_text(lambda s : self.ui.tabWidget.setTabText(0, s), self._tr_tab_general)
4050
self.bind_text(self.ui.languageLabel.setText, self._tr_general_language)
51+
self.bind_text(self.ui.mainPipelineGroupBox.setTitle, MainWindowInterface.tr_toolname_maininput)
52+
self.bind_text(self.ui.debugModeCheckBox.setText, self._tr_general_debug)
53+
self.bind_text(self.ui.debugModeCheckBox.setToolTip, self._tr_general_debug_desc)
4154
self.ui.languageComboBox.clear()
4255
for lang_code, lang_name in SettingsDict._langs_dict.items():
4356
self.ui.languageComboBox.addItem(lang_name, lang_code)
4457
self.ui.languageComboBox.setCurrentIndex(self.ui.languageComboBox.findData(SettingsDict.get_current_language()))
4558
self.ui.languageComboBox.currentIndexChanged.connect(self.on_languageComboBox_currentIndexChanged)
59+
self.ui.debugModeCheckBox.setChecked(True if SettingsDict.instance().get("mainpipeline/debug", False) else False)
60+
self.ui.debugModeCheckBox.toggled.connect(self.on_debugModeCheckBox_toggled)
4661

4762
def on_languageComboBox_currentIndexChanged(self, index):
4863
lang_code = self.ui.languageComboBox.currentData()
4964
self.language_updated(lang_code)
5065

66+
def on_debugModeCheckBox_toggled(self, checked):
67+
SettingsDict.instance()["mainpipeline/debug"] = True if checked else False
68+
5169
@classmethod
5270
def getToolInfo(cls, **kwargs) -> ToolWidgetInfo:
5371
return ToolWidgetInfo(
@@ -62,13 +80,6 @@ def initialize():
6280
if lang := SettingsDict.instance().get("language"):
6381
SettingWidget.setLanguage(lang)
6482

65-
def get_initial_value(self, key : str):
66-
match key:
67-
case "language":
68-
return SettingsDict.get_current_language()
69-
case _:
70-
raise RuntimeError("Unexpected key")
71-
7283
def language_updated(self, lang):
7384
if lang == SettingsDict.get_current_language():
7485
return

0 commit comments

Comments
 (0)