316 lines
9.7 KiB
C
316 lines
9.7 KiB
C
|
|
/**
|
||
|
|
* @file renumber.c
|
||
|
|
* @brief RENUMBER: move every line, and rewrite 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.
|
||
|
|
*
|
||
|
|
* The rewrite 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.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <ctype.h>
|
||
|
|
#include <inttypes.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <strings.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @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 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, rewriting 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 map Old line to new line.
|
||
|
|
* @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(const int64_t *map, const char **src, char **dest, const char *limit)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
const char *p = *src;
|
||
|
|
char *out = *dest;
|
||
|
|
|
||
|
|
for ( ;; ) {
|
||
|
|
int64_t line = 0;
|
||
|
|
int 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;
|
||
|
|
}
|
||
|
|
written = snprintf(out, (size_t)(limit - out), "%" PRId64, mapped(map, line));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (written > 0 && out + written < limit), AKBASIC_ERR_BOUNDS,
|
||
|
|
"RENUMBER: a rewritten line does not fit");
|
||
|
|
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 map Old line to new line.
|
||
|
|
* @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(const int64_t *map, 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 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;
|
||
|
|
}
|
||
|
|
|
||
|
|
for ( i = 0; i < BRANCH_VERB_COUNT; i++ ) {
|
||
|
|
verblen = strlen(BRANCH_VERBS[i]);
|
||
|
|
if ( strncasecmp(p, BRANCH_VERBS[i], verblen) == 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");
|
||
|
|
memcpy(out, p, verblen);
|
||
|
|
out += verblen;
|
||
|
|
p += verblen;
|
||
|
|
PASS(errctx, rewrite_targets(map, &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.
|
||
|
|
*/
|
||
|
|
if ( strncasecmp(p, "COLLISION", 9) == 0 && !isalnum((unsigned char)p[9]) ) {
|
||
|
|
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(map, &p, &out, limit));
|
||
|
|
}
|
||
|
|
statementstart = false;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
statementstart = false;
|
||
|
|
*out = *p;
|
||
|
|
out += 1;
|
||
|
|
p += 1;
|
||
|
|
}
|
||
|
|
*out = '\0';
|
||
|
|
(void)statementstart;
|
||
|
|
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];
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
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(map, obj->source[i].code,
|
||
|
|
rewritten[target].code, sizeof(rewritten[target].code)));
|
||
|
|
rewritten[target].lineno = target;
|
||
|
|
}
|
||
|
|
|
||
|
|
memcpy(obj->source, rewritten, sizeof(obj->source));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|