Goto Considered Helpful

This is an article that I wanted to write for a very long time. Alas I could not for the lack of a suitable example. Now the time switch code gives plenty of examples. But lets start with the begin of the story. There is a very famous paper of Turing Award Winner Edsger Wybe Dijkstra A Case Against The Go To Statement. I have actually read this paper and this guy is right. However I also have been exposed to teachers and “experts” who summarize the paper as “GOTO considered harmful”. Interestingly enough these experts ignore that there world class experts like for example Donald Ervin Knuth Structured Programming with go to Statements. According to the Wikipedia article on goto this view seems to be shared by Linus Torvalds as well. Also recent empirical research confirms the need of goto. So with regard to the goto statement bascially everything has already been said. The only thing that kept annoying me was the lack of a reasonable small set of some “real world” examples.

Until now 🙂 As it turns out my time switch requirements are structured in such a way that I could take advantage of the goto statement in three different ways. I am still quite sure that other alternatives – for example tail recursion – would have been worse. Lets see how my switch works and how the often abused goto statement can be utilized for good.

The ISRs

The most important function of time switch is to determine the proper time and to set the outputs accordingly. Determination of the time is done with my DCF77 library. This library works very well. However it has a very hard performance requriement. It needs to sample the input pin once per millisecond (with the function “uint8_t sample_input_pin()”). Hence timer interrupts must never be blocked for more than 1 ms. Otherwise it can not properly sample the inputs.

This has some serious implications. As it turns out determination of the output states takes more than 1 millisecond. Thus it must be done outside the interrupt service routines. On the other hand I want to drive the outputs from inside an ISR to ensure that it will have as little frequency jitter / phase error as possible. The solution: precompute the output for the next second and then flush it at the start of the next second. Flushing is pretty easy. It is done by the following pieces of code. Notice that the clock library will call the output_handler exactly once per second at the start of the second.

    void flush_output() {
        for (uint8_t channel_pin = time_switch_channel_0_pin;
             channel_pin <time_switch_channel_0_pin+16;
             ++channel_pin) {

            digitalWrite(channel_pin, staged_output & 1);
            staged_output >>= 1;
        }
    }

    void output_handler(const DCF77_Clock::time_t &decoded_time) {
        tick = 0;
        start_of_second = true;

        if (has_staged_data) {
            if (output_enabled) {
                flush_output();
            }
            has_staged_data = false;
        }
    }

Main Loop

Since the ISR will flush the output once per second but does not compute it this has to be done by the main loop. The first ingredient is the library function “void read_future_time(time_t &now_plus_1s);”. This is called in the main loop and allows us to compute the output ahead of time. Then I can compute the future output and stage it. The main loop is pretty simple …

void loop() {
    if (ISR::start_of_second) {
        ISR::start_of_second = false;

        DCF77_Clock::time_t now_plus_1s;
        DCF77_Clock::read_future_time(now_plus_1s);
        ISR::stage_output(Alarm_Timer::trigger(now_plus_1s));
        DCF77_Clock::time_t now;
        DCF77_Clock::read_current_time(now);
        if (now.month.val > 0) {
            const uint8_t state = DCF77_Clock::get_clock_state();
            if (state != DCF77::useless) {
                if (!parser::parser_controled_output()) {
                    switch (state) {
                        case DCF77::useless: Serial.print(F("useless")); break;
                        case DCF77::dirty:   Serial.print(F("dirty: ")); break;
                        case DCF77::synced:  Serial.print(F("synced:")); break;
                        case DCF77::locked:  Serial.print(F("locked:")); break;
                    }

                    Serial.print(F(" 20"));
                    padded_print(now.year);
                    Serial.print('-');
                    padded_print(now.month);
                    Serial.print('-');
                    padded_print(now.day);
                    space();
                    Serial.print(now.weekday.val);
                    space();
                    padded_print(now.hour);
                    Serial.print(':');
                    padded_print(now.minute);
                    Serial.print(':');
                    padded_print(now.second);

                    Serial.print(F(" UTC+0"));
                    Serial.print(now.uses_summertime? '2': '1');

                    Serial.print("  Channels: ");

                    Alarm_Timer::channels_t observed_output;
                    for (uint8_t channel_pin = time_switch_channel_0_pin+16-1;
                         channel_pin >= time_switch_channel_0_pin;
                         --channel_pin) {
                        observed_output <<= 1;
                        observed_output |= digitalRead(channel_pin);
                    }
                    binary_print(observed_output);
                    Serial.println();
                }
            }
        }
    }

    parser::parse();
}

