wren-lang / wren

The Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.
http://wren.io
MIT License
6.86k stars 550 forks source link

[RFC] Switch Statement #956

Open clsource opened 3 years ago

clsource commented 3 years ago

It would require to add a switch keyword. But case and default can be replaced with other available symbols like | and else. It has implicit return.

Other alternatives to else as suggested by @CrazyInfin8 : _, default, otherwise. But it would probably be needed another keyword. If else is not preferred my vote would be for _ (underscore).


// simple
switch (value) {
    | 1 : true,
    | 2 : false,
    | else : Fiber.abort()
}

// multiple instructions per case
switch (value) {
    | "green": 
        myfunc.call()
        myfunc2.call()
        return true
    | 2: 
        myfunc2.call()
        return myClass.new()
    | else:
        return Fiber.abort()
}

// fall throught
switch (value) {
  | 1 : continue
  | 2 : true // if the value is 1 or 2 would return true
  | 3 : Fiber.abort()
  | else : false
}

Another way without adding a new keyword. if and is


// simple
if (value) is {
    | 1 : true,
    | 2 : false,
    | else : Fiber.abort()
}

// multiple instructions per case
if (value) is {
    | "green": 
        myfunc.call()
        myfunc2.call()
        return true
    | 2: 
        myfunc2.call()
        return myClass.new()
    | else:
        return Fiber.abort()
}

// fall throught
if (value) is {
  | 1 : continue
  | 2 : true // if the value is 1 or 2 would return true
  | 3 : Fiber.abort()
  | else : false
}

Another syntax variation


// simple
if (value) is {
    | 1 | : true
    | 2 | : false
} 

// simple (mutiple cases combined)
if (value) is {
 | [1, 2, 3, 4] | : true // value is 1 or 2 or 3 or 4
 | > 4 | : false
} else {
  Fiber.abort()
}

// multiple instructions per case
if (value) is {
    | "green" | : 
        myfunc.call()
        myfunc2.call()
        return true
    | 2 | :
        myfunc2.call()
        return myClass.new()
} else {
   Fiber.abort()
}

// fall throught
if (value) is {
  | 1 | : continue
  | 2 | : true // if the value is 1 or 2 would return true
  | 3 | : Fiber.abort()
} else {
  return false
}
mhermier commented 3 years ago

I think we should enforce the block after the ':' for multiline to follow the logic of the rest of the grammar. Single line statement is a bit confusing, but I would expect it to continue by default like would do an if statement and not return of the function, so that part of your implementation is a no go for me.

If possible I would prefer to have something more like this:

switch (value) {
  == 42: {
    System.print("The answer")
    System.print("to the universe")
    continue
  }
  .isEven: {
    System.print("This is odd")
    break
  }
  is String: System.print("How did we get there") // Implicit continue
  else System.print("Karamba")
}

Optionnaly, for ease it would be nice to have a Object::map(fn) { fn.call(this) } so we can write something like:

switch (value) {
  .map {|value| etc... } : etc...
  etc...
}
PureFox48 commented 3 years ago

I like the way that @mhermier has done this which is very natural and flexible. It's also easy to cater for ranges and multiple values:

switch (value) {
   in 1..6: {
      // blah
   }
   in [7, 10, 12]: {
      // blah
   }
}

However, my personal preference would be for the case clauses (whether blocks or single statements) not to fall through at all because, most of the time, this is what you want.

The break and continue statements are best employed to break out of or continue a loop enclosing the switch statement if there is one.

Instead, I'd introduce a second new keyword fallthrough which would do what it says on the tin. Go uses this word and it's unlikely to have been used much as an ordinary identifier in the past.

Unfortunately, switch probably will have been used a lot and, although I'd prefer to still use it, a possible alternative here would be when.

ChayimFriedman2 commented 3 years ago

Although I really like complicated pattern matching, I don't think it's a good fit for Wren, and even not generally for dynamic languages (although Python will have them in 3.10).

I vote for simple switch, but without fallthrough:

switch (a) {
  case 1: System.print("1")
  case "2":
    System.print("1")
    System.print("A string")
  default: System.print("Nothing of the above")
}

