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 am seeing more and more more and more Stack Overflow Errors while running my app in the simulator. Until now I could run it as a regular build (no prettier optimizer) that made it easy to debug. But now I get Stack Overflow Error at some point. I think the main reason for this is the way I write the code: since this is a datafield that supports all but epix (gen1) it is highly optimized for memory size. This means that I use the following pattern:

    1. for each feature I have an annotation: feature1 or no_feature1. This is in monkey.jungle (generated based on device capabilities)

    2. for parts of the code that is not relevant for some (usually older) devices I do something like:

    function foo() {
        executeFeature1(x, y, z, ...);
    }

    (:no_feature1, :inline) private function executeFeature1(x, y, z, ...) {}
    (:feature1) private var onlyUsedInFeature1;
    (:feature1, :inline) private function executeFeature1(x, y, z, ...) {
      ...
    }

    This has the advantage that when I compile with prettier optimizer then only the code and variables that are used on the device are there. And for functions that are only called from 1 place I use the :inline annotation, because this code basically should've been in-place, and the only reason I split it is to be able to remove it AND variables, constants that are only used by newer devices that have the feature from old devices. This means that I usually pass many parameters (some of them are local variables from foo(), and some of them are local variables from foo that are a local copy of a class variable (I know prettier does this optimization, but I learn from it, so I do it almost instinctively when I use a class variable more than once in a function: var x = mX; and then use x instead of mX).

    I think that all these things together (big functions, many local variables in the functions, many functions call other functions, many parameters passed) use up the stack. It still works when building with the optimizer (because then when everything is inlined then all these passed parameters are not doubled on the stack, and also it re-uses local variables after they're not needed any more) but now I can't run the app without the optimizer, which in turn makes it hard to debug (because the code that runs is not the code that I edit, the breakpoints (I add in the file in source/Foo.mc) are not stopping the flow. Adding breakpoints in the generated source code is not very useful as I switch between devices, and because of the way my monkey file looks each device is different so it's always a different file for every device.

    I wonder if anyone using Prettier has some tips how to be able to continue to debug.

  • because this code basically should've been in-place, and the only reason I split it is to be able to remove it AND variables

    Originally, Garmin's compiler didn't remove code guarded by consts or enums - but as of the last few years, it does.

    So rather than pulling out an inline function with lots of arguments, can't you just define consts based on your exclude annotations, and then put the code directly in the function it belongs in, guarded by an if?

  • No, because of variables like onlyUsedInFeature1 the compiler gives errors.

  •   FYI: I did some comparisons with SDK 9.1.0, fr955 and it looks like some old tricks not only don't improve code size but even hurt it. Specifically I checked what happens when I remove some of the local variables that I added in functions that used the same number multiple times. I learned this trick a from the optimized code of prettier many SDK versions ago. However when I removed these (i.e: zero = 0, one = 1, two = 2) and used the number literals instead the code size dropped.

    project.optimization = 3z
    It's possible that some older devices still gain from this trick, but apparently the new ones don't.
  • Yes, anything that supports the new bytecodes can generate small constants very efficiently.

    Specifically, zero is a single byte opcode, 1-255 are 2 byte opcodes, and -32768 through 32767 are 3 byte opcodes, and 24 bit signed integers can use a 4 byte opcode (everything else uses the original 5 byte opcode). So anything from 0 to 255 never benefits from a local; 16-bit numbers outside that range will benefit if used often enough - it will be 3 bytes to initialize the variable, and then you save one byte per use, so you need 4 or more uses to actually save.

    For Longs, Floats and Doubles, the corresponding zero has a 1-byte opcode, but everything else requires 5 for Floats, and 9 for Longs and Doubles, as before.

    The post build optimizer should undo most of those inserted variables, whether they were inserted manually or by the source to source optimizer - but I think there are still some edge cases that it misses. I should do some more work on that...

    Similarly there are cheaper ways to generate references to globals. In some cases, it needs to be 3 or more uses, rather than two or more to get a win - but unlike the constants, it's not much better than before.

  • Well, if onlyUsedInFeature1 is only read by the guarded code, you could just define a constant to keep Garmin's compiler happy (Garmin's compiler removes constants).

    If it's also written to, you could still "invert" your current pattern. Leave the code where it should be, guarded by a constant, but have an inline setter for onlyUsedInFeature1. The setter is empty in the unused case, and just sets the variable otherwise.

    That should keep Garmin's compiler happy, and their compiler should eliminate the guarded code. I don't think it will then eliminate the (now unused) setter, but that's no worse than the current situation. The advantage is that you don't now have two functions with large stack frames calling each other. Just one with a large stack frame calling a setter with a very small frame (and on the older devices, it doesn't call the setter at all).

  • No, this doesn't work, because the compiler first checks that every reference can be resolved and only afterwards removes the code guarded by the constants:

    (:no_foo) const FOO = false;
    (:foo) const FOO = true;
    
    class Foo {
        (:foo) private var fooOnly as Boolean?;
        function foo() as Void {
            if (FOO) {
                fooOnly = true;
            }
        }
    }

    base.excludeAnnotations = foo
     
    ERROR: fr955: Foo.mc:8,12: Undefined symbol ':fooOnly' detected.
    It only works by only referencing variables with :foo annotation only from functions, classes with :foo annotation.
    Long shot, but opened a feature request:  feature-request: make the compiler do the constant folding earlier 
  • That's not what I'm suggesting. I said define a constant for fooOnly, and if you need to modify it in the excluded code, define a setter

    (:no_foo) const FOO = false;
    (:foo) const FOO = true;
    
    class Foo {
        (:foo) private var fooOnly as Boolean?;
        (:no_foo) private const fooOnly as false;
        (:foo,:inline)
        function setFooOnly(b as Boolean) as Void {
            fooOnly = b;
        }
        (:no_foo,:inline)
        function setFooOnly(b as Boolean) as Void {}
        
        function foo() as Void {
            if (FOO) {
                setFooOnly(true);
            }
        }
    }

    For this case, it's no better (and you don't even need the if (FOO) - but presumably in your real code the assignment would be part of a bigger block that's all excluded).

    But the point was that you're currently pulling out a big function with lots of parameters (hence both the caller and callee have big stack frames). With this approach the caller still has a big frame, but the callees (the setters) don't (and when my optimizer is enabled, the setters get inlined and removed anyway).

    Long shot, but opened a feature request

    That feels like a bad solution - if they did that, a program could be valid at some optimization levels, but not at others...

  • Yeah, that works.

    I just found some strange things about constant folding in the Garmin compiler:

    const A = true;
    const B = true;
    const C = true;
    class Blah {
      const D = 6;

      function x() as Void {
        var foo = true; // in reality it comes from a property that the user can set
        if (A && B && C && D > 0 && foo) {
        }
      }
    }

    At the beginning my code only had if (foo), but since I'm moving code here from annotated functions, and all the annotations already have their constants, I added A && B && D > 0 just to be on the safe side, and wasn't expecting any change in code size, but it increased, So I played with it a bit, and I see that adding:
    A &&
    A && B &&
    A && B && C &&
    D > 0 &&
    doesn't increase the size, but adding 
    any combination of {A, B, C} && D > 0 does increase it.
    I'm not really sure what to make of it, to me it looks like a Garmin compiler bug. 

  • That feels like a bad solution - if they did that, a program could be valid at some optimization levels, but not at others...

    Well yes, but we already have some ways a given code would only compile given a specific set of parameters (i.e type checl level) They could add it for -O 3z. Or 4z.