… execpt that it also has to deal with the input from the serial line (calling parser::parse()). Obviously this parser must never block the main loop for more than a second. Especially it has to deal with the fact that a message might not be fully received at the end of the second. So it somehow needs to keep its internal state. On the other hand it must not allocate to much memory. More on that below.

Time Switch and Matching Logic

Lets have a look at the primary function of the time switch. This function gets called (from the main loop) once per second and it also gets called by the “view” option.

    channels_t trigger(const DCF77_Clock::time_t & now) {
        boolean start_next_group = true;

        day_and_time_t best_trigger;
        channels_t best_trigger_channels;
        best_trigger_channels = 0;

        channels_t output_channels;
        output_channels = 0;

        for (uint8_t i=0; i<max_alarms; ++i) {
            alarm_t current_alarm;
            eeprom::read_alarm(current_alarm, i);
            const boolean preceeds_a_new_group = current_alarm.control.group_end_marker || (i==max_alarms);

            if (current_alarm.control.active && current_alarm.weekdays) {
                day_and_time_t current_trigger = get_best_approximation(now, current_alarm);
                if (start_next_group || equal_day_and_time(current_trigger, now) ||
                    (is_trigger_1_before_trigger_2(now, best_trigger, current_trigger) && !equal_day_and_time(best_trigger, now))
                ) {
                    best_trigger          = current_trigger;
                    best_trigger_channels = current_alarm.channels;
                }
                start_next_group = preceeds_a_new_group;
            } else {
                start_next_group |= preceeds_a_new_group;
            }

            if (start_next_group) {
                output_channels |= best_trigger_channels;
                // no need to initialize best_trigger_channels as
                // this is already contained in the output
            }
        }
        output_channels |= best_trigger_channels;
        return output_channels;
    }

If we ignore the grouping logic for a moment we see that it just loops over the triggers and searches for the latest alarm before or equal the current time. Obviously this is a very simple algorithm. The devil is in the details though. Since the triggers may contain “jokers” they do not have a specific value. Instead the most relevant value with regard to the current time must be computed. The computation for this is implemented as follows. Notice how this code does not use goto statements to jump recklessly back and forth. All goto statements are always jumping downwards.

    day_and_time_t get_best_approximation(const DCF77_Clock::time_t & now,
                                          const alarm_t             & alarm) {
        day_and_time_t best;

        if (!(alarm.weekdays & (bitmask_monday << (now.weekday.val - 1)))) goto match_earlier_weekday;
        best.weekday = now.weekday;

        if (!is_match(alarm.hour, now.hour)) goto match_earlier_hour;
        best.hour = now.hour;

        if (!is_match(alarm.minute, now.minute)) goto match_earlier_minute;
        best.minute = now.minute;

        if (!is_match(alarm.second, now.second)) goto match_earlier_second;
        best.second = now.second;

        goto done;


        match_earlier_second:
        best.second = get_earlier_match(alarm.second, now.second);
        if (best.second.val <= 0x61) goto done;

        match_earlier_minute:
        best.minute = get_earlier_match(alarm.minute, now.minute);
        if (best.minute.val <= 0x60) goto maximize_second;

        match_earlier_hour:
        best.hour = get_earlier_match(alarm.hour, now.hour);
        if (best.hour.val <= 0x24) goto maximize_minute;

        match_earlier_weekday:
        best.weekday = get_earlier_weekday(alarm.weekdays, now.weekday);
        if (best.weekday.val <= 7) goto maximize_hour;


        // maximize_day:
        best.weekday = get_highest_weekday(alarm.weekdays);

        maximize_hour:
        best.hour = get_highest_match(alarm.hour);
        if (best.hour.val > 0x23) { best.hour.val = 0x23; }

        maximize_minute:
        best.minute = get_highest_match(alarm.minute);

        maximize_second:
        best.second = get_highest_match(alarm.second);

        done:
        return best;
    }