Though I'm not sure this is necessary.

PureFox48 commented 3 years ago

Although a simple switch (without fallthrough) is nice, I'm not sure there's enough value in it to be worth doing.

What I liked about @mhermier's approach is that we can do some simple pattern matching in a natural way.

If we don't bother with fallthrough at all (no great loss IMO), it might even be possible for the compiler to just turn it into an if/else if/else ladder.

Apart from languages which don't support ranges, fallthrough isn't often needed in my experience and, if it is, you could do stuff like:

switch (value) {
    in 1..2: {
        if (value == 1) doSomething.call()
        doSomethingMore.call()
    }
    // etc
}
mhermier commented 3 years ago

In that case I suggest that the keyword fallthrough is added, and must be the last expression in the block. Having switch nested in loops is pretty frequent and reusing continue here would be to much impractical.

PureFox48 commented 3 years ago

Go also insists on fallthrough being the last statement in a case clause. In other words, it can't be conditional which is presumably to make the implementation easier.

mhermier commented 3 years ago

Well it can be augmented in a second revision. This is a bare minimum so it is working.

clsource commented 3 years ago

I personally struggle a little bit with the word fallthrough since it has many double letters and gh position. How about pass?

PureFox48 commented 3 years ago

The problem is that fallthrough is the commonly accepted term for this behavior. pass just doesn't have the same meaning - it's used in Python to mean 'do nothing' - and it's a commonly used identifier in any case.

As well as Go, fallthrough is used by Swift and by C++ 17 (as an attribute). I couldn't find any alternatives at all.

I suppose we could spell it fallthru or use a made-up word such as nextcase but neither look right to me. Perhaps if you think of it as two words, which is really what it is, then it'll seem more palatable.

CrazyInfin8 commented 3 years ago

I have some ideas I want to share.

switch (expr) {
  |  _: System.print("value is a different number (This is default case)")
  // Alternative default case
  | default: System.print("alternative default case")
  |  1, 2, 3: System.print("value is 1, 2, or 3")
  |  4: System.print("value is 4 but will fall through to next case")
  |> 5: System.print("value may be 4 or 5, because 4 can fall though")
  |  6:
    System.print("this case...")
    System.print("has multiple")
    System.print("instructions!")
}
clsource commented 3 years ago

I like the idea of using |> to indicate falltrought. My preference would be using it at the right position since using if before can be confused with "greater than".

Another option instead of default or underscore (_). Would be using true since it will always be true regardless of the case.

switch (expr) {
  |  1, 2, 3 : System.print("value is 1, 2, or 3")
  |  4      |> System.print("value is 4 but will fall through to next case")
  |  5      : System.print("value may be 4 or 5, because 4 can fall though")
  |  6      :
    System.print("this case...")
    System.print("has multiple")
    System.print("instructions!")
 | true : System.print("This is the default case")
}

This more or less can be translated to ifs/elses

  if (expr == 1 || expr == 2 || expr == 3) {
    return System.print("value is 1, 2, or 3")
  }

if (expr == 4 || expr == 5) {
  if (expr == 4) {
   System.print("value is 4 but will fall through to next case")
  }
  if (expr == 5) { 
     return System.print("value may be 4 or 5, because 4 can fall though")
  }
  return 
}

  if (expr == 6) {
    System.print("this case...")
    System.print("has multiple")
    return System.print("instructions!")
  }

 return System.print("This is the default case")
}
PureFox48 commented 3 years ago

What syntax would be used to express the following cases:

  1. expr > 6
  2. expr != 2
  3. expr in 1..50
  4. expr.isEven
  5. expr is String
PureFox48 commented 3 years ago

Regarding the placing of the default case first in the switch, it's something I've never personally liked even though there may be a performance benefit if the default case is much more common than the other cases.

But would it even be possible in a language such as Wren with a single pass compiler?

CrazyInfin8 commented 3 years ago

I think one of the things I hoped switch statements should do was optimize itself for list of numbers so it isn't checking every option like an if-else ladder. because of that, I was also thinking that we should only hold values in cases instead of expressions (similar to how it is done in C).


