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]

  • Yes, it's the 3rd case. I generated some constants and code from the simulator.json, and I still don't use all of them (and I'm not sure if I ever will, because I'm trying to do the code-generator general so it'll be kind of a boilerplate that can be used, but not all of it will be)

    I'm thinking about a better ("automatic") solution to do this, because it's not very user-friendly with excludeAnnotations. Is there some way I can help the current optimizer? For example if I move both the constants and the functions into a module? Or any other trick?

  • Well, it would be fairly easy for me to add an option to re-run the optimizer and dead code removal every time dead code removal does something (I have to re-run the optimizer, because thats what collects usage information). I've resisted doing that because it's very inefficient, and once I do it, I'll be less motivated to implement a good solution. But as an option, its probably ok...

    But I do have an alternative suggestion for you. If you're generating code, you should probably be generating the jungle file too. Or at least, generate a supplemental jungle file with the device specific bits. At any rate, thats what I found - once I started generating device specific code via compiler/simulator.json, I also wanted to generate device (and device-family) specific excludeAnnotations and sourcePaths. After all, the generator knows exactly which code applies to which devices...

    I guess I started on my code generator before I wrote the optimizer; so there was no choice but to exclude code that didn't apply - otherwise everything got really bloated (not to mention duplicate symbols etc).

  • I generated some constants and code from the simulator.json, and I still don't use all of them

    v2.0.52 adds an "Iterate Optimizer" option, which defaults to false. If you set it to true, it will keep running the optimizer until it doesn't make any changes.

    It also adds more features to the post build optimizer - I'm getting a 170 byte reduction on an 11k binary now.

  • something is odd here:

    2.0.51: code: 11659 data: 2632 => code: 11606 data: 2632

    2.0.52: code: 11606 data: 2632 => code: 11496 data: 2632

    2.0.52 iterate optimizer: code: 11659 data: 2632 => code: 11496 data: 2632

    In other words, when I enable iterate optimizer then the 1st phase is worse than when not enabling it (exactly like it was in 2.0.51) ("=>" means the 1st and 2nd run, what is printed as optimized-Foo.original => optimized-Foo)

  • something is odd here

    Yes - that looks very odd.

    My first impression is that you're reporting those backwards. 11606 with iterate enabled, and 11659 with it disabled. Or that maybe somehow things didn't get cleaned between the builds (which should happen automatically).

    I would mostly expect the source level optimizer to be the same between 2.0.51 and 2.0.52 - so it would make sense that with iteration disabled there was no change in size (there's a small change in the order in which things are done, so with it disabled there *could* be a change in size, but I wouldn't expect it).

    So, to check, how about trying these build tasks (change the device to something suitable):

    {
    	"version": "2.0.0",
    	"tasks": [
    		{
    			"type": "omonkeyc",
    			"device": "fenix5xplus",
    			"simulatorBuild": true,
    			"label": "Optimized fenix5xplus: Iterate",
    			"outputPath": "bin/iterate",
    			"iterateOptimizer": true,
    			"releaseBuild": true,
    			"postBuildOptimizer": true
    		},{
    			"type": "omonkeyc",
    			"device": "fenix5xplus",
    			"simulatorBuild": true,
    			"label": "Optimized fenix5xplus: No Iterate",
    			"outputPath": "bin/noiterate",
    			"iterateOptimizer": false,
    			"releaseBuild": true,
    			"postBuildOptimizer": true
    		}
    	]
    }

    Stick that in tasks.json (or just add the two configs to your existing one if you have one). Then run them via Cmd-Shift-P => Run Tasks => "Optimized Iterate" or "Optimized No Iterate". Note that they each have their own output directory, so there shouldn't be any stale build issues.

    If those confirm your results, my only explanation would be that more inlining is happening on subsequent iterations; and it's making things worse. But the coincidences between the different options, and before/after sizes are amazing. Even the fact that iterate makes a difference, but then the post build optimizer exactly cancels it out is rather surprising - the kind of things that iterate should do (remove whole functions/constants) are completely unrelated to the things that the post build optimizer can do...

  • I won't be able to recheck until tomorrow but I'm sure that what I wrote is correct because in exactly that order i tested them. I even called clear optimized build between them. So the 2nd build was right after upgrade, without changing the settings, and the 3rd after enabling it.

    One thing that could explain it if the bug is that the settings is the opposite of what you think it is :)

  • downgraded to 2.0.51, ran clean optimized build, then build:

    > Sizes for optimized-ExtHRM.original-fenix6: code: 11664 data: 2640 <
    > Sizes for optimized-ExtHRM-fenix6: code: 11611 data: 2640 <

    upgraded to 2.0.52, ran clean optimized build, made sure that iterate optimizer is disabled in the settings, then build:

    > Sizes for optimized-ExtHRM.original-fenix6: code: 11664 data: 2640 <
    > Sizes for optimized-ExtHRM-fenix6: code: 11501 data: 2640 <

    enabled iterate optimizer in settings, ran clean optimized build, then build:

    > Sizes for optimized-ExtHRM.original-fenix6: code: 11664 data: 2640 <
    > Sizes for optimized-ExtHRM-fenix6: code: 11501 data: 2640 <

    So at least now it's not awkward. I'm not sure if the small changes I did since the last time I ran it had to do anything with it though.

    I also removed now the annotations that I added a few days ago to exclude the unused but already generated Arrays, Dictionaries by my python script, and tested both with and without iterations and I can confirm that the iterations work, they removed the unused dictionaries.

  • So at least now it's not awkward

    Yes, those results are exactly what I would expect.

  • I don't understand something: why here it tells me: Foo.mc:130:13: While inlining moreThanTrialTime: This function can only be inlined in statement, assignment, if or return contexts

        (:ciq_2_3_0, :inline)
        hidden function moreThanTrialTime() as Boolean {
            var info = Activity.getActivityInfo();
            return info != null && info.startTime != null && Time.now().subtract(info.startTime as Time.Moment).value() > TRIAL_SECONDS;
        }
        (:ciq_2_3_0, :inline)
        hidden function isTrialExpired() as Boolean {
            return ExtHRMApp.isTrial() && moreThanTrialTime();
        }
        (:inline)
        hidden function computeHRV(data as ExtHRMData) as Void {
            var isTrialExpired = isTrialExpired();
        }

    but when I do this then it does inline isTrialExired without a problem?

        (:ciq_2_3_0, :inline)
        hidden function isTrialExpired() as Boolean {
            var info = Activity.getActivityInfo();
            return ExtHRMApp.isTrial() && info != null && info.startTime != null && Time.now().subtract(info.startTime as Time.Moment).value() > TRIAL_SECONDS;
        }
        (:inline)
        hidden function computeHRV(data as ExtHRMData) as Void {
            var isTrialExpired = isTrialExpired();
        }

  • but when I do this then it does inline isTrialExired without a problem

    because the first one only calls Activity.getActivityInfo() when ExHRMApp.isTrial() is true, but the second one calls it unconditionally.

    Inlining moreThanTrialTime() involves inserting a bit of code looking like

        var moreThanTrialTimeResult;
        {
            var info = Activity.getActivityInfo();
            moreThanTrialTimeResult = info != null && info.startTime != null && Time.now().subtract(info.startTime as Time.Moment).value() > TRIAL_SECONDS;
        }
        ...
        <use moreThanTrialTimeResult>

    somewhere. But there's nowhere safe to insert it. Obviously, I *could* rewrite isTrialExpired as

        (:ciq_2_3_0, :inline)
        hidden function isTrialExpired() as Boolean {
            var result;
            if (ExtHRMApp.isTrial()) {
              result = moreThanTrialTime();
            } else {
              result = false;
            }
            return result;
        }
    

    and now moreThanTrialTime() is in assignment context, and can be inlined. But doing this completely generically is non-trivial, and could have significant code size costs. So I only do a small set of such rewrites, which are easy to recognize (for the compiler at least!) and are guaranteed to be cheap. Also note that before that rewrite, isTrialExpired can be inlined anywhere (assuming moreThanTrialTime doesn't get inlined), but afterwards, it can only be inlined in assignment/statement/return/if contexts. So again, doing it automatically may not be ideal (because then you'll be posting about why *that* function can't be inlined somewhere; because as written, it should be inlinable anywhere).

    So it's up to you to do that rewrite if thats what you want. Or you can decide (as you did, apparently) that it's ok to call Activity.getActivityInfo() unconditionally, and rewrite it that way.