/** * @file sink_stdio.c * @brief The stdio-backed text sink. * * This is what makes the golden corpus runnable with no SDL on the machine. It * writes exactly what the reference's fmt.Printf/fmt.Println mirror wrote, byte * for byte, including the newline writeln appends -- an error line therefore * ends in two newlines, because the caller's message already carries one. That * is the acceptance contract, not an accident. See TODO.md section 1.8. */ #include #include #include #include #include #include static akerr_ErrorContext *stdio_write(akbasic_TextSink *self, const char *text) { PREPARE_ERROR(errctx); akbasic_StdioSink *state = NULL; int count = 0; FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER, "NULL argument in sink write"); state = (akbasic_StdioSink *)self->self; PASS(errctx, aksl_fprintf(&count, state->out, "%s", text)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *stdio_writeln(akbasic_TextSink *self, const char *text) { PREPARE_ERROR(errctx); akbasic_StdioSink *state = NULL; int count = 0; FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER, "NULL argument in sink writeln"); state = (akbasic_StdioSink *)self->self; PASS(errctx, aksl_fprintf(&count, state->out, "%s\n", text)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof) { PREPARE_ERROR(errctx); akbasic_StdioSink *state = NULL; size_t used = 0; FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER, "NULL argument in sink readline"); FAIL_ZERO_RETURN(errctx, (len > 1), AKBASIC_ERR_BOUNDS, "Read buffer of %zu bytes is too small", len); state = (akbasic_StdioSink *)self->self; *eof = false; dest[0] = '\0'; if ( fgets(dest, (int)len, state->in) == NULL ) { *eof = true; SUCCEED_RETURN(errctx); } /* * Strip the line terminator. The scanner treats \r and \n as end-of-line * anyway, but leaving them on would make a stored source line differ from * the same line typed at the REPL. */ used = strlen(dest); while ( used > 0 && (dest[used - 1] == '\n' || dest[used - 1] == '\r') ) { dest[used - 1] = '\0'; used -= 1; } SUCCEED_RETURN(errctx); } static akerr_ErrorContext *stdio_clear(akbasic_TextSink *self) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in clear"); /* A terminal has no screen to clear that the golden corpus would agree on. */ SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_sink_init_stdio(akbasic_TextSink *obj, akbasic_StdioSink *state, FILE *out, FILE *in) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL sink in init"); FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "NULL sink state in init"); state->out = (out != NULL ? out : stdout); state->in = (in != NULL ? in : stdin); obj->self = state; obj->write = stdio_write; obj->writeln = stdio_writeln; obj->readline = stdio_readline; obj->clear = stdio_clear; /* * The optional entry points, cleared rather than left alone. A stream has no * character grid, so `CHAR` and `WINDOW` must refuse by name -- and they * decide that by testing the pointer for NULL. A caller with a sink on the * stack would otherwise hand them whatever was in that memory. */ obj->moveto = NULL; obj->window = NULL; SUCCEED_RETURN(errctx); }