aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Watson <urchlay@slackware.uk>2024-06-13 15:32:22 -0400
committerB. Watson <urchlay@slackware.uk>2024-06-13 15:32:22 -0400
commitce201121b5785d9ecb2829d262c23050e537fa9e (patch)
tree41bdafdd40e86244590a7d68ed4de216c17d2d02
parentec9bb5b6b6512fcf157119b8075b6df0a8decff5 (diff)
downloadbw-atari8-tools-ce201121b5785d9ecb2829d262c23050e537fa9e.tar.gz
add renumbas.
-rw-r--r--renumbas.1184
-rw-r--r--renumbas.c403
-rw-r--r--renumbas.rst116
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