diff options
-rw-r--r-- | renumbas.1 | 184 | ||||
-rw-r--r-- | renumbas.c | 403 | ||||
-rw-r--r-- | renumbas.rst | 116 |
3 files changed, 703 insertions, 0 deletions
diff --git a/renumbas.1 b/renumbas.1 new file mode 100644 index 0000000..3cd0e65 --- /dev/null +++ b/renumbas.1 @@ -0,0 +1,184 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "RENUMBAS" 1 "2024-06-13" "0.2.1" "Urchlay's Atari 8-bit Tools" +.SH NAME +renumbas \- Renumber Atari 8-bit BASIC programs +.SH SYNOPSIS +.sp +renumbas [\fB\-v\fP] [\fB\-s\fP \fIstart\-lineno\fP] [\fB\-i\fP \fIincrement\fP] [\fB\-f\fP \fIfirst\-lineno\fP] \fIinput\-file\fP \fIoutput\-file\fP +.SH DESCRIPTION +.sp +\fBrenumbas\fP reads a tokenized Atari 8\-bit BASIC program and writes a +renumbered copy of the program. +.sp +\fBinput\-file\fP must be a tokenized (SAVEd) Atari BASIC program. Use +\fI\-\fP to read from standard input, but \fBrenumbas\fP will refuse to +read from standard input if it\(aqs a terminal. +.sp +\fBoutput\-file\fP will be the renumbered BASIC program. If it already +exists, it will be overwritten. Use \fI\-\fP to write to standard output, +but \fBrenumbas\fP will refuse to write to standard output if it\(aqs a +terminal (since tokenized BASIC is binary data and may confuse the +terminal). +.sp +Line number references are changed in every line where they occur, so +e.g. if line 100 gets changed to 200, any other line that does a GOTO +100 (or GOSUB, RESTORE, TRAP, etc) will be updated with the new line +number. +.sp +Computed line numbers can\(aqt be updated (e.g. GOTO A or GOSUB +1000+A*100). These will draw warnings on stderr, so you can fix them +manually. +.sp +Line numbers that don\(aqt exist will not be changed (e.g. TRAP 40000). +.sp +Remember that the maximum line number for Atari BASIC is 32767. +Renumbering will fail, if the chosen start and increment values +would result in lines with numbers higher than this. +.SH OPTIONS +.sp +Options may appear in any order. The first non\-option argument is used +for \fBinput\-file\fP; the second is \fBoutput\-file\fP\&. A third non\-option +argument is an error. +.SS General Options +.INDENT 0.0 +.TP +.B \fB\-\-help\fP +Print usage message and exit. +.TP +.B \fB\-\-version\fP +Print version number and exit. +.TP +.B \fB\-v\fP +Verbose operation. When displaying a number in verbose mode, it will +be prefixed with \fI$\fP if it\(aqs in hex, or no prefix for decimal. +.UNINDENT +.SS Renumber Options +.INDENT 0.0 +.TP +.B \fB\-s\fP \fIstart\-lineno\fP +First line number in the renumbered program. Default: 10. +.TP +.B \fB\-i\fP \fIincrement\fP +Line number increment between successive lines. Default: 10. +.TP +.B \fB\-f\fP \fIfirst\-lineno\fP +Line number in original program where renumbering will start. Lines +numbered lower that this will not be renumbered. Default: 0. +.UNINDENT +.SH LIMITATIONS +.SS Computed line numbers with ON +.sp +If an ON/GOTO or ON/GOSUB uses computed line numbers (and causes a +warning), none of the line numbers after the first computed one will +be updated, even if they are constant. Example: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +100 ON X GOTO 10,20*Y,30 +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +The 10 will be changed to whatever line 10 got renumbered to, as expected. The 20 +will \fInot\fP be changed. \fBrenumbas\fP just gives up, after the first computed +line number. +.sp +A pathological case: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +100 ON X GOTO 10+0,20+0 +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +The 10+0 and 20+0 are considered computed line numbers, even though +the results of the computation are constant. This is because neither +Atari BASIC nor \fBrenumbas\fP does constant folding. +.sp +None of this should be a real\-world problem: computed line numbers in +ON/GOTO or ON/GOSUB are exceedingly rare. The whole \fIpoint\fP of ON is +to avoid computing line numbers. +.SS Warning line numbers +.sp +Any warning that includes a line number (such as "Computed line number") will +have the original line number, \fInot\fP the renumbered one. This is because +the warnings are generated while scanning the program to find line number +references, which happens before the actual renumbering (so the new number +isn\(aqt known yet). +.SH EXIT STATUS +.sp +0 for success, 1 for failure. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBblob2xex\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBdumpbas\fP(1), +\fBf2toxex\fP(1), +\fBfenders\fP(1), +\fBprotbas\fP(1), +\fBrenumbas\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBunprotbas\fP(1), +\fBvxrefbas\fP(1), +\fBxexamine\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1), +\fBxex\fP(5), +\fBatascii\fP(7). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/renumbas.c b/renumbas.c new file mode 100644 index 0000000..272b832 --- /dev/null +++ b/renumbas.c @@ -0,0 +1,403 @@ +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <time.h> + +#include "bas.h" + +/* remove/comment to turn off debug printing */ +#define DEBUG + +#ifdef DEBUG +# define IFDEBUG(x) x +#else +# define IFDEBUG(x) +#endif + +int startlineno = 10; +int increment = 10; +int limit = 0; + +unsigned short *linerefs[32768]; +int linerefcounts[32768]; + +void print_help(void) { + fprintf(stderr, "Usage: %s [-v] [-s start-lineno] [-i increment] [-f first-lineno] <inputfile> <outputfile>\n", self); + fprintf(stderr, " -v: Verbose.\n"); + fprintf(stderr, " -s <num>: Starting line number (default: 10).\n"); + fprintf(stderr, " -i <num>: Increment (default: 10).\n"); + fprintf(stderr, " -f <num>: Don't renumber lines less than <num> (default: 0).\n"); +} + +unsigned short getlineno(char opt, const char *arg) { + int lineno; + char *e; + + lineno = (int)strtol(arg, &e, 10); + + if(*e) { + fprintf(stderr, "%s: Invalid line number for -%c option: %s is not a number.\n", + self, opt, arg); + exit(1); + } + + if(lineno < 0 || lineno > 32767) { + fprintf(stderr, "%s: Invalid line number for -%c option: %d > 32767.\n", + self, opt, lineno); + exit(1); + } + + return ((unsigned short)lineno); +} + +void parse_args(int argc, char **argv) { + int opt; + + while( (opt = getopt(argc, argv, "vs:i:f:")) != -1) { + switch(opt) { + case 'v': verbose = 1; break; + case 's': startlineno = getlineno(opt, optarg); break; + case 'i': increment = getlineno(opt, optarg); break; + case 'f': limit = getlineno(opt, optarg); break; + default: + print_help(); + exit(1); + } + } + + if(optind >= argc) + die("No input file given (use - for stdin)."); + else + open_input(argv[optind]); + + if(++optind >= argc) + die("No output file given (use - for stdout)."); + else + output_filename = argv[optind]; +} + +unsigned char bcd2int(unsigned char bcd) { + return (bcd >> 4) * 10 + (bcd & 0x0f); +} + +unsigned char int2bcd(unsigned char i) { + return ((i / 10) << 4) | (i % 10); +} + +unsigned short fp2int(const unsigned char *fp) { + unsigned short result = 0; + + /* examine the exponent/sign byte */ + if(fp[0] == 0) return 0; /* special case */ + if(fp[0] & 0x80) die("negative numbers not supported"); + + switch(fp[0]) { + case 0x40: + result = bcd2int(fp[1]); break; + case 0x41: + result = bcd2int(fp[1]) * 100 + bcd2int(fp[2]); break; + case 0x42: + result = bcd2int(fp[1]) * 10000 + bcd2int(fp[2]) * 100 + bcd2int(fp[3]); break; + default: + die("number out of range"); break; + } + + return result; +} + +void int2fp(unsigned short num, unsigned char *fp) { + memset(fp, 0, 6); + + if(num == 0) return; + + if(num >= 10000) { + fp[0] = 0x42; + fp[3] = int2bcd(num % 100); + num /= 100; + fp[2] = int2bcd(num % 100); + num /= 100; + fp[1] = int2bcd(num); + } else if(num >= 100) { + fp[0] = 0x41; + fp[2] = int2bcd(num % 100); + num /= 100; + fp[1] = int2bcd(num); + } else { + fp[0] = 0x40; + fp[1] = int2bcd(num); + } +} + +void addlineref(unsigned short refaddr) { + int target = fp2int(program + refaddr); + unsigned short *p = linerefs[target]; + int c = linerefcounts[target]; + + IFDEBUG(printf("addlineref: target=%d, 0x%04x\n", target, refaddr)); + + if(c) { + p = realloc(p, sizeof(unsigned short) * (c + 1)); + } else { + p = malloc(sizeof(unsigned short)); + } + + if(!p) die("Out of memory."); + + linerefs[target] = p; + p[c] = refaddr; + c++; + linerefcounts[target] = c; +} + +void printlinerefs(void) { + int i, j; + printf("linerefs:\n"); + for(i = 0; i < 32768; i++) { + if(linerefcounts[i]) { + printf("%d: ", i); + for(j = 0; j < linerefcounts[i]; j++) { + printf("%04x ", linerefs[i][j]); + } + putchar('\n'); + } + } +} + +void freelinerefs(void) { + int i; + for(i = 0; i < 32768; i++) { + if(linerefcounts[i]) { + free(linerefs[i]); + } + } +} + +/* tokens that can take line numbers: + Commands: + GOTO 0x0a + GO TO 0x0b + GOSUB 0x0c + TRAP 0x0d + LIST 0x04 (but don't bother) + RESTORE 0x23 + + Operators: + GOTO 0x17 (as in, ON (0x1e) GOTO) + GOSUB 0x18 (ON = 0x1e again) + THEN 0x1b (but not really!) + + beware: e.g. GOTO 1000+A should not have the 1000 changed. + + numeric constant introduced with 0x0e, followed by 6 BCD bytes. + string constant 0x0f, length byte, then (length) bytes. +*/ + +int is_xfer_cmd(unsigned char tok) { + int ret; + switch(tok) { + case CMD_GOTO: + case CMD_GO_TO: + case CMD_GOSUB: + case CMD_TRAP: + case CMD_LIST: + case CMD_RESTORE: + ret = 1; break; + default: + ret = 0; break; + } + IFDEBUG(printf("is_xfer_cmd(%02x) == %d\n", tok, ret)); + return ret; +} + +int skip_op_token(int pos) { + switch(program[pos]) { + case OP_EOS: + return pos + 2; /* skip next-statement offset */ + case OP_EOL: + return pos + 3; /* skip 2-byte line number */ + case OP_NUMCONST: + return pos + 7; /* skip 6-byte BCD float */ + case OP_STRCONST: + return pos + 2 + program[pos + 1]; /* 2nd byte is string len */ + default: + return pos + 1; + } +} + +/* ON/GOTO and ON/GOSUB can have any number of arguments, separated + by OP_COMMA. *Normally* these are simple FP constants, since the + whole point of ON is to avoid computed line numbers... but they're + allowed to be expressions so we have to check. */ +int handle_on_goto(int lineno, int pos) { + unsigned char tok, nexttok, main_tok; + + IFDEBUG(printf("handle_on_goto(%02x)\n", pos)); + + main_tok = program[pos]; /* save this, for use in 'computed' warning */ + + pos++; /* skip GOTO/GOSUB token */ + + while(1) { + tok = program[pos]; + if(tok == OP_EOS || tok == OP_EOL) + break; + if(tok == OP_COMMA) { + pos++; + continue; + } + nexttok = program[pos + 7]; + if(tok != OP_NUMCONST || !(nexttok == OP_COMMA || nexttok == OP_EOS || nexttok == OP_EOL)) { + fprintf(stderr, "Computed line number in ON/%s at line %d.\n", + (main_tok == OP_GOTO ? "GOTO" : "GOSUB"), lineno); + break; + } + addlineref(pos + 1); + pos += 7; + } + + return pos; +} + +/* IF/THEN can be followed by a simple line number (not a full expression) + or a statement offset (*without* an OP_EOS token!) followed by a + statement. + The right way to do this would be to track the statement offsets, but + this works fine. It relies on the fact that line numbers always have + an exponent byte of 0x40 to 0x42, and the fact that 0x40 to 0x42 are + not valid command tokens. + */ +int handle_then(int pos) { + unsigned char tok1 = program[pos + 1]; + unsigned char tok2 = program[pos + 2]; + if(tok1 == OP_NUMCONST && (tok2 >= 0x40 && tok2 <= 0x42)) { + addlineref(pos + 2); + return pos + 7; + } else { + return 0; + } +} + +void renumber(void) { + int pos = codestart, nextpos; + int lineno, offset; + int newno, i; + unsigned char tok; + unsigned char fpnewno[6]; + + /* pass 1: find references to line numbers, flag them. */ + while(pos < filelen) { + lineno = getword(pos); + if(lineno == 32768) break; + offset = program[pos + 2]; + IFDEBUG(printf("checking line %d, pos %04x...\n", lineno, pos)); + if(offset < 6) + die("Can't renumber a code-protected program, unprotect it first."); + nextpos = pos + offset; + + pos += 4; /* skip line number, line length, 1st statement length */ + + /* loop over the statements in this line */ + while(pos < nextpos) { + /* every statement starts with a command token */ + tok = program[pos]; + + if(tok == CMD_REM || tok == CMD_DATA || tok == CMD_ERROR) + break; /* ignore rest of line */ + + if(is_xfer_cmd(tok)) { + unsigned char nexttok = program[pos + 8]; + if(program[pos + 1] == OP_NUMCONST && (nexttok == OP_EOS || nexttok == OP_EOL)) { + addlineref(pos + 2); + } else { + fprintf(stderr, "Computed line number at line %d.\n", lineno); + } + } + + pos++; + + /* rest of statement is expressions/operators */ + while(pos < nextpos) { + tok = program[pos]; + + if(tok == OP_EOL) break; + + if(tok == OP_EOS) { + pos += 2; /* end statement, skip statement length of next one */ + break; + } + + if(tok == OP_GOTO || tok == OP_GOSUB) { + pos = handle_on_goto(lineno, pos); + continue; + } else if(tok == OP_THEN) { + i = handle_then(pos); + if(i) { + pos = i; + continue; + } else { + pos += 2; /* skip statement length */ + break; + } + } + + IFDEBUG(printf("tok is %02x, pos was %02x before skip_op_token... ", tok, pos)); + pos = skip_op_token(pos); + IFDEBUG(printf("pos now %02x\n", pos)); + } + } + + pos = nextpos; /* point to next line */ + } + + IFDEBUG(printlinerefs()); + + /* pass 2: renumber the lines, and update the references in other lines */ + newno = startlineno; + pos = codestart; + while(pos < filelen) { + if(newno >= 32768) { + fprintf(stderr, "New line number %d out of range, renumber failed.", newno); + exit(1); + } + + lineno = getword(pos); + offset = program[pos + 2]; + + if(lineno >= limit) { + if(lineno == 32768) break; + + IFDEBUG(printf("renumbering line %d as %d, %d refs\n", lineno, newno, linerefcounts[lineno])); + + /* update refs to old line number with new one */ + int2fp(newno, fpnewno); + for(i = 0; i < linerefcounts[lineno]; i++) + memmove(program + linerefs[lineno][i], fpnewno, 6); + + /* update the actual line number */ + setword(pos, newno); + + newno += increment; + } + pos += offset; + } + + freelinerefs(); +} + +int main(int argc, char **argv) { + set_self(*argv); + parse_general_args(argc, argv, print_help); + parse_args(argc, argv); + + readfile(); + parse_header(); + + renumber(); + + open_output(output_filename); + writefile(); + + return 0; +} diff --git a/renumbas.rst b/renumbas.rst new file mode 100644 index 0000000..a0b79ba --- /dev/null +++ b/renumbas.rst @@ -0,0 +1,116 @@ +======== +renumbas +======== + +----------------------------------- +Renumber Atari 8-bit BASIC programs +----------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== +renumbas [**-v**] [**-s** *start-lineno*] [**-i** *increment*] [**-f** *first-lineno*] *input-file* *output-file* + +DESCRIPTION +=========== +**renumbas** reads a tokenized Atari 8-bit BASIC program and writes a +renumbered copy of the program. + +**input-file** must be a tokenized (SAVEd) Atari BASIC program. Use +*-* to read from standard input, but **renumbas** will refuse to +read from standard input if it's a terminal. + +**output-file** will be the renumbered BASIC program. If it already +exists, it will be overwritten. Use *-* to write to standard output, +but **renumbas** will refuse to write to standard output if it's a +terminal (since tokenized BASIC is binary data and may confuse the +terminal). + +Line number references are changed in every line where they occur, so +e.g. if line 100 gets changed to 200, any other line that does a GOTO +100 (or GOSUB, RESTORE, TRAP, etc) will be updated with the new line +number. + +Computed line numbers can't be updated (e.g. GOTO A or GOSUB +1000+A*100). These will draw warnings on stderr, so you can fix them +manually. + +Line numbers that don't exist will not be changed (e.g. TRAP 40000). + +Remember that the maximum line number for Atari BASIC is 32767. +Renumbering will fail, if the chosen start and increment values +would result in lines with numbers higher than this. + +OPTIONS +======= + +Options may appear in any order. The first non-option argument is used +for **input-file**; the second is **output-file**. A third non-option +argument is an error. + +General Options +--------------- +**--help** + Print usage message and exit. + +**--version** + Print version number and exit. + +**-v** + Verbose operation. When displaying a number in verbose mode, it will + be prefixed with *$* if it's in hex, or no prefix for decimal. + +Renumber Options +---------------- +**-s** *start-lineno* + First line number in the renumbered program. Default: 10. + +**-i** *increment* + Line number increment between successive lines. Default: 10. + +**-f** *first-lineno* + Line number in original program where renumbering will start. Lines + numbered lower that this will not be renumbered. Default: 0. + +LIMITATIONS +=========== + +Computed line numbers with ON +----------------------------- +If an ON/GOTO or ON/GOSUB uses computed line numbers (and causes a +warning), none of the line numbers after the first computed one will +be updated, even if they are constant. Example:: + + 100 ON X GOTO 10,20*Y,30 + +The 10 will be changed to whatever line 10 got renumbered to, as expected. The 20 +will *not* be changed. **renumbas** just gives up, after the first computed +line number. + +A pathological case:: + + 100 ON X GOTO 10+0,20+0 + +The 10+0 and 20+0 are considered computed line numbers, even though +the results of the computation are constant. This is because neither +Atari BASIC nor **renumbas** does constant folding. + +None of this should be a real-world problem: computed line numbers in +ON/GOTO or ON/GOSUB are exceedingly rare. The whole *point* of ON is +to avoid computing line numbers. + +Warning line numbers +-------------------- +Any warning that includes a line number (such as "Computed line number") will +have the original line number, *not* the renumbered one. This is because +the warnings are generated while scanning the program to find line number +references, which happens before the actual renumbering (so the new number +isn't known yet). + +EXIT STATUS +=========== + +0 for success, 1 for failure. + +.. include:: manftr.rst |