Skip to content

fixes various issues with translating assignment operators...#241

Merged
Perryvw merged 3 commits intoTypeScriptToLua:masterfrom
tomblind:assignment_fixes
Oct 18, 2018
Merged

fixes various issues with translating assignment operators...#241
Perryvw merged 3 commits intoTypeScriptToLua:masterfrom
tomblind:assignment_fixes

Conversation

@tomblind
Copy link
Copy Markdown
Collaborator

@tomblind tomblind commented Oct 14, 2018

fixes #221
fixes #225

There's a lot of cases here but here's the gist:

Simple statements remain largely the same. The exception being statements where the left-side is a "complex" property access:

arr[getIndex()] += 1;
getObj().prop += 1;
do local __TS_obj, __TS_index = arr, (getIndex())+1; __TS_obj[__TS_index] = (__TS_obj[__TS_index]+(1)); end;
do local __TS_obj, __TS_index = getObj(), "prop"; __TS_obj[__TS_index] = (__TS_obj[__TS_index]+(1)); end;

"Complex" meaning the object or property is not a simple literal or variable.

Expressions are similar but will cache the right-side in a temp if it is complex:

z = x = obj.prop;
z = obj.prop += 1;
z = ((function() local __TS_tmp = obj.prop; x = __TS_tmp; return __TS_tmp end)());
z = (function() local __TS_tmp = obj.prop+1; obj.prop = __TS_tmp; return __TS_tmp end)();

And if the left-side is a complex property access everything gets cached (using function args):

z = getObj().prop += 1;
z = (function(o, i, v) v = (o[i]+v); o[i] = v; return v end)(getObj(), "prop", 1);

The plan was to use lib polyfills for these but I have opted to skip that for now. It only affects edge-cases with complex property accesses. Also, bitwise operators would require separate implementations for each lua target.

There are also special implementations for destructuring assignments:

[x, y] = xTup;
xTup = [x, y] = yTup;
do local __TS_tmp0,__TS_tmp1 = table.unpack(xTup); x,y = __TS_tmp0,__TS_tmp1 end;
xTup = ((function(...) x,y = ...; return {...} end)(table.unpack(yTup)));

I discovered that in JS (or node.js at least), desturcturing assignments evaluate the right side first and then the left. I assume this is to make it simple for older ES polyfills to use temp variables. This is why I implemented the statement version with temps - to ensure execution order matches the equivalent JS.

There's an extensive set of tests added (both functional and translation). Feel free to drop questions on me as this is fairly complex and has a lot of what-if scenarios.

Copy link
Copy Markdown
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.

Nice changes, looks like you put a lot of thought into this. I have a few remarks/questions:
First of all the implementation of transpileAssignmentExpression looks very low level and I would prefer if it could be generalized a little. I am afraid it is too focussed on the current issues and will not deal well with cases we missed at this current moment.

Secondly, did you consider deeper nested access expressions, for example:

const a = [[1], [99]];
let i = 0;
function index() { return i++; }
a[index()][0] += 1;
// a should be 2 here

Similarly for arrays of objects etc. This would also make a nice test case.

src/TSHelper.ts Outdated
}

// Returns true for expressions that may have effects when evaluated
public static isExpressionWithEffect(node: ts.Expression): boolean {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the name of this (and the next) function is a little unclear, maybe something like isComplexAccessExpression or isExpressionWithEvaluationEffect would be clearer?

src/TSHelper.ts Outdated
// If expression is property/element access with possible effects from being evaluated, returns true along with the
// separated object and index expressions.
public static isAccessExpressionWithEffects(node: ts.Expression, checker: ts.TypeChecker):
[true, ts.Expression, ts.Expression] | [false, null, null] {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was this type chosen over something like [boolean, ts.Expression, ts.Expression] which seems like it encompasses both cases?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I did this out of habit because I usually use strict null checks and this allows TS to deduce the subsequent types in the tuple by evaluating the first. But you're right, without strict null checks they are equivalent and the later is more succinct.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see. I agree in that case that is actually a nice way of defining the type.

result = `(function() local __TS_tmp = ${rhs}; ${lhs} = __TS_tmp; return __TS_tmp end)()`;
} else {
result = `(function() ${lhs} = ${rhs}; return ${rhs} end)()`;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Very large switch case, would refactor to its own method.

@tomblind
Copy link
Copy Markdown
Collaborator Author

the implementation of transpileAssignmentExpression looks very low level and I would prefer if it could be generalized a little. I am afraid it is too focussed on the current issues and will not deal well with cases we missed at this current moment.

Did you have something particular in mind? I can break it down into smaller manageable methods and generalize a tiny bit between a couple of the cases, but most of them are fairly distinct and tough to generalize because of subtle differences (which values get cached in temps, etc...).

@tomblind
Copy link
Copy Markdown
Collaborator Author

Alrighty, I've made changes and added tests based on your feedback. I refactored transpileAssignmentExpression in a way that hopefully makes it a bit more maintainable. It now generates an assignment statement and decides to wrap it as an expression at the end instead of having separate code paths for expressions/statements. This changes the output lua a bit, but makes it more consistent which is probably a good thing. The important thing is that it provides a single place to add new exceptions/rules as situations we haven't thought of yet come up. let me know what you think.

Thanks for taking the time to comb through this!

Copy link
Copy Markdown
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.

Nice changes!

@TestCase("of().p.d += af()[i()][i()]", "o=9;a=6")
@TestCase("of().p.d -= af()[i()][i()]", "o=-3;a=6")
@TestCase("of().p.d *= af()[i()][i()]", "o=18;a=6")
@TestCase("af()[i()][i()] /= of().p.d", "o=3;a=2.0")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems to be exactly what I would expect, maybe it would be nice to also check the other elements in a have not changed though, maybe.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point

do local __TS_obj, __TS_index = getObj(), "prop"; local __TS_tmp = (__TS_obj[__TS_index] << (getArr()[(getIndex())+1])); __TS_obj[__TS_index] = __TS_tmp; end;
do local __TS_obj, __TS_index = getObj(), "prop"; local __TS_tmp = (__TS_obj[__TS_index] >> (getArr()[(getIndex())+1])); __TS_obj[__TS_index] = __TS_tmp; end;
z = ((function() x = y; return y end)());
z = ((function() obj.prop = x; return x end)());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a general remark regarding translation tests: We don't really value them that much (and are considering maybe just getting rid of them in favor of functional tests). This under the reasoning that we don't really care that much about what our resulting lua looks like, as long as it works. For now I'm fine with these though since this does ensure the stability of our translation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I agree the translation tests should not be taken as seriously as the functional, but I wouldn't remove them as they force you to pay attention when something changes in the output lua that may have unintended consequences. Perhaps something the functional tests miss, or maybe the result is still correct but the new generated lua is terribly inefficient. But yes, I think they should only be a back-up to the functional tests, not a replacement.

@Perryvw Perryvw merged commit 3237c20 into TypeScriptToLua:master Oct 18, 2018
@tomblind tomblind deleted the assignment_fixes branch October 18, 2018 19:54
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.

Problems with assignments as expressions Issues with non-lua operators on properties of objects

2 participants