2016-09-23 06:14:08 +03:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
|
|
import re
|
|
|
|
import sys
|
|
|
|
|
|
|
|
def debug(*args, **kwargs):
|
|
|
|
print(*args, file=sys.stderr, **kwargs)
|
|
|
|
|
|
|
|
def parse_args():
|
|
|
|
parser = argparse.ArgumentParser(description='Preprocess Basic code.')
|
2016-10-31 03:15:24 +03:00
|
|
|
parser.add_argument('infiles', type=str, nargs='+',
|
|
|
|
help='the Basic files to preprocess')
|
|
|
|
parser.add_argument('--mode', choices=["cbm", "qbasic"], default="cbm")
|
2017-09-15 07:50:15 +03:00
|
|
|
parser.add_argument('--sub-mode', choices=["noui", "ui"], default="noui")
|
2016-09-23 06:14:08 +03:00
|
|
|
parser.add_argument('--keep-rems', action='store_true', default=False,
|
|
|
|
help='The type of REMs to keep (0 (none) -> 4 (all)')
|
|
|
|
parser.add_argument('--keep-blank-lines', action='store_true', default=False,
|
|
|
|
help='Keep blank lines from the original file')
|
|
|
|
parser.add_argument('--keep-indent', action='store_true', default=False,
|
|
|
|
help='Keep line identing')
|
2016-09-24 06:36:17 +03:00
|
|
|
parser.add_argument('--skip-misc-fixups', action='store_true', default=False,
|
|
|
|
help='Skip miscellaneous fixup/shrink fixups')
|
2016-10-31 03:15:24 +03:00
|
|
|
parser.add_argument('--skip-combine-lines', action='store_true', default=False,
|
|
|
|
help='Do not combine lines using the ":" separator')
|
2016-09-23 06:14:08 +03:00
|
|
|
|
2016-09-24 06:36:17 +03:00
|
|
|
args = parser.parse_args()
|
2017-09-15 07:50:15 +03:00
|
|
|
args.full_mode = "%s-%s" % (args.mode, args.sub_mode)
|
2016-10-31 03:15:24 +03:00
|
|
|
if args.keep_rems and not args.skip_combine_lines:
|
|
|
|
debug("Option --keep-rems implies --skip-combine-lines ")
|
|
|
|
args.skip_combine_lines = True
|
|
|
|
|
|
|
|
if args.mode == 'qbasic' and not args.skip_misc_fixups:
|
|
|
|
debug("Mode 'qbasic' implies --skip-misc-fixups")
|
|
|
|
args.skip_misc_fixups = True
|
2016-09-24 06:36:17 +03:00
|
|
|
|
|
|
|
return args
|
2016-09-23 06:14:08 +03:00
|
|
|
|
|
|
|
# pull in include files
|
2017-09-15 07:50:15 +03:00
|
|
|
def resolve_includes(orig_lines, args):
|
2016-09-23 06:14:08 +03:00
|
|
|
included = {}
|
2017-09-15 07:50:15 +03:00
|
|
|
lines = orig_lines[:]
|
|
|
|
position = 0
|
|
|
|
while position < len(lines):
|
|
|
|
line = lines[position]
|
|
|
|
m = re.match(r"^(?:#([^ ]*) )? *REM \$INCLUDE: '([^'\n]*)' *$", line)
|
|
|
|
if m:
|
|
|
|
mode = m.group(1)
|
|
|
|
f = m.group(2)
|
|
|
|
if mode and mode != args.mode and mode != args.full_mode:
|
|
|
|
position += 1
|
|
|
|
elif f not in included:
|
2016-09-23 06:14:08 +03:00
|
|
|
ilines = [l.rstrip() for l in open(f).readlines()]
|
2017-09-15 07:50:15 +03:00
|
|
|
if args.keep_rems: lines.append("REM vvv BEGIN '%s' vvv" % f)
|
|
|
|
lines[position:position+1] = ilines
|
|
|
|
if args.keep_rems: lines.append("REM ^^^ END '%s' ^^^" % f)
|
2016-09-23 06:14:08 +03:00
|
|
|
else:
|
|
|
|
debug("Ignoring already included file: %s" % f)
|
|
|
|
else:
|
2017-09-15 07:50:15 +03:00
|
|
|
position += 1
|
2016-09-23 06:14:08 +03:00
|
|
|
return lines
|
|
|
|
|
2017-09-15 07:50:15 +03:00
|
|
|
def resolve_mode(orig_lines, args):
|
2016-10-31 03:15:24 +03:00
|
|
|
lines = []
|
|
|
|
for line in orig_lines:
|
2016-11-05 05:46:45 +03:00
|
|
|
m = re.match(r"^ *#([^ \n]*) *([^\n]*)$", line)
|
2016-10-31 03:15:24 +03:00
|
|
|
if m:
|
2017-09-15 07:50:15 +03:00
|
|
|
if m.group(1) == args.mode:
|
|
|
|
lines.append(m.group(2))
|
|
|
|
elif m.group(1) == args.full_mode:
|
2016-10-31 03:15:24 +03:00
|
|
|
lines.append(m.group(2))
|
|
|
|
continue
|
|
|
|
lines.append(line)
|
|
|
|
return lines
|
|
|
|
|
2016-09-23 06:14:08 +03:00
|
|
|
def drop_blank_lines(orig_lines):
|
|
|
|
lines = []
|
|
|
|
for line in orig_lines:
|
2016-10-26 09:26:05 +03:00
|
|
|
if re.match(r"^\W*$", line): continue
|
2016-09-23 06:14:08 +03:00
|
|
|
lines.append(line)
|
|
|
|
return lines
|
|
|
|
|
|
|
|
|
|
|
|
def drop_rems(orig_lines):
|
|
|
|
lines = []
|
|
|
|
for line in orig_lines:
|
|
|
|
if re.match(r"^ *REM", line):
|
|
|
|
continue
|
|
|
|
m = re.match(r"^(.*): *REM .*$", line)
|
|
|
|
if m:
|
|
|
|
lines.append(m.group(1))
|
|
|
|
else:
|
|
|
|
lines.append(line)
|
|
|
|
return lines
|
|
|
|
|
|
|
|
def remove_indent(orig_lines):
|
|
|
|
lines = []
|
|
|
|
for line in orig_lines:
|
2016-11-05 05:46:45 +03:00
|
|
|
m = re.match(r"^ *([^ \n].*)$", line)
|
2016-09-23 06:14:08 +03:00
|
|
|
lines.append(m.group(1))
|
|
|
|
return lines
|
|
|
|
|
2016-09-24 06:36:17 +03:00
|
|
|
def misc_fixups(orig_lines):
|
|
|
|
text = "\n".join(orig_lines)
|
2016-11-03 07:36:44 +03:00
|
|
|
|
|
|
|
# Remove GOTO after THEN
|
2016-10-24 06:18:08 +03:00
|
|
|
text = re.sub(r"\bTHEN GOTO\b", "THEN", text)
|
2016-11-03 06:19:51 +03:00
|
|
|
|
2016-11-03 07:36:44 +03:00
|
|
|
# Remove spaces after keywords
|
2016-10-24 06:18:08 +03:00
|
|
|
text = re.sub(r"\bIF ", "IF", text)
|
2016-11-03 06:19:51 +03:00
|
|
|
text = re.sub(r"\bPRINT *", "PRINT", text)
|
2016-11-03 07:36:44 +03:00
|
|
|
text = re.sub(r"\bDIM ", "DIM", text)
|
|
|
|
text = re.sub(r"\OPEN ", "OPEN", text)
|
|
|
|
text = re.sub(r"\bGET ", "GET", text)
|
2016-11-05 05:46:45 +03:00
|
|
|
text = re.sub(r"\bPOKE ", "POKE", text)
|
2016-12-10 19:50:40 +03:00
|
|
|
text = re.sub(r"\bCLOSE ", "CLOSE", text)
|
|
|
|
text = re.sub(r"\bFOR ", "FOR", text)
|
|
|
|
text = re.sub(r" TO ", "TO", text)
|
|
|
|
text = re.sub(r"\bNEXT ", "NEXT", text)
|
2016-11-03 07:36:44 +03:00
|
|
|
|
|
|
|
# Remove spaces around GOTO/GOSUB/THEN
|
2016-11-03 06:19:51 +03:00
|
|
|
text = re.sub(r" *GOTO *", "GOTO", text)
|
|
|
|
text = re.sub(r" *GOSUB *", "GOSUB", text)
|
|
|
|
text = re.sub(r" *THEN *", r"THEN", text)
|
2016-11-03 07:36:44 +03:00
|
|
|
|
2016-12-10 19:50:40 +03:00
|
|
|
# Remove spaces around AND/OR except after ST
|
|
|
|
text = re.sub(r"(?<!ST) *AND *", r"AND", text)
|
2016-11-03 06:19:51 +03:00
|
|
|
text = re.sub(r"([^A-Z]) *OR *", r"\g<1>OR", text)
|
2016-11-03 07:36:44 +03:00
|
|
|
|
2016-09-24 06:36:17 +03:00
|
|
|
return text.split("\n")
|
|
|
|
|
2017-09-15 07:50:15 +03:00
|
|
|
def finalize(lines, args):
|
2016-09-24 06:36:17 +03:00
|
|
|
labels_lines = {}
|
|
|
|
lines_labels = {}
|
2016-10-26 09:26:05 +03:00
|
|
|
call_index = {}
|
2016-09-24 06:36:17 +03:00
|
|
|
|
2016-10-26 09:26:05 +03:00
|
|
|
cur_sub = None
|
|
|
|
|
|
|
|
# number lines, remove labels (but track line number), and replace
|
|
|
|
# CALLs with a stack based GOTO
|
|
|
|
src_lines = lines
|
|
|
|
lines = []
|
|
|
|
lnum=1
|
|
|
|
for line in src_lines:
|
|
|
|
|
|
|
|
# Drop labels (track line number for GOTO/GOSUB)
|
2016-11-05 05:46:45 +03:00
|
|
|
m = re.match(r"^ *([^ :\n]*): *$", line)
|
2016-10-26 09:26:05 +03:00
|
|
|
if m:
|
|
|
|
label = m.groups(1)[0]
|
|
|
|
labels_lines[label] = lnum
|
|
|
|
lines_labels[lnum] = label
|
|
|
|
continue
|
|
|
|
|
2016-11-05 05:46:45 +03:00
|
|
|
if re.match(r".*CALL *([^ :\n]*) *:", line):
|
2016-10-26 09:26:05 +03:00
|
|
|
raise Exception("CALL is not the last thing on line %s" % lnum)
|
|
|
|
|
|
|
|
# Replace CALLs (track line number for replacement later)
|
|
|
|
#m = re.match(r"\bCALL *([^ :]*) *$", line)
|
2016-11-05 05:46:45 +03:00
|
|
|
m = re.match(r"(.*)CALL *([^ :\n]*) *$", line)
|
2016-10-26 09:26:05 +03:00
|
|
|
if m:
|
|
|
|
prefix = m.groups(1)[0]
|
|
|
|
sub = m.groups(1)[1]
|
|
|
|
if not call_index.has_key(sub):
|
|
|
|
call_index[sub] = 0
|
|
|
|
call_index[sub] += 1
|
|
|
|
label = sub+"_"+str(call_index[sub])
|
|
|
|
|
|
|
|
# Replace the CALL with stack based GOTO
|
2017-09-15 07:50:15 +03:00
|
|
|
if args.mode == "cbm":
|
2016-11-05 05:46:45 +03:00
|
|
|
lines.append("%s %sQ=%s:GOSUBPUSH_Q:GOTO%s" % (
|
2016-11-03 06:19:51 +03:00
|
|
|
lnum, prefix, call_index[sub], sub))
|
|
|
|
else:
|
|
|
|
lines.append("%s %sX=X+1:X%%(X)=%s:GOTO %s" % (
|
|
|
|
lnum, prefix, call_index[sub], sub))
|
2016-10-26 09:26:05 +03:00
|
|
|
lnum += 1
|
|
|
|
|
|
|
|
# Add the return spot
|
|
|
|
labels_lines[label] = lnum
|
|
|
|
lines_labels[lnum] = label
|
|
|
|
continue
|
|
|
|
|
|
|
|
lines.append("%s %s" % (lnum, line))
|
|
|
|
lnum += 1
|
|
|
|
|
|
|
|
# remove SUB (but track lines), and replace END SUB with ON GOTO
|
|
|
|
# that returns to original caller
|
|
|
|
src_lines = lines
|
|
|
|
lines = []
|
|
|
|
lnum=1
|
|
|
|
for line in src_lines:
|
|
|
|
# Drop subroutine defs (track line number for CALLS)
|
2016-11-05 05:46:45 +03:00
|
|
|
m = re.match(r"^([0-9][0-9]*) *SUB *([^ \n]*) *$", line)
|
2016-10-26 09:26:05 +03:00
|
|
|
if m:
|
|
|
|
lnum = int(m.groups(1)[0])+1
|
|
|
|
label = m.groups(1)[1]
|
|
|
|
cur_sub = label
|
|
|
|
labels_lines[label] = lnum
|
|
|
|
lines_labels[lnum] = label
|
|
|
|
continue
|
2016-09-24 06:36:17 +03:00
|
|
|
|
2016-10-26 09:26:05 +03:00
|
|
|
# Drop END SUB (track line number for replacement later)
|
|
|
|
m = re.match(r"^([0-9][0-9]*) *END SUB *$", line)
|
|
|
|
if m:
|
|
|
|
if cur_sub == None:
|
|
|
|
raise Exception("END SUB found without preceeding SUB")
|
|
|
|
lnum = int(m.groups(1)[0])
|
|
|
|
index = call_index[cur_sub]
|
|
|
|
|
|
|
|
ret_labels = [cur_sub+"_"+str(i) for i in range(1, index+1)]
|
2017-09-15 07:50:15 +03:00
|
|
|
if args.mode == "cbm":
|
2016-11-05 05:46:45 +03:00
|
|
|
line = "%s GOSUBPOP_Q:ONQGOTO%s" % (lnum, ",".join(ret_labels))
|
2016-11-03 07:36:44 +03:00
|
|
|
else:
|
|
|
|
line = "%s X=X-1:ON X%%(X+1) GOTO %s" % (lnum, ",".join(ret_labels))
|
2016-10-26 09:26:05 +03:00
|
|
|
cur_sub = None
|
|
|
|
|
|
|
|
lines.append(line)
|
|
|
|
|
|
|
|
def update_labels_lines(text, a, b):
|
2016-10-24 06:18:08 +03:00
|
|
|
stext = ""
|
|
|
|
while stext != text:
|
|
|
|
stext = text
|
2016-11-03 06:19:51 +03:00
|
|
|
text = re.sub(r"(THEN *)%s\b" % a, r"\g<1>%s" % b, stext)
|
2016-10-24 06:18:08 +03:00
|
|
|
#text = re.sub(r"(THEN)%s\b" % a, r"THEN%s" % b, stext)
|
2017-09-15 07:50:15 +03:00
|
|
|
if args.mode == "cbm":
|
2016-11-03 06:19:51 +03:00
|
|
|
text = re.sub(r"ON *([^:\n]*) *GOTO *([^:\n]*)\b%s\b" % a, r"ON\g<1>GOTO\g<2>%s" % b, text)
|
|
|
|
text = re.sub(r"ON *([^:\n]*) *GOSUB *([^:\n]*)\b%s\b" % a, r"ON\g<1>GOSUB\g<2>%s" % b, text)
|
|
|
|
else:
|
|
|
|
text = re.sub(r"(ON [^:\n]* *GOTO *[^:\n]*)\b%s\b" % a, r"\g<1>%s" % b, text)
|
|
|
|
text = re.sub(r"(ON [^:\n]* *GOSUB *[^:\n]*)\b%s\b" % a, r"\g<1>%s" % b, text)
|
|
|
|
text = re.sub(r"(GOSUB *)%s\b" % a, r"\g<1>%s" % b, text)
|
|
|
|
text = re.sub(r"(GOTO *)%s\b" % a, r"\g<1>%s" % b, text)
|
2016-10-24 06:18:08 +03:00
|
|
|
#text = re.sub(r"(GOTO)%s\b" % a, r"\1%s" % b, text)
|
|
|
|
return text
|
|
|
|
|
2016-10-26 09:26:05 +03:00
|
|
|
# search for and replace GOTO/GOSUBs
|
|
|
|
src_lines = lines
|
|
|
|
text = "\n".join(lines)
|
|
|
|
for label, lnum in labels_lines.items():
|
|
|
|
text = update_labels_lines(text, label, lnum)
|
|
|
|
lines = text.split("\n")
|
2016-09-24 06:36:17 +03:00
|
|
|
|
2016-10-26 09:26:05 +03:00
|
|
|
# combine lines
|
2016-10-31 03:15:24 +03:00
|
|
|
if not args.skip_combine_lines:
|
2016-10-24 06:18:08 +03:00
|
|
|
renumber = {}
|
2016-09-24 06:36:17 +03:00
|
|
|
src_lines = lines
|
|
|
|
lines = []
|
|
|
|
pos = 0
|
|
|
|
acc_line = ""
|
2016-10-24 06:18:08 +03:00
|
|
|
def renum(line):
|
|
|
|
lnum = len(lines)+1
|
|
|
|
renumber[old_num] = lnum
|
|
|
|
return "%s %s" % (lnum, line)
|
2016-09-24 06:36:17 +03:00
|
|
|
while pos < len(src_lines):
|
|
|
|
line = src_lines[pos]
|
|
|
|
m = re.match(r"^([0-9]*) (.*)$", line)
|
2016-10-24 06:18:08 +03:00
|
|
|
old_num = int(m.group(1))
|
|
|
|
line = m.group(2)
|
2016-09-24 06:36:17 +03:00
|
|
|
|
|
|
|
if acc_line == "":
|
|
|
|
# Starting a new line
|
2016-10-24 06:18:08 +03:00
|
|
|
acc_line = renum(line)
|
|
|
|
elif old_num in lines_labels or re.match(r"^ *FOR\b.*", line):
|
|
|
|
# This is a GOTO/GOSUB target or FOR loop so it must
|
|
|
|
# be on a line by itself
|
2016-09-24 06:36:17 +03:00
|
|
|
lines.append(acc_line)
|
2016-10-24 06:18:08 +03:00
|
|
|
acc_line = renum(line)
|
2016-11-03 06:19:51 +03:00
|
|
|
elif re.match(r".*(?:GOTO|THEN|RETURN).*", acc_line):
|
2016-10-24 06:18:08 +03:00
|
|
|
# GOTO/THEN/RETURN are last thing on the line
|
2016-09-24 06:36:17 +03:00
|
|
|
lines.append(acc_line)
|
2016-10-24 06:18:08 +03:00
|
|
|
acc_line = renum(line)
|
|
|
|
# TODO: not sure why this is 88 rather than 80
|
|
|
|
elif len(acc_line) + 1 + len(line) < 88:
|
2016-09-24 06:36:17 +03:00
|
|
|
# Continue building up the line
|
2016-10-24 06:18:08 +03:00
|
|
|
acc_line = acc_line + ":" + line
|
2016-09-24 06:36:17 +03:00
|
|
|
# GOTO/IF/RETURN must be the last things on a line so
|
|
|
|
# start a new line
|
2016-11-03 06:19:51 +03:00
|
|
|
if re.match(r".*(?:GOTO|THEN|RETURN).*", line):
|
2016-09-24 06:36:17 +03:00
|
|
|
lines.append(acc_line)
|
|
|
|
acc_line = ""
|
|
|
|
else:
|
|
|
|
# Too long so start a new line
|
|
|
|
lines.append(acc_line)
|
2016-10-24 06:18:08 +03:00
|
|
|
acc_line = renum(line)
|
2016-09-24 06:36:17 +03:00
|
|
|
pos += 1
|
|
|
|
if acc_line != "":
|
|
|
|
lines.append(acc_line)
|
|
|
|
|
2016-10-24 06:18:08 +03:00
|
|
|
# Finally renumber GOTO/GOSUBS
|
|
|
|
src_lines = lines
|
|
|
|
text = "\n".join(lines)
|
|
|
|
# search for and replace GOTO/GOSUBs
|
|
|
|
for a in sorted(renumber.keys()):
|
|
|
|
b = renumber[a]
|
|
|
|
text = update_labels_lines(text, a, b)
|
|
|
|
lines = text.split("\n")
|
|
|
|
|
2017-09-15 07:50:15 +03:00
|
|
|
# Force non-UI QBasic to use text console. LINE INPUT also needs
|
|
|
|
# to be used instead in character-by-character READLINE
|
|
|
|
if args.full_mode == "qbasic-noui":
|
|
|
|
# Add console program prefix for qb64/qbasic
|
|
|
|
lines = ["$CONSOLE",
|
|
|
|
"$SCREENHIDE",
|
|
|
|
"_DEST _CONSOLE"] + lines
|
2016-09-24 06:36:17 +03:00
|
|
|
|
|
|
|
return lines
|
2016-09-23 06:14:08 +03:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
args = parse_args()
|
|
|
|
|
2016-10-31 03:15:24 +03:00
|
|
|
debug("Preprocessing basic files: "+", ".join(args.infiles))
|
2016-09-23 06:14:08 +03:00
|
|
|
|
|
|
|
# read in lines
|
2016-10-31 03:15:24 +03:00
|
|
|
lines = [l.rstrip() for f in args.infiles
|
|
|
|
for l in open(f).readlines()]
|
|
|
|
debug("Original lines: %s" % len(lines))
|
2016-09-23 06:14:08 +03:00
|
|
|
|
|
|
|
# pull in include files
|
2017-09-15 07:50:15 +03:00
|
|
|
lines = resolve_includes(lines, args)
|
2016-10-31 03:15:24 +03:00
|
|
|
debug("Lines after includes: %s" % len(lines))
|
|
|
|
|
2017-09-15 07:50:15 +03:00
|
|
|
lines = resolve_mode(lines, args)
|
2016-10-31 03:15:24 +03:00
|
|
|
debug("Lines after resolving mode specific lines: %s" % len(lines))
|
2016-09-23 06:14:08 +03:00
|
|
|
|
|
|
|
# drop blank lines
|
|
|
|
if not args.keep_blank_lines:
|
|
|
|
lines = drop_blank_lines(lines)
|
2016-10-31 03:15:24 +03:00
|
|
|
debug("Lines after dropping blank lines: %s" % len(lines))
|
2016-09-23 06:14:08 +03:00
|
|
|
|
|
|
|
# keep/drop REMs
|
|
|
|
if not args.keep_rems:
|
|
|
|
lines = drop_rems(lines)
|
2016-10-31 03:15:24 +03:00
|
|
|
debug("Lines after dropping REMs: %s" % len(lines))
|
2016-09-23 06:14:08 +03:00
|
|
|
|
|
|
|
# keep/remove the indenting
|
|
|
|
if not args.keep_indent:
|
|
|
|
lines = remove_indent(lines)
|
|
|
|
|
2016-09-24 06:36:17 +03:00
|
|
|
# apply some miscellaneous simple fixups/regex transforms
|
|
|
|
if not args.skip_misc_fixups:
|
|
|
|
lines = misc_fixups(lines)
|
|
|
|
|
|
|
|
# number lines, drop/keep labels, combine lines
|
2017-09-15 07:50:15 +03:00
|
|
|
lines = finalize(lines, args)
|
2016-10-31 03:15:24 +03:00
|
|
|
debug("Lines after finalizing: %s" % len(lines))
|
2016-09-23 06:14:08 +03:00
|
|
|
|
|
|
|
print("\n".join(lines))
|