On the other side this example also justifies Dijkstra’s point to some extend. It is somewhat hard to grasp. But then again this is the structure of the underlying problem. Click on the picture below to enlarge the flowchart.

Matching_Logic_lo

As you can see it has some snake like structure. Of course the goto statements could be eliminated by means of tail recursion or with the help of some suitable switch statement. But I am convinced that the use of goto statements reflects the logic best.

The Parser

As already mentioned the parser must deal with the fact that input is not 100% ready at any given point in time. On the other hand I did not want to buffer the input. That is I wanted to have a stream parser.

The “outer part” of the parser is very simple.

    boolean parse() {  // deliver true if some character was available
        static char previous_char;
        if (Serial.available()) {
            char c = (char) Serial.read();

            // interpret tab as whitespace
            if (c == 0x09) {
                c = ' ';
            }

            // map newline and carriage return to command separator
            if (c==0x0A || c==0x0D) {
                c = command_separator;
            }

            // ignore successive separators
            if ((c == option_separator || c == command_separator) && (c == previous_char)) {
                return true;
            }

            // map everything to lower case
            if (('A' <= c) && (c <= 'Z')) {
                c+= 'a' - 'A';
            }

            static_parse(c);
            if (c != ' ') {
                previous_char = c;
            }

            return true;
        }
        return false;
    }

This piece of code just normalizes the input. That is it reads input and then maps characters to lowercase, maps tabs to whitespace, maps carriage returns and newlines to command separators and removed redundant separators. If a character is ready for parsing it will hand the hard work to “void static_parse(char c)”. To get an idea of how hard this will be just have a look at the syntax diagram for the set command.

Timeswitch_Parser_s

A typical approach is to implement this is as a state engine with suitable switch statements, e.g. like I did in the flexible sweep experiment. Another approach to stream parsing is the use of coroutines. Since C does not support coroutines out of the box this is rather uncommon in the C world. In other languages that support coroutines or at least generators this technique is well known though. To quote Eli Bendersky (no clue if this is an original quote): Co-routines are to state machines what recursion is to stacks. Interestingly enough Co-routines are not a new concept at all. They are even covered in Donald Knuth’s famous book “The Art of Computer Programming” (Volume 1 Section 1.4.2).

