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]

  • I don't know if the bug is new. It could be there forever, I didn't have the try/catch there before, just added it a few minutes before I reported the problem. What I was saying is that I was expecting the "can't be inlined" warning I was familiar with and instead I saw this new "phenomenon".

  • import Toybox.Lang;
    import Toybox.Activity;
    import Toybox.Time;
    
    class Test {
        protected var mResult2 as String or Numeric or Null = null;
    
        public function compute() as Lang.Numeric or Time.Duration or Lang.String or Null {
            var extHr = computeExtHR();
            if (mResult2 != null) {
                extHr += " " + mResult2;
            }
            return extHr;
        }
    
        typedef AlphaNumeric as String or Number;
    
        (:inline)
        hidden function computeExtHR() as AlphaNumeric {
            var result;
            var result2 = mResult2;
            mResult2 = result2;
            return result;
        }
    }
    

  • Thanks - that made it easy to debug. As I mentioned, I use some heuristics to break accesses into groups to try to find some profitable variables to extract, even if I can't do all of them. But the groups were limited to basic-block boundaries in the control flow graph (nothing to do with { } in the source - just wherever a branch may occur). And in your example, the first block has a read, then a write, then another read, and the second block has a read. So the choices were to do the first three accesses, to do the last access, or to do all 4. And none of those is profitable.

    So I just did a post-processing step that breaks a block any time it finds a read and a write of the same variable. That fixed your example, and over all of my test projects PRE now does much better. But looking at the before/after optimized code, Im seeing some a few places where it was doing better before the change. So I'm going to debug those... but overall this looks pretty good.

  • Glad this could help.

    I am seeing another "issue". This is not an error more like "wishful thinking" from my side. And the "wish" is that if I do some "manual" optimizations then the optimizer should produce code at least as good as it would without my manual optimizations. I'll give you an example, but probably there are many more types it could be applied to.

    Sometimes you have a function f, that uses some literals and it calls another function g that is inlined and g also uses some literals. So my manual optimization would be to pass the variable I created to replace the literal from f to g.

        public function f() as Void {
            some code where the following literals are used:
            1 - once
            2 - twice
            3 - three times
            4 - 4 times
            7 - once
            g();
        }
    
        (:memory16K, :inline)
        hidden function g() as Void {}
        (:memory23K, :inline)
        hidden function g() as Void {
            some code where the following literals are used:
            1 - once
            2 - once
            3 - twice
            4 - 3 times
            7 - 3+ times
        }

    The numbers (how many times the literals are used) are not really important, I think you get the idea. If any Number would be used at least 3 times in any of the functions I would probably create a variable like: var three = 3; and use that in the same function. If I already created a variable that is useful in g then maybe I would notice and pass it (no matter how many times it's used in g), etc...

    To make things a bit more interesting g is doing something else (it's usually but not always: nothing) for some devices.

    Now the thing is that if I would only care about either one of the 4 combinations:

    memory16K, no-optimizer vs memory32K, no-optimizer vs memory16K, optimizer vs memory32K, optimizer

    then I can test each variation and do what's best for that combination. However I care for at least all the different devices with the optimizer (for obvious reasons, but to give some more context why even that is not always trivial, you might think that it really only matters for the devices with the least amount of memory, but its not always good enough: until about a month ago I did most of my optimizations in order to be able to squeeze as much functionality to devices with 16K, and on the way I was also gaining some free space for 32K devices, but then I reached a point where even the 32K devices started to have problems with the memory, so I added a new split: memory16K, memory16Kplus, memory32K, memory32Kplus)

    And to make things even "worse": I like to be able to debug as much as possible without the optimizer so I'd rather not pass these literal variables because that makes the non-optimized version worse sometimes (when there are less than 3 usages in either f or g or both and only have three or four usages in f+g combined).

    So my wish (and I know it's a big one, but hopefully PRE will be able to help in some cases) is to be able to keep the code "manually optimized" for the compiler without prettier-optimizer, but that prettier-optimizer would be clever enough to do these "automatic optimizations".

    In my above example, 7 is something that is better not to pass without prettier but better to pass with.
    I'll also add, that in my case I already have var seven = 7; in g(), so maybe that makes it a bit less obvious for the optimizer to spot it. I didn't try it how it would score if I just had the literal 7-s.

  • v2.0.39 is out.

    This includes a pretty much fully rewritten optimizer, using a type analysis pass to enable better optimizations.

    I've put a lot of effort into verifying that this doesn't break anything, but it is a big change. If anything breaks, please report it (here, or in the GitHub issues), with as much detail as you can.

    Most of the optimizer changes are listed here.

  • I compared 2.0.38 and 2.0.39 on f3, f6 with my app and in both cases the new version gives worse results:
    f3: 2.0.38: code: 5075, data: 2833 ; 2.0.39: code: 5061, data: 2851 => -14+28=+14
    f6: 2.0.38: code: 10244, data: 3610 ; 2.0.39: code: 10344, data: 3712 => +100+102=+202 and what's even worse:
    f6: 2.0.38: memory usage: 23.6, PEAK MEMORY: 24.8 ; 2.0.39: memory usage: 23.8, PEAK MEMORY: 25.1

    I diff-ed the outputs, and here are the 1st things I saw:

    1. new version keeps the constant definitions. Probably a good thing with SDK 1.4.7

    2. new: recognizes that it can use the same pre_0 in the inlined function, and it does use it, but it still creates the unnecessary and unused pmcr_zero_0 = pre_0 (old version did pmcr_zero_0 = 0 but it used it at least). So it's good that it sees it doesn't need to use a new variable for 0 but it accidentally still created the pmcr_zero_0.

    3. This is not a bug just a bit strange that it renames variables from <x> to pre_<x> which is fine, the generated code is bit-by-bit equivalent but it makes it harder to compare.

    4. another place IMHO similar to #2:
    const X = 5;
    function foo() {
      var x = X;
      var ticker;
      ticker = moo == too ? x : zero;
      if (ticker < x) {}
      else {
        if (ticker == x) {}
      }
    }

    optimized:
    function foo() {
      var pre_5 = 5;
      var x = pre_5; // x is not mentioned anywhere else in the optimized code
      var ticker;
      ticker = moo == too ? pre_5 : zero;
      if (ticker < pre_5) {}
      else {
        if (ticker == pre_5) {}
      }
    }

    5. this is an interesting one. I myself wasn't sure if I should do what the optimizer decided to do, we'll need to do the calculations, here's the code:

        public function initialize() {
            var heart_rate = "ANT_HR";
            f(heart_rate);
    
            var name = "avg_" + heart_rate;
            f(name);
            f(name);
    
            name = "max_" + heart_rate;
            f(name);
            f(name);
    
            name = "min_" + heart_rate;
            f(name);
            f(name);
        }
    }

    became:

        public function initialize() {
            var heart_rate = "ANT_HR";
            f(heart_rate);
    
            f("avg_ANT_HR");
            f("avg_ANT_HR");
    
            f("max_ANT_HR");
            f("max_ANT_HR");
    
            f("min_ANT_HR");
            f("min_ANT_HR");
        }
    }

    6. Again, not a bug, but probably you shouldn't rename variables for no apparent reason:

    public function onTimerLap() as Void {
      var zero = 0;
      mLapRecordCount = zero;
      mHRLapTotal = zero;
      mHRLapMax = zero;
      mHRLapMin = 255;
    }
    public function onTimerLap() as Void {
      var pre_0;
      pre_0 = 0;
      mLapRecordCount = pre_0;
      mHRLapTotal = pre_0;
      mHRLapMax = pre_0;
      mHRLapMin = 255;
    }

     

  • here's also a constant related new bug:

    const HR_ZONES_MOCK = null as Array<Number>;
    (:inline)
    hidden function setHeartRateZonesMock() as Void {
      if (MOCK && HR_ZONES_MOCK != null && mHeartRateZones == null) {
        setHeartRateZones(HR_ZONES_MOCK);
      }
    }

    is being transformed into:

    public function handleSettingUpdate() as Void {
      var pre_HR_ZONES_MOCK;
      pre_HR_ZONES_MOCK = HR_ZONES_MOCK;
      {
        if (pre_HR_ZONES_MOCK != null && mHeartRateZones == null) {
          setHeartRateZones(pre_HR_ZONES_MOCK);
        }
      }
    }

    it should've realize that HR_ZONES_MOCK is null and remove the whole block without copying it to a var.
  • Here's something that got "un-optimized":

    I manually optimized my code not to include the same 1 character string twice:

    var a = "-";
    var b = a;

    and the optimizer undid it:
    var a = "-";
    var b = "-";

    When I compare the above 2 variants (without the optimizer) then then the 2nd uses 3 bytes more code. 

  • and here's the reason why f6 became so much worse: some juggling with annotations + constant got to the wrong conclusion:

    my code:

    (:debug, :memory32K) const LOG = false;
    
    (:debug, :inline)
    function log(msg as String) as Void {
        if (LOG) {
            logRelease(msg);
        }
    }
    function logRelease(msg as String) as Void {
        System.println(Time.now().value() + " " + msg);
    }
    
    log("sAN: " + (name == null ? "null" : name) + ", " + (isNewName ? 1 : 0) + ", p:" + mPrevAntId + ", a:" + antId);
    

    2.0.38 correctly removed this whole block (the call to log() INCLUDING the calculation of it's parameter)

    and 2.0.39 partially included it:

          {
            var msg =
              "sAN: " +
              (name == null ? "null" : name) +
              ", " +
              (isNewName ? pre_1 : pre_0) +
              ", p:" +
              mPrevAntId +
              ", a:" +
              antId;
          }
    

    Again: it created the variable that it doesn't use later. log() is not called, so It wasn't obvious to see the difference in functionality, but it included a huge chuck of "noop" code.

  • This is not an error more like "wishful thinking" from my side

    This is something that is almost working in the next release. Yet again, it depended on type propagation, so hasn't been possible until now; and I decided to release what I had, rather than keep adding more features (I've already added more features than I had intended for the initial type-propagation release; but every time I add a feature, I see how much better it could be if I added just one more thing...).