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]

  • I also see optimized code like:

    function f() {
      var pre_7, pre_3, pre_1, pre_2, pre_0, pre_4, pre_8;
      pre_8 = 8;
      pre_4 = 4;
      pre_0 = 0;
      pre_7 = 0x07;
      // other code (10 lines)
      
    pre_1 = 1;
      // other code
      // inlined code:
      {
        /// ...
        pre_2 = 2;
        pre_3 = 3;
        //...
        use pre_1;
      }
    }

    I wonder why some pre_ variables are assigned in the beginning of the function, why others like pre_1 only later (and why so soon when it's only used in the inlined code) and why the variables that are only used in the inlined code are declared at the beginning of the function?

    Do you know if declaring a adds it to the stack or only when it's assigned? Because if it's added to the stack then declaring them too early is not the best (it uses some memory, so in theory it could cause out of memory error, while only declaring it in the block where it's really needed might be better in some cases IMHO.

  • This is a tricky one: in some places in my code I do some minor optimizations that are similar to what you do and the 2 together result in a less efficient code than when only one of us optimize.

    function f() {
        var zero = 0;
        // use zero at least 3 times
        g();
    }

    (:inline)
    function g() {
        var zero = 0;
        // use zero at least 3 times
    }

    When I compile my code without the optimizer then doing this micro-optimization results in better code than using the literal "0" 3 times in both functions.
    When I compile the code that has the 6 literal 0-s with the optimizer then it results in the smallest code.
    But when I compile the above code with the optimizer then it results in a slightly bigger code.

    I THOUGHT I found a compromise: when I change the "var" to "const" it creates the smallest code with the optimizer, but the problem is that it fails to compile without the optimizer. This is probably because the optimizer does the constant substitution, the const is eliminated, replaced with literals, and then the literals are replaced with the pre_X variable.

    So it would be nice to have some solution.

    Ah and I also see another small problem:

    const INVALID_HR = 0x00;
    function f(payload as Array<Number>) {
      var heartRate = payload[7] & 0xFF;
      if (...) {
        heartRate = INVALID_HR;
      }
      self.heartRate = heartRate;
      g(payload);
    }

    (:inline)
    function g(payload as Array<Number>) {
      var zero = 0;
      // use zero
    }

    it is optimized to:

    function f(payload as Array<Number>) {
      var heartRate = payload[7] & 0xFF;
      self.heartRate = heartRate;
      {
        var zero = 0;
        // use zero
      }

    }

    In this case for some reason (I guess the order of the steps that replaces const and that replaces multiple uses of the same literal) it doesn't recognize that the it could do a var pre_0 = 0; in the beginning of f() and use it instead of 0x00 and also instead of zero (ok I realize it might be more trickier, but I am pretty sure that you know that zero is not changed in g and it's not passed to another function, so it's kind of a const) Here again if I use the 3 0 literals instead of zero in g() then it works exactly as I ask it to be :)

  • possible optimization: if/else switching:

    var b as Boolean;

    if (!b) {
       // foo
    } else {
      // bar 
    }

    by changing the if and else branches the code will be 1 byte smaller:
    if (b) {
       // bar
    } else {
      // foo
    }

  • Of course it's smaller because there is no ! operator and you should write it correctly and don't count on the optimizer.

  • I agree that you CAN manually switch it, but the point of the optimizer is to optimize ;) 

  • (the point is to add the definition at or 1 line before the opening { of the block)

    Sounds reasonable... but I assume you wouldn't want this when a function is inlined in expression context?

    eg

    (:inline)
    function foo(x) { return x * 2; }
    function bar(y) { return y + foo(y) + 2; }
    
    // currently becomes
    function bar(y) { return y + y * 2 + 2; }
    

  • possible optimization: if/else switching

    So as with optimizing "x + 0", this requires knowing the type of the condition, which currently I don't. (because of course, !x, when x is a Number is equivalent to ~x, so the sense of the if *isn't* inverted).

    But I'm in the middle of a significant rewrite that adds type propagation, and in fact I'm already doing both of those optimizations when appropriate; its just not quite ready for release yet.

  • This is a tricky one: in some places in my code I do some minor optimizations

    Well, again, it turns out I'm already doing this... just not quite released yet. I did write a wiki page though :-)

  • I wonder why some pre_ variables are assigned in the beginning of the function, why others like pre_1 only later

    The algorithm is complex and mysterious :-)

    Generally speaking, it  needs to put it early enough that it covers all uses; and in the case of non-constants, it also needs to ensure there are no modifications between the initialization and all uses. Note that it often can't put it just before the first use eg:

    function foo(x) {
    
      if (x) {
        // inserting pre_0 just before the first use 
        // doesn't work, because the variable wouldn't 
        // always be initialized by the last use
        bar(0, 0);
      }
    
      return 0;
    }

    But I agree, it's not always obvious why it makes the particular decisions it makes. Usually when I dig into it though, there *is* a reason (although sometimes its not a good one, and I fix it).

    The next release (not the type propagation release) will hopefully be out later today, and pre is much better wrt non constants (the analysis of which functions might change a particular global variable is much more precise).

    But on the other questions... it makes no difference to the code size where the variables are initialized. Space is allocated on the stack by a single instruction at the start of the function. Other than that one instruction, variable declarations also generate no code; which explains why garmin's compiler issues an error and refuses to compile if it can't prove that a variable is initialized on all paths (in my opinion, a better solution would be to just initialize the variable to null in those cases; but it does at least make sense).

  • I would even include it then (though it can become hard to read when nesting lots of inlines, but it's even then it's adding value to the code IMHO):

    function bar(y) { return y + /*foo*/(y * 2) + 2; }

    In this case I'm not sure if it should be /* foo */  or /* foo(x) */, I'll leave that up to you ;)