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]

  • app migration is rather simple...

    if Garmin claims new device is compatible with any with your iq new device is added

    but if device get new firmware and you have in code false instead of runtime HAS you have to rebuild app

  • What's the safe way you use to determine that Y doesn't exist?

    Essentially, if it's not defined anywhere.

    It's only going to be useful for user classes/modules right now. Eg if you have

    class X {
      function foo() {}
      (:bar)
      function bar() {}
    }

    then "X has :foo" won't be optimized. "X has :bar" will be optimized to false if "bar" is set as an excludeAnnotation, but will be left alone otherwise.

    Alternatively if you have two definitions of X in different files, and one has foo and one doesn't, and you use sourcePath to choose between them, then "X has :foo" will be optimized to false for devices that use the definition without foo.

    For what I would *like* to do:

    All the ToyBox symbols are "documented" in connectiq/<sdk>/bin/api.mir. Every symbol is tagged with an api version (amongst other things). And then every device is described in connectiq/Devices/<device>/compiler.json. Each device can have multiple part numbers (amongst other things, different part numbers correspond to the device as sold to different parts of the world) and each part number has a CIQ version associated with it. So given a device, you can figure out the min (and max) CIQ version for it.

    But thats where the problem comes. I'm pretty sure those version numbers are bumped up over time, so a device that was released as ciq-3.0 may get ciq-3.1 at a later time, for example. But there's no way to know that any particular device has actually been updated.

    Unless installing a new app also makes sure the device's firmware is up to date (that would be nice but I don't see any evidence for it)?

    But even then there are api's which get taken away. eg look at BufferedBitmap. The example on that page does

    if (Toybox.Graphics has :BufferedBitmap) { ... new BufferedBitmap(...); }

    but that fails with newer devices, because you're supposed to use Graphics.createBufferedBitmap if it exists. I guess thats not really relevant to this issue, because code that *just* checks for has :BufferedBitmap is going to fail whether I converted it to true or not.

    But what about AppBase.setProperties which showed up in another couple of threads. It seems likely that at some point, new devices will stop supporting that.

  • The problem with compiler.json is that it can't be used to determined max version (well it can be for NOW, but then every time there's an update to any of the devices we'll have to recompile IF we used any feature that depends on the MAX version.

    And compiler.json unfortunately can't even be used to determine MIN version. I thought it can be, but when I updated https://github.com/flocsy/garmin-dev-tools I realized that they're removing the older versions and I guess the file always represents the newest CIQ version for that part available when the compiler.json was downloaded. This is a pity, because a) min versions could be easily organized by Garmin and b) this would enable us to use some features that are based on the MIN version. And BTW this is what I kind of use today (manually) when I do things like:

    (:no_api2, :inline, :typecheck(false))
    function getConfig(key as PropertyKeyType) as PropertyValueType {
        return ExtHRMApp.getProperty(key);
    }
    (:api2, :inline)
    function getConfig(key as PropertyKeyType) as PropertyValueType {
        return Properties.getValue(key);
    }
    

    The idea is to decrease the code size and use only the older code for devices that can only have the older version AND for devices that can either have the older or the newer version. The only problem is that there's no way to know the min versions :( See: forums.garmin.com/.../improve-documentation-add-api-levels-and-system-levels-to-device-reference

    BTW why would anyone use X has :foo when X is a class in my code and if there are differences in whether X has :foo will return true or false is anyway determined by the monkey.jungle and most probably I could use the annotation? I do use this a lot (together with :inline to minimize duplicate code)

    // Instead of:
    class Y {
       function a() {
           if (X has :bar) {
               X.bar();
           } else {
               X.foo();
           }
       }
    }
    
    // I use:
    class Y {
        (:no_bar)
       function a() {
           X.foo();
       }
       (:bar)
       function a() {
           X.bar();
       }
    }

  • BTW why would anyone use X has :foo when X is a class in my code

    Well, here's an example: https://github.com/voseldop/timeless/blob/e733403028ae2bb004f3ff04f5ae0e2c63ec2937/source/timelessView.mc#L33-L35

    Seems quite reasonable to me. He has his resourcePath setup for each device. Yes, he *could* setup a parallel set of excludeAnnotations; but he doesn't need them. And if he did, it would be possible to misconfigure things (ie select the resource file that defines the font, but the excludeAnnotation that says it's not defined). This way, it's self contained, and guaranteed to be consistent.

    btw, the reason I don't optimize to true at the moment is simply that right now I do nothing special for api.mir - so it appears to the optimizer that every class/method/constant exists unconditionally. This means that optimizing to false is safe, but optimizing to true would absolutely not be. But I think as long as I avoid optimizing anything to do with the ToyBox, it should be fine to make this optimization symmetric.

    At which point I think the first version of your code above (using if/has) would be clearer (and just as efficient) as the excludeAnnotation version.

  • I know it's not your code, but that example looks strange. large_thin is used in line 196 but there's no check whether it is null or not...

    And actually even with your optimization it would only eliminate the if and 1 line in the constructor, but _probably_ the whole block around line 196 should also be in an if like: if (Rez.Fonts has :id_large_thin) and that would save lot of code :)

  • I know it's not your code, but that example looks strange. large_thin is used in line 196 but there's no check whether it is null or not

    You're right - but the principle stands.

    E.g https://github.com/srwalter/garmin-tesla/blob/c6e43c19394ebff5130d5e5a6577bb16b8f39d17/source/MainView.mc#L41-L45 as a better example.

    It also happens with code. With your example, it's an if-else; some devices do one thing, but other devices do another. But in some cases, it's just some devices do a thing, and the rest don't. In that case it makes sense to put the doAThing() code in a separate file and select it via sourcePath in your jungle. Then you *could* choose to use excludeAnnotations to decide whether to call it or not; but "has" is straightforward, and again avoids the issue of making sure the (otherwise unneeded) excludeAnnotations match the sourcePaths for every device.

    Also, if the call to doAThing() is deep within some other function, you either need to make two copies of that function which you select via exclude annotations (involving code duplication, and the risk of the two versions getting out of sync); or you need to use the exclude annotations to create a const boolean flag which you test. And (without my optimizer) doing that is actually more expensive (in code size) than doing the has test - and again suffers from the fact that its not clear to the reader that the flag corresponds to the existence of the function.

    But back to "why would anyone do that". I guess this is the reason I wrote the optimizer to begin with. When I first started writing monkeyc code, I tried to write clear, concise, self documenting code, and I assumed that would be efficient. But it turned out that it wasn't. Using Toybox constants is expensive; using my own named constants (whether const or enums) is even more expensive. So I wrote the optimizer so that I could write clear code AND get good results. This is just one more case of that. It gives the option of writing code whose meaning is clear, with minimal scaffolding, and still getting optimal results.

  • Also, if the call to doAThing() is deep within some other function, you either need to make two copies of that function which you select via exclude annotations (involving code duplication, and the risk of the two versions getting out of sync); or you need to use the exclude annotations to create a const boolean flag which you test. And (without my optimizer) doing that is actually more expensive (in code size) than doing the has test - and again suffers from the fact that its not clear to the reader that the flag corresponds to the existence of the function.

    The other thing you can do is use line-by-line conditional compilation. It's ancient (and not implemented in any modern language except C#, for good reasons) but it works. I have a system that uses the C preprocessor - it's janky but it works.

    Haven't ported it to VS Code tho (in terms of automatically running the preprocessor as a pre-build step).

  • Thanks for the feedback on how to optimize Toybox feature testing.

    I think the conclusion is that its not safe to convert "Toybox.foo has :bar" to true based on api.mir and compiler.json because (as I suspected and Gavriel confirmed) the minimum version gets updated as new firmware is rolled out; but there's no guarantee that any given device actually got the update. Even if I build a list of known *initial* firmware releases for every device, there's no guarantee that an api won't be deprecated, and eventually removed (eg AppBase.getProperty is looking like that will happen).

    On the other hand, determining that a given device doesn't support a given api should be safe, but does suffer from the problem that:

    but if device get new firmware and you have in code false instead of runtime HAS you have to rebuild app

    This isn't a disaster though; it just means the App continues to work as it did before, even though it *could* now use some new feature. And it only takes a rebuild after updating the devices via the sdk manager to fix that.

    So my plan is:

    • extend the current optimization to optimize checks of user defined things to both true and false
    • add an option to optimize tests of Toybox apis to false when the optimizer can tell that (based on the currently installed Sdk and Devices folders) the api doesn't exist on the current device. The option will default to false.
  • The other thing you can do is use line-by-line conditional compilation. It's ancient (and not implemented in any modern language except C#, for good reasons) but it works. I have a system that uses the C preprocessor - it's janky but it works

    Right - I saw a couple such implementations before I started writing my optimizer. My issue with that is that now you're writing in a different language. I wanted everything to be in monkeyc, using features supported by the language. And in fact, with my optimizer, you can do line-by-line conditional compilation using regular ifs, testing constants defined under excludeAnnotations. eg:

    (:feature)
    const hasFeature = true;
    (:noFeature)
    const hasFeature = false;
    
    function foo() {
      doSomething();
      if (hasFeature) { doFeature(); }
      doSomethingElse();
      if (!hasFeture) { featureLess(); }
    }

    This works, with or without my optimizer; but with the optimizer there is no code overhead.

    What I'm trying to do here is make it more natural:

    function foo() {
      doSomething();
      if ($ has :doFeature) { doFeature(); }
      doSomethingElse();
      if ($ has :featureLess) { featureLess(); }
    }

    So now all you have to do is arrange for doFeature to be defined or not (which you had to do anyway). Note, as of the current release this isn't no overhead; when the function doesn't exist, the code will be cleanly removed, but when it *does* exist, the if and test will remain.

  • Note, as of the current release this isn't no overhead; when the function doesn't exist, the code will be cleanly removed, but when it *does* exist, the if and test will remain.

    Yep that's basically my objection. I agree that your way is nicer, but as noted above, I am also kind of concerned about transforming what's normally a run-time check (has) into a compile-time check. At least with line-by-line conditional compilation, the line between runtime conditionals and compile-time conditionals is clear. As a dev, I don't want to worry about stuff being inappropriately optimized away.