Skip to content

add support for continue statement#112

Merged
lolleko merged 6 commits intoTypeScriptToLua:masterfrom
InfiniteRain:master
Jun 7, 2018
Merged

add support for continue statement#112
lolleko merged 6 commits intoTypeScriptToLua:masterfrom
InfiniteRain:master

Conversation

@InfiniteRain
Copy link
Contributor

This implements the support for TS's continue statement. Normally, tstl would throw an exception whenever the continue statement is used, saying that Lua does not support the statement.

However, while Lua indeed does not support the continue statement, it is still perfectly possible to implement the functionality of a continue statement by changing how loops get transpiled.

Here's a simple example:

The following TS

for (let i = 1; i <= 100; i++) {
  if (i < 25) {
    continue;
  }
  
  print(i);
}

Gets transpiled to

local i = 1
while(i<=100) do
    local ____continue = false
    repeat
        if i<25 then
            ____continue = true
            break
        end
        print(i)
        ____continue = true
    until true
    i=i+1
    if not ____continue then break end
end

As can be seen from the example above, the functionality of a continue statement gets achieved via a clever usage of the repeat until true block, which acts a simple do end block, but allows for a possibility of stopping the execution of the block via a break statement. The ____continue local allows for differentiation between a break statement for the repeat until true and the parent loop. If a break statement was used without setting the ____continue local to true, then it will have an effect of a normal break (break from repeat until true and break from the parent loop). However, if a break statement was used after setting the ____continue local to true, then it will act with accordance to how the effect of the continue statement should be, which is: stop executing the current cycle, and jump to the next one (break repeat until true, but do not break the parent loop). The ____continue = true gets automatically added at the end of the body of the parent loop.

So, the continue statement simply gets transpiled to:

____continue = true
break

With this implementation, you can nest multiple loops inside of each other, and the ____continue locals of the nested loops will not interfere with each other, thanks to how scoping works in Lua.

The following loops were used for testing:

declare function print(...messages: any[]): void;

for (let i = 1; i <= 100; i++) {
  if (i < 25) {
    continue;
  }
  print(i);

  for (let x = 1; x <= 10; x++) {
    if (x < 5) {
      continue;
    }
    print(x)
  }
}

let d = 10
while (d > 0) {
  d--;
  if (d > 5) {
    let o = 4;
    while (o > 0) {
      o--;
      if (o > 2) {
        continue;
      }
      print(o);
    }
    continue;
  }
  if (d == 2) {
    continue;
  }
  print(d);
}

let e = 10
do {
  e--;
  if (e > 5) {
    let o = 4;
    do {
      o--;
      if (o > 2) {
        continue;
      }
      print(o);
    } while (o > 0)
    continue;
  }
  if (e == 2) {
    continue;
  }
  print(e);
} while (e > 0)

for (let i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
  if (i < 3) {
    continue;
  }

  for (let i of [5, 6, 7, 8]) {
    if (i !== 6) {
      continue
    }
    print(i)
  }

  if (i > 8) {
    continue;
  }

  print(i);
}

for (let i in {
  a: 1,
  b: 2,
  c: 3,
  d: 4
}) {
 if (i == 'a') {
   continue;
 }

 if (i == 'b') {
   continue;
 }

 print(i);
}

The transpiled code:

-- Generated by TypescriptToLua v0.2.1
-- https://github.com/Perryvw/TypescriptToLua
require("typescript_lualib")
local i = 1
while(i<=100) do
    local ____continue = false
    repeat
        if i<25 then
            ____continue = true
            break
        end
        print(i)
        local x = 1
        while(x<=10) do
            local ____continue = false
            repeat
                if x<5 then
                    ____continue = true
                    break
                end
                print(x)
                ____continue = true
            until true
            x=x+1
            if not ____continue then break end
        end
        ____continue = true
    until true
    i=i+1
    if not ____continue then break end
end
local d = 10

while d>0 do
    local ____continue = false
    repeat
        d=d-1
        if d>5 then
            local o = 4

            while o>0 do
                local ____continue = false
                repeat
                    o=o-1
                    if o>2 then
                        ____continue = true
                        break
                    end
                    print(o)
                    ____continue = true
                until true
                if not ____continue then break end
            end
            ____continue = true
            break
        end
        if d==2 then
            ____continue = true
            break
        end
        print(d)
        ____continue = true
    until true
    if not ____continue then break end
