Conditional Compilation in MonkeyC

I'm curious as to whether anyone else has hacked together a conditional compilation setup for MonkeyC.

It seems like something that would be nice to have built-in to MonkeyC since there's such a priority on small, efficient code; rather than have debug code, etc wrapped in feature flags, it's preferable to have it excised entirely.

I've written a Python script that hunts for the following tags // [strip:begin <tag>] and // [strip:end <tag>] and conditionally comments/uncomments code between those markers.

This allows me to quickly add remove logging/test fixtures in my test and release builds.

Anyone else doing something similar? Would there be any interest in me publishing this code? Or is this a bad idea for some reason?

Curious what folks thoughts are here...
  • check out "Build File Exclusions" in the "Overriding Resources" section of the developer's guide.

    With it, you can have different code based on the device type, for example, or enable/disable things in the code.

    Conditional compile is there already.
  • Wow, totally missed that. What I was doing definitely had a 'code smell', so glad to see there's a more elegant, built-in, solution here.
  • I'd been hoping for some conditional things for months, and it wasn't until someone first starting with CIQ had a question about it (they were reading the whole Programmer's guide), did I hear about it, so I looked. It's nice, but only a few lines in the Programmer's Guide and easy to miss!
  • Build exclusions are similar, but you can't completely compile stuff out. It does seem to work okay to allow you to write code that is used for one group of devices where an alternative exists for other devices. Unfortunately, it doesn't work well in cases where you want to eliminate some code entirely.

    As an example, consider this conditionally compiled logging module...

    (:debug) module Logging
    {
    function trace(msg) {
    Sys.println(msg);
    }

    function trace_format(fmt, args) {
    Sys.println(Lang.format(fmt, args));
    }
    }


    This seems great at first. I have my Logging functionality that is only available when not compiling with -x debug. But if I compile with -x debug any code that uses Logging.trace() will cause the program to fail with an undefined symbol error. So I have to either conditionally define every function that calls Logging.trace() or I have to write a stub class that is available in non-debug builds. The former option seems like a non-starter (unless the developer wants to maintain two complete sets of code), so we're left with the latter...

    (:release) module Logging
    {
    function trace(msg) {
    // do nothing
    }

    function trace_format(fmt, args) {
    // do nothing
    }
    }


    That seems reasonable enough, but it isn't free. There is cost associated with the module and the empty functions (they do reduce the memory available to the application) and there is a processor time cost as the VM must call these no-op functions. Of course this overhead could be small, but it can be big as well. Even worse is that there is no good way avoid allocating space for and initializing function parameters. For example...

    class MyView extends Ui.View
    {
    function initialize() {
    Logging.trace_format("MyView.initialize $1$", [ Sys.getTimer() ]);
    }
    }


    In release builds, the program will allocate and deallocate an array that is never going to get used.. It may even make a temporary string (I haven't disassembled to look at how string works). To work around those issues, we can use a constant...

    const debug = false;

    class MyView extends Ui.View
    {
    function initialize() {
    if (debug) {
    Logging.trace_format("MyView.initialize $1$", [ Sys.getTimer() ]);
    }
    }
    }


    Now we've avoided the unnecessary allocations, but we've introduced a bunch of runtime checks that we know will never be evaluated after the program is compiled, and made the code a lot more difficult to read. Additionally, the program will still have stored a literal string which takes up program space. I can step back and avoid these additional costs by adding a conditionally compiled helper function...

    class MyView extends Ui.View
    {
    function initialize() {
    debug_initialize();
    }

    (:debug) function debug_initialize() {
    Logging.trace_format("MyView.initialize $1$", [ Sys.getTimer() ]);
    }

    (:release) function debug_initialize() {
    }
    }


    And now we're back to something similar to the first option... Defining debug/release code for everything. Ugh.

    It would be great if we could conditionally compile individual lines or blocks of code within a function, something like this.

    class MyView extends Ui.View
    {
    function initialize() {
    (:debug) Logging.trace_format("MyView.initialize $1$", [ Sys.getTimer() ]);
    }
    }


    Travis
  • Yeah build exclusions looks interesting, but doesn't really seem flexible enough (or as Travis mentioned, 'free' enough) for my use-case.

    The tag-based auto-comment preprocessor I wrote has worked decently well enough to remove entire modules (like my custom Logging module), or selectively remove test fixture code that I don't want going out in release builds.

    What's nice about it is for larger general purpose libraries, I can selectively whitelist certain functions to include and not pay the penalty of including anything unnecessary (even though I might want that code for other projects).
  • Sorry to necro this thread, but everything that Travis said is stuff that I "independently" figured out when I started building apps for different watches.

    Basically, to save memory, my code is an almost-unmaintainable mess of duplicate functions for:
    - CIQ 1 versus CIQ 2
    - Large memory vs small memory devices
    - App versus widget

    Sometimes I'll have duplicates of a huge function with just a few lines that are different, to avoid the overhead of adding function calls and/or creating empty functions.

    I would never code like this professionally, but as a hobbyist dev for CIQ, I don't see any another choice but to ignore most standard practices for maintainable code just so my apps can fit on every platform.

    Of course, the alternative is to build our own preprocessor, like rconradharris. Not hard to do, but time-consuming and an unnecessary duplication of effort if everyone does it. Especially for hobbyists.

    Please give us the ability to exclude arbitrary sections of code, not just symbols.
  • With this thread being fairly old (it was right after Build File Excludes came out), a fair amount has changed - namely Monkey Barrels and Jungles.

    Barrels are a really good way to share code between projects. For all my watchfaces, I have about 6 barrels - all the "behind the UI" stuff, like settings, sunrise/set. common icon logic, common data field logic, etc. Based on the WF, i can use 1 or 2, or up to all 6. I wrote them in a way I can also just link to the .mc files or resources to support devices that may not be "barrel friendly", but the end result it a single source for a whole bunch of the things I commonly use. Both in watchfaces as well as widgets and apps.

    With Jungles, you can do something similar to build file excludes, except it's looking at things the other way around - you don't exclude what you don't want, you include the bits you do want. The most recent thing I'm using that for is an app that can working with the mapping API on devices with local maps. Based on the target, I build with the mapping code or with the breadcrumbs I use on other devices.

    And with excludeAnnotations, you can have what is really close to conditional compiling. Include a function or don't have based on target, alternate versions of a function based on target, etc. You can't do things line by line, but function by function.

    I had an app that I converted to use Build File exclude, and maintenance became a bit of a hassle when new targets were added, but I'm now using Barrels and Jungles in that app, and it makes things like adding targets much easier.

    It might take a bit to get used to Barrels and Jungles, but when you do, they can really help
  • Thanks jim_m_58, I understand how excludeAnnotations work. That's what I use for conditional compilation, but it has the same limitation as everything else:

    You can't do things line by line, but function by function.


    Yes, that's my point exactly.Everything that Travis said still applies, which is my entire point.

    Annotations, barrels and jungles are not an option for conditional compilation for the primary purpose of saving memory because of the overhead associated with symbols, modules, classes, and functions.... Just checking if a symbol exists is a waste of code. Please correct me if I missed something here.

    So I have to choose between maintainability and saving memory.... I chose the latter. I am not making one single additional function call that I don't have to.

    If I didn't want care about supporting older CIQ1 devices, I guess it wouldn't matter. And in some cases, my apps are right up against the limit for CIQ 2 "small memory" (e.g. 32 KB for data fields).

    I guess I've never coded in such a resource-constrained environment before (even though I've done plenty of embedded systems.) Even 50-100 bytes of wasted code is a big deal for me now.

    Thanks tho.
  • I think there's a trade off in maintainability and memory. I'll burn a few bytecode if it means the code is more maintainable. I have too much code I wrote 5 years ago and not looked at in years, that I know you can burn up a fair amount of time just trying to figure out why you did something in a certain way if you just want to make a minor change

    Some of it also depends on how you structure your code. With excludeAnnotaions, you can save a fair amount. In the case of maps/no maps I mentioned, it saved a ton, and and interesting thing there is the maps version is a far amount smaller, as the view does all the work.

    I started doing Assembly, C and embedded decades ago, so I know what you mean about "every byte counts", but there are also times you just have to say "I simply don't have the memory to add that"

    Writing code is easy. Supporting it is hard! :)
  • jim_m_58 agreed on all points, but I have an app that can take unlimited amounts of user input, so for that app I am not wasting a single byte of code. If I add 50-100 bytes, I could break somebody's previously working use case.

    For other apps, I might be a bit more lax until I hit the CIQ1 or CIQ2 small memory limit.

    Then again, with conditional compilation based on a preprocessor instead of symbols, you could get the best of both worlds. I can see why there's not much incentive to provide this.

    I like the use of symbols, it's really programmer-friendly, but there's a lot of overhead....