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]

  • Well I found why (almost) "it" worked. This works:

    using Toybox.Application as App;
    App.AppBase.getProperty(key);

    and BTW the above is 3 bytes less in code than:
    Application.getApp().getProperty(key);
     
    but this doesn't work, that was a mistake in my post:
    import Toybox.Application;
    Application.AppBase.getProperty(key);

    nor does:
    AppBase.getProperty(key);

  • Oh - thats odd. It seems to work for me. I can even do MyAppName.getProperty(key), which saves a few more bytes.

    I'm just surprised, because its not documented as working, and getProperty is documented as an instance method, not a static method. I guess Ive also not tested it on an actual device; so maybe it only works in the simulator. Or maybe it only works on some devices.

  • On the inlining front, I just released v2.0.13 which will inline suitable functions with the (:inline) annotation if it seems safe.

    Currently suitable functions are ones that consist of a single return statement, and its a bit conservative about what "seems safe". It only deals with calls that have literals, identifiers, and member-expressions (eg foo.bar) as arguments, and depending on the body of the function, it may be more conservative than that.

    eg

    (:inline)
    function foo(x) {
      return f() + x;
    }
    
    var g;
    
    function bar() {
      foo(g); // won't be inlined because f() might alter g before its read.
    }
    
    

    One exception is that functions that just return their only argument - possibly with a type cast - will be inlined regardless of how they're called, and even without an (:inline) tag. This means you can write type-safe functions to cast away nullness (or any other feature). eg

    function nonNullNumber(x as Number?) as Number {
      return x as Number;
    }

    Its not a great solution, but its better than littering your code with "x as Number", since you still get type checking for everything except nullness (when you just throw in "x as Number" you could accidently apply it to something that was actually "String or Null").

    You can also use (:inline_foo) which will only be inlined if foo is set as an excludeAnnotation. This means that you could eg have a function be inlined on devices with lots of memory, but not on ones with very little.

  • I think being conservative is ok. Can you add a warning if I added the annotation but in some place I used the function you decided not to inline? That would give the developer one more chance to tweak the function. (Especially if it would also give some info about the reason it choose not to inline)

    I'm not sure I understood how the :inline_foo works, can you give an example?

    BTW what's the point in inlining something if it doesn't shrink the code size? Just for readability?

  • I might have a candidate, maybe with this you can explain the inline_foo:

        (:no_api1_3, :no_api2, :inline)
        hidden function setAntName(name as String?) as Void {
            mName = name;
        }
        (:api1_3, :inline)
        hidden function setAntName(name as String?) as Void {
            mName = name;
            mFitContributor.setAntName(mSensor, name);
        }

    setAntName is called 4 times in the code. I tested if adding :inline to the list of the annotations changed anything in the code size, but it did not neither for epix (that uses the no_api1_3 version) nor for fenix3 (that uses the api1_3 version). Is it possible that the inline only works if it's the only annotation on a function?

    Here's another place, setConfig is uses 3 times, for some reason it wasn't inlined either:

    (:api2, :memory32K, :inline)
    function setConfig(key as PropertyKeyType, val3 as PropertyValueType) as Void {
        Properties.setValue(key, val3);
    }
    

  • setAntName is called 4 times in the code

    and

    Is it possible that the inline only works if it's the only annotation on a function

    Well, you just made me realize that I didn't write any tests specifically for that, but the code is certainly designed to check all the annotations.

    But in this case its because (from my post):

    Currently suitable functions are ones that consist of a single return statement

    This was to make sure that no matter where the call occurs, it's possible to do the substitution.

    The next thing to look at is functions whose result isn't used, and which are called as a statement, rather than as part of a bigger expression, which looks like it will cover your examples. And after that maybe calls whose result is directly assigned to a variable, or directly used as the return value of the calling function.

    I'm not sure I understood how the :inline_foo works, can you give an example?

    If I put:

    base.excludeAnnotations = foo
    fr235.excludeAnnotations = bar

    in my monkey.jungle, then anything annotated (:inline_foo) will be inlined on all devices *except* fr235 (ie all devices that have foo as an excludeAnnotation), and anything annotated (:inline_bar) will be inlined on the fr235 but nothing else.

    BTW what's the point in inlining something if it doesn't shrink the code size?

    Presumably performance, and power consumption - although that depends on having some way to measure, I suppose.

    For example, (not really inline related), the Garmin docs imply that "$.Foo.Bar.baz" is more efficient than just "baz" (assuming both resolve to the same thing). Presumably because it will lookup baz in each block scope out to the containing function, then any class, and then each containing module until it finds it. When it sees "$" it starts from the global scope, and looks up each symbol directly.

    But "$.Foo.Bar.baz" produces much more code. One of the optimizations I'm planning to add is to replace qualified names by the shortest possible sequence that identifies the same object; that would definitely help with size. But when I do I should probably also add an option to go the other way... fully qualify everything for performance.

  • BTW what's the point in inlining something if it doesn't shrink the code size?

    It would reduce the amount of items on the call stack (although I don't think this has a big impact on memory or performance)

  • I'm still not sure I understand the real-life usage for :inline_foo, because in most cases either there's a function that is not needed let's say for old devices, only in new, or there's a pair of functions, one for the old devices, one for the new. Either case the other annotations already take care of which should be used (compiled/removed), so there's only one left. Now you say that I can either inline or not inline that based on ANOTHER excludeAnnotation, but also I think your annotation is counterintuitive because it works the opposite way (though this might be only my way of looking at it). [The excludeAnnotations are themselves a bit counterintuitive to be honest]: in the normal (garmin) annotations when you have the above in your monkey.jungle then the function annotated with (:bar) is EXCLUDED on fr235, so it is only present on other devices. In your implementation :include_bar kind of behaves the opposite way: on fr235 it does the include, and on others it doesn't. Anyway I still haven't came up with a use-case when I would find it useful, since I anyway already have the other excludeAnnotations that are based on HW capabilities, so I think for me that + simple :inline is enough. I might start to use the :inline_foo later when I'll see the light ;)

  • Not to contradict anything else written in this thread (which I have only skimmed), but I've found memory gains in my own apps by manually inlining functions which consisted only of a single line. Probably depends on exactly how complicated that line is, though.

  • It also depends on how many times you call the function, but the smaller the code in the function the inclining gains more, because calling the function adds a few bytes, and the biggest gain is deleting the function itself