pawn-lang / compiler

Pawn compiler for SA-MP with bug fixes and new features - runs on Windows, Linux, macOS
Other
307 stars 72 forks source link

Feature proposal: switch expressions #496

Open Daniel-Cortez opened 4 years ago

Daniel-Cortez commented 4 years ago

Issue description:

This feature is not really that new in general; you could have already seen it in C#, Java, Rust and other languages.

In some situations an ability to use switch in expressions could be very handy. For example, let's suppose we have the following code: https://github.com/NextGenerationGamingLLC/SA-MP-Development/blob/a059abaebda0f2dff3c2e0f95ee4c9c5696406c7/GMs/includes/core/acceptcancel.pwn#L2604..L2612

new Float: fueltogive;
switch(PlayerInfo[playerid][pMechSkill])
{
    case 0 .. 49: fueltogive = 2.0;
    case 50 .. 99: fueltogive = 4.0;
    case 100 .. 199: fueltogive = 6.0;
    case 200 .. 399: fueltogive = 8.0;
    default: fueltogive = 10.0;
}

or https://github.com/NextGenerationGamingLLC/SA-MP-Development/blob/a059abaebda0f2dff3c2e0f95ee4c9c5696406c7/GMs/includes/core/acceptcancel.pwn#L2209..L2215

new Float:fueltogive;
new level = PlayerInfo[playerid][pMechSkill];
if(level >= 0 && level < 50) { fueltogive = 2.0; }
else if(level >= 50 && level < 100) { fueltogive = 4.0; }
else if(level >= 100 && level < 200) { fueltogive = 6.0; }
else if(level >= 200 && level < 400) { fueltogive = 8.0; }
else if(level >= 400) { fueltogive = 10.0; }

(NOTE: originally the second example actually had PlayerInfo[RefillOffer[playerid]][pMechSkill] instead of PlayerInfo[playerid][pMechSkill], but I changed it here for the sake of simplicity.)

With switch expressions the examples above could be simplified to something like this:

new Float:fueltogive = switch (PlayerInfo[playerid][pMechSkill];
    0..49: 2.0;
    50..99: 4.0;
    100..199: 6.0;
    200..399: 8.0;
    _: 10.0;
);

or this:

new Float:fueltogive = switch(PlayerInfo[playerid][pMechSkill]; 0..49: 2.0; 50..99: 4.0; 100..199: 6.0; 200..399: 8.0; _: 10.0);

Please note that this is only a draft syntax which may be changed later; I'm open to suggestions.

Minimal complete verifiable example (MCVE):

Workspace Information:

eoussama commented 4 years ago

Neat syntactic sugar. A side note I'd like to leave, the second example you provided is more optimized than the first one.

Daniel-Cortez commented 4 years ago

Well, I wouldn't call it only a syntactic sugar, as it also has potential for more optimal compiled code (less duplicated code for storing the value into a variable etc.)

Also, I experimented with this concept back in December (just didn't get around to post about it until now) and have a working implementation since then. @pawn-lang, would it be OK if I make a draft PR to showcase it?

Y-Less commented 4 years ago

So I do have a few thoughts:

1) If there were some integration of more expression-control, it should maybe be more general than just switch, but I don't yet know how much further. We already have expression-if essentially (?:). What about expression functions?

2) Speaking of which, you compared this to two existing versions, but not the most direct comparison:

new level = PlayerInfo[playerid][pMechSkill];
new Float:fueltogive = 
    (0 <= level < 50) ?  2.0 :
    (level < 100)     ?  4.0 :
    (level < 200)     ?  6.0 :
    (level < 400)     ?  8.0 :
                        10.0 ;

Or more accurately matching your switch expression example:

new level = PlayerInfo[playerid][pMechSkill];
new Float:fueltogive = 
    (0 <= level <= 49)    ?  2.0 :
    (50 <= level <= 99)   ?  4.0 :
    (100 <= level <= 199) ?  6.0 :
    (200 <= level <= 399) ?  8.0 :
                            10.0 ;

