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]

  • var x = jparts[0]; // julian 2000.0 date, a double
    var y = null as Boolean;
    testType(x, y);
    
    public function testType(arg1 as Number, arg2 as Boolean) {
    	System.println("arg1 " + arg1 + ", arg2 " + arg2);
    }
    

    Gives this:

    arg1 8701.133116, arg2 null
    arg1 8701.133128, arg2 null
    arg1 8701.133139, arg2 null
    arg1 8701.133151, arg2 null
    
    

    So, yeah, I'm not seeing the point of specifying types for the compiler. Time spent trying to resolve compiler type warnings was and is wasted. I assume that's why everyone says to turn off the type checker and not worry about it. Maybe the documentation should lead with that.

  • TL;DR Monkey Types can be useful if you know what you're doing. Monkey Types would be more useful if Garmin fixed a few things, and if they had chosen a slightly different design for certain things.

    The history of Monkey C and Monkey Types is similar to the history of Javascript and Typescript. In the case of Monkey C and Javascript, both languages were designed to be loosely typed to make it easier for devs (as opposed to Java and C# where types are mandatory everywhere.) This ease of use obviously comes with the tradeoff that it's very easy to have runtime type errors that the compiler can't prevent.

    e.g.

    function f(x) {
      return 42 + x; // IIRC, this will crash if x is a string, for example
    }

    Let's say f is called with a value that comes from the (untyped) JSON response to a network request in CIQ. (The response has to be a dictionary, but the dictionary's value types aren't known at compile-time.) There's no way that the compiler can tell whether the call to f will crash or not.

    Monkey Types and Typescript both represent the pendulum swinging the other way to where it's possible to specify types for everything and (hypothetically) make your code bullet-proof in terms of run-time type errors. They also represent a compromise where you don't have to specify types when the compiler can infer them (in the case of Monkey Types, you *can't* do so), and you don't have to specify types for everything.

    The point of Monkey Types (compile-time type checking for Monkey C), as with Typescript, is that if you "use it correctly", you can actually prevent certain types of run-time type errors (by having the compiler generate an error at compile time.) Monkey Types are not the same thing as Monkey C run-time types, but the former can say something about the latter.

    The problem with Monkey Types is it's not yet mature enough (imo) nor is it as well-designed as it could be --  "using it correctly" isn't straightforward in some cases.

    Even with Typescript, if you incorrectly use "as" (which is always a cast and never a type declaration), you can circumvent the type checker and allow certain types of runtime errors to slip through the cracks.

    Monkey Types exacerbates the weaknesses of a Typescript-ish compile-time type checker by:

    - forcing you to use "as" (cast) in common scenarios such as array/dictionary usage (maybe this will be fixed eventually)

    - using the same syntax ("as") for type declarations and type casts (because again, type casts are dangerous and should be used sparingly whereas type declarations should be used as often as possible)

    - not preventing the dev from casting between 2 unrelated types. (In typescript, "let x = null as Boolean" is an error)

    [https://www.typescriptlang.org/play?#code/DYUwLgBAHhC8EDsCuxgQIYGcICED2eo6CA3EA]

    Of course, you will note that in the case of external input like a network request, simply specifying the complete complex type of the response in either Monkey Types or Typescript won't guarantee that the actual response fits the specified type. That's one case where run-time type checks such as instanceof or has are still useful and necessary.

  • It's a fundamental rule of programming to move code that always gives the same result independent of the loop and just do it once before entering the loop

    No. That's a fundamental rule of optimizing for speed. The primary goal of my optimizer is to optimize for size - which often *also* optimizes for speed, but not always.

    In any case, I admit that I was surprised to see it doing that (but after thinking about it, I realize it's exactly what the algorithm is supposed to do). But I can easily add an option to prevent doing single-use copy-prop into the body of a loop, so I'll do that when I get time.

  • It may compile in both cases, but this optimizer obscures a run time error, which to me is a problem.

    I mostly agree, and tried to ensure that the behavior of the optimized code is the same as the unoptimized code. But there are a few cases where I do make an optimization on the assumption that the original code would not have crashed (ie the behavior is guaranteed to be the same when the code doesn't crash, but if the original code crashed the optimized code might do something else).

    eg

    • accessing a Toybox constant without the right permissions - I'll just replace it with the value of the constant.
    • similarly, accessing a non-static constant from a static method. If the method was called statically this would have crashed; but if it was called non statically (eg from a non-static method of the class) then it works - and its pretty obvious that the user *wanted* the value of the constant.

    There are probably a few others that I can't think of. And obviously, you think this is unacceptable; but you weren't going to use it anyway, so...

  • So how does the code you generate relate back to the original code when it comes to things like the stack trace in ciq_log and ERA?   That to me is a bigger issue.

  • As I've said before, the best optimizer is located between the chair and keyboard and there is a learning curve for this with CIQ

    This is certainly true. Better algorithms will almost always beat better optimizations.

    But an enormous amount of work has still gone into many different compilers over the years, and it's not because the compiler writers don't understand this. It's because in many cases there are optimizations that you either can't do at all at the language level; or that would result in unmaintainable code.

    Obviously with a source to source compiler the first type isn't relevant (if a source to source optimizer can do it, a human can do it), but readability and maintainability is very important. Making the kind of changes my optimizer makes (or at least, some of them) would lead to horribly unmaintainable code.

    And of course, that first one is the reason I added the post build optimizer; I ran up against things that I simply couldn't do at the source level, but which were easy in bytecode (on the other hand, by the time you get to the bytecode, there's no type information, so I still need both optimizers).

  • So how does the code you generate relate back to the original code when it comes to things like the stack trace in ciq_log and ERA?   That to me is a bigger issue

    Sure, thats one of the downsides. But if you read 's post on this a few replies up, his description of how he thinks it should work is pretty much exactly how it does...

    First, you can configure an export task that generates the source code anywhere you like (by default it goes in the bin directory, and isn't expected to be preserved). So you just add an exports directory to your project, and commit the results along with the rest of your source code as part of your release process.

    Second, if you install a specific version of @markw65/monkeyc-optimizer in your project, the extension will use that, rather than the one that's included in the extension. As long as you commit your package-lock.json along with the rest of your sources that means you can recreate the original build just by checking out the release, re-running "npm install" and re-building your project.

    So yes, era will point to the optimized source. But its easy to ensure that you keep a copy of that; and it's relatively easy to ensure that you can recreate it exactly.

  • But this isn't part of a compiler.  It generates different code that's sent to the compiler. The post mod of bytecodes scares me more, as the debug.xml may no longer even reflect what's in the modified bytecodes.

    This is why the original code should be sent to the compiler, but things can be noted by the extension which "the optimizer between chair and keyboard" can make if appropriate

  • This makes things very complex, especially for new users.

    so if the stack trace points to line 100 (in the modified source) how do you find what line that is in the original source? An example would help.

  • The primary goal of my optimizer is to optimize for size - which often *also* optimizes for speed, but not alway

    Since we are constrained on both memory and number of instructions we can execute in one call to onUpdate / onPartialUpdate and probably a few others, optimizing should target both goals. Saving a few bytes for a few thousand opcode executions is not likely to be a good trade-off.