forked from kanaka/mal
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbasicpp.py
More file actions
executable file
·347 lines (298 loc) · 11.9 KB
/
Copy pathbasicpp.py
File metadata and controls
executable file
·347 lines (298 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
#!/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.')
parser.add_argument('infiles', type=str, nargs='+',
help='the Basic files to preprocess')
parser.add_argument('--mode', choices=["cbm", "qbasic"], default="cbm")
parser.add_argument('--sub-mode', choices=["noui", "ui"], default="noui")
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')
parser.add_argument('--skip-misc-fixups', action='store_true', default=False,
help='Skip miscellaneous fixup/shrink fixups')
parser.add_argument('--skip-combine-lines', action='store_true', default=False,
help='Do not combine lines using the ":" separator')
args = parser.parse_args()
args.full_mode = "%s-%s" % (args.mode, args.sub_mode)
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
return args
# pull in include files
def resolve_includes(orig_lines, args):
included = {}
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:
ilines = [l.rstrip() for l in open(f).readlines()]
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)
else:
debug("Ignoring already included file: %s" % f)
else:
position += 1
return lines
def resolve_mode(orig_lines, args):
lines = []
for line in orig_lines:
m = re.match(r"^ *#([^ \n]*) *([^\n]*)$", line)
if m:
if m.group(1) == args.mode:
lines.append(m.group(2))
elif m.group(1) == args.full_mode:
lines.append(m.group(2))
continue
lines.append(line)
return lines
def drop_blank_lines(orig_lines):
lines = []
for line in orig_lines:
if re.match(r"^\W*$", line): continue
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:
m = re.match(r"^ *([^ \n].*)$", line)
lines.append(m.group(1))
return lines
def misc_fixups(orig_lines):
text = "\n".join(orig_lines)
# Remove GOTO after THEN
text = re.sub(r"\bTHEN GOTO\b", "THEN", text)
# Remove spaces after keywords
text = re.sub(r"\bIF ", "IF", text)
text = re.sub(r"\bPRINT *", "PRINT", text)
text = re.sub(r"\bDIM ", "DIM", text)
text = re.sub(r"\OPEN ", "OPEN", text)
text = re.sub(r"\bGET ", "GET", text)
text = re.sub(r"\bPOKE ", "POKE", text)
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)
# Remove spaces around GOTO/GOSUB/THEN
text = re.sub(r" *GOTO *", "GOTO", text)
text = re.sub(r" *GOSUB *", "GOSUB", text)
text = re.sub(r" *THEN *", r"THEN", text)
# Remove spaces around AND/OR except after ST
text = re.sub(r"(?<!ST) *AND *", r"AND", text)
text = re.sub(r"([^A-Z]) *OR *", r"\g<1>OR", text)
return text.split("\n")
def finalize(lines, args):
labels_lines = {}
lines_labels = {}
call_index = {}
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)
m = re.match(r"^ *([^ :\n]*): *$", line)
if m:
label = m.groups(1)[0]
labels_lines[label] = lnum
lines_labels[lnum] = label
continue
if re.match(r".*CALL *([^ :\n]*) *:", line):
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)
m = re.match(r"(.*)CALL *([^ :\n]*) *$", line)
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
if args.mode == "cbm":
lines.append("%s %sQ=%s:GOSUBPUSH_Q:GOTO%s" % (
lnum, prefix, call_index[sub], sub))
else:
lines.append("%s %sX=X+1:X%%(X)=%s:GOTO %s" % (
lnum, prefix, call_index[sub], sub))
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)
m = re.match(r"^([0-9][0-9]*) *SUB *([^ \n]*) *$", line)
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
# 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)]
if args.mode == "cbm":
line = "%s GOSUBPOP_Q:ONQGOTO%s" % (lnum, ",".join(ret_labels))
else:
line = "%s X=X-1:ON X%%(X+1) GOTO %s" % (lnum, ",".join(ret_labels))
cur_sub = None
lines.append(line)
def update_labels_lines(text, a, b):
stext = ""
while stext != text:
stext = text
text = re.sub(r"(THEN *)%s\b" % a, r"\g<1>%s" % b, stext)
#text = re.sub(r"(THEN)%s\b" % a, r"THEN%s" % b, stext)
if args.mode == "cbm":
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)
#text = re.sub(r"(GOTO)%s\b" % a, r"\1%s" % b, text)
return text
# 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")
# combine lines
if not args.skip_combine_lines:
renumber = {}
src_lines = lines
lines = []
pos = 0
acc_line = ""
def renum(line):
lnum = len(lines)+1
renumber[old_num] = lnum
return "%s %s" % (lnum, line)
while pos < len(src_lines):
line = src_lines[pos]
m = re.match(r"^([0-9]*) (.*)$", line)
old_num = int(m.group(1))
line = m.group(2)
if acc_line == "":
# Starting a new line
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
lines.append(acc_line)
acc_line = renum(line)
elif re.match(r".*(?:GOTO|THEN|RETURN).*", acc_line):
# GOTO/THEN/RETURN are last thing on the line
lines.append(acc_line)
acc_line = renum(line)
# TODO: not sure why this is 88 rather than 80
elif len(acc_line) + 1 + len(line) < 88:
# Continue building up the line
acc_line = acc_line + ":" + line
# GOTO/IF/RETURN must be the last things on a line so
# start a new line
if re.match(r".*(?:GOTO|THEN|RETURN).*", line):
lines.append(acc_line)
acc_line = ""
else:
# Too long so start a new line
lines.append(acc_line)
acc_line = renum(line)
pos += 1
if acc_line != "":
lines.append(acc_line)
# 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")
# 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
return lines
if __name__ == '__main__':
args = parse_args()
debug("Preprocessing basic files: "+", ".join(args.infiles))
# read in lines
lines = [l.rstrip() for f in args.infiles
for l in open(f).readlines()]
debug("Original lines: %s" % len(lines))
# pull in include files
lines = resolve_includes(lines, args)
debug("Lines after includes: %s" % len(lines))
lines = resolve_mode(lines, args)
debug("Lines after resolving mode specific lines: %s" % len(lines))
# drop blank lines
if not args.keep_blank_lines:
lines = drop_blank_lines(lines)
debug("Lines after dropping blank lines: %s" % len(lines))
# keep/drop REMs
if not args.keep_rems:
lines = drop_rems(lines)
debug("Lines after dropping REMs: %s" % len(lines))
# keep/remove the indenting
if not args.keep_indent:
lines = remove_indent(lines)
# apply some miscellaneous simple fixups/regex transforms
if not args.skip_misc_fixups:
lines = misc_fixups(lines)
# number lines, drop/keep labels, combine lines
lines = finalize(lines, args)
debug("Lines after finalizing: %s" % len(lines))
print("\n".join(lines))