# An implementation of Dartmouth BASIC (1964) # from ply import * import basiclex tokens = basiclex.tokens precedence = ( ('left', 'PLUS', 'MINUS'), ('left', 'TIMES', 'DIVIDE'), ('left', 'POWER'), ('right', 'UMINUS') ) # A BASIC program is a series of statements. We represent the program as a # dictionary of tuples indexed by line number. def p_program(p): '''program : program statement | statement''' if len(p) == 2 and p[1]: p[0] = {} line, stat = p[1] p[0][line] = stat elif len(p) == 3: p[0] = p[1] if not p[0]: p[0] = {} if p[2]: line, stat = p[2] p[0][line] = stat # This catch-all rule is used for any catastrophic errors. In this case, # we simply return nothing def p_program_error(p): '''program : error''' p[0] = None p.parser.error = 1 # Format of all BASIC statements. def p_statement(p): '''statement : INTEGER command NEWLINE''' if isinstance(p[2], str): print("%s %s %s" % (p[2], "AT LINE", p[1])) p[0] = None p.parser.error = 1 else: lineno = int(p[1]) p[0] = (lineno, p[2]) # Interactive statements. def p_statement_interactive(p): '''statement : RUN NEWLINE | LIST NEWLINE | NEW NEWLINE''' p[0] = (0, (p[1], 0)) # Blank line number def p_statement_blank(p): '''statement : INTEGER NEWLINE''' p[0] = (0, ('BLANK', int(p[1]))) # Error handling for malformed statements def p_statement_bad(p): '''statement : INTEGER error NEWLINE''' print("MALFORMED STATEMENT AT LINE %s" % p[1]) p[0] = None p.parser.error = 1 # Blank line def p_statement_newline(p): '''statement : NEWLINE''' p[0] = None # LET statement def p_command_let(p): '''command : LET variable EQUALS expr''' p[0] = ('LET', p[2], p[4]) def p_command_let_bad(p): '''command : LET variable EQUALS error''' p[0] = "BAD EXPRESSION IN LET" # READ statement def p_command_read(p): '''command : READ varlist''' p[0] = ('READ', p[2]) def p_command_read_bad(p): '''command : READ error''' p[0] = "MALFORMED VARIABLE LIST IN READ" # DATA statement def p_command_data(p): '''command : DATA numlist''' p[0] = ('DATA', p[2]) def p_command_data_bad(p): '''command : DATA error''' p[0] = "MALFORMED NUMBER LIST IN DATA" # PRINT statement def p_command_print(p): '''command : PRINT plist optend''' p[0] = ('PRINT', p[2], p[3]) def p_command_print_bad(p): '''command : PRINT error''' p[0] = "MALFORMED PRINT STATEMENT" # Optional ending on PRINT. Either a comma (,) or semicolon (;) def p_optend(p): '''optend : COMMA | SEMI |''' if len(p) == 2: p[0] = p[1] else: p[0] = None # PRINT statement with no arguments def p_command_print_empty(p): '''command : PRINT''' p[0] = ('PRINT', [], None) # GOTO statement def p_command_goto(p): '''command : GOTO INTEGER''' p[0] = ('GOTO', int(p[2])) def p_command_goto_bad(p): '''command : GOTO error''' p[0] = "INVALID LINE NUMBER IN GOTO" # IF-THEN statement def p_command_if(p): '''command : IF relexpr THEN INTEGER''' p[0] = ('IF', p[2], int(p[4])) def p_command_if_bad(p): '''command : IF error THEN INTEGER''' p[0] = "BAD RELATIONAL EXPRESSION" def p_command_if_bad2(p): '''command : IF relexpr THEN error''' p[0] = "INVALID LINE NUMBER IN THEN" # FOR statement def p_command_for(p): '''command : FOR ID EQUALS expr TO expr optstep''' p[0] = ('FOR', p[2], p[4], p[6], p[7]) def p_command_for_bad_initial(p): '''command : FOR ID EQUALS error TO expr optstep''' p[0] = "BAD INITIAL VALUE IN FOR STATEMENT" def p_command_for_bad_final(p): '''command : FOR ID EQUALS expr TO error optstep''' p[0] = "BAD FINAL VALUE IN FOR STATEMENT" def p_command_for_bad_step(p): '''command : FOR ID EQUALS expr TO expr STEP error''' p[0] = "MALFORMED STEP IN FOR STATEMENT" # Optional STEP qualifier on FOR statement def p_optstep(p): '''optstep : STEP expr | empty''' if len(p) == 3: p[0] = p[2] else: p[0] = None # NEXT statement def p_command_next(p): '''command : NEXT ID''' p[0] = ('NEXT', p[2]) def p_command_next_bad(p): '''command : NEXT error''' p[0] = "MALFORMED NEXT" # END statement def p_command_end(p): '''command : END''' p[0] = ('END',) # REM statement def p_command_rem(p): '''command : REM''' p[0] = ('REM', p[1]) # STOP statement def p_command_stop(p): '''command : STOP''' p[0] = ('STOP',) # DEF statement def p_command_def(p): '''command : DEF ID LPAREN ID RPAREN EQUALS expr''' p[0] = ('FUNC', p[2], p[4], p[7]) def p_command_def_bad_rhs(p): '''command : DEF ID LPAREN ID RPAREN EQUALS error''' p[0] = "BAD EXPRESSION IN DEF STATEMENT" def p_command_def_bad_arg(p): '''command : DEF ID LPAREN error RPAREN EQUALS expr''' p[0] = "BAD ARGUMENT IN DEF STATEMENT" # GOSUB statement def p_command_gosub(p): '''command : GOSUB INTEGER''' p[0] = ('GOSUB', int(p[2])) def p_command_gosub_bad(p): '''command : GOSUB error''' p[0] = "INVALID LINE NUMBER IN GOSUB" # RETURN statement def p_command_return(p): '''command : RETURN''' p[0] = ('RETURN',) # DIM statement def p_command_dim(p): '''command : DIM dimlist''' p[0] = ('DIM', p[2]) def p_command_dim_bad(p): '''command : DIM error''' p[0] = "MALFORMED VARIABLE LIST IN DIM" # List of variables supplied to DIM statement def p_dimlist(p): '''dimlist : dimlist COMMA dimitem | dimitem''' if len(p) == 4: p[0] = p[1] p[0].append(p[3]) else: p[0] = [p[1]] # DIM items def p_dimitem_single(p): '''dimitem : ID LPAREN INTEGER RPAREN''' p[0] = (p[1], eval(p[3]), 0) def p_dimitem_double(p): '''dimitem : ID LPAREN INTEGER COMMA INTEGER RPAREN''' p[0] = (p[1], eval(p[3]), eval(p[5])) # Arithmetic expressions def p_expr_binary(p): '''expr : expr PLUS expr | expr MINUS expr | expr TIMES expr | expr DIVIDE expr | expr POWER expr''' p[0] = ('BINOP', p[2], p[1], p[3]) def p_expr_number(p): '''expr : INTEGER | FLOAT''' p[0] = ('NUM', eval(p[1])) def p_expr_variable(p): '''expr : variable''' p[0] = ('VAR', p[1]) def p_expr_group(p): '''expr : LPAREN expr RPAREN''' p[0] = ('GROUP', p[2]) def p_expr_unary(p): '''expr : MINUS expr %prec UMINUS''' p[0] = ('UNARY', '-', p[2]) # Relational expressions def p_relexpr(p): '''relexpr : expr LT expr | expr LE expr | expr GT expr | expr GE expr | expr EQUALS expr | expr NE expr''' p[0] = ('RELOP', p[2], p[1], p[3]) # Variables def p_variable(p): '''variable : ID | ID LPAREN expr RPAREN | ID LPAREN expr COMMA expr RPAREN''' if len(p) == 2: p[0] = (p[1], None, None) elif len(p) == 5: p[0] = (p[1], p[3], None) else: p[0] = (p[1], p[3], p[5]) # Builds a list of variable targets as a Python list def p_varlist(p): '''varlist : varlist COMMA variable | variable''' if len(p) > 2: p[0] = p[1] p[0].append(p[3]) else: p[0] = [p[1]] # Builds a list of numbers as a Python list def p_numlist(p): '''numlist : numlist COMMA number | number''' if len(p) > 2: p[0] = p[1] p[0].append(p[3]) else: p[0] = [p[1]] # A number. May be an integer or a float def p_number(p): '''number : INTEGER | FLOAT''' p[0] = eval(p[1]) # A signed number. def p_number_signed(p): '''number : MINUS INTEGER | MINUS FLOAT''' p[0] = eval("-" + p[2]) # List of targets for a print statement # Returns a list of tuples (label,expr) def p_plist(p): '''plist : plist COMMA pitem | pitem''' if len(p) > 3: p[0] = p[1] p[0].append(p[3]) else: p[0] = [p[1]] def p_item_string(p): '''pitem : STRING''' p[0] = (p[1][1:-1], None) def p_item_string_expr(p): '''pitem : STRING expr''' p[0] = (p[1][1:-1], p[2]) def p_item_expr(p): '''pitem : expr''' p[0] = ("", p[1]) # Empty def p_empty(p): '''empty : ''' # Catastrophic error handler def p_error(p): if not p: print("SYNTAX ERROR AT EOF") bparser = yacc.yacc() def parse(data, debug=0): bparser.error = 0 p = bparser.parse(data, debug=debug) if bparser.error: return None return p