/** * @file main.c * @brief The standalone driver. * * Everything that belongs to a program rather than to a library lives here: argv * handling, sink selection, the unbounded run loop, and a FINISH_NORETURN -- * which belongs only in a main(). The interpreter library itself never * terminates the process. * * The runtime is static rather than automatic because it carries every pool the * interpreter owns -- several megabytes -- and that will not fit on a default * stack. An embedding game would place it in its own state for the same reason. */ /* clock_gettime and CLOCK_MONOTONIC. C99 alone does not declare either. */ #define _POSIX_C_SOURCE 199309L #include #include #include #include #include #include #include #include static akbasic_Runtime RUNTIME; static akbasic_TextSink SINK; static akbasic_StdioSink SINKSTATE; /** * @brief Milliseconds off a monotonic clock, for akbasic_runtime_settime(). * * The library reads no clock of its own -- it owns no loop and must not block -- * so somebody has to tell it what time it is, and for a standalone program that * is this. A game does the same thing once a frame off whatever clock it already * keeps. * * Monotonic rather than wall-clock: the interpreter only ever compares these * values, and an NTP step backwards in the middle of a tune would hold a note * for however long the correction was. */ static int64_t monotonic_ms(void) { struct timespec now; if ( clock_gettime(CLOCK_MONOTONIC, &now) != 0 ) { /* * A clock that cannot be read leaves time frozen, which expires every * duration immediately: the program still runs and the music simply * rushes. Better than refusing to start over a note length. */ return 0; } return ((int64_t)now.tv_sec * 1000) + (now.tv_nsec / 1000000); } /** * @brief Step the interpreter to completion, refreshing the clock as it goes. * * One step at a time rather than a single unbounded run(), because the driver is * the thing that owns the clock: a PLAY string whose notes all measured * themselves against a frozen zero would rush the whole tune out at once. * * A function of its own rather than a loop inside main()'s ATTEMPT, for two * reasons that both come from the error protocol. CATCH expands to a `break`, so * inside a loop it would escape only the loop and leave the rest of the ATTEMPT * running with an error pending. And PASS expands to a `return` of the context, * which main() cannot do -- it returns an int. Wrapping the loop is what the * protocol prescribes for exactly this shape. */ static akerr_ErrorContext AKERR_NOIGNORE *drive(akbasic_Runtime *obj) { PREPARE_ERROR(errctx); while ( obj->mode != AKBASIC_MODE_QUIT ) { PASS(errctx, akbasic_runtime_settime(obj, monotonic_ms())); PASS(errctx, akbasic_runtime_run(obj, 1)); } SUCCEED_RETURN(errctx); } int main(int argc, char **argv) { PREPARE_ERROR(errctx); FILE *program = NULL; int rc = EXIT_SUCCESS; ATTEMPT { if ( argc > 1 ) { /* * A file argument: read the program from it in RUNSTREAM mode, which * files each line under its line number and then switches to RUN. */ CATCH(errctx, aksl_fopen(argv[1], "r", &program)); CATCH(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, program)); CATCH(errctx, akbasic_runtime_init(&RUNTIME, &SINK)); CATCH(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUNSTREAM)); } else { CATCH(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, stdin)); CATCH(errctx, akbasic_runtime_init(&RUNTIME, &SINK)); CATCH(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_REPL)); } CATCH(errctx, drive(&RUNTIME)); } CLEANUP { if ( program != NULL ) { IGNORE(aksl_fclose(program)); } } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "akbasic terminated on an unhandled error"); rc = EXIT_FAILURE; } FINISH_NORETURN(errctx); return rc; }