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]

  • This inheritance is technical, but could be replaced by inlining code

    So this looks like something that could literally be handled by reusing the (:inline) attribute... just add it to your class, and any class that extends it gets the contents of the class "inlined" into it.

    The only issues I see would be name conflicts, and the fact that any privates in the base class would become accessible from the derived class (but not any classes derived from that). So you could write code that worked with the optimizer, but failed without. But thats probably not a big deal. Or the inlining process could rename the privates so that any attempted references from the derived class would fail. Or it could spit out an error message, and refuse to inline...

    For your "abstract function" idea, I think the problem is making it work with and without the optimizer. Although as you wrote it, you could just put (:inline) on each of the implementations, and y would get inlined into x...

  • Indeed, I could use :inline but that is only useful if the function is static and doesn't use anything from the class, in which case it's not related to the inheritance / abstract / class topic that much. The real deal would be to be able to use everything in the function that I would be able to use if I wrote it inside the class.

  • I believe you meant: "any class that extends it gets the contents _and 'extends'_ of the class "inlined" into it"

  • I saw another pattern that can be improved: let's call it "duplicate code" (though I'm sure it's not that easy, and it probably split to many cases)

    The simplest example is when something like "i+2" is repeated multiple times:

        hidden function replaceFormatImpl(str as String) as String {
            var chars = str.toCharArray();
            var size = chars.size();
            for (var i = 0; i < size; i++) {
                var char = chars[i];
                if (char == '{' && i + 2 < size && chars[i + 2] == '}') {
    

    The generated code is: 

    if (char == '{' && i + 2 < size && chars[i + 2] == '}') {
    but it could be improved:
    var i2 = i + 2;
    if (char == '{' && i2 < size && chars[i2] == '}') {
  • I saw another pattern that can be improved:

    This is in some ways a pretty straightforward extension of the existing size-based-pre pass. In fact when I started implementing that, I was aiming to handle expressions like these, and the way its written, its (almost) a case of choosing which expressions to throw in as PRE "candidates", and the algorithm takes care of the rest. But there are a few issues.

    The main problems - and why I ended up restricting it initially - are canonicalization (are "i+2" and "2+i" the same expression, or different?) and overlap (given "return x[i+2] + y[i+2] + 2" do I pull out "i+2" or "2" or both?). The current restrictions avoid both these issues.

    Canonicalization is yet another case of something I can't determine without type analysis - and I hope to have an initial release of that soon. Overlap is mostly just a case of coming up with a strategy. eg

    • pick the candidate that gives the most savings, pull that out, and then re-evaluate the remaining candidates, and pick the next best, until there are none left. Its not guaranteed to give the best results though; and it could give worse results than the current algorithm (eg I pull out one candidate that saves 8 bytes, but by doing so prevent pulling out 3 candidates that save 6 each).
    • start with the simplest candidates; that way I never do worse than my current approach, and sometimes do better; but that will often do worse than my first suggestion!
    • try every possible order, and see which one gives the best results (in case its not obvious, this is not a realistic approach!)

    There are some other issues, like determining when an expression might change, but thats already mostly covered; I already have to determine when some of the candidates might change (eg a module scope variable, or member variable); but if I allow more general expressions, I would have to check them recursively (or keep a map of modifiable subexpressions for each one).

    But yes, it's definitely doable. I may come back to it after the type analysis is stable. 

  • Regarding canonization (or is it canonicalization?) I understand the problems, but doing any one of them is probably better than not doing. This kind of reminds me :) It's hard to decide life decisions when you don't have all the inputs, and it can paralize you, but usually any decision is better than being paralized :)

    I would argue that doing the simplest is enough: only recognize i+2 and i+2 already covers the most used use cases. From my experience things written by one person tend to use the same style. So this is a good, and easy and safe start. Later if you want to recognize when can i+2 and 2+i be the same? Or 1 + n + 2 and 1 + m + 2 by reordering and summarizing them to n + 3 and m + 3 or other smart decisions is a nice to have.

    For the later optimizations there can be so many things. Yesterday I managed to cut off lot more bytes from a simple function with ~10 lines of code. I had i, i+1, i+2, i++ in it, all of them multiple times, and is specific order (i++ was not only at the end), so I did more or less what you explained above: did my best, then looked at it again and saw new opportunities, etc... I was shocked to see how pricey i++ is, and that I didn't really needed it once I anyway already had the i1=i+1 and i2=i+2 I could do without the extra i++-s. But these are harder to do automatically. Or easier :) it depends how you look at it, because it wasn't trivial to see these opportunities, and certainly couldn't see the 3rd step at the beginning, only after I finished with the first 2 steps.

    And picking the candidates? I think the greedy algorithm you described is good, especially if you can copmare it's result to the current algorithm's result and pick the better one.

  • This inheritance is technical, but could be replaced by inlining code. I see why it's not that simple, mostly because it's probably a very special case, I don't know how many others use this

    This technique will be used by others as I've promoted this technique a few years ago on the Garmin developer blog. When the developer blog died I moved the article to my own blog here: https://starttorun.info/tackling-connect-iq-versions-screen-shapes-and-memory-limitations-the-jungle-way/

    I don't think you would need any extra code annotations, as soon as you see an extends on a class that's not a connect iq base class (and this class is not directly used someplace) then you could safely merge all logic into the child class. 

    Challenges are that there can be multiple levels in the inheritance tree. Code could be overriden in the child class or it can add to the class by inheritance

    When this would be implemented memory gains could be massive as there's quite a bit of overhead to defining the extra classes in your inheritance tree.

  • Exactly, so I already added the (:incline) to my base classes, so when this feature will be added I'll be ready ;)

  • I don't think you would need any extra code annotations,

    Well, not need per se. But if the base class is used more than once, you probably don't want the optimizer to do it automatically. Also, as I pointed out, there could be name conflicts, or subtle changes in behavior, so making it explicit seems safer.

    Code could be overriden in the child class

    That's relatively easy to deal with though; if the base class code is called from the derived class (eg Base::initialize) you just inline that call into the no-longer-derived class's method.

    as soon as you see an extends on a class that's not a connect iq base class

    Actually, something I realized a while ago, is that you can often (maybe always) drop an extend of a ciq base class, as long as you explicitly implement all the relevant methods. eg there's no need for your delegates to extend MenuInputDelegate or whatever, as long as they provide all the methods that particular delegate is supposed to provide. Usually, the "extra" methods are just "return true" or "return false". The optimizer already knows which base class methods aren't overridden, so it would only need a list of what the default behavior is supposed to be, and it could provide those methods (or if it found a missing method, and didn't know how to provide it, it could just give up). The one downside is that Garmin's type checker *does* complain that you've provided the wrong type. But you can shut that up with an explicit cast...

  • I was thinking: when we optimize we optimize the code+data, but many of the optimizations increase of the memory footprint of the function. For example the pre_ variables take some memory. So it's possible that while we managed to decrease the code size by 1-2 bytes we increased the memory usage by 4 bytes, and the overall gain is negative?

    For example:

    var a = width / two;
    var b = width / two;
    var c = width / two;

    var w2 = width / two;
    var a = w2;
    var b = w2;
    var c = w2;
    The optimized code code is 2 bytes less, but when it runs then w2 is held in the memory until the end of the function, and it is the "only function" (or better phrased one of the entrypoints of our code, like onUpdate or compute) or any function inlined into them, then w2 will kept in the memory more or less all the time, and instead of gaining 2 bytes we probably use 2 bytes more memory than before the optimization.
    And also I see a specific case when IMHO it could be easily optimized, when a class variable is used more than once (as right value):

    if (mResult2 != null) {
      extHr += " " + mResult2;
    }
    this wasn't optimized, though it could have been 2 bytes less:
    var result2 = mResult2;
    if (result2 != null) {
      extHr += " " + result2;
    }
    I'm sure this is some glitch that can be fixed, but as I know you you'll also tell us why it's not (always) that easy:) However maybe when it's not that easy, can we have this feature (maybe with a setting to enable/disable): when you detect that a class variable is used more than once as right-value then display a warning. This could at least tell us to look at it and in most cases we could manually eliminate it.