So I want to implement a coroutine but the compiler does not support it? Other people like e.g. Simon Tatham have already thought about this for a while. Unfortunately I like none of these solutions. So I will roll my own. What I want to have is something similar to Python’s yield statement. I am willing to allow to exploit some gcc specific magic. The YIELD_NEXT_CHAR macro is the key to the magic.

   void static_parse(const char c) {
/* ... */
        static void * parser_state = &&l_parser_start;
/* ... */
        goto *parser_state;
        #define LABEL(N) label_ ## N
        #define XLABEL(N) LABEL(N)
        #define YIELD_NEXT_CHAR                                               \
            do {                                                              \
                parser_state = &&XLABEL(__LINE__); return; XLABEL(__LINE__):; \
            } while (0)

        l_parser_start: ;
/* ... */

How does this work? The macro ends by defining a label that contains the current line number. This is a poor attempt to make the label reasonable unique. Thus the macro can only be used once per line. Good enough for me though. The macro starts by dereferencing this label into the variable static void * parser_state. Then it exits the function. With other words the label address for continuing at the next call of the function is saved in parser_state. Then with the next call to static_parse the goto *parser_state recovers processing at this label. The only question that remains is how I keep the rest of the state? Well, as long as I do not call any subroutine I can get away with static variables. This is the reason why static_parse is such a long function.

In practice this makes parsing now very simple. I just code “YIELD_NEXT_CHAR” whenever I want to parse the next character. Once the next character is available it will continue after the macro. No additional housekeeping. Very simple and effective.

The full truth is below.

   void static_parse(const char c) {
        // static_parse is a co-routine for parsing.
        // That is all variables will be declared static
        // and the program counter will be stored in
        // static void * parser_state.
        // The idea is that the YIELD_NEXT_CHAR
        // macro can be used to return control to the
        // caller while keeping the internal state.
        // The caller in turn will push the
        // next available character into static_parse
        // whenever it has one character ready.
        // This is a means of cooperative multi tasking

        // For more background on what is going on you might want to
        // follow the two links below.
        //     https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
        //     http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html

        static void * parser_state = &&l_parser_start;

        output_state = (output_state_t) (output_state | implicit_output_control);
        if (parser_state < &&l_generic_error) {
            Serial.print(c);
        }

        if (c == ' ') {
            return;
        }

        if (c == command_separator) {
            Serial.println();
        }

        goto *parser_state;
        #define LABEL(N) label_ ## N
        #define XLABEL(N) LABEL(N)
        #define YIELD_NEXT_CHAR                                               \
            do {                                                              \
                parser_state = &&XLABEL(__LINE__); return; XLABEL(__LINE__):; \
            } while (0)

        l_parser_start: ;

        if (c == ' ') {
            // ignore leading space
            YIELD_NEXT_CHAR;
        }

        switch (c) {
            case '?':
            case 'h': {
                help();
                // ignore all characters till command end
                while (c != command_separator) {
                    YIELD_NEXT_CHAR;
                }
                goto l_done;
            }

            case 'p': { // pause
                YIELD_NEXT_CHAR;
                if (c == command_separator) {
                    Serial.println(F("Pause output"));
                    output_state = (output_state_t) (output_state | explicit_output_control);
                    ISR::pause_output();
                    goto l_done;
                }
                goto l_syntax_error;
            }

            case 'b': { // break
                YIELD_NEXT_CHAR;
                if (c == command_separator) {
                    Serial.println(F("Break"));
                    output_state = (output_state_t) (output_state | explicit_output_control);
                    ISR::pause_output();
                    ISR::clear_output();
                    goto l_done;
                }
                goto l_syntax_error;
            }

            case 'r': { // run
                YIELD_NEXT_CHAR;
                if (c == command_separator) {
                    eeprom::write_crc16();
                    Serial.println(F("Run"));
                    output_state = (output_state_t) (output_state & implicit_output_control);
                    ISR::enable_output();
                    goto l_done;
                }
                goto l_syntax_error;
            }

            case 'e': { // erase all EEPROM contents
                YIELD_NEXT_CHAR;
                if (c != 'r') { goto l_syntax_error; }

                YIELD_NEXT_CHAR;
                if (c != 'a' ) { goto l_syntax_error; }

                YIELD_NEXT_CHAR;
                if (c != 's') { goto l_syntax_error; }

                YIELD_NEXT_CHAR;
                if (c != 'e') { goto l_syntax_error; }

                YIELD_NEXT_CHAR;
                if (c != command_separator) { goto l_syntax_error; }

                Serial.println(F("Erase EEPROM"));
                eeprom::init();
                goto l_done;
            }

            case 'v': { // view
                static DCF77_Clock::time_t test_time;

                // parse weekday
                YIELD_NEXT_CHAR;
                if (!is_decimal_digit(c)) { goto l_digit_expected; }
                test_time.weekday.val = parse_decimal_digit(c);
                if (test_time.weekday.val < 1 || test_time.weekday.val > 7) { goto l_out_of_range_error; }

                // parse hours
                YIELD_NEXT_CHAR;
                if (!is_decimal_digit(c)) { goto l_digit_expected; }
                test_time.hour.digit.hi = parse_decimal_digit(c);
                if (test_time.hour.digit.hi > 2) { goto l_out_of_range_error; }

                YIELD_NEXT_CHAR;
                if (!is_decimal_digit(c)) { goto l_digit_expected; }
                test_time.hour.digit.lo = parse_decimal_digit(c);
                if (test_time.hour.val > 0x23) { goto l_out_of_range_error; }

                YIELD_NEXT_CHAR;
                if (c != ':') { goto l_separator_error; }

                // parse minutes
                YIELD_NEXT_CHAR;
                if (!is_decimal_digit(c)) { goto l_digit_expected; }
                test_time.minute.digit.hi = parse_decimal_digit(c);
                if (test_time.minute.digit.hi > 5) { goto l_out_of_range_error; }

                YIELD_NEXT_CHAR;
                if (!is_decimal_digit(c)) { goto l_digit_expected; }
                test_time.minute.digit.lo = parse_decimal_digit(c);

                YIELD_NEXT_CHAR;
                if (c != ':') { goto l_separator_error; }

                // parse seconds
                YIELD_NEXT_CHAR;
                if (!is_decimal_digit_or_joker(c)) { goto l_digit_or_joker_expected; }
                test_time.second.digit.hi = parse_decimal_digit(c);
                if ((test_time.second.digit.hi > 5)) { goto l_out_of_range_error; }

                YIELD_NEXT_CHAR;
                if (!is_decimal_digit_or_joker(c)) { goto l_digit_or_joker_expected; }
                test_time.second.digit.lo = parse_decimal_digit(c);

                YIELD_NEXT_CHAR;
                if (c != command_separator) { goto l_syntax_error; }

                Serial.print(F("Alarm Test: "));
                Serial.print(test_time.weekday.val);
                space();
                padded_print(test_time.hour);
                Serial.print(':');
                padded_print(test_time.minute);
                Serial.print(':');
                padded_print(test_time.second);

                Serial.print(F("  Channels: "));
                const Alarm_Timer::channels_t channels = Alarm_Timer::trigger(test_time);
                binary_print(channels);
                Serial.println();
                goto l_done;
            }

            case 'd': { // dump
                static uint8_t dump_from;
                static uint8_t dump_to;
                static boolean skip_inactive;

                skip_inactive = false;

                // digit efault range
                dump_from = 0;
                dump_to = eeprom::data_records - 1;

                YIELD_NEXT_CHAR;
                if (c == command_separator) { goto l_dump_eeprom; }
                if (c == '-')               { skip_inactive = true; goto l_parse_upper_bound; }
                if (!is_decimal_digit(c))   { goto l_digit_expected; }

                skip_inactive = false;
                dump_from = parse_decimal_digit(c);
                dump_to = dump_from;


                YIELD_NEXT_CHAR;
                if (c == command_separator) { goto l_dump_eeprom; }
                if (c == '-')               { goto l_parse_upper_bound; }
                if (!is_decimal_digit(c))   { goto l_digit_expected; }

                dump_from = 10 * dump_from + parse_decimal_digit(c);
                dump_to = dump_from;


                YIELD_NEXT_CHAR;
                if (c == command_separator) { goto l_dump_eeprom; }
                if (c != '-')               { goto l_separator_error; }

                l_parse_upper_bound:
                dump_to = eeprom::data_records - 1;


                YIELD_NEXT_CHAR;
                if (c == command_separator) { goto l_dump_eeprom; }
                if (!is_decimal_digit(c))   { goto l_digit_expected; }

                skip_inactive = false;
                dump_to = parse_decimal_digit(c);


                YIELD_NEXT_CHAR;
                if (c == command_separator) { goto l_dump_eeprom; }
                if (!is_decimal_digit(c))   { goto l_digit_expected; }

                dump_to = 10 * dump_to + parse_decimal_digit(c);


                YIELD_NEXT_CHAR;
                if (c == command_separator) { goto l_dump_eeprom; }
                goto l_separator_error;

                l_dump_eeprom:
                if (dump_from <= dump_to) {
                    eeprom::dump_data(dump_from, dump_to, skip_inactive);
                } else {
                    eeprom::dump_data(dump_to, dump_from, skip_inactive);
                }
                goto l_done;
            }

            case 's': { // set
                static uint8_t alarm_index;
                static Alarm_Timer::alarm_t alarm;

                // determine the channel that will be set
                YIELD_NEXT_CHAR;
                if (!is_decimal_digit(c)) { goto l_digit_expected; }

                alarm_index = parse_decimal_digit(c);

                YIELD_NEXT_CHAR;
                if (is_decimal_digit(c)) {
                    alarm_index = 10*alarm_index + parse_decimal_digit(c);
                    YIELD_NEXT_CHAR;
                }

                if (c == command_separator) { goto l_after_set; }

                // now the desired alarm_index is known
                eeprom::read_alarm(alarm, alarm_index);

                // parse the different options of the set command
                // if the same option is parsed multiple times --> last one wins
                while (true) {
                    static int8_t digit_index;

                    switch (c) {
                        case option_separator:
                            YIELD_NEXT_CHAR;
                            continue;

                        case 'a': // activation
                            YIELD_NEXT_CHAR;
                            if (!is_binary_digit(c)) { goto l_binary_digit_expected; }
                            alarm.control.active = parse_binary_digit(c);
                            break;

                        case 'c': // channels
                            alarm.channels = 0;

                            YIELD_NEXT_CHAR;
                            if (c == option_separator) { continue; }
                            if (c == 'x') {
                                for (digit_index = 0; digit_index < 4; ++digit_index) {
                                    YIELD_NEXT_CHAR;
                                    if (c == option_separator) { break; }
                                    if (c == command_separator) { goto l_set_now; }
                                    if (!is_hexadecimal_digit(c)) { goto l_hexadecimal_digit_expected; }
                                    alarm.channels = (alarm.channels<<4) + parse_hexadecimal_digit(c);
                                }
                                break;
                            }
                            if (c == 'b') {
                                for (digit_index = 0; digit_index < 16; ++digit_index) {
                                    YIELD_NEXT_CHAR;
                                    if (c == option_separator) { break; }
                                    if (c == command_separator) { goto l_set_now; }
                                    if (!is_binary_digit(c)) { goto l_binary_digit_expected; }
                                    alarm.channels = (alarm.channels<<1) + parse_binary_digit(c);
                                }
                                break;
                            }
                            goto l_syntax_error;

                        case 'd': // weekdays
                            alarm.weekdays = 0;

                            for (digit_index = 0; digit_index < 7; ++digit_index) {
                                YIELD_NEXT_CHAR;
                                if (!is_binary_digit(c)) { goto l_binary_digit_expected; }

                                if (parse_binary_digit(c)) {
                                    alarm.weekdays += (Alarm_Timer::bitmask_monday << digit_index);
                                }
                            }

                            break;

                        case 't': // time
                            YIELD_NEXT_CHAR;
                            if (!is_decimal_digit_or_joker(c)) { goto l_digit_or_joker_expected; }
                            alarm.hour.digit.hi = parse_decimal_digit_or_joker(c);
                            if ((alarm.hour.digit.hi > 2) && !is_joker(c)) { goto l_out_of_range_error; }

                            YIELD_NEXT_CHAR;
                            if (!is_decimal_digit_or_joker(c)) { goto l_digit_or_joker_expected; }
                            alarm.hour.digit.lo = parse_decimal_digit_or_joker(c);
                            if ((alarm.hour.digit.lo == 0xf) != (alarm.hour.digit.hi == 0xf)) { goto l_joker_expected; }
                            if (alarm.hour.val > 0x23 && !is_joker(c)) { goto l_out_of_range_error; }

                            YIELD_NEXT_CHAR;
                            if (c != ':') { goto l_separator_error; }

                            // parse minutes
                            YIELD_NEXT_CHAR;
                            if (!is_decimal_digit_or_joker(c)) { goto l_digit_or_joker_expected; }
                            alarm.minute.digit.hi = parse_decimal_digit_or_joker(c);
                            if ((alarm.minute.digit.hi > 5) && !is_joker(c)) { goto l_out_of_range_error; }

                            YIELD_NEXT_CHAR;
                            if (!is_decimal_digit_or_joker(c)) { goto l_digit_or_joker_expected; }
                            alarm.minute.digit.lo = parse_decimal_digit_or_joker(c);

                            YIELD_NEXT_CHAR;
                            if (c != ':') { goto l_separator_error; }

                            // parse seconds
                            YIELD_NEXT_CHAR;
                            if (!is_decimal_digit_or_joker(c)) { goto l_digit_or_joker_expected; }
                            alarm.second.digit.hi = parse_decimal_digit_or_joker(c);
                            if ((alarm.second.digit.hi > 5) && !is_joker(c)) { goto l_out_of_range_error; }

                            YIELD_NEXT_CHAR;
                            if (!is_decimal_digit_or_joker(c)) { goto l_digit_or_joker_expected; }
                            alarm.second.digit.lo = parse_decimal_digit_or_joker(c);

                            break;

                        case '+':
                            alarm.control.group_end_marker = 0;
                            goto l_ready_to_set;

                        case '#':
                            alarm.control.group_end_marker = 1;
                            goto l_ready_to_set;

                        case ';':
                            goto l_set_now;

                        default: goto l_syntax_error;
                    }

                    YIELD_NEXT_CHAR;
                }

                l_ready_to_set:
                YIELD_NEXT_CHAR;
                if (c != command_separator) { goto l_separator_error; }
                l_set_now:
                eeprom::write_alarm(alarm, alarm_index);
                l_after_set:
                eeprom::dump_data(alarm_index, alarm_index, false);

                goto l_done;
            }

            //'h': help
            //'?': help
            default: goto l_unknown_command;
        }

        l_unknown_command:
        Serial.println();
        Serial.print(F(" Unknown command"));
        goto l_generic_error;

        l_joker_expected:
        Serial.println();
        Serial.print(F(" * expected"));
        goto l_generic_error;

        l_digit_or_joker_expected:
        Serial.println();
        Serial.print(F(" Digit or * expected"));
        goto l_generic_error;

        l_digit_expected:
        Serial.println();
        Serial.print(F(" Digit expected"));
        goto l_generic_error;

        l_binary_digit_expected:
        Serial.println();
        Serial.print(F(" Binary digit expected"));
        goto l_generic_error;

        l_hexadecimal_digit_expected:
        Serial.println();
        Serial.print(F(" Hexadecimal digit expected"));
        goto l_generic_error;

        l_separator_error:
        Serial.println();
        Serial.print(F(" Unexpected or missing separator"));
        goto l_generic_error;

        l_out_of_range_error:
        Serial.println();
        Serial.print(F(" Digit out of range error"));
        goto l_generic_error;

        l_syntax_error:
        Serial.println();
        goto l_generic_error;

        l_generic_error:

        Serial.print(F(" error at '"));
        Serial.print(c);
        if (c== command_separator) {
            Serial.println('\'');
        } else {
            Serial.print(F("' before "));
            do {
                YIELD_NEXT_CHAR;
                Serial.print(c);
            } while (c != command_separator);

            Serial.println();
        }
        Serial.println();
        Serial.println(F("Use h for help!"));
        goto l_done;

        l_done:
        output_state = (output_state_t) (output_state & explicit_output_control);
        Serial.println();
        parser_state = &&l_parser_start;
        return;
    }

You can see at the end of this function another use of goto statements. I use them for error handling (instead of more heavy weight exceptions).

If you have read through this lengthy article you now have an understanding of my timeswitch implementation as well as 3 helpful real world uses of goto 🙂 In my opinion these uses are fully justified as they make the code easier to understand (compared to the alternatives).

To repeat it in short, I used goto statements for 3 different purposes.

  1. Establishing a non standard (snake like) control flow structure
  2. Establishing a macro for a coroutine
  3. Error handling

All of them I consider valid cases (in C) in favour of the goto statement.

This leaves just one more question open? Without doubt the code of the time switch is pretty complex for its size. How do you test such a thing? Well, this will be the subject of another article.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.