Skip to content

Pattern matching with alternatives is compiled to "throw null" with Scala.js #9268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
devkat opened this issue Jul 1, 2020 · 13 comments
Closed

Comments

@devkat
Copy link

devkat commented Jul 1, 2020

Minimized code

https://github.com/scala-js/scala-js/blob/master/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/FormatterTest.scala#L626

  @Test def leftAlignWithoutWidthThrows(): Unit = {
    for (conversion <- "bBhHsHcCdoxXeEgGf%") {
      val fmt = "ab%-" + conversion + "cd"
      val arg = conversion match {
        case 'e' | 'E' | 'g' | 'G' | 'f' => 5.5
        case _ => 5
      }
      val e =
        expectFormatterThrows(classOf[MissingFormatWidthException], fmt, arg)
      assertEquals(fmt, "%-" + conversion, e.getFormatSpecifier)
    }
  }

Output

Scala.js IR:

  @hints(1) private def leftAlignWithoutWidthThrows$$anonfun$1;C;V(conversion: char) {
    val fmt: java.lang.String = (("ab%-" +[string] conversion) +[string] "cd");
    val arg: double = matchResult1[double]: {
      val x1: char = conversion;
      throw null
    };
    val e: java.util.MissingFormatWidthException = this.expectFormatterThrows;Ljava.lang.Class;Ljava.lang.String;Lscala.collection.immutable.Seq;Ljava.lang.Throwable(classOf[java.util.MissingFormatWidthException], fmt, mod:scala.runtime.ScalaRunTime$.genericWrapArray;Ljava.lang.Object;Lscala.collection.immutable.ArraySeq(java.lang.Object[](arg))).asInstanceOf[java.util.MissingFormatWidthException];
    mod:org.junit.Assert$.assertEquals;Ljava.lang.String;Ljava.lang.Object;Ljava.lang.Object;V(fmt, ("%-" +[string] conversion), e.getFormatSpecifier;Ljava.lang.String())
  }

JavaScript:

  leftAlignWithoutWidthThrows__V() {
    const len = $uI("bBhHsHcCdoxXeEgGf%".length);
    let i = 0;
    while ((i < len)) {
      const index = i;
      $uI("bBhHsHcCdoxXeEgGf%".charCodeAt(index));
      throw null
    }
  };

Expectation

This (equivalent?) match expression:

      val arg = conversion match {
        case _: ('e' | 'E' | 'g' | 'G' | 'f') => 5.5
        case _ => 5
      }

is compiled to the following IR:

    val arg: double = matchResult1[double]: {
      val x1: char = conversion;
      if ((((int)'e') ==[int] ((int)x1)) || (((int)'E') ==[int] ((int)x1)) || (((int)'g') ==[int] ((int)x1)) || (((int)'G') ==[int] ((int)x1)) || (((int)'f') ==[int] ((int)x1))) {
        return@matchResult1 5.5d
      };
      return@matchResult1 5.0d
    };

and the following JavaScript code:

const arg = ((((((arg1 === 101) || (arg1 === 69)) || (arg1 === 103)) || (arg1 === 71)) || (arg1 === 102)) ? 5.5 : 5.0);
@smarter
Copy link
Member

smarter commented Jul 1, 2020

@sjrd what could cause the scalajs backend to emit "throw null" here?

@sjrd
Copy link
Member

sjrd commented Jul 1, 2020

@smarter
Copy link
Member

smarter commented Jul 1, 2020

Ah I see :). Would you recommend a straight port of genMatch from the scala 2 plugin? According to https://github.com/scala-js/scala-js/blob/c0b8f67c91a0c94796c8b8df94119c60902199e9/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala#L3434-L3487 there's some complexity related to handling guards, does that also apply to dotty?

@sjrd
Copy link
Member

sjrd commented Jul 1, 2020

