fixes various issues with translating assignment operators...#241
fixes various issues with translating assignment operators...#241Perryvw merged 3 commits intoTypeScriptToLua:masterfrom
Conversation
Perryvw
left a comment
There was a problem hiding this comment.
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 hereSimilarly 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 { |
There was a problem hiding this comment.
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] { |
There was a problem hiding this comment.
Why was this type chosen over something like [boolean, ts.Expression, ts.Expression] which seems like it encompasses both cases?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I see. I agree in that case that is actually a nice way of defining the type.
src/Transpiler.ts
Outdated
| result = `(function() local __TS_tmp = ${rhs}; ${lhs} = __TS_tmp; return __TS_tmp end)()`; | ||
| } else { | ||
| result = `(function() ${lhs} = ${rhs}; return ${rhs} end)()`; | ||
| } |
There was a problem hiding this comment.
Very large switch case, would refactor to its own method.
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...). |
|
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! |
test/unit/math.spec.ts
Outdated
| @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") |
There was a problem hiding this comment.
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.
| 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)()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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:
"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:
And if the left-side is a complex property access everything gets cached (using function args):
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:
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.