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]

  • Thanks. So some corner cases to contend with:

    class
    	private var _cPics as Array<Symbol> = [:id_usmc_sm, :id_usmc_md, :id_usmc_lg] as Array<Symbol>;
    
    function
    		var cPic = _cPics[(_isLowColor) ? 0 : ((_XYpole < 135) ? 1 : 2)];
    		_centerPic = WatchUi.loadResource((Rez.Drawables as Array)[cPic] as Symbol) as BitmapResource;
    
    terminal:
    WARNING> fenix7x: ...trimmed filepath...: The type Module<Rez.Drawables> cannot be converted to Array because they have nothing in common

    I get that it is probably a non-standard way to load an image, but it works. It would be even better if I could enumerate the resource id's.

    One more:

    function
    		faceBBMOpts.put(:colorDepth, 4);
    		faceBBMOpts.put(:palette, lowPalette);
    
    terminal:
    WARNING> fenix7x: ...trimmed filepath...: Argument 2 to $.Toybox.Lang.Dictionary.put expected to be Float but got Number<4>
    WARNING> fenix7x: ...trimmed filepath...: Argument 2 to $.Toybox.Lang.Dictionary.put expected to be Float but got Array<Number as Toybox.Graphics.ColorValue>

    Why does the optimizer think dictionary values must be floats?

  • I get that it is probably a non-standard way to load an image

    I actually have a couple of similar functions; one for loading strings, and one for jsonData.

    So the problem here is that Garmin's type checker doesn't understand that you can index a Module with a Symbol. So you had to lie, and tell it that the module Rez.Drawables is an Array. And as you say, it does in fact work at runtime.

    My type checker does understand the code, and doesn't need the cast. But my optimizer's type checker doesn't allow (or at least warns about) dangerous looking casts, while Garmin's doesn't (see this bug report for example).

    So one option would be to turn off Garmin's type checker, and just rely on mine; but that's less than ideal because there are still quite a few things that my type checker doesn't report. I do plan to fix that eventually...

    A better alternative would be to move the WatchUi.loadResource into a separate function, and just disable the type checker for that one function:

    (:typecheck(false))
    function loadDrawable(drawable as Symbol) as Drawable {
      return WatchUi.loadResource(Rez.Drawables[drawable]) as Drawable;
    }

    In fact, if you load drawables in more than one place in your code (even if you don't need this trick in the other places), it's probably a code size win to use this function everywhere. If this is the only use though, this solution is going to end up generating more code. But you can fix that by marking the function inline:

    (:inline, :typecheck(false))
    function loadDrawable(drawable as Symbol) as Drawable {
      return WatchUi.loadResource(Rez.Drawables[drawable]) as Drawable;
    }

    Why does the optimizer think dictionary values must be floats?

    Well, you didn't post the declaration of faceBBMOpts, but I think this is probably because my type checker infers the types of Arrays and Dictionaries, while Garmin's doesn't. eg

    const mArray as Array<Number> = [1,2,3];
    const mDict as Dictionary<Symbol, String or Number> = { 
      :foo => "Hello", 
      :bar => 42,
    };

    Both these assignments are errors according to Garmin's type checker, because you're assigning an untyped array to a typed array, and an untyped dictionary to a typed dictionary. So my type checker infers the types of array and dictionary literals - which means its happy with these, but can cause problems with code like:

    function foo() {
      var dict = { :foo => 4.0, :bar => 42 };
      // dict now has type Dictionary<Symbol, Number or Float>
      dict.put(:baz, "hello"); // error because "hello" isn't a Number or Float.
    }

    In this case, Garmin's type checker thinks that dict is an untyped dictionary, so allows you to put anything into it, while mine will only allow Symbol => Number or Float. The solution is to cast the dictionary initializer:

    function foo() {
      var dict = { 
        :foo => 4.0, :bar => 42
      } as Dictionary<Symbol, Number or Float or String>;
      dict.put(:baz, "hello"); // ok
    }

    And note that with the cast in place, Garmin's type checker will do a better job too; it will only allow Symbol keys, and it will only allow the specified list of values.

    I agree that its less than ideal...

  • For "var faceBBMOpts = {:width=>dcW, :height=>dcH};" both dcW and dcH are floats, so that's where it came from.

    Looking at the optimized code it looks like my benefits come from the fixups with constants I couldn't do myself or variable reuse I wouldn't do so it that the code would still be maintainable. Are there any coding styles / conventions that would help hint the optimizer

  • but if you add (:inline, :typecheck(false)) to loadDrawable() then doesn't it meant that after inoining the typechecker will be disabled in the inlining function as well?

  • Are there any coding styles / conventions that would help hint the optimizer

    Probably, but I'm not really thinking of much right now. Off the top of my head:

    Make sure constant sub-expressions can be folded (if thats what you want). eg var "x = y + 5 - 6;" is parsed as "x = (y + 5) - 6;". If the optimizer knows enough about the types, it will re-arrange the expression to "x = y + (5-6)" and then optimize to "x = y - 1". But eg if y might be Float or Double, it won't do that, because it changes the results - possibly in a significant way. So if you want the constants to be folded, write it as "x = y + (5 - 6)" to begin with. Obviously this is a silly example, but Im assuming it was originally something like "x = y + K1 - K2", with K1 and K2 being possibly build dependent (eg dependent on the device screen resolution, for example).

    Try not to use symbols that have the same names as const or enum names (or other names for that matter), because its hard to prove that you don't end up accessing the const through the symbol (similar to your Rez.Drawables[symbol] example).

    e.g

    module M {
      const K = 42;
    }
    
    function foo(sym as Symbol) {
      return M[sym];
    }
    
    function bar() {
      System.println(foo(:K));
    }

    Given the above, the optimizer can't remove K because it's actually used at runtime. But even without foo() and bar() if you mention :K anywhere, it will probably end up preventing the optimizer from removing it, just because it can't prove you didn't do that...

  • doesn't it meant that after inoining the typechecker will be disabled in the inlining function as well

    Yes. But hopefully you build both with and without the optimizer, so the caller does get type checked in the unoptimized case...

  • I get that it is probably a non-standard way to load an image, but it works

    I'll just note that in general, if you really need to cast something to an incorrect type, and you want to satisfy both Garmin's type checker, and mine, you can add a cast to "Object?" first (this is similar to the way typescript lets you cast anything via "unknown").

    For example if you try to do:

    function foo(x as Number) {
      if (x) {
        // do something
      }
      // ...
    }

    Garmin's type checker will complain that x should be Boolean - even though at runtime, this works the same as "if (x != 0) {" (but saves 6 bytes, and 2 byte codes). So to keep Garmin's type checker happy, you have to lie and say "if (x as Boolean) {". But now my type checker warns you that the cast is incorrect, because it knows that x can never be a Boolean. So to keep my type checker happy, you have to add "if (x as Object? as Boolean) {".

    This works because Number is a subtype of Object?, so anything that accepts an Object? can also accept a Number. And then the cast to Boolean is ok because an Object? might be a Boolean. Its not guaranteed to be a Boolean, but the whole point of casts in MonkeyC is for the programmer to give the compiler information about the types that it can't figure out for itself.

    This trick doesn't (currently) work in your case, because the type of Rez.Drawables is Module, and MonkeyC doesn't have a way to name that type, and in particular, Module isn't a subtype of Object?. What we really want is to use Any, instead of Object?, but although the MonkeyC documentation talks about the Any type, there's no way to actually name it.

    But in the next release, I'm just going to add a special case so that casting to Object? never causes a warning. So that will give you another way to do it: "(Rez.Drawables as Object? as Array)[cPic] as Symbol".

  • v2.0.69 is out.

    • minor improvements to the post build optimizer
    • minor improvements to the type checker
  • I'm not sure why, or how to diagnose it, but with this extension enabled editing is painfully slow. I disabled real-time checking, but that hasn't helped. How can I find and fix the cause of the slowness? Right now I have to disable this extension until I really want to compile the smallest code possible, and then just for one-time compiles.

    On that subject, I have noticed that the code is reduced by around 4k, but the data is increased by about 2k. This still gives a net win, but I got the impression that this was reducing the data size through reuse. Why does data size increase?

  • I suspect that the extension is synchronously blocking VSCode's extension host process/thread.  I'm not sure either how to to diagnose, but I've seen it happen before where extensions will do CPU-bound operations directly on the extension host thread instead of a worker thread or separate process.