I do recommend a straight port of genMatch from the Scala 2 plugin, minus all the LabelDef-related stuff. There should be no complexity to handling guards in Dotty since patmat already takes care of emitting Labeled blocks, which are directly translated to Scala.js Labeled blocks.

@smarter
Copy link
Member

smarter commented Jul 1, 2020

Great thanks, @devkat: feel free to give this a go :).

@devkat
Copy link
Author

devkat commented Jul 1, 2020

case 'e' | 'E' | 'g' | 'G' | 'f' => 5.5
CaseDef(Alternative(List(Literal(Constant(e)), Literal(Constant(E)), Literal(Constant(g)), Literal(Constant(G)), Literal(Constant(f)))),EmptyTree,Literal(Constant(5.5)))

I didn't realize at first that the working code is a simple type test for a union type. Apparently this is straightforward enough to emit an If tree in PatternMatcher:

case _: ('e' | 'E' | 'g' | 'G' | 'f') => 5.5
CaseDef(Typed(Ident(_),TypeTree[OrType(OrType(OrType(OrType(ConstantType(Constant(e)),ConstantType(Constant(E))),ConstantType(Constant(g))),ConstantType(Constant(G))),ConstantType(Constant(f)))]),EmptyTree,Literal(Constant(5.5)))

I'll try to find the time to look into genMatch and port it to Dotty.

devkat added a commit to devkat/dotty that referenced this issue Jul 2, 2020
@devkat
Copy link
Author

devkat commented Jul 3, 2020

Now the JS looks good at first glance, but strangely the match expression is never true and the result is always 5.0:

      const arg = ((($bC(arg1) === $bC(101)) || (($bC(arg1) === $bC(69)) || (($bC(arg1) === $bC(103)) || (($bC(arg1) === $bC(71)) || ($bC(arg1) === $bC(102)))))) ? 5.5 : 5.0);

@sjrd
Copy link
Member

sjrd commented Jul 3, 2020

That code is trying to compare boxed chars with an identity comparison. That will always be false. In a switch, the scrutinee and the cases must all be converted to Ints.

Also I encourage you to look at the IR as the point of reference. Not at the generated JS.

@devkat
Copy link
Author

devkat commented Jul 3, 2020

The IR looks like this:

      if ((x1 === 'e') || (x1 === 'E') || (x1 === 'g') || (x1 === 'G') || (x1 === 'f')) {
        return@matchResult1 5.5d
      } else {
        return@matchResult1 5.0d
      }

I'm not sure if the boxed chars comparison is in the scope of this ticket, but I'll take a closer look.

@sjrd
Copy link
Member

sjrd commented Jul 3, 2020

=== is not the correct operator to use here. The correct one would be Int_==, after having converted the chars into ints using UnaryOp(UnaryOp.CharToInt, c).

devkat added a commit to devkat/dotty that referenced this issue Jul 5, 2020
devkat added a commit to devkat/dotty that referenced this issue Jul 5, 2020
@devkat
Copy link
Author

devkat commented Jul 5, 2020

Here's a first draft:

https://github.com/devkat/dotty/blob/scalajs-tests/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala#L3128-L3782

This is basically a copy of the genMatch method and its dependencies from GenJSCode with minor modifications to make it compile.

Some open issues:

@sjrd
Copy link
Member

sjrd commented Jul 5, 2020

Can you already submit a PR with what you have? It's easier to discuss the details via line comments in a PR.

@devkat
Copy link
Author

devkat commented Jul 6, 2020

Done – thanks a lot in advance for reviewing!

sjrd added a commit to sjrd/dotty that referenced this issue Jul 22, 2020
This implementation has been ported from the scalac backend, but is
much simpler because it does not have to deal with hacks around
scalac's LabelDefs.
@sjrd sjrd closed this as completed in bb5a93b Jul 23, 2020
sjrd added a commit that referenced this issue Jul 23, 2020
Fix #9268: Scala.js: Add support for switch-Matches.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants