Jump to content

Comma Separated Expressions in For-Loops


Fenix Eldritch
 Share

You are about to reply to a thread that has been inactive for 239 days.

Please take a moment to consider if this thread is worth bumping.

Recommended Posts

A recent jira caught my eye calling attention to an (as far as I'm aware) undocumented feature in LSL: the initializer and incrementer components of the for-loop header can accept comma separated statements. I don't know much about this, but it's apparently a feature of C (though I'm having trouble searching for the correct terminology).

integer a;
integer b;
integer c;
for (a=0, b=1, c=2; a < 10; a++, b*=2, c--)
{
    llOwnerSay(" A=" + (string)a + " B=" + (string)b + " C=" + (string)c );
}

The above sample will compile in LSL and print out:

 A=0 B=1 C=2
 A=1 B=2 C=1
 A=2 B=4 C=0
 A=3 B=8 C=-1
 A=4 B=16 C=-2
 A=5 B=32 C=-3
 A=6 B=64 C=-4
 A=7 B=128 C=-5
 A=8 B=256 C=-6
 A=9 B=512 C=-7

The condition segment of the loop header doesn't seem to accept similar comma separated statements - but that can easily be recreated with a more complex conditional in the first place.

This might be obvious to more experienced coders, but thought it would be good to share this here regardless.

Edited by Fenix Eldritch
typos
  • Like 1
  • Thanks 5
Link to comment
Share on other sites

Wow, that's surprising, I always assumed comma-separated statements were not available anywhere in LSL since you can't do multiple definitions (with or without assignments) like "integer a = 3, b = 4, c = 5". The for loop has unique privileges.

Two other observations since I had to see for myself which other C-like features would be available:

  • You can leave out the initializer and the per-loop statements, but you can't leave out the termination condition statement. "for(;TRUE;)" does the same as "while(TRUE)", but in C you could just do "for(;;)".
  • Likewise, the termination condition can't be comma separated, it wants a single value, no more, no less. In C comma separation is legal anywhere and the rightmost value is what the entire comma-separated statement evaluates to. "for(a = 100, b = 0; a < 0, b < 100; ++a, ++b)" is valid but dumb C since even if "a < 0" obviously always evaluates to false, the entire statement gets its value from "b < 100".
Edited by Frionil Fang
  • Like 1
Link to comment
Share on other sites

13 minutes ago, Frionil Fang said:
  • You can leave out the initializer and the per-loop statements, but you can't leave out the termination condition statement. "for(;TRUE;)" does the same as "while(TRUE)", but in C you could just do "for(;;)".
  • Likewise, the termination condition can't be comma separated, it wants a single value, no more, no less. In C comma separation is legal anywhere and the rightmost value is what the entire comma-separated statement evaluates to. "for(a = 100, b = 0; a < 0, b < 100; ++a, ++b)" is valid but dumb C since even if "a < 0" obviously always evaluates to false, the entire statement gets its value from "b < 100".

These parts don't surprise me to much, as the "termination condition" statement is basically a "boolean/integer expression" - it must resolve a (numeric) value. 

I assume the "requirement" for a "termination condition" statement (cannot default to empty) was an oversight when the LSL syntax parser was developed.

The reason this is so interesting to me is, my in-progress parser will need to take this into consideration when I switch gears from BASIC=>LSL to LSL=>LSL.

Conceptually, if I want to parse valid LSL, then for me the for() parameters will be treated as:

1) Initialization: Ok if blank, otherwise treat as a CSV series of "statements" (expressions that should not return a value).

2) Termination condition: Requires a numeric expression (any expression that evaluates to a numeric value).

3) Loop expression: Similar to Initialization.

tl;dr - I'm adding "NEXT", "NEXT [varname]", "EXIT FOR", etc. to my parser as extensions for the LSL "for()" statement.

 

Edited by Love Zhaoying
Link to comment
Share on other sites

4 minutes ago, Love Zhaoying said:

Conceptually, if I want to parse valid LSL then for me the for() parameters will be treated as:

1) Initialization: Ok if blank, otherwise treat as a CSV series of "statements" (expressions that should not return a value).

2) Termination condition: Requires a numeric expression (any expression that evaluates to a numeric value).

3) Loop expression: Similar to Initialization.

For 1), they can have values but the value is simply ignored (I know, doesn't matter here). Assignment produces a value; "a = 2" has the value 2, so you can do a = b = 2. Can't do "(a = b) = 2" since that would be trying to assign a value to a value.

For 2), the expression has no type restrictions, it works like the if statement and coerces it to a truth value. "if("")" is false, "if("abcd")" is true.

