Prettier code formatter for monkey-c

[EDIT]

The plugin seems to be working well (ok, there's room for debate, but it does for monkeyc what prettier does for javascript).

 - if you're already using npm, you can 'npm install --save-dev @markw65/prettier-plugin-monkeyc', and install the VSCode Prettier plugin.

 - if you just want to use prettier for monkeyc code, and only want to use it inside of vscode, and dont want to mess around with npm, you can just install https://marketplace.visualstudio.com/items?itemName=markw65.prettier-extension-monkeyc and it should just work.

[/EDIT] 

So far there doesn't seem to be a code formatter for monkey-c. I've been writing quite a lot of monkey-c code recently, and I've really missed the auto-formatting provided by prettier for javascript (and many other languages).

A few days ago I decided to look into how to extend prettier. javascript isn't too different from monkey-c (or at least, monkey-c maps fairly cleanly onto a subset of javascript/typescript), so it seemed like it should be doable. I found a parser generator which already had a javascript grammar which produced an ast in the format prettier expects. In fact, the ast was fully compatible with prettier's estree printer. So I started from there, and modified it to parse monkey-c instead. Then I was able to delegate most of the printing to the existing estree printer in prettier. So after a couple of days of fiddling around I have something that prettifies all of my code in an acceptable way, and the code still works...

I've attempted to test it more thoroughly by running it on all of the Garmin sdk samples. It took a while to get everything to run through without errors (I guess there are a lot of features I don't use in my own code), and then a bit longer to get it to produce compilable code(!). But finally everything compiles, and if I compile in release mode, the prettified binaries are identical to those produced by the original code - so I didn't change the meaning of the code.

I created a GitHub repo to show what it does to the Garmin samples. There's a branch, original, pointing to the code as it was in the sdk. Then I prettified everything (branch pretty). You can browse the code, or just look at the actual changes.

I need to do a bit more cleanup and testing on my code, but I hope to publish the it on GitHub and npm as a Prettier extension within a few days - then it will be usable in VSCode via the Prettier extension. Meanwhile, feedback on the changes it made to the Garmin sample code would be appreciated.

Top Replies

