UndertaleModTool icon indicating copy to clipboard operation
UndertaleModTool copied to clipboard

Decompilation bug with switch statements with a single block with default and additional cases

Open colinator27 opened this issue 5 years ago • 16 comments

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;
}

colinator27 avatar May 13 '19 22:05 colinator27

Some more information. The assertion error simply occurs due to the lack of cases, and here's the graph for that first example: graph

colinator27 avatar May 13 '19 22:05 colinator27

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.

Kneesnap avatar May 20 '19 07:05 Kneesnap

It has been cleaned up I think as far as we can clean it. https://github.com/Kneesnap/UndertaleModTool/compare/c73134618e80...7dd6271f2cfe

Kneesnap avatar May 20 '19 07:05 Kneesnap

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!")

Kneesnap avatar May 20 '19 08:05 Kneesnap

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.

  1. Cases which do not break include the following code until a break.
  2. 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.

Kneesnap avatar May 27 '19 21:05 Kneesnap

@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.

Kneesnap avatar May 28 '19 05:05 Kneesnap

I am currently working on this, I should have something that works tomorrow.

Kneesnap avatar Jun 05 '19 10:06 Kneesnap

Fixed in https://github.com/krzys-h/UndertaleModTool/commit/22b1f4e987497daf1644346bb9d623280b8859c2

Kneesnap avatar Jun 07 '19 00:06 Kneesnap

Still need to make continues work in switches.

Kneesnap avatar Jun 07 '19 01:06 Kneesnap

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
}

Kneesnap avatar Jun 07 '19 01:06 Kneesnap

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;
}

Kneesnap avatar Jun 08 '19 02:06 Kneesnap

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

colinator27 avatar Jun 10 '19 21:06 colinator27

Known remaining issues:

Continuing in switch statements.

Bad output decompiling:

switch (a)
{
    default:
         a = 1
         break
}

Kneesnap avatar Jun 16 '19 03:06 Kneesnap

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.

Kneesnap avatar Jun 18 '19 21:06 Kneesnap

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.
}

Kneesnap avatar Jun 18 '19 21:06 Kneesnap

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;
}

Jacky720 avatar Mar 05 '24 18:03 Jacky720