Link to comment
Share on other sites

27 minutes ago, Frionil Fang said:

You can leave out the initializer and the per-loop statements

Interestingly, this is in fact documented on the wiki page:

Quote

for( initializer; condition; incrementloop

•  initializer Executed once just before checking condition.  
•  condition If this executes as true then loop is executed.  
•  increment Executed after loop, then condition is checked again.  
•  loop Can be either a single statement, a block statement, or a null statement.  


Only the condition is required. The initializer, increment, and loop body are optional and can be left empty.

And my op referred to the termination component not accepting this - but I called it the "condition" in following with the wiki's naming convention - but yeah it's interesting how the for statement seems to get this special case!

Edited by Fenix Eldritch
  • Like 2
Link to comment
Share on other sites

Just now, Frionil Fang said:

For 1), they can have values but the value is simply ignored (I know, doesn't matter here). Assignment produces a value; "a = 2" has the value 2, so you can do a = b = 2. Can't do "(a = b) = 2" since that would be trying to assign a value to a value.

Yeah, I forgot - I did add that to my Parser Schemas recently. If someone tries to use a "function" as a "statement", it ignores the result (unless the warning is enabled).

1 minute ago, Frionil Fang said:

For 2), the expression has no type restrictions, it works like the if statement and coerces it to a truth value. "if("")" is false, "if("abcd")" is true.

Understood. I need to check if I've done that yet for "string expressions". 

A little odd that "empty string" resolves to FALSE and not vice-versa.  Isn't it basically using the "string length" as the return? "empty string" = Length 0 = FALSE.

 

 

Link to comment
Share on other sites

6 minutes ago, Fenix Eldritch said:
Quote

 

  increment Executed after loop, then condition is checked again.  
•         

 

And my op referred to the termination component not accepting this - but I called it the "condition" in following with the wiki's naming convention - but yeah it's interesting how the for statement seems to get this special case!

Google confirms it makes absolutely no difference if people use "++i" or "i++" in the "increment" section.

Funny how people insist on using one or the other in most cases.

Yes, I understand it matters in other cases.

Edited by Love Zhaoying
Link to comment
Share on other sites

writing complex for loops can get a bit murky obscure as the 3rd part can accept any kind of assignment as per the C language spec

for example, there is a list of ranked players and we want to create a knockout tournament where in the 1st round, the 1st ranked plays the lowest ranked, the 2nd ranked plays the next lowest rank, etc

default
{
    state_entry()
    {      
        list players = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];

        list draw;

        integer i;
        integer j;
        for (i = 0, j = llGetListLength(players)-1; i < j; draw += [llList2String(players, i) + " vs " + llList2String(players, j)], ++i, --j);

        // shorten it
        // integer i;
        // integer j = llGetListLength(players)-1;
        // for (; i < j; draw += [llList2String(players, i++) + " vs " + llList2String(players, j--)]);
                
        llOwnerSay(llDumpList2String(draw, ", "));
        
        // says: a vs j, b vs i, c vs h, d vs g, e vs f
    } 
}

 

i tend to write this kind of code myself for my own stuff, but yeah is murky and I have moved away from posting this kind of code on the forums as I sometimes end up having to explain it in IM, which is not useful to anyone really, when it can be written in longhand in a way which the greatest number of LSL scripters can more readily understand, particularly those coming from a non-C experience

so I just post this as an example of being obscure

Link to comment
Share on other sites

From that "null oddity" exploration a couple weeks back, the string coercion for "if(string_variable)" appears to work like "if(string_variable != "")", since null is TRUE and it's not even checking the length. Likewise, list coercion "if(list_variable)" appears to be equivalent to "if(llGetListLength(list_variable))", since coercing a null list caused an exception.

1 minute ago, Love Zhaoying said:

Funny how people insist on using one or the other in most cases.

I was always taught that pre-increment is better for general use, since it's fewer (virtual) machine code instructions. A smart compiler would of course detect that and optimize it for you, but I think we've established the LSL compiler does the bare minimum. :P

Link to comment
Share on other sites

16 minutes ago, Frionil Fang said:

From that "null oddity" exploration a couple weeks back, the string coercion for "if(string_variable)" appears to work like "if(string_variable != "")", since null is TRUE and it's not even checking the length. 

Since that discussion, I've worked on a solution to basically promote (move) all "local" variables to the start of the function, before the "jump" that would have missed their declaration.  Making all the variables "globals" was a workable solution, but I finally wanted "local" variables too.

16 minutes ago, Frionil Fang said:

