/** * @file renumber.c * @brief Walking a program's branch targets, and the two things that do it. * * **RENUMBER** moves every line and rewrites every reference to one. Moving the * lines is easy -- source is filed under its line number in `source[]`, so it is * a permutation. Rewriting every `GOTO`, `GOSUB`, `RUN`, `RESTORE`, `TRAP` and * `COLLISION` target to match is the job, and it is why this was deferred rather * than written early: a `RENUMBER` that moved lines without fixing their * references would silently break every branch in the program, which is worse * than not having the verb at all. * * **akbasic_runtime_check_targets()** walks the same targets without moving * anything, refusing a program that branches by number to a line the program * never numbered. It shares this file because it needs exactly the same walk, * and a second copy of it would drift. * * The walk is textual, and the only thing it has to understand about the rest of * the language is where a string literal starts and stops -- `PRINT "GOTO 10"` * must come through untouched. That is the same statement-walking the label and * DATA prescans do. What the two callers differ in is one function pointer: * given a target, say what text should stand in its place, and raise if it is not * acceptable. */ #include #include #include #include #include #include #include #include /** * @brief Verbs whose numeric arguments name a line. * * `THEN` and `ELSE` are deliberately absent. Commodore BASIC lets `IF X THEN 100` * stand for `THEN GOTO 100`; this dialect does not -- the corpus writes * `THEN GOTO 100` -- so a number after `THEN` here is an expression, not a target. * * `LIST` and `DELETE` are absent too, and for a different reason: their * arguments are ranges typed at a prompt, not references stored in a program. * Renumbering a `DELETE` inside a listing would be rewriting something nobody * meant as a branch. */ static const char *BRANCH_VERBS[] = { "GOTO", "GOSUB", "RUN", "RESTORE", "TRAP" }; /** @brief How many entries #BRANCH_VERBS has. */ #define BRANCH_VERB_COUNT ((int)(sizeof(BRANCH_VERBS) / sizeof(BRANCH_VERBS[0]))) /** * @brief What a walk does with each numeric branch target it finds. * * The one thing RENUMBER and the target check differ in. `visit` is handed a * target and writes into @p dest the text that should stand in its place -- * the new number for RENUMBER, the same number for a check -- and may raise to * refuse the line outright. */ typedef struct akbasic_TargetWalk akbasic_TargetWalk; struct akbasic_TargetWalk { void *self; akerr_ErrorContext *(*visit)(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len); }; /** * @brief Where a line moved to, or the line itself when it did not move. * * A target naming a line that does not exist is left alone rather than * rewritten: `GOTO 9999` in a program with no line 9999 is already broken, and * inventing a destination for it would hide that. */ static int64_t mapped(const int64_t *map, int64_t line) { if ( line < 0 || line >= AKBASIC_MAX_SOURCE_LINES ) { return line; } return (map[line] >= 0 ? map[line] : line); } /** * @brief Copy a run of comma-separated line numbers, visiting each. * * `ON X GOTO 100, 200, 300` is why this takes a list rather than one number: the * targets follow the `GOTO`, so handling `GOTO` handles `ON` for free. * * @param walk What to substitute for each target, and whether to allow it. * @param src Reads from here; advanced past what was consumed. * @param dest Writes to here; advanced past what was written. * @param limit One past the last byte @p dest may write. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKBASIC_ERR_BOUNDS When the rewritten line does not fit. */ static akerr_ErrorContext *rewrite_targets(akbasic_TargetWalk *walk, const char **src, char **dest, const char *limit) { PREPARE_ERROR(errctx); const char *p = *src; char *out = *dest; for ( ;; ) { char replacement[32]; int64_t line = 0; size_t written = 0; while ( *p == ' ' || *p == '\t' ) { FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); *out = *p; out += 1; p += 1; } if ( !isdigit((unsigned char)*p) ) { /* * Not a number: a label, an expression, or nothing at all. Labels are * the reason RENUMBER is survivable in the first place -- a program * written with `GOTO DONE` needs no rewriting and cannot be broken by * one. */ break; } while ( isdigit((unsigned char)*p) ) { line = (line * 10) + (*p - '0'); p += 1; } PASS(errctx, walk->visit(walk, line, replacement, sizeof(replacement))); /* * The fit is decided before the copy rather than read back out of a * return value afterwards: aksl_strcpy refuses rather than truncates, * so the bounds check has to come first to keep the diagnosis this * function already gives. */ PASS(errctx, aksl_strlen(replacement, &written)); FAIL_ZERO_RETURN(errctx, (written > 0 && out + written < limit), AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); PASS(errctx, aksl_strcpy(out, (size_t)(limit - out), replacement)); out += written; /* * A comma continues the list; anything else ends it. The spaces are * skipped to find out which, and put back if the answer is "ends" -- * without that, `THEN GOTO 9 ELSE ...` came back as `THEN GOTO 30ELSE`. */ { const char *afterdigits = p; while ( *p == ' ' || *p == '\t' ) { p += 1; } if ( *p != ',' ) { p = afterdigits; break; } } FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); *out = ','; out += 1; p += 1; } *src = p; *dest = out; SUCCEED_RETURN(errctx); } /** * @brief Rewrite every branch target in one line of source. * * @param walk What to substitute for each target, and whether to allow it. * @param code The line to read. * @param dest Where the rewritten line goes. * @param len Capacity of @p dest. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKBASIC_ERR_BOUNDS When the rewritten line does not fit. */ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *code, char *dest, size_t len) { PREPARE_ERROR(errctx); const char *p = code; char *out = dest; const char *limit = dest + len - 1; bool statementstart = true; bool instring = false; int cmp = 0; int i = 0; while ( *p != '\0' ) { size_t verblen = 0; bool matched = false; FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); if ( instring ) { instring = (*p != '"'); *out = *p; out += 1; p += 1; continue; } if ( *p == '"' ) { /* A string literal is copied through untouched: `PRINT "GOTO 10"`. */ instring = true; statementstart = false; *out = *p; out += 1; p += 1; continue; } if ( *p == ':' ) { statementstart = true; *out = *p; out += 1; p += 1; continue; } if ( *p == ' ' || *p == '\t' ) { *out = *p; out += 1; p += 1; continue; } /* * **Both ends, not just the trailing one.** The check below used to look * only at the character *after* a match, so a name ending in a branch * verb matched inside itself: `RCOLLISION(1, 1)` found `COLLISION` at its * second character, read the `(1,` that followed as a handler argument, * and refused the whole program with "branch to line 1, which the program * did not number" -- naming a line that contains no branch at all. * * The comment below is right that the trailing check is what protects * `GOTOX#`. Nothing protected `XGOTO#`, and `RCOLLISION` is the first * name in the language to reach it. */ for ( i = 0; i < BRANCH_VERB_COUNT; i++ ) { PASS(errctx, aksl_strlen(BRANCH_VERBS[i], &verblen)); if ( p > code && isalnum((unsigned char)p[-1]) ) { break; } PASS(errctx, aksl_strncasecmp(p, BRANCH_VERBS[i], verblen, &cmp)); if ( cmp == 0 && !isalnum((unsigned char)p[verblen]) ) { matched = true; break; } } /* * A branch verb may appear anywhere a statement may, and `GOTO` also * follows `THEN`, `ELSE` and `ON X` -- none of which is a statement * start. So the match is not gated on statementstart; what protects * `A$ = "GOTO"` is the string check above, and what protects a variable * called `GOTOX#` is the alphanumeric check here. */ if ( matched ) { FAIL_ZERO_RETURN(errctx, (out + verblen < (size_t)(limit - dest) + dest), AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); PASS(errctx, aksl_memcpy(out, p, verblen)); out += verblen; p += verblen; PASS(errctx, rewrite_targets(walk, &p, &out, limit)); statementstart = false; continue; } /* * COLLISION's handler is its *second* argument, so the list rewrite * cannot be pointed at it directly. Copy the type and the comma, then * rewrite what follows. */ PASS(errctx, aksl_strncasecmp(p, "COLLISION", 9, &cmp)); if ( cmp == 0 && !isalnum((unsigned char)p[9]) && !(p > code && isalnum((unsigned char)p[-1])) ) { PASS(errctx, aksl_memcpy(out, p, 9)); out += 9; p += 9; while ( *p != '\0' && *p != ',' && *p != ':' ) { FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); *out = *p; out += 1; p += 1; } if ( *p == ',' ) { *out = ','; out += 1; p += 1; PASS(errctx, rewrite_targets(walk, &p, &out, limit)); } statementstart = false; continue; } statementstart = false; *out = *p; out += 1; p += 1; } *out = '\0'; (void)statementstart; SUCCEED_RETURN(errctx); } /* ----------------------------------------------------------- RENUMBER -- */ /** @brief RENUMBER's visitor: substitute the number the line moved to. */ static akerr_ErrorContext *visit_renumber(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len) { PREPARE_ERROR(errctx); const int64_t *map = (const int64_t *)walk->self; int written = 0; PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, mapped(map, target))); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int64_t increment, int64_t oldstart) { PREPARE_ERROR(errctx); static int64_t map[AKBASIC_MAX_SOURCE_LINES]; static akbasic_SourceLine rewritten[AKBASIC_MAX_SOURCE_LINES]; akbasic_TargetWalk walk = { map, visit_renumber }; int64_t next = newstart; int64_t i = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in renumber"); FAIL_ZERO_RETURN(errctx, (increment > 0), AKBASIC_ERR_VALUE, "RENUMBER's increment must be positive, not %" PRId64, increment); FAIL_ZERO_RETURN(errctx, (newstart > 0), AKBASIC_ERR_VALUE, "RENUMBER cannot start at line %" PRId64, newstart); for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { map[i] = -1; } /* * Build the whole map before touching anything. A rewrite needs to know * where *every* line ended up, including ones it has not reached yet -- * a backward `GOTO` is as common as a forward one. */ for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { if ( obj->source[i].code[0] == '\0' || i < oldstart ) { continue; } FAIL_ZERO_RETURN(errctx, (next < AKBASIC_MAX_SOURCE_LINES), AKBASIC_ERR_BOUNDS, "RENUMBER: line %" PRId64 " would become %" PRId64 ", past the %d line limit", i, next, AKBASIC_MAX_SOURCE_LINES - 1); /* * A renumbered line must not land on a kept one. Refused rather than * overwritten: losing a line to a renumbering is not recoverable. */ FAIL_ZERO_RETURN(errctx, (next >= oldstart || obj->source[next].code[0] == '\0'), AKBASIC_ERR_VALUE, "RENUMBER: line %" PRId64 " would become %" PRId64 ", which already exists", i, next); map[i] = next; next += increment; } PASS(errctx, aksl_memset(rewritten, 0, sizeof(rewritten))); for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { int64_t target = 0; if ( obj->source[i].code[0] == '\0' ) { continue; } /* * Every line is rewritten, not just the moved ones: a line before * `oldstart` can branch into the region that moved. */ target = mapped(map, i); PASS(errctx, rewrite_line(&walk, obj->source[i].code, rewritten[target].code, sizeof(rewritten[target].code))); rewritten[target].lineno = target; /* * Every line comes out numbered, whether or not it went in that way. * Asking for numbers is what RENUMBER is, and a program that has been * through it can be branched into by number -- which is the whole point * of running it over source that arrived without any. */ rewritten[target].numbered = true; } PASS(errctx, aksl_memcpy(obj->source, rewritten, sizeof(obj->source))); SUCCEED_RETURN(errctx); } /* ------------------------------------------------- the target prescan -- */ /** @brief What visit_check() needs to name the line it refused. */ typedef struct { const akbasic_Runtime *runtime; int64_t lineno; } CheckState; /** * @brief The prescan's visitor: allow the target, or refuse the program. * * A number that names a line the program never numbered is refused. In a script * written without line numbers `GOTO 100` finds the hundredth line and branches * there -- plausible, silent, and wrong, which is the worst kind of defect to be * handed at run time. * * A target naming an *empty* slot is allowed through, for the same reason * mapped() leaves one alone: `GOTO 9999` in a program with no line 9999 is * already broken, and refusing it here would be inventing a rule about lines * that do not exist. The substitution is the number itself, because nothing is * being rewritten. */ static akerr_ErrorContext *visit_check(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len) { PREPARE_ERROR(errctx); CheckState *state = (CheckState *)walk->self; int written = 0; PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, target)); if ( target >= 0 && target < AKBASIC_MAX_SOURCE_LINES ) { FAIL_NONZERO_RETURN(errctx, (state->runtime->source[target].code[0] != '\0' && !state->runtime->source[target].numbered), AKBASIC_ERR_SYNTAX, "Line %" PRId64 ": branch to line %" PRId64 ", which the program did not number. Branch by LABEL, or RENUMBER first", state->lineno, target); } SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_runtime_check_targets(akbasic_Runtime *obj) { PREPARE_ERROR(errctx); /* * Static for the same reason akbasic_renumber()'s buffers are: a line's worth * of rewrite does not belong on a default stack alongside the rest of a * step(). Nothing is read back out of it -- the walk needs somewhere to put * the text it would have written, and this is it. */ static char discard[AKBASIC_MAX_LINE_LENGTH * 2]; CheckState state = { NULL, 0 }; akbasic_TargetWalk walk = { &state, visit_check }; int64_t entry = 0; int64_t i = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in check_targets"); state.runtime = obj; entry = obj->environment->lineno; for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { if ( obj->source[i].code[0] == '\0' ) { continue; } state.lineno = i; /* * Point the runtime at the line being walked, so the `? N :` prefix * akbasic_runtime_error() builds from environment->lineno names the * offending line rather than whichever one the loader stopped on. The * message repeats the number because the other three prescans do not do * this and get it wrong -- see TODO.md. * * PASS inside the loop, never CATCH: CATCH expands to a break, which * would leave this loop rather than an enclosing ATTEMPT. That also means * the cursor is deliberately *not* restored on the failure path, which is * the whole point of setting it. */ obj->environment->lineno = i; PASS(errctx, rewrite_line(&walk, obj->source[i].code, discard, sizeof(discard))); } /* Nothing was refused, so leave the cursor as the caller had it. */ obj->environment->lineno = entry; SUCCEED_RETURN(errctx); }