A tiny bit more verbose, but already exists.

3) I'm in two minds about the overloading of _. For one, it already has at least two uses, one of which is as a tag, so using the _: syntax, and is a valid symbol name as well. On the other hand, the other use is for default parameters, so has some precedent as meaning default.

An alternate syntax could be to do away with _: altogether. It's a bit strange having the trailing ; in your first example, and the second example does away with it anyway, so why not embrace that?

new Float:fueltogive = switch (PlayerInfo[playerid][pMechSkill];
    0..49: 2.0;
    50..99: 4.0;
    100..199: 6.0;
    200..399: 8.0;
    10.0
);

Making the syntax something like:

switch (<var>;[<case>:<result>;]*<default>)

Thus even switch (var; 5) becomes valid. This would be the only instance where ; was valid in an expression though, which goes back to my first point about making it more generic somehow.

Having said that:

4) why get rid of case and default at all? That's just introducing two ways of doing the same thing in different places.

5) Have you tested this with tag overrides?

Daniel-Cortez commented 4 years ago

We already have expression-if essentially (?:). What about expression functions?

From how I can imagine it, expression functions would require reimplementation of almost all functionality and workarounds of regular functions, such as:

  1. Local variables. 1.1. Both new and static. 1.2. Destructors. 1.3. Ability to access local variables from the containing function.
  2. Labels. They can be accessed from anywhere within the whole function, so we can't have 2 or more labels with the same name per function (otherwise one would shadow another since they are contained within the same table for all local symbols), but then how are we going to handle such labels in different expression functions within one function?
  3. Probably a lot of other details/problems either I can't remember at the moment, or they can be realized only when trying to actually implement the said feature.

In other words, the idea of expression functions is clearly much more far-fetched compared to switch expressions.

It's a bit strange having the trailing ; in your first example, and the second example does away with it anyway

Yes, I intended the trailing ; after the default case to be optional, as otherwise it might look strange when the whole switch expression is being put on one line. This is not something new, as we already have optional trailing semicolons in enumerations.

This would be the only instance where ; was valid in an expression though

And this is exactly why I used it (or rather had to). Originally I wanted to use , instead, but since it already can be used in expressions, the part after , would be misinterpreted as a part of the expression (for example, 1: 5, 2,3: 4 would be read as 1: (5, 2,3): 4, which would result in a syntax error). Therefore I needed something that can't be interpreted as a part of expression, and since ; is already used to end expressions/statements, it seemed as the most reasonable option.

why get rid of case and default at all?

Mainly because of less verbose syntax, which could be more convenient for putting the whole expression on a single line. (Also I could or couldn't have been inspired by the syntax of switch expressions in C#.)

Have you tested this with tag overrides?

Yes, I tested tag overrides on both case values and expressions that follow them. To begin with, we already have switch statements, where tag overrides can't be used on case values directly (for example, case Tag:0: is incorrect syntax), but the said value can be wrapped in parentheses along with the tag override (so case (Tag:0): would be valid). I think the same rule should apply to switch expressions as well, especially since _: is reserved for default cases. Regarding the expressions that follow each case, they're parsed in a standard manner, with function expression() from sc3.c, which also handles tag overrides. Also all tags in both case values and expressions are checked for tag mismatch.

Y-Less commented 4 years ago

By expression functions I just meant:

stock GetSomeVar() gSomeVar;

Not local functions.

Daniel-Cortez commented 4 years ago

By expression functions I just meant:

stock GetSomeVar() gSomeVar;

Not local functions.

Then how would this be supposed to work?

stock Func() random(10);

Would Func() be an "expression function" returning a random number, or rather a normal function that returns nothing? Though, even if we try to implement something similar, maybe we should discuss this concept in a separate issue?

Daniel-Cortez commented 4 years ago

Also, regarding switch expressions, as I mentioned earlier, I already have a working implementation. Would anyone mind if I make a PR that demonstrates its functionality?