Likewise, list coercion "if(list_variable)" appears to be equivalent to "if(llGetListLength(list_variable))", since coercing a null list caused an exception.

I was going to add earlier, but decided not to:  Lists are "special" in that you can't really compare lists, just list length IIRC.

So for the "for()" case, you'd think it behaves similar to lists as with strings.

..is what I was thinking and can't shut up. 🙂

 

Edited by Love Zhaoying
Link to comment
Share on other sites

1 hour ago, Fenix Eldritch said:

Interestingly, this is in fact documented on the wiki page:

Guilty as charged. 😋

 

About the comma-for-initializers thing, in other languages (like C, C++, C#) the comma is considered an operator that explicitly discards the result of an expression. It's not the same as the syntax for declaring multiple variables (like int a, b, c;) and not the same as function parameters/arguments (like llSay(0, "text");)

https://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html#The-Comma-Operator

https://learn.microsoft.com/en-us/cpp/cpp/comma-operator?view=msvc-170

  • Like 4
Link to comment
Share on other sites

15 hours ago, Love Zhaoying said:

Interesting way of looking at it!!

That's not a way of looking at it, it's actually how it's defined.  The comma — when not consumed as a syntactic element — is an actual operator.

I ran into it a bunch of years back, and have lamented in the groups on a number of occasions since that I wish they made the comma operator a proper thing in LSL, too…  It's much nicer than that (listVar=[])+listVar construct…  Especially since we're fairly sure LSL isn't smart enough to optimise out that addition.  The comma-operator version is much the same, but makes what you're doing a whole lot more obvious, and there's no point adding an empty list to your actual list; hopefully, the addition operator at least short-circuits when it notices the LHS is empty — though that's something that can only be done because of immutability, so, not counting on it.  If it isn't short-circuiting the addition (and simply returning the RHS untouched), then it'll be effectively just making a copy of the RHS, and there seems to be a window during which that can blow your script heap (though I have a suspicion that might actually be a different issue lots of people seem to not quite understand, because I haven't seemed to run into it, myself).

17 hours ago, Frionil Fang said:

I was always taught that pre-increment is better for general use, since it's fewer (virtual) machine code instructions. A smart compiler would of course detect that and optimize it for you, but I think we've established the LSL compiler does the bare minimum. :P

I'm rather famous for jumping in on that discussion whenever it comes up.  Pretty sure I've got at least two posts on this forum along those lines.

It's not even a very smart optimisation — for simple values like an integer, most compilers pretty much do it by accident during register selection.

Personally, I don't care what someone does in their own code.  I'll explain the issue if it comes up, though…  What I do care about, is people in a position to teach coding (or writing wiki examples, or helping others on forums), blindly following the bad habits they themselves were taught because they've never stopped to actually think it through (or worse, don't understand what they're teaching well enough to even realise the issue exists) — it can and has bitten people hard, when they move on to real programming languages out in the real world.  And it's a simple enough thing — just fix it.  Use the pre form unless you specifically need the post form.  Make doing it the better way the new habit, especially when you're teaching others, even if you can't be bothered for your own stuff.  Otherwise, for me, it's just one of several red flags telling me they don't truly know what they're doing.

The prime motivation for just plain "learning it right", that I often mention, was (a year or three ago now, so no, I don't have a link to the post) a company made a seemingly small change in an unrelated part of their code, that took a worrying slice off their performance, but it was new code, hadn't been optimised yet, so they kept going, and they had a schedule to keep and all that.  But the servers just kept getting slower, even while they were optimising other stuff trying to make up the time elsewhere, something they couldn't identify kept eating it right back up and more, especially when it actually went live and had to deal with real life customers.  Eventually they hired a consultant to just target that specific performance problem, and he tracked it down to this very issue — people using var++ instead of ++var in their for() loops (this is optimised C++, not LSL, so it shouldn't matter, right?  That's why none of their employees ever thought to check.  Everyone just does it that way!  Heck, some companies even recommend it in their coding style guides "for familiarity, because it doesn't actually matter").  All up, between the extra employee time spent hunting it down, and optimising other things that didn't actually need optimising yet, then the consultants time, plus the extra servers they needed to spin up to handle the regular load (they were expecting a little of that, like getting a little slower until they had time to properly profile and characterise to figure out where best to put in the optimisation effort, but not THAT much), this tiny little thing cost them quite a few millions of real life US dollars.

Still think it's not worth caring about?

Edited by Bleuhazenfurfle
Link to comment
Share on other sites

You are about to reply to a thread that has been inactive for 239 days.

Please take a moment to consider if this thread is worth bumping.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...