I'm unfamilliar with the bytecode for now so this is more my concept rather than a what the VM can actually do right now but...

  1. The compiler makes a map for each case value which points to a set of instructions.
  2. If the next case is a fallthrough, then the current case could also make a reference to the next cases and it's instructions (could maybe pose a challange for a single pass compiler but may not be the only way to get the next set of instructions)
  3. If the expression passed to the switch statement matches a case's value, execute the instructions. Or if the expression passed does not match any of the cases but a default case was passed, then run the instructions the default case points to.
  4. if the case selected references any fallthough cases, select that case and execute it's instructions. repeat this step if that case references more fallthough cases.

I guess it may be limiting to not put expressions in a switch statement cases though so maybe we could also have a match statement or switch statement without an expression passed to it. This could behave more like if-else ladders

something like:

// checks for first truthy value
switch {
  |  expr > 5: // do stuff
  |  expre.method: // do other stuff
}
// maybe might reduce ambiguity
match {
  |  expr > 5: // do stuff
  |  expre.method: // do other stuff
}

Also, in my opinion, I disagree with each cases interacting with switch statements. I think operators should still have expressions on both sides, type checks should be as they are, and methods should still contain their receivers even in switch cases.

so none of these would work:

switch (expr) {
  |  >= 5:
  |  .isOdd:
  |  > 7:  // thus reducing ambiguity with my previous example
  |  is String:
}

Lastly, I don't like this very much either

Another option instead of default or underscore (_). Would be using true since it will always be true regardless of the case.

It may not be often that people are comparing bools in a switch but it still could be possible and this suggestion brings ambiguity,

var aBool = false
switch (aBool) {
  |  true: System.print("aBool is definitely true")
  |  false: System.print("aBool is definitely false")
  |  5: System.print("aBool isn't even a bool")
}
PureFox48 commented 3 years ago

Adding a switch statement was in fact first discussed back in #352 and I think the conclusion then was that it needs to go some way towards pattern matching to be worth doing at all.

Although one can argue about the details, I did feel that @mhermier's variant satisfied that criterion but the other variants discussed did not though I'm grateful to @clsource for giving the matter another airing.

It seems to me that the other variants can't really handle pattern matching without repeating the control variable in each case which negates one of the benefits of having a switch statement in the first place. The only benefit it really leaves us with is nicer, more readable syntax and the possibility of fallthrough compared to an if/else ladder.

If some sort of 'jump table' were possible on the lines you suggest, then that would be another reason why a switch statement might be advantageous though I don't know how feasible that would be in Wren.

mhermier commented 3 years ago

I don't understand the point of the | as a separator, parsing wise at best it serve as a separator for blocks but it fells out off place for some reasons...

CrazyInfin8 commented 3 years ago

I don't understand the point of the | as a separator, parsing wise at best it serve as a separator for blocks but it fells out off place for some reasons...

I think the | character as a separator seems similar to saying "or", so something like value1 | value2 is like value1 or value2. The syntax suggested there is the syntax used in Rust.

In my opinion (and this is just an opinion fueled by my bias so it can be disregarded), I don't want wren to resemble Rust as it's sometimes pretty awkward and Rust traumatizes me ;_;

CrazyInfin8 commented 3 years ago

Also I might add, having the pipe character precede case values may make it easier for the single pass compiler to tell that this is a start of a new case instead of just having variable names, lists, and maps followed by an arrow. Again just guessing as I haven't dug too deep into wren source code.

mhermier commented 3 years ago

In all the case _ as default is not an option since it is a valid member variable name.

CrazyInfin8 commented 3 years ago

In all the case _ as default is not an option since it is a valid member variable name.

