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

  • sizeBasedPRE basically looks for expressions that are used repeatedly, and puts them into locals to reduce code size (which increases free memory). Currently the kinds of expression it will find are constants, globals, and member expressions (ie things like X.Y.z).

    Whenever sizeBasedPRE does something, it will reduce code size, which will reduce the memory footprint. For globals and member expressions, its pretty much guaranteed to improve performance too, while for consents, as I said above, its not clear whether its a performance win or not; but the difference is unlikely to be measurable.

  • ah, I did notice sizeBasedPRE reduces the peak memory in the simulator.

    Forgive my ignorance but could you explain why reducing coded size leads reduces runtime memory?

  • could you explain why reducing coded size leads reduces runtime memory?

    Your app gets a single (limited) pool of memory for the code, data and heap. So whatever you can remove from code and data increases the amount of memory left for your app to use.

  • Does sizeBasedPRE deduplicate method lookups?  Like, if I do `dc.drawPixel(...); dc.drawPixel(...);` then does the method lookup done with spush, getv, frpush get stored in a local?  I think that would reduce code size and also shave cycles.

  • There's no way to do that in the source to source optimizer, because you can't store a method in a local variable and then call it (you *can* store a method in a global/member variable and then call it, but that doesn't help here).

    In the post build optimizer, it is (sometimes) possible, but frpush is problematic, because it's stateful. As far as I can tell it looks at the base operand to the most recent getv, and pushes something related to that. Exactly what depends on what the operand to getv was, and exactly how the symbol was found.

    As a result, for now, the post build optimizer only deduplicates the spush itself. So you get something like this (but note that the compiler doesn't actually accept this):

    var drawPixelLocal = :drawPixel;
    dc[drawPixelLocal](...);
    dc[drawPixelLocal](...);
    

    But yes, with enough smarts, the post build optimizer could include the getv in some cases (and your example is one of them). So

       lgetv dc
       spush drawPixel
       getv
       frpush // in this case, the frpush pushes dc

    could be converted to

       lgetv DcDrawPixel // assuming we've previously set this up
       lgetv dc

  • Oh, interesting, thanks for the info.  How does the stack look before and after frpush?  Based on your comment "// in this case, the frpush pushes dc" I imagine the following:

    lgetv dc
    # Stack: pointer to dc
    spush :drawPoint
    # Stack: pointer to dc, :drawPoint symbol
    getv
    # Stack: pointer to dc, pointer to Dc.drawPoint
    frpush
    # Does frpush re-push the second-to-top element of the stack? Does it do more?
    # Stack: pointer to dc, pointer to Dc.drawPoint, pointer to dc
    lgetv x
    lgetv y
    # Stack: pointer to dc, pointer to Dc.drawPoint, pointer to dc, x, y
    invoke 3
    # Means "call function, pass 3 args"
    # Methods are functions that accept instance as first arg
    # This removes 4 items from stack (function and 3 args) and replaces with return value
    # Stack: pointer to dc, return value of drawPoint()

    Is frpush essentially re-pushing the previous item on the stack?  In what situation would frpush not push dc?  I can't get the simulator to show me the stack, do you know of any tricks for inspecting the stack directly?

  • Is frpush essentially re-pushing the previous item on the stack?

    Not quite. In your example, on line 6 the only thing on the stack is the Dc.drawPaint function (ie getv pops both of its arguments, and pushes the result of the lookup). This is what I meant by getv/frpush being stateful. The runtime remembers the last thing that getv operated on. Which means that if you replace the "lgetv dc; spush :drawPoint; getv; frpush" with "lgetv temp; frpush" where temp has been loaded with the result of that getv, it won't work, because the frpush will push the value related to the most recently executed getv.

    In what situation would frpush not push dc?

    One example is eg MyClass.myFunc(). If you do that outside of a member function, the getv will operate on MyClass and :myFunc, and the frpush will push MyClass (as you might expect). But if you do it inside a non-static member function it will push self (which it gets from local 0). This is what makes eg MyBaseClass.initialize() work inside MyDerivedClass.initialize(){}, for example.

    Note that this also leads to some interesting behavior, where you can invoke a non-static class method in such a way that its "self" is a completely unrelated class (with all the undesirable consequences you might expect).

    do you know of any tricks for inspecting the stack directly

    Not really. My ad-hoc solution is to spit out byte code manually (ie modify the post-build optimizer to replace the body of a particular function with the byte code sequence I want to test, and use dup <n> to pick off the nth item on the stack and then return it).

  • Dang, that's funky.  Ok, so the value pushed by frpush is tricky to obtain.  Once we have it on the stack, is it possible to store it into a local, to be used later?

    lgetv dc
    spush :drawPoint
    getv
    frpush
    lsetv local_dc_drawPoint_frpush
    lsetv local_dc_drawPoint
    
    # Then later, to call the method
    lgetv local_dc_drawPoint
    lgetv local_dc_drawPoint_frpush
    lgetv x
    lgetv y
    invoke 3

    EDIT fixed lsetv, I had typoed it as setv

  • Once we have it on the stack, is it possible to store it into a local, to be used later?

    Yes, that should work. But note that frpush is a 1 byte opcode, and both lputv (not lsetv btw) and lgetv are 2 byte opcodes. Your rewrite is certainly still a win if there are enough calls to drawPoint with the same dc though.

    Note that if you know that dc is a Graphics.Dc, and hence that dc.drawPoint is Dc.drawPoint, you *can* assume that the frpush will just push dc, so you don't really need a new variable for it. One issue I have in the post build optimizer is that I currently don't have the type info, even if the original was fully typed. I keep meaning to add a way to pass that through...

  • I was focused mostly on the performance (speed) hit of all those repeated getv calls.  It was after thinking about Techniques for faster pixel-level drawing? - Discussion - Connect IQ - Garmin Forums

    The author wants to make a ton of dc calls in a tight loop.  I have not actually benchmarked, but I think each method call requires getv, which does a linear scan of the field definitions on Dc.  I assume pulling from a couple locals would be faster, though I haven't benchmarked it.

    If it's a single call to drawPoint happening in a loop, executed ~100 times, then this proposed optimization will make the binary slightly larger, but will improve execution time.