UndertaleModTool
UndertaleModTool copied to clipboard
Decompilation bug with switch statements with a single block with default and additional cases
It's better to see an example. The initial one I discovered with the problem has this form:
switch (abc)
{
case 123:
default:
foo = "bar";
break;
}
This throws an assertion error, and decompiles it slightly incorrectly as (note: it's not just optimized or anything like that, it just fails entirely):
switch abc
{
}
foo = "bar"
Now, the reason why this happens is because it fails the initial while loop which compiles the list of cases. It realizes that the next true block is the meeting point, so it doesn't detect anything. But it technically should? I mean, technically the code functions the same, so if we got rid of the assertion check then it would lazily solve the issue, but I feel like we can do better than that.
Also keep in mind it needs to be able to decompile:
switch (abc)
{
case 123:
case 456:
default:
if (c)
exit;
foo = "bar";
if (a)
exit;
break;
}
Some more information. The assertion error simply occurs due to the lack of cases, and here's the graph for that first example:
The bad news is, the default case is placed outside the decompiler and there isn't away to distinguish the case from the code that goes after the switch case.
But, the good news is, I'll have a commit pushed shortly that cleans them up as best I could and makes them function properly.
It has been cleaned up I think as far as we can clean it. https://github.com/Kneesnap/UndertaleModTool/compare/c73134618e80...7dd6271f2cfe
Examples of Decompiled Scripts:
var abc, c, a, foo;
abc = 0
c = 0
a = 0
foo = "baaa"
switch abc
{
case 123:
case 456:
default:
break
}
if c
exit
foo = "bar"
if a
exit
show_debug_message("this a test!")
var abc, c, a, foo;
abc = 0
c = 0
a = 0
foo = "baaa"
switch abc
{
case 69:
foo = "goo-goo"
case 1337:
foo = "gaa-gaa"
break
case 42:
foo = "poo-poo"
case 3203381950:
foo = "pee-pee"
case 3735928559:
foo = "hee-hee"
case 195948557:
foo = "barf"
break
case 123:
case 456:
default:
if c
exit
foo = "bar"
if a
exit
break
}
show_debug_message("this a test!")
NOTE: This issue should not be closed yet. The bytecode does not match, and we should still be able to make the resulting output perfect.
Both of these issues do not impact code functionality but do make the bytecode differ, which is still something we want to avoid.
- Cases which do not break include the following code until a break.
- default statements do not include their in certain circumstances, rather it is put after the switch statement. This only happens in cases where it does not break functionality.
These should be fixed, to keep the bytecode integrity of decompiled scripts, and well, for accuracy.
@colinator27 Alright, I spent quite a bit of time on this, it's not quite perfect, but #1 is fixed. The problem with fixing #2 is that I can't seem to access the block which contains the break that says where the switch statement ends.
I am currently working on this, I should have something that works tomorrow.
Fixed in https://github.com/krzys-h/UndertaleModTool/commit/22b1f4e987497daf1644346bb9d623280b8859c2
Still need to make continues work in switches.
Test Cases:
switch argument0
{
case 0:
a = 1
case 1:
return 101;
}
return 102;
switch argument0
{
case 0:
a = 1
case 1:
return 101;
case 2:
return 102;
}
switch argument0
{
case 0:
a = 1
case 1:
return 101;
default:
return 102;
}
while (condition)
{
switch (a)
{
case 123:
test = 1;
continue;
}
}
while (condition)
{
switch (a)
{
case 123:
test = 1;
continue;
default:
test = 2;
return 5;
}
}
while (condition)
{
switch (a)
{
case 123:
test = 1;
return 5;
default:
test = 2;
continue;
}
}
switch (a)
{
case 1:
a = "b"
break
case 2:
c = "d"
break
}
More Tests:
//TODO: Get other tests.
// Basic Testing:
switch abc
{
case 123:
case 456:
default:
if c
return;
foo = "bar"
if a
return;
break
}
switch argument0
{
case 0:
a = 1
case 1:
return 101;
}
return 102;
switch argument0
{
case 0:
a = 1
case 1:
return 101;
case 2:
return 102;
}
switch argument0
{
case 0:
a = 1
case 1:
return 101;
default:
return 102;
}
// Continue Testing
while (condition)
{
switch (a)
{
case 123:
test = 1;
continue;
}
}
while condition
{
switch a
{
case 123:
test = 1
continue
case 456:
test = 2
break
default:
test = 3
return 5;
}
test = -1
}
while (condition)
{
switch (a)
{
case 123:
test = 1;
return 5;
case 456:
test = 2;
break;
default:
test = 3;
continue;
}
test = -1
}
// Break Testing:
switch (a)
{
case 1:
a = "b"
break
case 2:
c = "d"
break
}
// 5 - no break -> break
switch (a) {
case 0:
case 1:
b = "Fallthrough skip.";
break;
case 2:
b = "This is a duplicate.";
break;
case 3:
b = "This is a duplicate.";
break;
case 4:
b = "This is a fallthrough (1).";
case 5:
b = "This is a fallthrough (2).";
}
switch (a) {
case 0:
case 1:
b = "Fallthrough skip.";
break;
case 2:
b = "This is a fallthrough testing break (1).";
case 3:
b = "This is a fallthrough testing break (2).";
break;
}
switch (a) {
case 0:
case 1:
b = "Fallthrough skip.";
break;
case 2:
b = "This is a fallthrough testing break (1).";
case 3:
b = "This is a fallthrough testing break (2).";
default:
b = "Default test.";
break;
}
switch (a) {
case 0:
case 1:
b = "Fallthrough skip.";
break;
case 2:
b = "This is a fallthrough testing break (1).";
case 3:
b = "This is a fallthrough testing break (2).";
default:
b = "Default test.";
}
switch (a) {
case 0:
case 1:
b = "Fallthrough skip.";
break;
case 2:
b = "This is a fallthrough testing break (1).";
case 3:
b = "This is a fallthrough testing break (2).";
break;
default:
b = "Default test.";
}
switch (a) {
case 0:
case 1:
b = "Fallthrough skip.";
break;
case 2:
b = "This is a fallthrough testing break (1).";
case 3:
b = "This is a fallthrough testing break (2).";
break;
default:
b = "Default test.";
break;
}
// Empty break tests.
switch (a) {
case 0:
a = "test";
default:
break;
}
switch (a) {
case 0:
a = "test";
default:
}
switch (a) {
case 0:
a = "test";
break;
default:
break;
}
switch (a) {
case 0:
a = "test";
break;
default:
}
switch (a) {
case 0:
a = "test";
}
switch (a) {
case 0:
a = "test";
break;
}
Another test case which is currently broken:
switch (a)
{
default:
a = 1
break
}
Right now it decompiles as
var _temp_local_var_1;
_temp_local_var_1 = a
a = 1
Known remaining issues:
Continuing in switch statements.
Bad output decompiling:
switch (a)
{
default:
a = 1
break
}
Fixed Issues:
The decompiler cannot distinguish between:
default: break
default:
It likes to include the default-case outside the switch statement, as well as inside it, sometimes.
Failing Case:
switch abc
{
case 123:
break
case 456:
default:
if c
return;
foo = "bar"
if a
return;
// break - If this is included, it decompiles just fine.
}
Failing case (may or may not fall through depending on b):
a = 1
b = false
switch (a) {
case 1:
c = 1
break;
case 2:
if (b)
break;
case 3:
c = 2;
break;
}