Ah true, I guess if we are putting expressions in the switch statement, might have to use else or default for the default case. In my opinion (gah I'm so opinionionated), I prefer default especially if the default case is not restricted to the last case because putting else sounds like the last case.

clsource commented 3 years ago

Other languages like Elixir have separate simple case conditions from pattern matched ones

https://elixir-lang.org/getting-started/case-cond-and-if.html

case is useful when you need to match against different values. However, in many circumstances, we want to check different conditions and find the first one that does not evaluate to nil or false. In such cases, one may use cond:


iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
"But this will"

This is equivalent to else if clauses in many imperative languages (although used much less frequently here).

If all of the conditions return nil or false, an error (CondClauseError) is raised. For this reason, it may be necessary to add a final condition, equal to true, which will always match:

iex> cond do
...>   2 + 2 == 5 ->
...>     "This is never true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   true ->
...>     "This is always true (equivalent to else)"
...> end
"This is always true (equivalent to else)"

Finally, note cond considers any value besides nil and false to be true:

iex> cond do
...>   hd([1, 2, 3]) ->
...>     "1 is considered as true"
...> end
"1 is considered as true"

The idea of using the pipe was taken from F#

https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/pattern-matching

let printColorName (color:Color) =
    match color with
    | Color.Red -> printfn "Red"
    | Color.Green -> printfn "Green"
    | Color.Blue -> printfn "Blue"
    | _ -> ()
clsource commented 3 years ago

Some ideas. Maybe two separate statements can be is and match.

promoting the existing keyword is to be part of an if.


if is (value) { // another possible way would be if (value) is {}
  | 1 : System.print("Case 1")
  | 2 :> System.print("Fallthough") // to case 3
  | 3 : System.print("Case 3.")
  | 4, 5 : System.print("Case 4 or 5")
  | true : System.print("considers any value besides null and false to be true")
} else {
  // The default case
}

match (value) {
  | in 1..6: System.print("Value is in range 1..6")
  | in [7, 10, 12]:> System.print("Value is 7 10 or 12") // fallthough to the next case
  | true: System.print("considers any value besides null and false to be true") 
} else {
  // default
}
PureFox48 commented 3 years ago

Given that we don't want to add more syntax to Wren than we need to, I think that switch (or whatever we decide to call it) needs to be a single statement and it needs to somehow combine traditional usage with simple pattern matching otherwise it's just not worth doing.

An if/else ladder can after all deal with anything except falling through to the next clause - it's only problem is that it lacks elegance.

PureFox48 commented 3 years ago

OK. following my previous post, I've tried to come up with a design which we might all be able to unite around by going back to traditional switch/case syntax, with no automatic fallthrough, but with default replaced by else to save a keyword. Blocks would not be required for the cases which could be one of the following:

  1. A single value.

  2. A list of values.

  3. A range of values.

  4. A type.

  5. A boolean expression.

Option 5 would only be possible if the switch expression were a variable (no problem in practice) which would itself feature in the boolean expression. Other variables which were in scope could also be included.

Fallthrough would be catered for by a new (and spellable) fallthru keyword which would have to be the final statement in a case.

If break and/or continue were present in the code, they would refer to an enclosing loop, not to the switch statement itself.

Here's how it would look:

switch (expr) {
    case 1 :
        System.print("A single value.")     
    case 2, 3, 5 :
        System.print("One of a list of values.")
    case 6..10 :
        System.print("One of a range of values.")
    case is Num :
        System.print("The type of the expression.")
    case expr <= 0 || expr == 4:
        System.print("A boolean expression.")
        fallthru // fall through here
    else :
        System.print("Default case.")
   }

The code would simply iterate through the cases until it found the first match. Cases could therefore overlap each other. It's possible that the compiler might be able to implement this as an if/else ladder though fallthrough might be tricky.

mhermier commented 3 years ago

Don't use acronym, autocompletion is a thing since more than 10 years... Also, I really don't like that each case is specific, it implies special handling in the compiler for each specific usage, aka code explosion in the compiler. And last case is not realistic at all. If expr has to be repeated, it is a simple if.

PureFox48 commented 3 years ago

I assume when you say: "don't use acronym" you mean don't use fallthru - use fallthrough and rely on auto-completion if you have trouble spelling it. I'm not going to argue with that as I don't like the former anyway.

The problem we have with option 5 is that, technically, option 1 is also a boolean expression which evaluates to true unless it happens to be false or null. Unless 'expr' is repeated in option 5 the compiler may therefore have a problem in distinguishing between the two. It also enables the usage of && and || and addresses the dislike some commenters have of cases beginning with an operator.

It seems to me that some 'code explosion' is inevitable if traditional and pattern matching approaches are to be combined in a single statement.

mhermier commented 3 years ago

My solution is a little bit more elegant, you only have to trick the compiler to dupe the value on each case, and start evaluating from there. While it seems to be a lot of case, in fact there is only the general evaluation of an implicit case value expanded with the explicit expression. The rest is boiler plate to control the execution flow.

With your solution each case must be parsed and compiled differently. And this is were you solution ends in code explosion.

I don't say my code is better at all. The importance is also in the ease of writing, learning and other considerations. But my solution should have less impact with more extensibility. But this not always the way to go. ex I really don't like the new tag system, I would have preferred python decorators, and a proper type annotation, or at minimum that they follow the wren syntax to avoid the introduction of a DSL...

cxw42 commented 3 years ago

Misc. thoughts:

clsource commented 3 years ago

Syntax: I prefer keywords to | since Wren mostly uses words rather than symbols outside of expressions.

Agree

"fallthrough": the Raku programming language uses "proceed"

Nicer alternative

OK. following my previous post, I've tried to come up with a design that we might all be able to unite around by going back to traditional switch/case syntax.

Considering the points made by @cxw42 I think the following syntax can be using only two new keywords: switch and proceed. This syntax was inspired by the @mhermier syntax.

switch (value) {
    (== 42) : // implicit value
      System.print("The answer")
      System.print("to the universe")
      proceed // fallthrough

    (value.isEven):  // or can be used explicit
      System.print("This is odd")

    (4):
     System.print("How did we get there")
 } else {
   System.print("Default case.")
 }
cxw42 commented 3 years ago

(By the way, thank you all for letting me join you in this discussion! I enjoy language design :) .)

