/** * @file main.c * @brief The standalone driver. * * Everything that belongs to a program rather than to a library lives here: argv * handling, which frontend to bring up, and a FINISH_NORETURN -- which belongs * only in a main(). The interpreter library itself never terminates the process. * * There are two drivers in here and the build option picks between them. Without * AKBASIC_HAVE_AKGL this is a terminal program: stdio in, stdout out, no window. * With it, the program is an SDL host -- window, font, event pump, frame loop -- * and its output goes to the window *and* to stdout, so a piped program produces * the same bytes either way. The whole of that second driver is * akbasic/frontend.h; what is left here is argv and the choice. * * 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, CLOCK_MONOTONIC and isatty. C99 alone declares none of them. */ #define _POSIX_C_SOURCE 199309L #include #include #include #include #include #include #include #include #include static akbasic_Runtime RUNTIME; #ifdef AKBASIC_HAVE_AKGL #include static akbasic_AkglFrontend FRONTEND; #else 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); } /** * @brief The terminal driver: no window, no SDL, output on stdout. * * @param program The already-opened program file, or NULL for an interactive REPL. * @param path That file's path, so a relative asset path a program writes can be resolved * against the program's own directory. NULL for an interactive REPL. */ static akerr_ErrorContext AKERR_NOIGNORE *run_stdio(FILE *program, const char *path) { PREPARE_ERROR(errctx); if ( program != NULL ) { /* * A file argument: read the program from it in RUNSTREAM mode, which * files each line under its line number and then switches to RUN. */ PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, program)); PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK)); /* * So an asset path a program writes -- `SPRSAV "ship.png", 1` -- can be * tried against the program's own directory as well as against the * working one. Without it a `.bas` stored beside its art only works when * it is launched from its own directory. */ PASS(errctx, akbasic_runtime_set_source_path(&RUNTIME, path)); PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUNSTREAM)); } else { PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, stdin)); PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK)); PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_REPL)); } PASS(errctx, drive(&RUNTIME)); SUCCEED_RETURN(errctx); } #endif /* !AKBASIC_HAVE_AKGL */ #ifdef AKBASIC_HAVE_AKGL /** * @brief Where to find the Commodore font. * * Compiled in by CMake, because the reference's relative "./fonts/..." only ever * worked from its own source directory. AKBASIC_FONT overrides it at runtime, * which is what an installed copy or a different font needs. */ #ifndef AKBASIC_FONT_PATH #define AKBASIC_FONT_PATH "fonts/C64_Pro_Mono-STYLE.ttf" #endif /** * @brief The SDL driver: an 800x600 window, and stdout still gets everything. * * @param program The already-opened program file, or NULL to type at the window. * @param path That file's path; see run_stdio(). NULL when there is no program file. */ static akerr_ErrorContext AKERR_NOIGNORE *run_akgl(FILE *program, const char *path) { PREPARE_ERROR(errctx); const char *fontpath = getenv("AKBASIC_FONT"); FILE *input = program; int mode = AKBASIC_MODE_RUNSTREAM; if ( fontpath == NULL ) { fontpath = AKBASIC_FONT_PATH; } if ( program == NULL ) { /* * With a terminal on the other end of stdin the window is the console * and typed lines come from its line editor. Piped or redirected, they * come from the pipe instead -- so `basic < program.bas` still behaves * in an AKGL build, and the golden corpus can be driven through this * binary as well as through the stdio one. */ input = (isatty(fileno(stdin)) ? NULL : stdin); mode = AKBASIC_MODE_REPL; } PASS(errctx, akbasic_frontend_akgl_init(&FRONTEND, "BASIC", AKBASIC_FRONTEND_WIDTH, AKBASIC_FRONTEND_HEIGHT, fontpath, AKBASIC_FRONTEND_FONT_SIZE, stdout, input)); PASS(errctx, akbasic_frontend_akgl_attach(&FRONTEND, &RUNTIME)); /* See the note in run_stdio(): this is what makes a relative asset path work. */ PASS(errctx, akbasic_runtime_set_source_path(&RUNTIME, path)); PASS(errctx, akbasic_runtime_start(&RUNTIME, mode)); PASS(errctx, akbasic_frontend_akgl_drive(&FRONTEND, &RUNTIME)); SUCCEED_RETURN(errctx); } #endif int main(int argc, char **argv) { PREPARE_ERROR(errctx); FILE *program = NULL; int rc = EXIT_SUCCESS; ATTEMPT { if ( argc > 1 ) { CATCH(errctx, aksl_fopen(argv[1], "r", &program)); } #ifdef AKBASIC_HAVE_AKGL CATCH(errctx, run_akgl(program, (argc > 1 ? argv[1] : NULL))); #else CATCH(errctx, run_stdio(program, (argc > 1 ? argv[1] : NULL))); #endif } CLEANUP { if ( program != NULL ) { IGNORE(aksl_fclose(program)); } #ifdef AKBASIC_HAVE_AKGL akbasic_frontend_akgl_shutdown(&FRONTEND); #endif } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "akbasic terminated on an unhandled error"); rc = EXIT_FAILURE; } FINISH_NORETURN(errctx); return rc; }