Last active
April 3, 2022 06:30
-
-
Save Pinacolada64/57edf11bdf847f89c803a0364f81a281 to your computer and use it in GitHub Desktop.
Claude BASIC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
import random | |
import re | |
import time | |
import sys | |
line = "" | |
matched = None | |
cursor = 0 | |
variables = {} | |
functions = {} | |
commands = {} | |
immediate_commands = {} | |
stack = [] | |
program = {} | |
program_counter = -1 | |
jumped = False | |
stopped = False | |
class ParsingError(Exception): | |
pass | |
################################################################################ | |
# Lexical analysis (breaking the text down into fundamental forms, or | |
# 'tokens') | |
# | |
def skip_whitespace(): | |
global cursor | |
while cursor < len(line) and line[cursor].isspace(): | |
cursor += 1 | |
def expect_kw(): | |
global cursor | |
skip_whitespace() | |
if cursor >= len(line): | |
raise ParsingError("Unexpected end of line (expecting a keyword.)") | |
mark = cursor | |
while cursor < len(line) and line[cursor].isalpha(): | |
cursor += 1 | |
if cursor > mark: | |
return line[mark:cursor] | |
else: | |
raise ParsingError("Expected a keyword") | |
def expect_kwlist(): | |
varnames = [expect_kw()] | |
while match(","): | |
varnames.append(expect_kw()) | |
return varnames | |
def match_kw(): | |
global matched | |
try: | |
matched = expect_kw() | |
return True | |
except ParsingError: | |
return False | |
def expect_string(): | |
global cursor | |
skip_whitespace() | |
if cursor >= len(line): | |
raise ParsingError("Unexpected end of line (expected string literal.)") | |
if line[cursor] != '"': | |
raise ParsingError("Expected string literal") | |
else: | |
cursor += 1 | |
mark = cursor | |
while line[cursor] != '"': | |
# Detect backslash escape sequences and skip over them (if they're | |
# valid) -- they're replaced below | |
if line[cursor] == '\\': | |
if (cursor + 1) < len(line) and line[cursor + 1] == '"': | |
cursor += 1 | |
elif (cursor + 1) < len(line) and line[cursor + 1] == '\\': | |
cursor += 1 | |
else: | |
raise ParsingError("Unknown backslash escape in string") | |
cursor += 1 | |
if not cursor < len(line): | |
raise ParsingError("Unterminated string literal") | |
# Skip the closing double quote, so we won't find it when we look | |
# for the next thing: | |
cursor += 1 | |
return line[mark:cursor - 1].replace("\\\\", "\\").replace("\\\"", '"') | |
def match_string(): | |
global matched | |
# This is bad -- it won't actually tell the user about errors in | |
# their strings, like erroneous backslash escapes... | |
try: | |
matched = expect_string() | |
return True | |
except ParsingError: | |
return False | |
def expect_num(): | |
global cursor | |
skip_whitespace() | |
if cursor >= len(line): | |
raise ParsingError("Unexpected end of line (expected number.)") | |
if not line[cursor].isdigit(): | |
raise ParsingError("Expected a number") | |
mark = cursor | |
while cursor < len(line) and line[cursor].isdigit(): | |
cursor += 1 | |
if cursor < len(line) and line[cursor] == ".": | |
cursor += 1 # Skip any `.' encountered | |
return float(line[mark:cursor]) | |
def match_num(): | |
global matched | |
try: | |
matched = expect_num() | |
return True | |
except ParsingError: | |
return False | |
def match(string): | |
global cursor | |
skip_whitespace() | |
if len(line) < (cursor + len(string)): | |
return False | |
if line[cursor:cursor + len(string)] == string: | |
cursor += len(string) | |
return True | |
else: | |
return False | |
# TODO: make sure there aren't any other places we should be using this? | |
def match_nocase(string): | |
global cursor | |
skip_whitespace() | |
mark = cursor | |
try: | |
kw = expect_kw().lower() | |
if kw == string: | |
return True | |
else: | |
cursor = mark | |
return False | |
except ParsingError: | |
return False | |
def expect_nocase(string): | |
if not match_nocase(string): | |
raise ParsingError("Expected `"+string.upper()+"'") | |
def expect_varname(): | |
global cursor | |
skip_whitespace() | |
if cursor >= len(line): | |
raise ParsingError("Expected a variable, not end of line") | |
if not line[cursor].isalpha(): | |
raise ParsingError("Expected a variable") | |
mark = cursor | |
while cursor < len(line) and (line[cursor].isalpha() or line[cursor].isdigit() or line[cursor] == "_"): | |
cursor += 1 | |
return line[mark:cursor].lower() | |
def expect_varlist(): | |
varnames = [expect_varname()] | |
while match(","): | |
varnames.append(expect_varname()) | |
return varnames | |
def match_identifier(): | |
global matched | |
try: | |
matched = expect_varname() | |
return True | |
except ParsingError: | |
return False | |
def expect_defvar(): | |
global cursor | |
vn = expect_varname() | |
if vn in variables: | |
return vn | |
else: | |
raise ParsingError("Undefined variable '" + vn + ".") | |
def match_defvar(): | |
global matched, cursor | |
# Might turn out not to be defined, but still consume part of the line | |
mark = cursor | |
try: | |
matched = expect_defvar() | |
return True | |
except ParsingError: | |
cursor = mark | |
return False | |
def expect(str): | |
"""Generically expect `str' to be the next non-whitespace portion of | |
input; raise error if not. As with all expect(), advances the cursor, | |
"consuming" part of the line.""" | |
if not match(str): | |
raise ParsingError("Expected `" + str + "'.") | |
################################################################################ | |
# Parsing/interpreting arithmetic. | |
# | |
operators = [] | |
# In order of _decreasing_ precedence: | |
# (I'm not sure if / should be higher than * ... I thought maybe it should, | |
# but ...) | |
# (It probably shouldn't. Meh.) | |
operators.append({ | |
"+": (lambda x,y: x + y), | |
"-": (lambda x,y: x - y)}) | |
operators.append({ | |
"/": (lambda x,y: x / y)}) | |
operators.append({ | |
"*": (lambda x,y: x * y)}) | |
def expect_value(): | |
"""Expect a value: either an atomic number, a variable (will in that case return | |
the contents of the variable), a function call, or a subexpression wrapped in | |
parentheses ('( ... )'), which will be evaluated like expect_arithmetic().""" | |
global cursor, matched | |
# Experimental, not sure if it will break things by eating -s where it shouldn't, | |
# but it doesn't seem to. | |
signum = 1 | |
if match("-"): | |
signum = -1 | |
if match_num(): | |
return float(matched) * signum | |
elif match_identifier(): | |
# Extra complicated, because for now we're hooking functions into the parser here too. | |
if matched in functions: | |
return call_fn(matched, expect_fn_args()) * signum | |
elif matched in variables: | |
return variables[matched] * signum | |
else: | |
raise ParsingError("Expected a variable or a function") | |
elif match("("): | |
res = expect_arithmetic() | |
if not match(")"): | |
raise ParsingError("Expected a closing parenthesis") | |
return res * signum | |
else: | |
raise ParsingError("Expected a value") | |
def match_operator(rank): | |
global cursor, matched | |
skip_whitespace() | |
if cursor < len(line) and line[cursor] in operators[rank]: # !!!!! FIXME -- won't work on two-character operators | |
matched = line[cursor] | |
cursor += 1 | |
return True | |
else: | |
return False | |
def expect_arithmetic(rank = 0): | |
# This function is the heart of parsing expressions... Do note : you might | |
# be able to make it more abstract by allowing caller to specify the | |
# operator table to use, and what function to use to acquire generic "atoms" | |
# (expect_value() here.) | |
global cursor, matched | |
accumulator = None | |
if rank < len(operators) - 1: | |
accumulator = expect_arithmetic(rank + 1) | |
else: | |
# We're the lowest level; we need to match fundamental/atomic values instead. | |
accumulator = expect_value() | |
while match_operator(rank): | |
op = matched | |
if rank < len(operators) - 1: | |
# There are 'levels' of operator below us. | |
accumulator = operators[rank][op](accumulator, expect_arithmetic()) | |
else: | |
# We're the lowest level of operators; only fundamental values are | |
# below us. | |
accumulator = operators[rank][op](accumulator, expect_value()) | |
return accumulator | |
################################################################################ | |
# Parsing/interpreting logic (expressions.) | |
# | |
def match_relation(): | |
global matched | |
relations = ["==", "<=", ">=", "!=", ">", "<"] | |
for r in relations: | |
if match(r): | |
matched = r | |
return True | |
return False | |
def expect_comparison(): | |
left = expect_arithmetic() | |
if not match_relation(): # i.e., no < or == or (etc.) found | |
return left | |
else: | |
op = matched | |
right = expect_arithmetic() | |
if op == "==": | |
return -(left == right) | |
elif op == "<=": | |
return -(left <= right) | |
elif op == ">=": | |
return -(left >= right) | |
elif op == "!=": | |
return -(left != right) | |
elif op == ">": | |
return -(left > right) | |
elif op == "<": | |
return -(left < right) | |
else: | |
raise ParsingError("Somehow found an invalid relation (this should never happen)") | |
# Basically does the same thing as the arithmetic parsing above, but less cleverly. | |
# I could maybe write an even more general function that would handle all cases | |
# like this -- somehow... I'm not sure if it would be that much better though. | |
def expect_negation_or_comparison(): | |
if match_nocase("not"): | |
return -(expect_comparison() == 0) # 'is equal to false' -- produces True (-1) when it's 'false' (0) | |
else: | |
return expect_comparison() | |
def expect_also_and(): | |
left = expect_negation_or_comparison() | |
while match_nocase("and"): | |
right = expect_negation_or_comparison() | |
left = -(left != 0 and right != 0) | |
return left | |
def expect_also_or(): | |
left = expect_also_and() | |
while match_nocase("or"): | |
right = expect_also_and() | |
left = -(left != 0 or right != 0) | |
return left | |
def expect_expression(): | |
# This is super recursive. | |
return expect_also_or() | |
################################################################################ | |
# Functions. | |
# | |
# They're hooked into the parser further above, in expect_value(). | |
# However, these routines do most of the work, and we also define our | |
# builtin functions here. | |
def expect_fn_args(): | |
if match("("): | |
if match(")"): | |
return [] | |
args = [expect_expression()] | |
while match(","): | |
args.append(expect_expression()) | |
expect(")") | |
return args | |
else: | |
return [] | |
def call_fn(fn, args): | |
if fn not in functions: | |
raise ParsingError("No such function `"+fn+"'.") | |
if len(args) != functions[fn]["args"]: | |
raise ParsingError("Arity mismatch (want "+str(functions[fn]["args"])+", got "+str(len(args))+")") | |
# Bother, we need `apply'. -- okay, write foo(*args, **kwargs) to do `apply'-like things | |
# (kwargs would be a dictionary, presumably; args is a list.) | |
if "builtin" in functions[fn]: | |
return functions[fn]["builtin"](*args) | |
elif "body" in functions[fn]: | |
return call_ufn(fn, args) | |
else: | |
raise ParsingError("Function exists but has no definition (this should never happen)") | |
# return 0 | |
# In BASIC, "function" is more like algebra's notion of a mathematical function | |
# -- they're essentially an expression packaged up into a reusable atom, not a | |
# "function" in the C or Python sense (there, you'd expect them to be more like | |
# subroutines.) BASIC uses GOSUB for subroutines. | |
functions["cos"] = {"args":1, "builtin":(lambda x:math.cos(x))} | |
functions["sin"] = {"args":1, "builtin":(lambda x:math.sin(x))} | |
functions["tan"] = {"args":1, "builtin":(lambda x:math.tan(x))} | |
functions["atan"] = {"args":1, "builtin":(lambda x:math.atan(x))} | |
functions["atan2"] = {"args":1, "builtin":(lambda x:math.atan2(x))} | |
functions["acos"] = {"args":1, "builtin":(lambda x:math.acos(x))} | |
functions["sqrt"] = {"args":1, "builtin":(lambda x:math.sqrt(x))} | |
functions["int"] = {"args":1, "builtin":(lambda x:math.trunc(x))} | |
functions["abs"] = {"args":1, "builtin":(lambda x:abs(x))} | |
functions["rnd"] = {"args":0, "builtin":(lambda:random.random())} | |
functions["timer"] = {"args":0, "builtin":(lambda:time.clock())} | |
# Let the user define their own functions. | |
def do_defn(): | |
global cursor, functions | |
expect_nocase("fn") | |
name = expect_varname() | |
expect("(") | |
if match(")"): | |
args = [] | |
else: | |
args = expect_varlist() | |
expect(")") | |
expect("=") | |
if name in functions and "builtin" in functions[name]: | |
raise ParsingError("Attempted to redefine a builtin function") | |
functions[name] = { | |
"args": len(args), | |
"body": line[cursor:], # Better not think you can do multiple statements | |
"arglist": args | |
} | |
cursor = len(line) # Consume the rest of the line. | |
commands["def"] = do_defn | |
# Figure out how to call the user's functions. | |
def call_ufn(fn, args): | |
# Mostly just a straight duplicate. | |
global variables, line, cursor | |
# The given strategy is to save a copy of the globals, then clobber them | |
# such that we can just use our existing expression parser to parse the | |
# stored expression (i.e., putting the `args' into ordinary BASIC | |
# variables.) I'm sure there's a better, more computer science-y way to do | |
# this, but I don't know what it is. | |
tvars = variables | |
tline = line | |
tcursor = cursor | |
this = functions[fn] | |
for argname in this["arglist"]: | |
variables[argname] = args[0] | |
del args[0] | |
line = this["body"] | |
cursor = 0 | |
# Basically copying the given method. Try ... finally because parsing can throw | |
# ParsingErrors. | |
try: | |
result = expect_expression() | |
finally: | |
line = tline | |
variables = tvars | |
cursor = tcursor | |
return result | |
################################################################################ | |
# Parsing/interpreting single statements. | |
# | |
def parse_statement(): | |
global cursor | |
s = expect_kw().lower() | |
if s == "rem": | |
cursor = len(line) | |
return | |
elif s in commands: | |
commands[s]() | |
else: | |
raise ParsingError("Unrecognised statement (" + s + ").") | |
def do_print(): | |
global cursor, matched, variables | |
skip_whitespace() | |
if cursor <= len(line): | |
msg = "" | |
while cursor < len(line): | |
if match_string(): | |
msg += matched | |
else: | |
msg += str(expect_expression()) | |
if not match(","): | |
break | |
print msg | |
else: | |
print() # a new line | |
commands["print"] = do_print | |
commands["p"] = do_print # shorthand for testing | |
def do_input(): | |
global variables | |
prompt = "" | |
if match_string(): | |
prompt = matched | |
if not match(","): | |
raise ParsingError("Expected ','") | |
varnames = expect_kwlist() | |
data = raw_input(prompt).split(",") | |
for n in varnames: | |
if len(data) >= 1: | |
variables[n] = float(data[0]) | |
data = data[1:] # Trim off first element each time | |
else: | |
variables[n] = 0 # Hmm | |
commands["input"] = do_input | |
def do_let(): | |
global variables | |
skip_whitespace() | |
if cursor >= len(line): | |
raise ParsingError("Unexpected end of line") | |
name = expect_varname() | |
if not match("="): | |
raise ParsingError("Expected =") | |
val = expect_expression() | |
variables[name] = val | |
commands["let"] = do_let | |
def do_if(): | |
global cursor | |
skip_whitespace() | |
if cursor >= len(line): | |
raise ParsingError("Unexpected end of line") | |
if expect_expression() != 0: # "not false" | |
if not match_nocase("then"): | |
raise ParsingError("Expected THEN") | |
parse_statement() | |
else: | |
cursor = len(line) | |
commands["if"] = do_if | |
def do_stop(): | |
global stopped | |
stopped = True | |
print "Program execution paused. DO NOT add or remove lines and attempt to `continue' \ | |
from this point. If you do, use `run' instead." | |
commands["stop"] = do_stop | |
# A statement to support the rnd() function: help make the RNG repeatable (or | |
# set it back to pseudo-unpredictable.) | |
def do_randomize(): | |
random.seed(expect_value()) | |
commands["randomize"] = do_randomize | |
################################################################################ | |
# Parsing lines (which might be lines to be stored into the program store) and | |
# blocks of statements. | |
# | |
def parse_line(): | |
global cursor | |
skip_whitespace() | |
mark = cursor | |
while cursor < len(line) and line[cursor].isdigit(): | |
cursor += 1 | |
if cursor > mark: | |
# We found a leading (line) number; store the line | |
if cursor >= len(line): | |
raise ParsingError("Expected code") | |
skip_whitespace() | |
program[int(line[mark:cursor])] = line[cursor:len(line)] | |
else: | |
# It's immediate mode. Just parse it. | |
parse_block() | |
def parse_block(): | |
parse_statement() | |
while match(":") and not jumped and not stopped: | |
parse_statement() | |
if not (cursor >= len(line)) and not jumped and not stopped: | |
raise ParsingError("Expected end of line") | |
################################################################################ | |
# GOTO, GOSUB, and friends | |
# | |
def do_goto(): | |
global program_counter, cursor, jumped | |
nx = expect_num() | |
if nx in program: | |
program_counter = sorted(program.keys()).index(nx) | |
cursor = 0 | |
jumped = True | |
else: | |
raise ParsingError("No such line (" + str(nx) + ").") | |
commands["goto"] = do_goto | |
def do_gosub(): | |
global program_counter, cursor, stack, jumped | |
nx = expect_num() | |
# This could be expect_expression(), which I guess would let you | |
# have dynamic GOSUBs assigned by the code earlier... | |
# `renumber' won't know about anything more complicated than a simple | |
# line number, however. | |
if nx in program: | |
stack.append({"ln":program_counter, "cursor":cursor}) | |
program_counter = sorted(program.keys()).index(nx) | |
cursor = 0 | |
# run() will go directly to this program line and cursor value if we set | |
# the `jumped' flag. | |
jumped = True | |
else: | |
raise ParsingError("No such line (" + str(nx) + ").") | |
commands["gosub"] = do_gosub | |
def do_return(): | |
global program_counter, cursor, stack, jumped | |
try: | |
program_counter = stack[len(stack)-1]["ln"] | |
cursor = stack[len(stack)-1]["cursor"] | |
stack.pop() | |
print "Return to " + str(sorted(program.keys())[program_counter]) + "," + str(cursor) | |
jumped = True | |
except IndexError: | |
raise ParsingError("Stack overflow, or invalid stack frame.") | |
commands["return"] = do_return | |
def do_end(): | |
global program_counter, stack | |
# Works by just 'jumping' to the end of the program. Apparently still works | |
# after the 'jumped' change -- probably because continue() just goes on to the | |
# next line, but still finds it's not there. | |
program_counter = len(program.keys()) | |
# Let's reset the stack. This will keep anything that might be left from, | |
# well, stacking up between RUNs or, worse, causing odd behaviour from | |
# program run to program run in some kind of odd case I can't be bothered to | |
# think of a specific example for. | |
stack = [] | |
# We could also reset the variables here. I'm not sure if we should. | |
# Maybe? | |
commands["end"] = do_end | |
################################################################################ | |
# Looping control structures | |
# | |
def do_loop_do(): | |
global stack | |
if program_counter+1 < len(program): | |
stack.append({"ln":program_counter+1, "cursor":0}) | |
else: | |
raise ParsingError("Unexpected end of program") | |
commands["do"] = do_loop_do | |
def do_loop_loop(): | |
global program_counter, stack, cursor, jumped | |
if len(stack) <= 0: | |
raise ParsingError("Stack underflow") | |
c = expect_kw().lower() | |
if c == "while": | |
if expect_expression() == 0: | |
# It's no longer true, so we don't jump back to loop start. Also | |
# take away our stack pointer. | |
del stack[len(stack)-1] | |
return | |
elif c == "until": | |
if expect_expression() != 0: | |
# It's true -- 'until' is satisfied, don't jump back. | |
del stack[len(stack)-1] | |
return | |
else: | |
raise ParsingError("Expected WHILE or UNTIL") | |
# Conditions passed.... let's jump back. | |
c = stack[len(stack)-1] | |
program_counter = c["ln"] | |
cursor = c["cursor"] | |
jumped = True | |
commands["loop"] = do_loop_loop | |
def do_loop_for(): | |
global stack, variables | |
vn = expect_varname() | |
expect("=") # Tempted to write expect("from") | |
fff = expect_value() # To allow for negative numbers. | |
expect("to") | |
ttt = expect_value() | |
sss = 1 | |
if ttt < fff: | |
sss = -1 # Target is LOWER than starting value, so we need to go down. | |
if match_nocase("step"): | |
sss = expect_value() | |
if sss == 0: | |
raise ParsingError("Infinite loop") # Just use GOTO (why would you want to?) | |
if program_counter >= len(program): | |
raise ParsingError("Expected more program") | |
stack.append({"ln": program_counter+1, "cursor":0}) | |
variables[vn] = fff | |
stack.append({"loop-to": ttt, "loop-step": sss}) | |
commands["for"] = do_loop_for | |
def do_loop_next(): | |
global stack, variables, program_counter, cursor, jumped | |
vn = expect_defvar() | |
try: | |
loopstuff = stack[len(stack)-1] | |
target = stack[len(stack)-2] | |
except IndexError: | |
raise ParsingError("Stack underflow") | |
try: | |
variables[vn] += loopstuff["loop-step"] | |
if (loopstuff["loop-step"] > 0 and variables[vn] <= loopstuff["loop-to"]) or \ | |
(loopstuff["loop-step"] < 0 and variables[vn] >= loopstuff["loop-to"]): | |
# We still want to loop, let's loop. | |
program_counter = target["ln"] | |
cursor = target["cursor"] | |
jumped = True | |
else: | |
# We're done looping. Let's clean up. | |
del stack[len(stack)-1] | |
del stack[len(stack)-2] | |
except KeyError: | |
raise ParsingError("Type mismatch with top of stack (mixed loop constructs?)") | |
commands["next"] = do_loop_next | |
################################################################################ | |
# do_run() and do_continue() | |
# | |
# You could probably describe these as the "heart of the interpreter" -- it's | |
# the main loop, without which there would be no meaningful computation because | |
# we wouldn't know how to run more than one line at a time, without which we | |
# won't be much more than a pocket calculator. | |
def do_run(): | |
global line, cursor, program_counter, jumped, stack | |
program_counter = 0 | |
stack = [] # We may as well clear it every now and then. Start in a predictable state. | |
do_continue() | |
def do_continue(): | |
global line, cursor, program_counter, jumped, stopped | |
lns = sorted(program.keys()) | |
cursor = 0 | |
stopped = False | |
if program_counter >= len(lns) or len(lns) == 0: | |
print "No more program to run." | |
return | |
while program_counter < len(lns) and not stopped: | |
# Retrieve line. | |
ln = lns[program_counter] | |
line = program[ln] | |
if jumped: | |
match(":") | |
# Kind of lousy, probably. In case we jumped back JUST in front of a new statement, | |
# skip any : to keep parse_line() from getting confused. | |
jumped = Falses | |
try: | |
parse_line() | |
# I think you can write '10 20 print "This overrides line 20! But | |
# it won't run till next RUN."' :) | |
# ... it works in practice, sort of, but causes an error for some | |
# reason as well... | |
except ParsingError as e: | |
# Report the error in a nice way. | |
print line | |
print " "*cursor + "^ error on line " + str(ln) + ": " + str(e) | |
cursor = len(line) | |
break | |
# The command above may have decided it wanted to jump to a different | |
# position -- line and cursor -- in the program. If it did, we should | |
# leave the new position in place. But if it didn't, we need to go | |
# onto the next line. | |
if not jumped: | |
program_counter += 1 | |
cursor = 0 | |
if stopped: | |
print "... on line " + str(lns[program_counter - 1]) | |
# -1 because we've already technically moved onto the next line | |
print "cursor=" + str(cursor) | |
immediate_commands["run"] = do_run | |
immediate_commands["continue"] = do_continue | |
################################################################################ | |
# Other immediate-mode commands | |
# | |
# These are stored separately from the language's commands. Because | |
# parse_block() doesn't handle them, they don't have to worry about leaving the | |
# interpreter in an odd state and messing up parse_block() when it tries to see | |
# if there's another statement to parse. They can ONLY be called interactively, | |
# from the interpreter itself line. | |
def do_save(): | |
fn = expect_string() | |
with open(fn, "w") as fx: | |
for ln in sorted(program.keys()): | |
fx.write(" " + str(ln) + "\t" + program[ln]) | |
fx.write("\n") | |
immediate_commands["save"] = do_save | |
immediate_commands["s"] = do_save | |
def do_load(filename = None): | |
global line, cursor | |
if filename: | |
fn = filename | |
else: | |
fn = expect_string() | |
with open(fn, "r") as fx: | |
for ln in fx: | |
line = re.sub("\n+$", "", ln) # Otherwise we end up with \ns read in on the ends of the lines. | |
cursor = 0 | |
try: | |
# Allow there to be blank lines (i.e. ones consisting of nothing but space) in the file. | |
if re.match(".*[^\s]", line): | |
parse_line() | |
except ParsingError as e: | |
print "Error while parsing file:" | |
print line | |
print " "*cursor + "^ " + str(e) | |
break | |
immediate_commands["load"] = do_load | |
immediate_commands["l"] = do_load | |
def do_help(): | |
# Elissa's addition | |
print "Immediate mode commands:" | |
# make a copy of the unsorted list so as not to disturb the original list. | |
# Sorts sort in place. | |
mydict = immediate_commands | |
for key in sorted(mydict.iterkeys()): | |
print ("%s\t" % key), | |
print("\n") | |
print "Program mode commands:" | |
mydict = commands | |
for key in sorted(mydict.iterkeys()): | |
print ("%s\t" % key), | |
print("\n") | |
# raw_input("Press Enter to continue: ") | |
immediate_commands["help"] = do_help | |
def do_new(): | |
global program | |
program.clear() | |
immediate_commands["new"] = do_new | |
def do_clear(): | |
global variables | |
variables.clear() | |
immediate_commands["clear"] = do_clear | |
################################################################################ | |
# Program manipulation (and viewing) -- all the commands that operate on sets of | |
# lines. | |
# | |
class LineRange: | |
"""Represents a range of line numbers, probably selected by the user.""" | |
def __init__(self, start, end): | |
if start < end or (start >= 0 and end == -1) or end is None: | |
self.start = start | |
self.end = end | |
else: | |
self.start = 0 | |
self.end = 0 | |
def lns(self): | |
"""Return a list of every line number in program[] that is within the | |
range.""" | |
lns = sorted(program.keys()) | |
if self.end is None: | |
# We only point to one, specific, line. | |
if not (self.start in lns): | |
raise ParsingError("No such line `" +str(int(self.start))+ "'.") | |
return [self.start] | |
else: | |
ret = [] | |
# We point to a range of lines. | |
for ln in lns: | |
if ln >= self.start and (ln <= self.end or self.end == -1): | |
ret.append(ln) | |
return ret | |
def expect_linerange(): | |
"""Read in a range of lines that the user wants to operate on.""" | |
firstl = expect_num() | |
if match("-"): | |
if match_num(): | |
secondl = matched | |
else: | |
secondl = -1 # i.e., "infinity" | |
else: | |
secondl = None | |
return LineRange(firstl, secondl) | |
def do_list(): | |
"""List the program, or part of the program.""" | |
try: | |
range = expect_linerange() | |
except ParsingError: | |
range = LineRange(0, -1) # start at 0, go to infinity | |
for line_number in range.lns(): | |
print str(int(line_number)) + "\t" + program[line_number] | |
immediate_commands["list"] = do_list | |
def do_renum(): | |
"""Renumber a range of lines -- makes it easier to manipulate programs when inserting | |
many lines, for instance. CAUTION!! Uncertain about this, it has to use regexps and | |
fix every command that refers to a line number!""" | |
global program | |
try: | |
r = expect_linerange() | |
except ParsingError: | |
r = LineRange(0, -1) # We'll remap the whole program by default. | |
target = 10 | |
if match_nocase("to"): | |
target = expect_num() | |
step = 10 | |
if match_nocase("step"): | |
step = expect_num() | |
oldp = program.copy() # Just in case. | |
counter = target | |
new = {} | |
# Move lines out of the program, renumbering as we go. This approach turns | |
# out to be much simpler than trying to renumber them in-place. Note that | |
# we also need to search through and renumber GOTO or GOSUB calls. | |
for ln in r.lns(): | |
renum_change_calls(ln, counter) | |
new[counter] = program[ln] | |
del program[ln] | |
counter += step | |
# Merge it back into the program. | |
for kx in sorted(new.keys()): | |
if kx in program: | |
print "Whoops, line `" +str(int(kx))+ "' already exists!" | |
print "Rolling back changes and aborting." | |
program = oldp | |
return | |
program[kx] = new[kx] | |
def renum_change_calls(ln, new): | |
global program | |
lns = sorted(program.keys()) | |
for line in lns: | |
program[line] = re.sub("(goto|gosub)\\s*"+str(int(ln)), "goto "+str(int(new)), program[line], flags=re.I) | |
immediate_commands["renumber"] = do_renum | |
immediate_commands["renum"] = do_renum | |
immediate_commands["rn"] = do_renum | |
def do_del(): | |
global program | |
nx = expect_num() | |
if match("-"): | |
nx2 = expect_num() | |
for ln in sorted(program.keys()): | |
# if ln > nx and ln < nx2: | |
if ln in range(nx, nx2+1): | |
del program[ln] | |
else: | |
if nx in program: | |
del program[nx] | |
immediate_commands["delete"] = do_del | |
immediate_commands["del"] = do_del | |
################################################################################ | |
# The interactive interpreter itself. | |
# | |
print("Crude BASIC interpreter") | |
do_help() | |
if len(sys.argv) > 1: | |
do_load(sys.argv[len(sys.argv)-1]) | |
print "... in " + sys.argv[len(sys.argv)-1] | |
while True: | |
try: | |
line = raw_input("* ") | |
if line.lower() == "bye": | |
break | |
except EOFError: | |
break | |
cursor = 0 | |
# Try matching immediate-mode commands. | |
mark = cursor | |
if match_kw() and matched.lower() in immediate_commands: | |
try: | |
immediate_commands[matched]() | |
except ParsingError as e: | |
print " "*(cursor+1) + "^" | |
print str(e) | |
except KeyboardInterrupt: | |
# Supposing the user gets into an infinite loop after typing `run', for example. | |
print "User interrupt." | |
else: | |
# If none was found, try running the line normally. | |
cursor = mark | |
try: | |
parse_line() | |
except ParsingError as e: | |
print (" " * (cursor+1)) + "^" # (Prompt is two chars wide, but cursor is 0-indexed.) | |
print str(e) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment