| # 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 |