I agree we are getting closer.

Syntax question


switch (value) {
...
    (value.isEven):  // or can be used explicit

@clsource is value a reserved word here? It is not currently in Wren, as far as I can tell, but rather a convention (ref). Would it need to be reserved for something like this to work?---

switch(2+2) {
  (value.isEven) System.print("yes")
}

Syntax comments

cxw42 commented 3 years ago

Semantic+syntactic comment

(== 42) : // implicit value

This looks like it would be tricky to parse. If nothing else, permitting (==42) might suggest that (42==) would be valid. I worry we would either break symmetry by forbidding the latter, or greatly complicate the expression parser.

Raku has a nice way to handle this, and I would like to suggest it here for consideration. This doesn't handle all possible cases yet, but I'd like your feedback if it might make sense for Wren, considered at a high level.

The parser knows how to handle the expressions, and each expression knows how to check if that case matches.

Two notes:

Let me know what you think! If you would like more detail, I would be happy to provide it, or see the linked pages. E.g., a comparison with JavaScript is here.

mhermier commented 3 years ago

I don't understand the point of adding '()' or || around the expression. I mean:

switch(2+2) {
  == 4: ...
  == "four": ...
  is Num: ...
  in 1..10: ... // Maybe it is not a thing, but it also show it is transparent to future binary operators
  ~~ Regexp.new("my fancy regexp") : ...
  ~~ Fn.new { |v| v<=1 || v>=5 }: ...
  ~~ Fn.new { |v| true }: ...
}

Is clear and not ambiguous, and can work even if the author of a library didn't thought at that usage. Maybe the point is to simulate adding case or allow multiline without adding a block... I don't get it.

mhermier commented 3 years ago

An idea, we need to discuss after this issue is resolved that I think comes from python or perl, would be to have the result of the switch to be in a meta variable like _, so it could be used inside the switch block as if a regular switch would be translated as:

{
  var _ = (expr)
  switch (_) {
    is Num: System.print(_) // example of usage
  }
}

If possible I would avoid _ character, since it might be needed to access method objects in the future unless we change the signature system...

PureFox48 commented 3 years ago

A few quick comments:

"fallthrough": the Raku programming language uses "proceed"

Although it's a nice enough word, I have reservations about proceed.

It sounds to me like it's trying to break out of the switch statement altogether and carry on with the rest of the code rather than moving to the next case. Also, from a backwards compatibility viewpoint, it's much more likely to have been used as an ordinary identifier than fallthrough.

I would rather not list the default case as a separate block, for the sake of fallthrough-to-default.

Yes, I think supporting fallthrough rules out the possibility of having a separate else block.

I don't understand the point of adding '()' or || around the expression.

Neither do I.

I've always liked @mhermier's approach here and, unless I'm missing something, I don't think it would necessarily mean that we have to use blocks for multi-line cases which I know some commenters don't like.

There is an binary operator called "smartmatch", spelled ~~. It checks whether the left side matches the right side.

That's a new concept to me - I don't know Raku at all. I'll need to think about it before commenting further.

PureFox48 commented 3 years ago

Well smartmatch is clearly a powerful concept from what is a terse but very expressive language (Raku).

To put something like that into Wren, I think you'd need to:

  1. Add the smartmatch binary operator ~~ (and/or an equivalent match method).

  2. Provide a default implementation in the Object class, perhaps based on object equality.

  3. Override the default implementation in each built-in class (and, where needed, in user-defined classes) as appropriate.

To use it effectively without having to look stuff up all the time, I think you'd need to try and remember how the built-in classes handle pattern matching which might be a high cognitive overhead for a simple language such as Wren.

Although I wouldn't rule this out for the future, I think for now we need to come up with something a bit simpler and more immediately intuitive.

Looking at @mhermier's latest example, I'm quite happy with how he handles the first 4 cases (though I think I might need to open a separate issue about extending the usage of the in operator).

Stuff like this is also easy:

switch(expr) {
    > 6 : ...

    != 7 : ...

    .isEven : ...

    in [1, 3, 5]  : ... // potentially easy anyway

    == true : ... // to catch anything else rather than using 'else'
}

The key point is that each case always begins with an operator which relates back to the switch expression.

What isn't easy with this syntax are general boolean expressions (involving && and || say) where you really need to pull in the switch variable and possibly other variables for the case to make sense. Unless someone can think of a way to handle these, perhaps it would be best to just be content with simple patterns for now and maybe add more powerful pattern matching later.

mhermier commented 3 years ago

About enforcing block, thinking at it again, I think we need to see it in action. I would prefer if we stay on the safe site, so it pairs with if syntax. I guess it is required to avoid a confusion with expression starting with unary left operator, and simplify the compiler.

mhermier commented 3 years ago

@PureFox48 I found this default case verbose and error prone, since if the expression evaluate to false it will not be triggered.

PureFox48 commented 3 years ago

Yes, you're right about the default case. I was trying to find a way which begins with an operator to be consistent with the rest of the syntax but it doesn't really work.

What would you suggest - sticking with else: which wouldn't require a new keyword?

mhermier commented 3 years ago

I think I agreed with that a while ago. Thought I wonder if user would be confused if default would happens in middle of a selection and catch all prematurely?

This can be solved by moving the else outside of the selection block. While I don't have nothing against it, maybe it is a little bit too much confusing teaching/usage wise, in particular in interaction with if.

PureFox48 commented 3 years ago

Well, as @cxw42 and I said earlier, I don't think we can move else outside of the switch block because of the need to cater for fallthrough.

To avoid potential confusion, perhaps we could mandate that the else case, if there is one, always comes last in the switch.

As far as enforcing blocks is concerned, do we need one if the case ends with a colon?

This, after all, seems to be how C identifies cases without using a block though admittedly it has the case keyword to help it. We'd just have an expression which begins with an operator (or else).

mhermier commented 3 years ago

Well I think it is required because potentially, unless I'm mistaken, the grammar would become LL(N). In the context of a multiline , : would need to be searched to determine if the expression is a case statement starting with a binary expression or a real statement starting with a unary expression. ( will not help obviously. | can be working though distinguishing with bit-wise OR is potentially problematic I guess.

cxw42 commented 3 years ago

I thought of a simplification late last night. No smartmatch, but yes parentheses:

No parser tweaks required because paren vs. lbrace tells you which case you are in, and no run-time overhead.

This doesn't handle literal ranges, which is a downside. It would be easy to check for ranges at runtime, but at the cost of a type check. Maybe the compiler could special-case literal ranges, but then the special case would have to handle things like (foo+2*bar)...(bat&3). I don't know how easy that is in the compiler.

cxw42 commented 3 years ago

Re. colons, I really think we should not add them if we don't absolutely have to --- @ruby0x1 said at https://github.com/wren-lang/wren/pull/952#issuecomment-815505251 that the syntax for blocks was not open for change right now (if I understood correctly). Parens and braces do the job using elements Wren already has, even if they look odd compared to other languages :) .

cxw42 commented 3 years ago

mandate that the else case, if there is one, always comes last in the switch.

Agreed

mhermier commented 3 years ago

There is no real confusion here, the grammar interpretation of the block here change because of the keyword switch. So it is safe to use : here, even if it looks a lot like a map.

@cxw42 using '(' since they can start a regular expression does not solve anything. Even considering | or [ or any other operator does not really seems to help. So unless I miss something the solution is either keyword or non ambiguous grammar to preserve LL(1) or LL(2).

ChayimFriedman2 commented 3 years ago

I'm in favor of C-style switch with Raku semantics.

That is,

switch (x) {
  case 1..5:
    System.print("x is in the range 1 to 5")
    break
  case "":
  case "s":
    System.print("Something")
    break
  case Num:
  case String:
    System.print("String or Num")
    break
  default:
    System.print("Other")
    break
}

In my experience, fallthrough is almost always not needed. If it is, you can always fall back to if-else. So I tend towards mandating the break, except for adjacent cases. If you think differently, we can allow fallthrough just like C - by omitting the break.

Regarding requiring a default clause, I don't think this is a good idea. Generally I do think we should test for exhaustiveness, but since we don't have pattern matching, I don't think we actually can and requiring default will just create noise.

I can think of two improvements over C's switch:

  1. If the case is one-line, imply the break:
    switch (x) {
    case 1..5: System.print("x is in the range 1 to 5")
    case "":
    case "s":
    System.print("Something")
    break
    case Num:
    case String:
    System.print("String or Num")
    break
    default: System.print("Other")
    }
  2. Guard clauses:
    switch (x) {
    case 1..10 if (x % 2) == 0:
    // ...
    break
    }

For the ~~ operator (or whatever it'll be named), we can implement it as the following (preferably in C when possible):

class Class { // Actually, `Class` methods can be implemented only in C, but you got the idea
  ~~(e) { e is this } // By default, classes matches all of their instances, so `case Num` matches all numbers
}

class Bool {
  ~~(e) { e == this }
}
class Num {
  ~~(e) { e == this }
}
class Null {
  ~~(e) { e == null }
}
class String {
  ~~(e) { e == this }
}

class Range {
  ~~(e) { (e is Num && contains(e)) || (e is Range && e == this) }
}

class List {
  ~~(e) { any {|element| element ~~ e } }
}
cxw42 commented 3 years ago

@mhermier I am trying to understand your points but am having trouble. Sorry about that!

I think the body of a switch can be expr, stmt, expr, stmt... , and Wren's grammar is simple enough that we only need one token of lookahead.

Could you give me a code example or something else to help me understand where we would need extra keywords or lookahead? If not, I could try writing a proof of concept, but it might take me a while :) .

mhermier commented 3 years ago

@cxw42 what I mean:

  switch(val) {
    /* without braces */
    long_check_expression: ... /* actual code follows*/
//  ^                     ^
//  |                     Parsing has to go till there to decide, that long_check_expression is a check expression and
//  |                     not a non brace expression, but it is too late to reinterpret long_check_expression
//  Adding a keyword here can solve the ambiguity, this is what C and most language do

  /* with braces*/
  long_check_expression: ... /* a one liner*/
  long_check_expression: {
   ... /* actual code follows*/
  }
  // There is no ambiguity here since there is no mix between `cases` and regular code
}

I just thought about an extra complication, handling variable declaration in this seems dirty because of fall-through. We might need to ban them.

mhermier commented 3 years ago

@ChayimFriedman2 I don't like implicit ~~ as a solution for switch, while perfectly functional, it remove flexibility for the user, and make user depends on the implementor of external code.

ChayimFriedman2 commented 3 years ago

Use can always create class Pattern and use full pattern matching. This is just simpler IMO.