All Replies

  • One other question - did you check to see if the optimized source is identical between the two compiler versions?

    Generally, the optimizer doesn't really care about the sdk version, but it does pull all the info about the Toybox from <sdk>/bin/api.mir, so its possible (but unlikely) that changes to api.mir could affect how it optimizes things. eg if they added a new method to a base class, and you happened to have an unused method of the same name in a class that extends that base class, the optimizer could delete your unused function with the old api.mir, but not with the new one. There are probably a few other edge cases where the generated code could be different.

    In any case, could you confirm whether the optimized source is the same with both sdks?

  • How do you want the params to be for 4.1.6? -O0 or -O2 ?

  • How do you want the params to be for 4.1.6? -O0 or -O2 ?

    I guess my question was, for the builds where you got a worse size with 4.1.6 (so no extra parameter), was there a difference in the generated source (also, the -O option shouldn't affect my optimizer). Ie did *my* optimizer do something different with 4.1.5 vs 4.1.6, or is it just that 4.1.6 produced worse code from the same sources.

    From the point of view of you getting back to the 4.1.5 results, I'm pretty sure that if you pass -O0 to 4.1.6 you'll get the exact same results as with 4.1.5; but if you do see a difference, let me know...

    Finally, if you're looking for the best possible results for your app, you probably want to use -O2. Again, if that *doesn't* give you better results than 4.1.5, I'll be surprised. I think I've seen 3 apps where that happens, and its worse by around 10 bytes...

  • Indeed the code,data size was the same with 4.1.5 and 4.1.6 with -O0.

    There are only 3 small differences between the generated code and only in 1 mc file:

    1.

    orig code:

    var ZONE_COLORS as Array<Graphics.ColorType> = [Graphics.COLOR_TRANSPARENT, Graphics.COLOR_LT_GRAY, Graphics.COLOR_BLUE, Graphics.COLOR_GREEN,
    Graphics.COLOR_ORANGE, Graphics.COLOR_RED, Graphics.COLOR_DK_RED] as Array<Graphics.ColorType>;

    4.1.5 optimized:

    var ZONE_COLORS as Array<Graphics.ColorType> =
    [
    -1 as Toybox.Graphics.ColorValue,
    11184810 as Toybox.Graphics.ColorValue,
    43775 as Toybox.Graphics.ColorValue,
    65280 as Toybox.Graphics.ColorValue,
    16733440 as Toybox.Graphics.ColorValue,
    16711680 as Toybox.Graphics.ColorValue,
    11141120 as Toybox.Graphics.ColorValue,
    ] as Array<Graphics.ColorType>;

    4.1.6 optimized:

    var ZONE_COLORS as Array<Graphics.ColorType> =
    [
    -1,
    11184810 as Toybox.Graphics.ColorValue,
    43775 as Toybox.Graphics.ColorValue,
    65280 as Toybox.Graphics.ColorValue,
    16733440 as Toybox.Graphics.ColorValue,
    16711680 as Toybox.Graphics.ColorValue,
    11141120 as Toybox.Graphics.ColorValue,
    ] as Array<Graphics.ColorType>;

    2.
    original:

    dc.setColor(foregroundColor, Graphics.COLOR_TRANSPARENT);

    4.1.5:
    dc.setColor(foregroundColor, pre__1 as Toybox.Graphics.ColorValue);

    4.1.6:
    dc.setColor(foregroundColor, pre__1);


    note, pre__1 in both cases is the same:

    pre__1 = -1;

    3.

    original:

    dc.setColor(foregroundColor, Graphics.COLOR_TRANSPARENT);

    4.1.5:
    dc.setColor(foregroundColor, pre__1 as Toybox.Graphics.ColorValue);

    4.1.6:
    dc.setColor(foregroundColor, pre__1);

    pre__1 is the same as above.

    You are right, that 4.1.6 -O2 is better than 4.1.5:

    4.1.5:          code:10936,data:4049,entrypoint:3984=14985 or 18969 with entrypoint

    4.1.6 -O3z: code:10696,data:4253,entrypoint:3888=14949 or 18837 with entrypoint

  • There are only 3 small differences between the generated code and only in 1 mc file

    Those differences are because they finally fixed https://forums.garmin.com/developer/connect-iq/i/bug-reports/no-negative-values-in-sdk-bin-api-mir/status, combined with what looks like a (pretty insignificant) bug in the optimizer. I'll fix it in the next release, but it really shouldn't affect anything.

    Meanwhile, it's still odd that there's so much more data, even with optimization enabled. I've found a couple of other projects with similar issues, so I'll see if I can isolate it, and figure out what the new compiler doesn't like...

    Btw, although the monkeyc binary reports that it supports O1, O2, and O3, and mentions z and p for size and performance, I'm seeing no difference in any generated binary between O2, O3 and O3z or O3p. So while they may have plans for more optimizations, I don't think they're doing anything beyond O2 yet...

  • Amazing work . I did notice some weird optimizations that might not be optimal.

    Actual hard-coded numbers are replaced with variables which seems to make the code more inefficient.

    Is this expected?

    var gScreen as Array<Float>?;
    
    public function loadGlobals(dc as Dc) {
      var w = dc.getWidth().toFloat();
      var h = dc.getHeight().toFloat();
      gScreen = [
        w / 2,
        h / 2,
        h / 3,
        (h * 2) / 3,
      ];
    }
    

    var gScreen as Array<Float>?;
    
    public function loadGlobals(dc as Dc) {
      var pre_2;
      pre_2 = 2;
      var w = dc.getWidth().toFloat();
      dc /*>h<*/ = dc.getHeight().toFloat();
      gScreen = [
        w / pre_2, // CenterX
        dc /*>h<*/ / pre_2, // CenterY
        dc /*>h<*/ / 3, // OneThirdY
        (dc /*>h<*/ * pre_2) / 3, // TwoThirdY
      ];
    }

  • Is there a way to exclude certain files to unexpected optimizations?

  • Actual hard-coded numbers are replaced with variables which seems to make the code more inefficient.

    Is this expected?

    Yes. The optimizer will do this when it makes the code smaller, which for Number and Float means the value is used at least 3 times (for Long and Double it only has to be used twice).

    It's hard to say whether it makes it less performant or not - the difference is so small that actually measuring it on a device would be hard.

    You can disable this optimization by setting prettierMonkeyC.sizeBasedPRE to false - but that will also prevent moving globals and member variables into locals, and overall sizeBasedPRE is one of the best code size optimizations.

  • Is there a way to exclude certain files to unexpected optimizations?

    Currently no. The optimizer design is based on the assumption that it sees/modifies the whole program. Excluding files could result in incorrect optimizations.

  • ah, thanks for the quick reply! Is there a benefit to having a smaller code size?

    Some performance optimization threads in this forum suggest that having local variables increases memory footprint.

    Does `sizeBasedPRE` only affect code size optimization?

    I am mainly focusing on memory and power optimization for the watch face app.