end
local e = 10

repeat
    local ____continue = false
    repeat
        e=e-1
        if e>5 then
            local o = 4

            repeat
                local ____continue = false
                repeat
                    o=o-1
                    if o>2 then
                        ____continue = true
                        break
                    end
                    print(o)
                    ____continue = true
                until true
                if not ____continue then break end
            until not (o>0)
            ____continue = true
            break
        end
        if e==2 then
            ____continue = true
            break
        end
        print(e)
        ____continue = true
    until true
    if not ____continue then break end
until not (e>0)
for _, i in ipairs({1,2,3,4,5,6,7,8,9,10}) do
    local ____continue = false
    repeat
        if i<3 then
            ____continue = true
            break
        end
        for _, i in ipairs({5,6,7,8}) do
            local ____continue = false
            repeat
                if i~=6 then
                    ____continue = true
                    break
                end
                print(i)
                ____continue = true
            until true
            if not ____continue then break end
        end
        if i>8 then
            ____continue = true
            break
        end
        print(i)
        ____continue = true
    until true
    if not ____continue then break end
end
for i, _ in pairs({a = 1,b = 2,c = 3,d = 4}) do
    local ____continue = false
    repeat
        if i=="a" then
            ____continue = true
            break
        end
        if i=="b" then
            ____continue = true
            break
        end
        print(i)
        ____continue = true
    until true
    if not ____continue then break end
end

The outcomes match.

I don't really know if there's a certain way that the pull requests should be made, or if there's some kind of standard for the workflow. If I've done something wrong with the presentation, then please don't hesitate to correct me. :)

@lolleko
Copy link
Member

lolleko commented Jun 4, 2018

Thanks for the PR, I don't think continue is good style and should be used at all. But its probably a good thing to have support for it anyway.

Also I think it would be much cleaner to use Lua 5.2s goto statement. The output looks a bit nicer that way.
http://lua-users.org/wiki/GotoStatement

And please add at least some translation tests and if you want some functional tests.

@InfiniteRain
Copy link
Contributor Author

InfiniteRain commented Jun 4, 2018

I have been searching for this kind of project for a while, since I have some old games that I would love to mod, but they support only Lua 5.1; that was the reason for me going with this solution. I could implement the goto solution for cases when Lua 5.2 or 5.3 is used, but I would also love to support Lua 5.1. So would you mind if I added the support for both?

And you're right, I should probably add more tests with this feature.

Edit: Also I disagree with the notion that continue should not be used, I have encountered a few cases where the implementation of continue helps a lot. But I guess, that boils down to opinion.

@lolleko
Copy link
Member

lolleko commented Jun 4, 2018

We dropped support for Lua versions below 5.3 a while ago because we wanted to implement all Lua features first, without having to think of backwards-compatibility all the time.

But we already have a system to transpile differently for different version, we use that for bit operations for example, so if you want to add 5.1 support please go ahead. (Unless @Perryvw has an issue with that.)

While your at it you can also look at some other features that won't work in 5.1 (e.g bit ops, can't think of more right now) and throw exceptions if those are used when the 5.1 flag is set.

Another thing make sure you don't add additional overhead if no continue is used in a loop and use goto in versions <5.1

@InfiniteRain
Copy link
Contributor Author

Alright then, I think that it would be wise for me to go with the goto implementation for this PR, and implement the 5.1 compatibility in a different batch with a different PR.

On a side note, is there a list of the Lua features that are already implemented and features that are still being worked on? If I remember correctly, things such as typeof keyword don't work properly yet.

@lolleko
Copy link
Member

lolleko commented Jun 4, 2018

Sounds good 👍

We have a list of things that don't work, but I don't think everything is listed there: https://github.com/Perryvw/TypescriptToLua/wiki/Limitations

@InfiniteRain
Copy link
Contributor Author

And tell me if I got this right: the plan is to get rid of all the limitations, correct?

@Perryvw
Copy link
Member

Perryvw commented Jun 4, 2018

I would say the nicest way to do this is maintain one 'main' transpiler class and have a different extension class for other versions that override required methods. We can then simply select between them from compiler.ts

@InfiniteRain
Copy link
Contributor Author

Alright, got it. Thank you.

Copy link
Member

@Perryvw Perryvw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple things, I am seeing a lot of code reuse: it would probably be a good idea to make a transpileLoopBody method.

