The missing bit was to mark labels are SEF when they are not referenced. Label references are now counted at parse time, enabling us to do this.
Also, make FoldStmt clearer when the node is an expression.
(float)"1.1754944e-38" is != 0
(float)"1.1754943e-38" is == 0
Yet, 1.1754944e-38 == 1.1754943e-38.
The fix is to perform the operations as doubles, and convert to F32 *after* comparing the denormal range.
LSO allows this. The compiler does too, but it chokes in RAIL.
This affected a test, which has been adjusted too.
Untyped lazy list elements can no longer be used in isolation in expression lists (including FOR initializator and iterator).
Also rename the terribly named 'self.forbidlabels' to 'self.optenabled' which is more descriptive.
This extremely uncommon coding pattern was becoming a hell to support. It has caused many bugs in past that need them being treated as special cases.
Getting rid of the possibility entirely seems like the best approach.
It's still supported if the code is not to be optimized (e.g. with --pretty).
While not strictly a bug because it would be caught later in the function (it passes the tests either way), it made me nervous to leave a dangling NextToken().
This may cause more trouble than it's worth, but it's how LSL behaves and one of our objectives is to document the darker corners of LSL. Mono chokes at the RAIL postprocessing stage, not in compilation proper. LSO chokes at runtime for string, key and list, and works fine for the other types.
That was long overdue. Obviously, this is a large commit.
The new nr (node record) class has built-in dump capabilities, rather than using print_node().
SEF always exists now, and is a boolean, rather than using the existence of SEF as the flag. This was changed for sanity. However, other flags like 'X' are still possibly absent, and in some cases the absence itself has meaning (in the case of 'X', its absence means that the node has not yet been analyzed).
Similarly, an event is distinguished from a UDF by checking for the existence of the 'scope' attribute. This trick works because events are not in the symbol table therefore they have no scope. But this should probably be changed in future to something more rational and faster.
A few minor bugfixes were applied while going through the code.
- Some tabs used as Unicode were written as byte strings. Add the u'\t' prefix.
- After simplifying a%1 -> a&0, fold again the node and return. It's not clear why it didn't return, and whether it depended on subsequent passes (e.g. after DCR) for possibly optimizing out the result. Now we're sure.
- A few places lacked a SEF declaration.
- Formatting changes to split lines that spilled the margin.
- Some comment changes.
- Expanded lazy_list_set definition while adapting it to object format. The plan was to re-compress it after done, but decided to leave it in expanded form.
- Added a few TODOs & FIXMEs, resisting the temptation to fix them in the same commit:
- TODO: ~-~-~-expr -> expr + -3.
- FIXME: Now that we have CompareTrees, we can easily check if expr + -expr cancels out and remove a TODO. Low-hanging fruit.
- TODO: Check what we can do when comparing non-SEF and non-CONST values in '>' (current code relies on converting '>' to '<' for applying more optimizations, but that may miss some opportunities).
- FIXME: Could remove one comparison in nt == '&&' or nt == '||'. Low-hanging fruit.
-a == -b -> a == b
If both a and b either are constants or have a minus sign, negate both.
!(a - b) can be optimized to a == b.
!(a + b) can be optimized to -a == b, relying on the first optimization to remove redundant minus signs.
int != int was not properly optimized, because the != was transformed into the equivalent !(int == int) at an earlier stage. Fixed.
!(a ^ b) can be optimized to a == b, so do it.
Our previous fix was incomplete, because it failed to detect the last IF in a chain of ELSE IFs. For example:
if (a == 2) llDie(); else if (a) llDie(); else if (a == 3) llDie();
That would be transformed by the IF swapper into:
if (a ^ 2)
if (a)
llDie();
else if (a == 3)
llDie();
else
llDie();
Note that the last 'else' would bind to the last 'if', not to the first one. So the condition is actually like this:
child[1] of an 'if' statement needs to be guarded in {} if the 'else' may belong to the wrong 'if'.
It will belong to the wrong 'if' if child[1] is a (possibly empty) chain of 'if {whatever} else ...', followed by an 'if' without 'else', that is:
if (cond) stmt;
(which was what our previous check did), but also e.g.:
if (cond) stmt; else if (cond) stmt; else if (cond) stmt;
which we neglected to consider in our previous fix.
Since our syntax extensions transform the source at parse time, all syntax extensions are disabled. The optimizations are disabled too, as it doesn't make sense to prettify and optimize at the same time (the optimizer would remove the constants that we're trying to keep).
Addresses #4 in a more user-friendly way.
097c054 introduced a bug that we hadn't caught until now.
In some occasions, it could swap nested conditions in such a way that the 'else' of the outer statement was made to belong to the inner one, like this:
if (a)
if (b)
stuff;
else
stuff;
That is of course parsed with the 'else' belonging to if(b).
Fix implemented at output time, by detecting 'if(a) stmt; else y;' with stmt being an 'if' without 'else', and wrapping the stmt in {} like this: 'if(a){if(b) x;} else y;'. This has some similarity with parenthesis addition.
But the fix has the corner case that, since {} hides visibility of labels, when the inner 'if' has a label as direct child, it can't be swapped lest the label becomes out of scope. So these cases are detected and skipped in the constant folding module.
In the case of 'if(cond);', we transform it to 'cond;', but we forgot to wrap the cond in an EXPR node as required. Fixed too.
Reorganize into different statements with early return.
Add constants, unary operators and binary operators. Check if operator is commutative and check with operands swapped when so.
Constant equality is somewhat sketchy at the moment: just compare the values with Python's ==.
Implements another TODO.
There was a TODO about a new counter per scope, but that makes no sense. The renamer only acts on global variables, global function and parameter names, state names, and event parameters. We're already restarting the counters at every function, which is the closest to what that TODO was about.
This has been a TODO item for long. Now that we have lsllastpass, it's actually easy to implement.
Adds an LSLTypeDefaults dictionary to lslcommon, just in case the state-changing function returns a value and we need to insert a return statement.
We've also added subtree-local info to lsllastpass (lost when we return to the parent after visiting a subtree).
This fixes a bug where naked switch statements could appear as a result of optimization, and cause the compilation to fail.
Still somewhat messy, but still reported as soon as it can be detected.
If an ELSE token is detected at the top level, for example, the error position will be rewound to the state change and reported there.
This means that in this situation:
x()
{
if (1)
{
state default;
x(2);
}
else ;
}
default{timer(){}}
an error will be reported in x(2), because the ELSE hasn't been found at that point, therefore the state change statement isn't found to be at fault yet.
However, in this case:
x()
{
if (1)
state default;
else
x(2);
}
default{timer(){}}
the error WILL be reported at the state change statement.
This commit also changes the position where the exception is reported, to be at the STATE token. As an inconsequential side effect, EParseCantChangeState takes precedence over undefined identifiers, in case the state change is to an undefined state, but only in cases where it can be immediately detected.
if (!cond) X; else Y; -> if (cond) Y; else X;
if (int1 == int2) X; else Y; -> if (int1 ^ int2) Y; else X;
When 'cond' is of a type other than 'key': if (cond) ; else X; -> if (!cond) X; (this required changing if(str) to its compiled equivalent if(!(str == "")), so that 'cond' is always either key or integer).
if (cond) ; -> cond; and folds it as a statement, which may eliminate it if it's SEF. This is done after eliminating 'else ;' so that it also optimizes 'if (cond) ; else ;' the same way.
This removes a TODO item.
Allows detection of empty events, for example, even if they have labels.
Also, it is OK if there's a label inserted in a nested {}; that case wasn't contemplated.
Gives us a few more opportunities for catching single-letter identifiers.
UsedNames was not restarted. It's unlikely that this had any detrimental effect on optimization, and it was certainly safe to not restart it. But it looks more correct like this.
When a constant was negative internally, it was output with the sign included. The code was not prepared to handle this, and could therefore cause double minus signs. For example, -2147483648 was output as --2147483648, and -4294967296 was output as --1.
Fixed by adding a space for floats, and by translating the number to the range 2147483648..4294967295 for integers (hex would have worked just as well).
The comment was wrong anyway. If one side changes x and the other side uses x, then order is still important, no matter whether one side is SEF.
But the reversal is safe when one side is a constant, so we still perform it, to enable optimization of some important cases.
For floats:
When const >= function.max, comparisons of function > const always yield FALSE.
When const < function.min, comparisons of function > const always yield TRUE.
When const > function.max, comparisons of function < const always yield TRUE.
When const <= function.min, comparisons of function < const always yield FALSE.
For integers:
When min = -1, cond(function > -1) is the same as cond(!~function).
When min = -1, cond(function < 0) is the same as cond(~function).
To implement the above, we got rid of the cond(x < 0) -> cond(x & 0x80000000) optimization, which has caused more trouble than it has solved for just 1 byte gain.
When min = 0, cond(function > 0) is the same as cond(function).
When min = 0, cond(function < 1) is the same as cond(!function).
Similar expressions can be obtained for max in [-1, 0], but it's not worth it, as there are no functions with -1 as maximum, and the ones with max=0 also have min=0 (always return 0).