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]

  • It's perfect for a datafile. If I had a watchface, probably I wouldn't use the optimizer.

  • After building app for store you have to make copy of directory with source code and optimised source code. If you want to find error from line 100 you have to look into optimised code than find the proper line in original code.

    Era usually doesn't show the proper line... And even shows errors  in l 'somewhere' not connected with flow...

    So if you have problem with memory and only solution is using prettier just because sdk doesn't offer enough good tool (and hasn't optimiser for years) - why don't use prettier?

    Build in optimiser is rather a preprocessor which at least doesn't treat cost, enums as var..

  • Era usually doesn't show the proper line... And even shows errors  in l 'somewhere' not connected with flow...

    Yeah, I mean the irony is that considering optimization is enabled in the Monkey C compiler by default now, the line number in stack traces is less likely to correspond to the real line number in the source than before.

    Similar to enabling optimization in gcc (the GNU C compiler) - due to optimization, the compiled code has less chance of being correlated with the source code than without optimization. (You would see this in stack traces or when stepping through the debugger "one line at a time")

    Build in optimiser is rather a preprocessor which at least doesn't treat cost, enums as var..

    Yeah it used to drive me crazy that the use of enums would waste memory in the form of code.

  • How much does the psuedo code in the mir files represent what's happening, because it looks like a new register is being allocated for EVERY operation?

  • You can't really conclude very much from reading it, because it basically represents the program straight out of the parser. The optimizer and code generator could do anything before actually spitting out bytecode.

    Also note that the .mir bytecode is register based, while the actual executable bytecode is stack based. So "allocating a new register" is basically meaningless as far as execution on the target goes.

    If you want to see what the generated code looks like, the .mir files are the wrong place to look.  Instead, add "-g" to the monkeyc command line options. That will spit out bytecode for the whole program.

    --

    If you're curious, the reason for all the register allocations is that the .mir is in SSA form (that's Static Single Assignment), which basically means that all your local variables (or registers) are assigned once, and never modified. So it's actually a requirement that each new value gets its own register. 

    If you've not come across it before, it probably seems like the opposite of what you want; but it has some very nice properties from an optimization point of view, and there's been an enormous amount of work done on optimizing programs in SSA form.

    As a trivial example:

    function foo(x) {
        var a;
        if (x) {
            a = 1;
        } else {
            a = 2;
        }
        return a;
    }

    Putting it into SSA form we get:

    function foo(x) {
        var a1, a2, a3;
        if (x) {
            a1 = 1;
        } else {
            a2 = 2;
        }
        a3 = phi(a1, a2);
        return a3;
    }

    This looks terrible - we've added that weird "phi" function amongst other things. The phi function is "magical" - it resolves to a1 if we came from the "then" branch, but to a2 if we came from the "else" branch. This might seem like it would be a huge overhead, but it's really just sleight of hand - at code gen time it will literally insert an assignment from a1 to a3 at the end of the then block, and an assignment of a2 to a3 at the end of the else block (and then register allocation will hopefully ensure that all three are in the same register, so the assignments are no-ops); but you can't do that in an SSA program because then you'd have two assignments to a3, not one. 

  • OK, so assuming all that, what rules, if any, are there that I can use that would help to simultaneously optimize for size, speed (reduced code size and minimize repeated operations) and readability? Someday I might upload my watchface for others, what can I do so the code Garmin generates from mine will be "optimum"?

  • Sorry, I didn't get a notification for this, and only just saw it.

    The answer, I'm afraid, is that in general, you can't. Optimal for size won't always be optimum for speed, and vice versa.

    On the other hand, there is a lot of overlap, and for the most part, the optimizations my optimizer does are good for both - but when there's a choice it goes after size rather than speed, always.

    Of all the things the optimizer does, the only ones that are likely to adversely affect speed are size-based-pre (which is often good for both, but could end up being bad for speed), and single-use-copy-prop (which is the one you're taking issue with).

    Fixing single-use-copy-prop should be fairly easy - just don't allow propagation into loops.

    Fixing size-based-pre is just a case of adding an alternate implementation that does traditional partial redundancy elimination. Various "optimal" PRE algorithms are well known - implementing it shouldn't be that hard - except that monkey c isn't expressive enough to do it properly (I had the same problem with size-based-pre, and the result was a somewhat heuristic algorithm, rather than a provably optimal one).

    But once it's done, you have to choose between the two. I guess there could be a top level option controlling which algorithm to use, and then I could use exclude-attributes to control which one to use on a function-by-function (or class-by-class, or module-by-module) basis.

    But as for what you can do - I guess just keep looking at the "-g" output from monkeyc, and figure out what works best (although again, my post-build optimizer will fix a lot of the more obvious issues).

  •  is it possible to turn off the optimizer's inlining for a couple of lines? I have an issue where I calculate screenwidth/2 quite often, and I'd like to calculate it once and store the value in the global namespace. However, I think the optimizer is inlining the calculation, making it slow. I know there was a (:inline) annotation, but I couldn't remember if there was something to prevent a constant from inlining. As always, I really appreciate this project, it has been super helpful!

    Ah, I was looking at the wrong output file. It looks like this is working correctly. Thanks!

  • It looks like this is working correctly

    Good!

    I will note that computing screenwidth/2 (assuming screenwidth is in a local) is probably faster than reading it from a global, and should be smaller too.

    Also, screenwidth is effectively a constant anyway - in which case the optimizer will compute screenwidth/2 as a constant too. For newer devices, the constants are available via the personality library (system_size__screen.width), and for older devices there's nothing to stop you creating your own mss file and defining system_size__screen there. Its a bit of a pain, but you only need one mss file per device family, so its not too bad.

  •  Thanks, that's a good idea about mss files.

    On a different note, I've been optimizing my app like crazy, and I came across a few tricks that I have found for apps super close on memory. I've noticed you can save a ton by wrapping some system calls in MonkeyC functions. For example:

    function Storage_getValue(key) {
    	return Storage.getValue(key);
    }
    Now, I get that this is not ideal, especially because this is a global function. However, in my experience, it has saved me a ton of memory. I'd bet it slows down each call to getValue at least a little bit, but it might be worth it in some cases. I also do the same with WatchUi.popView, WatchUi.loadResource, System.getDeviceSettingsWatchUi.requestUpdate, Storage.setValue, and System.println. I would sure appreciate it if you added an option to do this automatically!
    Also, another thing that would be super cool would be the ability to automatically move hardcoded strings above a certain length (probably anything greater than 15-30 chars) into strings.xml, and use WatchUi.loadResource. That could save a bit of memory as well.
    A final idea would be to automatically remove WatchUi.loadResource calls in system functions that don't need them. For example, I had a bunch of titles to Menu2s that looked like this:
    toggleMenu.addItem(new WatchUi.ToggleMenuItem(WatchUi.loadResource(Rez.Strings.DarkMode), Rez.Strings.Curr_Off, :item_1, false, null));
    // when the below seems to work just fine, and saves a ton of memory
    toggleMenu.addItem(new WatchUi.ToggleMenuItem(Rez.Strings.DarkMode, Rez.Strings.Curr_Off, :item_1, false, null));
    One thing to note, on the first two ideas I mentioned, they may not work correctly in the background, so you might want to exclude anything annotated with background from these tricks.
    I hope these ideas are helpful! Thanks for your hard work on this!