Big update to prettier-extension-monkeyc

I've posted about prettier-extension-monkeyc before, but I've added a bunch of new features that developers will probably like (well, I've been missing them, so maybe you have too).

The new features it implements for VSCode include:

  • Goto Definition. Point at a symbol, Ctrl/Cmd click, and it will take you to the definition. Or F12
  • Goto References. Right click on a symbol and select "Goto References". It will show you all the references. Or Shift-F12
  • Peek Definition/Peek References. Same as above, but in a popup window so you don't lose your place in the original document.
  • Rename Symbol. Right click on a local, function, class or module name, and select "Rename Symbol". It will rename all the references. It doesn't yet work for class members/methods.
  • Goto Symbol. Type Ctrl/Cmd-Shift-O and pick a symbol from the drop down (which has a hierarchical view of all symbols in the current file). This also appears as an outline across the top of the file.
  • Open Symbol By Name. Type Ctrl/Cmd-T, then start typing letters from a symbol name. A drop down will be populated with all matching symbols from anywhere in your project.

Older features include a prettier based formatter for monkeyc, and a monkeyc optimizer that will build/run/export an optimized version of your project.

[edit: My last couple of replies seem to have just disappeared, and the whole conversation seems to be in a jumbled order, so tldr: there's a new test-release at https://github.com/markw65/prettier-extension-monkeyc/releases/tag/v2.0.9 which seems to work for me on linux. I'll do more verification tomorrow, and push a proper update to the vscode store once I'm sure everything is working]

  • were replaced with "{}". So what's different here

    I don't know - but if it ever did that, it was a bug. What it's doing now is correct.

    function foo() as String {
      System.println("foo");
      return "bar";
    }
    
    (:inline)
    function nothing(msg as String) as Void {
    }
    
    function something() {
      nothing(foo());
    }

    Think about it. If that program doesn't print "foo" on the console, there's something wrong.

    The optimizer *does* know that certain system functions don't have side effects, and so it will remove those - and I intend to keep expanding the list. But anything I've not explicitly told it about, it assumes it can't drop the call.

  • UPDATE: renaming makeWebRequest to makeWebRequest2 fixes it

    This isn't actually the optimizer, it's the parser/formatter. If you just format your test case, it will break it in exactly the same way. I think the parser must be caching something from the first makeWebRequest and using a shared copy in the second. Just not sure exactly what yet...

    Actually, it *is* the formatter, but I just misread the original code. It looks like the formatter is doing two things wrong. First, its added a ";" after the second Method. That's just wrong, but I see where it's coming from. The other is that it's adding parentheses around the whole thing, which Garmin's parser doesn't like.

    Both of these happen deep inside some Prettier code for formatting javascript that I hijacked - which makes it harder to tweak. But I think I can fix them without too much trouble...

  • hmm, I checked it now, and indeed there are all kinds of "leftovers" of the same disabled log all over. For example if you can somehow add Object.toString (and of course what I really mean is anything that extend's Object :) because that's the most used function (I know it looks like 1 function but it's many...) in these disabled log statements. Here's the 1st one I found:

    l("onStart(" + (state != null ? state.toString() : "null") + ") mock:" + MOCK.toString() + ", antId:" + antId);

    and what's left of it in the generated code:

    if (state != null) {
    state.toString();
    }
    false.toString();
    We would really need macros, and then log(x) would be replaced by if (LOG) {x}
    What would be your recommendation about these? I have lots of these, and it seems to have a non-negligable effect on my code size. Should I nest all of them in such an if manually?
    Maybe you could add a macro? I mean an annotation: (:macro) it would imply :inline, and in case it ends up being removed it also removes the parameter. But it should be clever enough:
     
    var foo = "bar";
    log(foo + "2");
    vs
    log("bar" + "2");
    in the 1st case it has to keep var foo = "bar" because it's not in the macro, but in the 2nd case it shouldn't keep the variable that probably it would add in some cases.
    Now I know what'll you tell. What about:
    log(foo += "2");
    and to that i say: just like inlining is not allowed in some cases (or in other words in certain circumstances it's refusing to inline) it should also refuse to do the macro elimination in this case (with a warning of course so the developer can check what's wrong)
  • UPDATE: renaming makeWebRequest to makeWebRequest2 fixes it

    So I found the problem, but the *real* issue is that you didn't write what you thought you did.

    This is what you wrote (I've changed the formatting to match what I think you meant).

    responseCallback as 
      Method(responseCode as Number, data as Dictionary or String or Null) as Void or
      Method(responseCode as Number, data as Dictionary or String or Null, context as Object) as Void

    ie what you want is that responseCallback is a method with one of two possible signatures. But thats not how it's parsed.

    Its parsed as (note that the parentheses on the second line aren't valid syntax - I'm putting them in to show how things are grouped):

    responseCallback as Method(responseCode as Number, data as Dictionary or String or Null) as 
     (Void or Method(responseCode as Number, data as Dictionary or String or Null, context as Object) as Void)

    ie its a method which returns Void or a Method. And given this, the formatter gets confused (so yes, you found a real bug). But given what you *intended* it works fine. ie if you write it like this (and this time the parentheses are valid):

    responseCallback as 
     (Method(responseCode as Number, data as Dictionary or String or Null) as Void) or 
     (Method(responseCode as Number, data as Dictionary or String or Null, context as Object) as Void)

    It means what you want it to mean, and it doesn't break the formatter...

    [EDIT: Sorry, I'm wrong. If you replace Void with Null (or any other type), then what I wrote is correct; but for Void, Garmin's compiler parses it the way you wanted it. So I need to change the parser to match the way that Garmin's parser works AND fix the formatter to output valid code when you have a method that returns a method)]

  • When you inline functions that have :typecheck, then you should inline the annotation as well. I think there was some talk about it, now I have a specific problem with :typecheck(disableBackgroundCheck:

    (:foreground, :background)
    class MyApp extends Application.AppBase {
        (:background, :foreground, :background_app)
        var isForeground as Boolean;
    
        (:foreground) private var mField as MyField?;
    
        (:background, :foreground)
        function initialize() {
            var isForeground = !isRunningInBackground();
            self.isForeground = isForeground;
            if (isForeground) {
                initForeground();
            }
        }
    
        (:foreground, :inline, :typecheck(disableBackgroundCheck))
        hidden function initForeground() as Void {
            mField = new MyField();
        }
    }

    This compiles and runs without the optimizer, but fails when compiled with the optimizer:

    ERROR: fr255: bin/optimized/group002-debug/source/source/MyApp.mc:48,8: Value 'ExtHRMField' not available in all function scopes.
    ERROR: fr255: bin/optimized/group002-debug/source/source/MyApp.mc:48,8: Value 'initialize' not available in all function scopes.

    line 48 in the generated code is: mField = new MyField();

  • The "way I want it" is copy & pasted from the documentation of Communications.makeJsonRequest and Communications.makeWebRequest ;) the only thing I changed IMHO was to remove "Lang."

  • The "way I want it" is copy & pasted from the documentation

    Right - I already acknowledged the mistake. And its due to Garmin's bizarre parsing rules.

    • "Method() as Null or String" is a method that either returns Null or it returns String
    • "Method() as Null or Method() as Whatever" is a method that either returns Null, or it returns a Method that returns Whatever
    • "(Method() as Null) or Method() as Whatever" is either a method returning null, or its a method returning Whatever.

    but

    • "Method() as Void or String" is either a method returning Void, or its a String
    • "Method() as Void or Method() as Whatever" is a either method that returns Void, or its a Method that returns Whatever"

    (note that Void is the exception - any type other than Void parses the same as Null does)

    I guess I see how this happened. It makes no sense to return "Void or String", so they disallow including Void in a union of types. Sure - I get it. But most languages would parse consistently, and then report the error, rather than avoiding the error at parse time. Because otherwise you end up with stupid parsing rules that give completely different meanings to essentially the same syntax...

    Especially given that Void methods in monkeyc actually return null, just like Null methods do...

  • When you inline functions that have :typecheck, then you should inline the annotation as well

    I do propagate :typecheck(false), but not other variants - mainly because the optimizer only understands :typecheck(false). But yes, I agree it needs to copy the others too.

    UPDATE:

    Actually, I remember why I did that. If you put more than one :typecheck directive on a function, they're all ignored. Also, you can't (as far as I can see) combine "false" with "disableBackgroundCheck", even though you can combine disableBackgroundCheck and disableGlanceCheck by putting them in square brackets.

    So I'll change it so that any :typecheck annotations from the inlined function get propagated to the caller, unless the caller already has annotations. And then I'll do nothing.

  • found a nice bug. I changed a small function to be :inline. I got 3 warnings for all the 3 places it was used for

    So I got to the bottom of this. It first tries to inline timeF (twice) as part of the parameter to l(...), and (correctly) fails to do so, so generates the diagnostic and puts it in a table for later. Then it inlines l, and as part of that it puts a copy of the parameter in a variable initializer, something like:

    {
      var msg = "updateStatus: " +
                        (lastTime == null ? null : timeF(lastTime)) +
                        ", " +
                        timeF(nextTime);
      ...
    }

    and now, since msg is unused, it drops the side-effect free parts of that, leaving both calls to timeF as statements. So now they do get inlined. But it lost track of the fact that they're the same calls that failed to inline earlier, and so the original diagnostics remain. I've fixed the code so that it does keep track of them, and now the two diagnostics corresponding to those two calls are gone.

    But the third one comes from logR. In logR, the result is used, so it never ends up in statement context, and it never gets inlined. So the warning (correctly) remains.

    But finally, at the end, it notices that nothing can call logR, so it removes that entirely - and now there are no calls left to timeF, but the optimizer only removes unused calls once (ie it removes unused calls, and doesn't go back to see whether thats resulted in anything else becoming unused).

    At some point I'm going to change the dead code removal to start from the entry points, and figure out everything thats reachable - rather than just looking at all the code, and seeing what isn't referenced from anywhere. Once I do that, problems like this will go away. But there's a few more things I want to do first.

    But the real issue is that when logR *is* used, timeF won't be inlined there. So the diagnostic (for that one call) is correct.

    I've also added lots more to the list of side-effect free functions, and now all the "junk" code is gone...

  • v2.0.48 is out.

    This fixes a few minor bugs and missed optimization opportunities.

    It also adds a new optimizer pass to do limited copy propagation. If an expression is assigned to a local, and then the local's value is only used once, and the expression's value hasn't changed between the assignment, and the use, the assignment will be deleted, and the original expression will be substituted for the use of the variable.

    function foo(a, b) {
      var x = a + b;
      ... // nothing modifies a or b, or uses x
      return x;
    }

    becomes

    function foo(a, b) {
      ... // nothing modifies a or b, or uses x
      return a + b;
    }