Furthermore you should move tests that just check transpilation string result to translation tests. Furthermore I would appreciate some unit tests where you actually execute some transpiled code with continues.


let result = this.indent + `while ${condition} do\n`;
this.pushIndent();
result += this.indent + "local ____continue = false\n";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 _ seems a little overkill, we generally use 2 for transpiler stuff

____continue = true
until true
if not ____continue then break end
end`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a translation test. (Just add the typescript to translation/ts and the lua to translation/lua with the same name and it will automatically include the test).

repeat
a=a+1
____continue = true
until true if not ____continue then break end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another translation test.

@InfiniteRain
Copy link
Contributor Author

Ok, I've done everything that was requested:

  • Since Lua 5.1 support is not considered (for now), I redone the continue functionality via the Lua 5.2's goto statements.
  • Removed additional overhead if continue statement is not used inside of the loop.
  • Created a function transpileLoop which heavily decreases code reusage.
  • Added translation tests to the correct places.
  • Added a bunch of unit tests.

@Perryvw
Copy link
Member

Perryvw commented Jun 6, 2018

Thanks for the update! I like the functional tests you added, this seems like a good test coverage.

I am not such a fan of the way this is implemented however. After discussing with @lolleko we came to a couple points:

  1. You can just add a label __continue{n} at the end of each loop. Generate the n by keeping a stack of loop names. When you start transpiling a loop push and increment genVarCounter, transpile continue as goto __continue{m} where m is the loop number at the top of the stack. Pop from the stack when you're done transpiling that loop.
  2. The structure we had before with transpileFor and transpileForOf etc was fine, what I meant with transpileLoopBody was purely refactoring the body, as indicated in the following image: https://i.imgur.com/FlsYmch.png
    I think the removal of the old structure is a step in the wrong direction and should probably be reverted.
  3. If you do the goto/label method you can get rid of hasContinue completely, we generally try to avoid getChildren(), and just labels will probably not have any performance impact anyway.

@InfiniteRain
Copy link
Contributor Author

My bad, for some reason I completely missed the Body part from your initial message. Changes will follow shortly.

@InfiniteRain
Copy link
Contributor Author

Question, should I be using the genVarCounter variable for keeping track of the stack? Or should I create a new variable designed to be used with loops only? Your message suggested that I should use genVarCounter, but if I do, it will interfere with switch/case statements.

@Perryvw
Copy link
Member

Perryvw commented Jun 6, 2018

I suggest creating a new stack to keep track of the numbers of nested loops for when you need to write a goto in one of them. You can simply populate the stack by using genVarCounter. If any time you start using a new genVarCounter value you simply increment it, it should not interfere with switch/case statements.

@Perryvw
Copy link
Member

Perryvw commented Jun 6, 2018

The namespace array is also used as a stack by the way, you can look at how that is used.

@InfiniteRain
Copy link
Contributor Author

Requested changes are now implemented.

Copy link
Member

@lolleko lolleko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented on some minor stuff. Other than that really good work, this looks like a solid, well tested solution now.

result += this.transpileStatement(node.statement);
this.popIndent();
result += this.indent + "end\n";
result += this.indent + `::__continue${this.loopStack[this.loopStack.length - 1]}::\n`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just use this.loopStack.pop() instead of this.loopStack[this.loopStack.length - 1]. and remove the pop on the next line. pop will return the last value.

const condition = this.transpileExpression(node.expression);

let result = this.indent + `while ${condition} do\n`;
this.pushIndent();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can push the indent inside the transpileLoopBody function, this will reduce code reuse even more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with this. If you look at the regular for loop it translates the incrementor after the loop body but inside the indent. If you move the indent inside the loop body method this no longer works. You could ofcourse move only the push inside but I think that is bad form, push and pop should be obvious in the same context.

Copy link
Member

@Perryvw Perryvw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes, they look very good! I have only one issue and that is that none of your translation tests actually contain a continue. Also having a test with a nested loop would be nice. Once you address that and the other minor comments in the code this looks ready to merge to me!

| ts.ForInStatement
): string {
this.genVarCounter++;
this.loopStack.push(this.genVarCounter);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Increment genVarCounter after you use it instead of before (same as switch does)

@lolleko lolleko merged commit 7f39d57 into TypeScriptToLua:master Jun 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants