diff --git a/README.md b/README.md index 06c415d..3bf22e1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,19 @@ $ python interpreter.py Although this started of as a personal project, it has been enhanced considerably by some other Github users. You can see them in the list of contributors! It's very much a group endeavour now. +## Recent Updates + +### Array INPUT and READ Support +The interpreter now supports reading data directly into array elements using both **INPUT** and **READ** statements: + +- **Array Elements as Targets**: Use syntax like `INPUT A(1), B(I+2)` and `READ N$(1), N$(2)` +- **Expression Indices**: Support for complex expressions in array indices like `A(I*2+1)` +- **Mixed Data Types**: Read numeric and string data into appropriate array types +- **Error Handling**: Proper SUBSCRIPT ERROR and OUT OF DATA error messages +- **Specification Compliant**: Follows classic BASIC behavior for all edge cases + +See the comprehensive test program `examples/array_input_read_tests.bas` for demonstrations of all functionality. + ## Operators A limited range of arithmetic expressions are provided. Addition and subtraction have the lowest precedence, @@ -135,6 +148,38 @@ The program may be erased entirely from memory using the **NEW** command: > ``` +Program line numbers can be renumbered using the **RENUMBER** command: + +``` +> 10 A = 1 +> 20 IF A = 1 THEN 100 +> 30 GOTO 10 +> 100 PRINT "DONE" +> LIST +10 A = 1 +20 IF A = 1 THEN 100 +30 GOTO 10 +100 PRINT "DONE" +> RENUMBER 100,100 +Program renumbered +> LIST +100 A = 1 +200 IF A = 1 THEN 400 +300 GOTO 100 +400 PRINT "DONE" +> +``` + +The RENUMBER command supports various parameter combinations: + +* **RENUMBER** - Renumber whole program starting at 10 with increments of 10 +* **RENUMBER 100** - Start renumbering at 100 with increments of 10 +* **RENUMBER 50,5** - Start at 50 with increments of 5 +* **RENUMBER 100,10,200,300** - Renumber only lines 200-300, starting at 100 +* **RENUMBER ,,200** - Renumber lines 200 and above using defaults + +The RENUMBER command automatically updates line number references in GOTO, GOSUB, IF...THEN, ON...GOTO, ON...GOSUB, and RESTORE statements while preserving line numbers in string literals and comments. + Finally, it is possible to terminate the interpreter by issuing the **EXIT** command: ``` @@ -168,8 +213,7 @@ Program terminated ### Statement structure As per usual in old school BASIC, all program statements must be prefixed with a line number which indicates the order in which the -statements may be executed. There is no renumber command to allow all line numbers to be modified. A statement may be modified or -replaced by re-entering a statement with the same line number: +statements may be executed. A statement may be modified or replaced by re-entering a statement with the same line number: ``` > 10 LET I = 10 @@ -220,7 +264,7 @@ are all invalid. Numeric variables have no suffix, whereas string variables are always suffixed by '$'. Note that 'I' and 'I$' are considered to be separate variables. Note that string literals must always be enclosed within double quotes (not single quotes). -Using no quotes will result in a syntax error. +Using no quotes will result in a syntax error. Array variables are defined using the **DIM** statement, which explicitly lists how many dimensions the array has, and the sizes of those dimensions: @@ -263,8 +307,7 @@ Empty array value returned in line 30 > ``` -As in all implementations of BASIC, there is no garbage collection (not surprising since all variables -have global scope)! +Since **all PyBasic variables have a global scope** and string memory is managed by the python interpreter, there is no garbage collection in PyBasic. ### Program constants @@ -312,8 +355,72 @@ Hello Another Line of Data > ``` -It is a limitation of this BASIC dialect that it is not possible to assign constants directly to array variables -within a **READ** statement, only simple variables. +#### Array Support in READ + +The **READ** statement now supports reading data directly into array elements, using expressions for array indices: + +``` +> 10 DIM A(5) +> 20 DIM N$(3) +> 30 DATA 10, 20, 30, "Hello", "World" +> 40 READ A(1), A(2), A(3), N$(1), N$(2) +> 50 PRINT A(1); A(2); A(3); N$(1); N$(2) +> RUN +102030HelloWorld +> +``` + +Array indices can be complex expressions including variables and arithmetic: + +``` +> 10 DIM B(10) +> 20 I = 2 +> 30 DATA 100, 200 +> 40 READ B(I*3), B(I+4) +> 50 PRINT "B(6)="; B(6) REM Shows 200 (second value overwrites first) +> RUN +B(6)=200 +> +``` + +Proper error handling is provided: +- **SUBSCRIPT ERROR**: When array indices are out of bounds or negative +- **OUT OF DATA**: When there are no more DATA items to read +- Index validation occurs BEFORE consuming DATA items + +#### Array Support in READ + +The **READ** statement now supports reading data directly into array elements, using expressions for array indices: + +``` +> 10 DIM A(5) +> 20 DIM N$(3) +> 30 DATA 10, 20, 30, "Hello", "World" +> 40 READ A(1), A(2), A(3), N$(1), N$(2) +> 50 PRINT A(1); A(2); A(3); N$(1); N$(2) +> RUN +102030HelloWorld +> +``` + +Array indices can be complex expressions including variables and arithmetic: + +``` +> 10 DIM B(10) +> 20 I = 2 +> 30 DATA 100, 200 +> 40 READ B(I*3), B(I+4) +> 50 PRINT "B(6)="; B(6) REM Shows 200 (second value overwrites first) +> RUN +B(6)=200 +> +``` + +Proper error handling is provided: +- **SUBSCRIPT ERROR**: When array indices are out of bounds or negative +- **OUT OF DATA**: When there are no more DATA items to read +- Index validation occurs BEFORE consuming DATA items + ### Comments @@ -450,6 +557,20 @@ Subroutines may be nested, that is, a subroutine call may be made within another A subroutine may also be called using the **ON-GOSUB** statement (see Conditional branching below). +Further note that **RETURN** passes control back to the next line numbered statement after the call. Subsequent statements after +the call in the same line will not be executed. For example: + +``` +> 10 PRINT "Print this":GOSUB 100:PRINT "This won't be printed" +> 20 STOP +> 100 PRINT "Print this too" +> 110 RETURN +> RUN +Print this +Print this too +> +``` + ### Loops Bounded loops are achieved through the use of **FOR-NEXT** statements. The loop is controlled by a numeric @@ -493,6 +614,49 @@ be replaced by the start value, it will not be evaluated. After the completion of the loop, the loop variable value will be the end value + step value (unless the loop is exited using a **GOTO** statement). +Conditional loops are achieved through the use of **WHILE-WEND** statements. The loop continues to execute +as long as the specified condition remains true. The condition is evaluated before each iteration, so if +the condition is false initially, the loop body will not execute at all. + +``` +> 10 LET I = 1 +> 20 WHILE I <= 3 +> 30 PRINT "Count: "; I +> 40 LET I = I + 1 +> 50 WEND +> RUN +Count: 1 +Count: 2 +Count: 3 +> +``` + +WHILE loops may be nested within one another and can also be nested within FOR loops and vice versa. +The loop will terminate when the condition becomes false or when exited using a **GOTO** statement. + +**Example of nested WHILE loops:** + +``` +> 10 LET I = 1 +> 20 WHILE I <= 2 +> 30 PRINT "Outer: "; I +> 40 LET J = 1 +> 50 WHILE J <= 2 +> 60 PRINT " Inner: "; J +> 70 LET J = J + 1 +> 80 WEND +> 90 LET I = I + 1 +> 100 WEND +> RUN +Outer: 1 + Inner: 1 + Inner: 2 +Outer: 2 + Inner: 1 + Inner: 2 +> +``` + ### Conditionals Conditionals are implemented using the **IF-THEN-ELSE** statement. The expression is evaluated and the appropriate @@ -707,6 +871,41 @@ Num, Str, Num: 22, hello!, 33 > ``` +#### Array Support in INPUT + +The **INPUT** statement now supports inputting data directly into array elements: + +``` +> 10 DIM A(3) +> 20 DIM N$(2) +> 30 INPUT "Enter 3 numbers: "; A(1), A(2), A(3) +> 40 INPUT "Enter 2 names: "; N$(1), N$(2) +> 50 PRINT A(1); A(2); A(3); N$(1); N$(2) +> RUN +Enter 3 numbers: 5, 10, 15 +Enter 2 names: Alice, Bob +51015AliceBob +> +``` + +Array indices can use expressions with variables: + +``` +> 10 DIM B(10) +> 20 I = 5 +> 30 INPUT "Enter value for B(I): "; B(I) +> 40 PRINT "B(5)="; B(5) +> RUN +Enter value for B(I): 42 +B(5)=42 +> +``` + +Error handling includes: +- **SUBSCRIPT ERROR**: When array indices are out of bounds +- **"? REDO FROM START"**: When input types don't match array types +- Index validation occurs BEFORE prompting for input + A mismatch between the input value and input variable type will trigger an error, and the user will be asked to re-input the values again. @@ -901,7 +1100,8 @@ calculate the corresponding factorial *N!*. * *PyBStartrek.bas* - A port of the 1971 Star Trek text based strategy game. -* *adventure-fast.bas* - A port of a 1979 text based Microsoft Adventure game. +* *adventure.bas* - A port of the original 1976 text based adventure game, originally developed for the PDP-10 by Will Crowther, +and expanded by Don Woods. * *bagels.bas* - A guessing game, which made its first appearance in the book 'BASIC Computer Games' in 1978. @@ -911,6 +1111,10 @@ calculate the corresponding factorial *N!*. * *life.bas* - An implementation of Conway's Game of Life. This version is a port of the BASIC program which appeared in 'BASIC Computer Games' in 1978. +* *Pneuma.bas* - A brief 21st century take on the venerable text adventure. + +* *array_input_read_tests.bas* - A comprehensive test program that validates array INPUT and READ functionality, including expression indices, boundary conditions, error handling, and mixed data types. Demonstrates all the features of array element input and data reading. + ## Informal grammar definition **ABS**(*numerical-expression*) - Calculates the absolute value of the result of *numerical-expression* @@ -1059,15 +1263,12 @@ a subroutine call, or program termination. This paradigm of using the parser to object to make control flow decisions and to track execution, and a signalling mechanism to allow the parser to signal control flow changes to the Program object, is used consistently throughout the implementation. -## Open issues +## Python Basic feature -* It is not possible to renumber a program. This would require considerable extra functionality. -* Negative values are printed with a space (e.g. '- 5') in program listings because of tokenization. This does not affect functionality. -* Decimal values less than one must be expressed with a leading zero (i.e. 0.34 rather than .34) -* User input values cannot be directly assigned to array variables in an **INPUT** or **READ** statement * Strings representing numbers (e.g. "10") can actually be assigned to numeric variables in **INPUT** and **READ** statements without an error, Python will silently convert them to integers. ## License PyBasic is made available under the GNU General Public License, version 3.0 or later (GPL-3.0-or-later). + diff --git a/basicparser.py b/basicparser.py index 9c026ec..5d2399e 100644 --- a/basicparser.py +++ b/basicparser.py @@ -56,6 +56,9 @@ def __init__(self, dimensions, elem_type): raise SyntaxError("Fractional array size specified") dimensions[i] = int(dimensions[i]) + # Store original dimensions for bounds checking + self.original_dims = dimensions.copy() + # MSBASIC: Initialize to Zero # MSBASIC: Overdim by one, as some dialects are 1 based and expect # to use the last item at index = size @@ -239,7 +242,7 @@ def __stmt(self): """ if self.__token.category in [Token.FOR, Token.IF, Token.NEXT, - Token.ON]: + Token.ON, Token.WHILE, Token.WEND]: return self.__compoundstmt() else: @@ -596,19 +599,7 @@ def __arrayassignmentstmt(self, name): ' in line ' + str(self.__line_number)) # Assign to the specified array index - try: - if len(indexvars) == 1: - BASICarray.data[indexvars[0]] = right - - elif len(indexvars) == 2: - BASICarray.data[indexvars[0]][indexvars[1]] = right - - elif len(indexvars) == 3: - BASICarray.data[indexvars[0]][indexvars[1]][indexvars[2]] = right - - except IndexError: - raise IndexError('Array index out of range in line ' + - str(self.__line_number)) + self.__assign_array_val(BASICarray, indexvars, right) def __openstmt(self): """Parses an open statement, opens the indicated file and @@ -673,10 +664,37 @@ def __openstmt(self): raise RuntimeError('File '+filename+' could not be opened in line ' + str(self.__line_number)) if accessMode == "r+": + # By checking the 'newlines' attribute the appropriate adjustment for + # the file being opened can be determined. + + try: + # newline attribute is only set after a line is read + # manually identify newlines in case newlines attribute isn't supported + fileline = self.__file_handles[filenum].readline() + newlines = "" + for ichar in range(len(fileline)-1,-1,-1): + if fileline[ichar] not in ['\n','\r']: + break + newlines = fileline[ichar:] + except: + newlines = None + + if hasattr(self.__file_handles[filenum],'newlines'): + newlines = self.__file_handles[filenum].newlines + self.__file_handles[filenum].seek(0) filelen = 0 + + if newlines is not None: + newlineAdj = len(newlines) - 1 + else: + # If we wern't able to determine an appropriate value and + # using version of Python that doesn't have newlines attribute + # use adjustment appropriate for Windows formatted files + newlineAdj = 1 + for lines in self.__file_handles[filenum]: - filelen += len(lines)+1 + filelen += len(lines)+newlineAdj self.__file_handles[filenum].seek(filelen) @@ -729,7 +747,7 @@ def __fseekstmt(self): def __inputstmt(self): """Parses an input statement, extracts the input from the user and places the values into the - symbol table + symbol table. Now supports array elements. """ self.__advance() # Advance past INPUT token @@ -762,61 +780,64 @@ def __inputstmt(self): prompt = self.__operand_stack.pop() self.__consume(Token.SEMICOLON) - # Acquire the comma separated input variables - variables = [] + # Parse the target variables (simple variables or array elements) + targets = [] if not self.__tokenindex >= len(self.__tokenlist): - if self.__token.category != Token.NAME: - raise ValueError('Expecting NAME in INPUT statement ' + - 'in line ' + str(self.__line_number)) - variables.append(self.__token.lexeme) - self.__advance() # Advance past variable + # Parse first target + targets.append(self.__parse_variable_target()) while self.__token.category == Token.COMMA: self.__advance() # Advance past comma - variables.append(self.__token.lexeme) - self.__advance() # Advance past variable + targets.append(self.__parse_variable_target()) valid_input = False while not valid_input: # Gather input from the user into the variables if fileIO: - inputvals = ((self.__file_handles[filenum].readline().replace("\n","")).replace("\r","")).split(',', (len(variables)-1)) + inputvals = ((self.__file_handles[filenum].readline().replace("\n","")).replace("\r","")).split(',', (len(targets)-1)) valid_input = True else: - inputvals = input(prompt).split(',', (len(variables)-1)) - - for variable in variables: - left = variable + inputvals = input(prompt).split(',', (len(targets)-1)) - try: - right = inputvals.pop(0) + try: + for target in targets: + try: + right = inputvals.pop(0) - if left.endswith('$'): - self.__symbol_table[left] = str(right) + if target['is_string']: + value = str(right) + else: + # Try to convert to numeric + try: + if '.' in right: + value = float(right) + else: + value = int(right) + except ValueError: + if not fileIO: + valid_input = False + print('Non-numeric input provided to a numeric variable - redo from start') + break + raise ValueError('Non-numeric input provided to a numeric variable ' + + 'in line ' + str(self.__line_number)) + + # Assign value to target (validates array indices if needed) + self.__assign_to_target(target, value) valid_input = True - elif not left.endswith('$'): - try: - if '.' in right: - self.__symbol_table[left] = float(right) - - else: - self.__symbol_table[left] = int(right) - - valid_input = True - - except ValueError: - if not fileIO: - valid_input = False - print('Non-numeric input provided to a numeric variable - redo from start') + except IndexError: + # No more input to process + if not fileIO: + valid_input = False + print('Not enough values input - redo from start') break + raise RuntimeError('Not enough input values in line ' + str(self.__line_number)) - except IndexError: - # No more input to process - if not fileIO: - valid_input = False - print('Not enough values input - redo from start') - break + except RuntimeError as e: + if 'SUBSCRIPT ERROR' in str(e): + raise e # Re-raise subscript errors immediately + else: + raise e def __restorestmt(self): @@ -832,50 +853,68 @@ def __datastmt(self): """Parses a DATA statement""" def __readstmt(self): - """Parses a READ statement.""" + """Parses a READ statement. Now supports array elements.""" self.__advance() # Advance past READ token - # Acquire the comma separated input variables - variables = [] + # Parse the target variables (simple variables or array elements) + targets = [] if not self.__tokenindex >= len(self.__tokenlist): - variables.append(self.__token.lexeme) - self.__advance() # Advance past variable + # Parse first target + targets.append(self.__parse_variable_target()) while self.__token.category == Token.COMMA: self.__advance() # Advance past comma - variables.append(self.__token.lexeme) - self.__advance() # Advance past variable + targets.append(self.__parse_variable_target()) - # Gather input from the DATA statement into the variables - for variable in variables: + # Process each target + for target in targets: + # Validate array indices BEFORE reading data (per specification) + if target['type'] == 'array': + BASICarray = self.__symbol_table[target['array_name']] + self.__validate_array_indices(BASICarray, target['indices']) + # Get data value if len(self.__data_values) < 1: - self.__data_values = self.__data.readData(self.__line_number) + try: + self.__data_values = self.__data.readData(self.__line_number) + except RuntimeError as e: + if "No DATA statements available" in str(e): + raise RuntimeError('? OUT OF DATA in line ' + str(self.__line_number)) + else: + raise e - left = variable - right = self.__data_values.pop(0) + if len(self.__data_values) < 1: + raise RuntimeError('? OUT OF DATA in line ' + str(self.__line_number)) - if left.endswith('$'): - # Python inserts quotes around input data - if isinstance(right, int): - raise ValueError('Non-string input provided to a string variable ' + - 'in line ' + str(self.__line_number)) + right = self.__data_values.pop(0) + if target['is_string']: + # String target - accept any data type as string + if isinstance(right, (int, float)): + # Convert numeric data to string + value = str(right) else: - self.__symbol_table[left] = right - - elif not left.endswith('$'): + # Already a string + value = right + else: + # Numeric target - must be numeric data try: - numeric = float(right) - if int(numeric) == numeric: - numeric = int(numeric) - self.__symbol_table[left] = numeric - + if isinstance(right, str): + # Try to parse string as number + numeric = float(right) if '.' in right else int(right) + else: + numeric = float(right) + if int(numeric) == numeric: + numeric = int(numeric) + value = numeric except ValueError: raise ValueError('Non-numeric input provided to a numeric variable ' + 'in line ' + str(self.__line_number)) + # Assign value to target + self.__assign_to_target(target, value) + def __expr(self): """Parses a numerical expression consisting of two terms being added or subtracted, @@ -1006,8 +1045,13 @@ def __factor(self): self.__operand_stack.append(self.__sign*self.__symbol_table[self.__token.lexeme]) else: - raise RuntimeError('Name ' + self.__token.lexeme + ' is not defined' + - ' in line ' + str(self.__line_number)) + # default variables values for undefined variables. + if self.__token.lexeme.endswith ("$"): + # default string + self.__operand_stack.append("") + else: + #default int + self.__operand_stack.append(0) self.__advance() @@ -1042,9 +1086,7 @@ def __get_array_val(self, BASICarray, indexvars): :return: The value at the indexed position in the array """ - if BASICarray.dims != len(indexvars): - raise IndexError('Incorrect number of indices applied to array ' + - 'in line ' + str(self.__line_number)) + self.__validate_array_indices(BASICarray, indexvars) # Fetch the value from the array try: @@ -1058,11 +1100,120 @@ def __get_array_val(self, BASICarray, indexvars): arrayval = BASICarray.data[indexvars[0]][indexvars[1]][indexvars[2]] except IndexError: - raise IndexError('Array index out of range in line ' + - str(self.__line_number)) + raise RuntimeError('SUBSCRIPT ERROR in line ' + str(self.__line_number)) return arrayval + def __validate_array_indices(self, BASICarray, indexvars): + """Validates array indices and raises SUBSCRIPT ERROR if invalid + + :param BASICarray: The BASICArray to validate against + :param indexvars: List of index values to validate + :raises RuntimeError: If indices are out of bounds + """ + if BASICarray.dims != len(indexvars): + raise RuntimeError('SUBSCRIPT ERROR in line ' + str(self.__line_number)) + + for i, index in enumerate(indexvars): + # Convert float to int (truncate toward zero) + if isinstance(index, float): + index = int(index) + indexvars[i] = index + + # Check bounds - negative or greater than declared dimension + if index < 0 or index > BASICarray.original_dims[i]: + raise RuntimeError('SUBSCRIPT ERROR in line ' + str(self.__line_number)) + + def __assign_array_val(self, BASICarray, indexvars, value): + """Assigns a value to an array element after validating indices + + :param BASICarray: The BASICArray + :param indexvars: List of indices + :param value: Value to assign + """ + self.__validate_array_indices(BASICarray, indexvars) + + try: + if len(indexvars) == 1: + BASICarray.data[indexvars[0]] = value + elif len(indexvars) == 2: + BASICarray.data[indexvars[0]][indexvars[1]] = value + elif len(indexvars) == 3: + BASICarray.data[indexvars[0]][indexvars[1]][indexvars[2]] = value + except IndexError: + raise RuntimeError('SUBSCRIPT ERROR in line ' + str(self.__line_number)) + + def __parse_variable_target(self): + """Parses a variable target which can be a simple variable or array element + + :return: Dictionary with 'type' ('simple' or 'array'), 'name' (variable name), + 'indices' (list of index values for arrays), 'is_string' (boolean) + """ + if self.__token.category != Token.NAME: + raise SyntaxError('Expecting variable name in line ' + str(self.__line_number)) + + var_name = self.__token.lexeme + is_string = var_name.endswith('$') + self.__advance() + + # Check if this is an array element + if self.__token.category == Token.LEFTPAREN: + # Array element target + array_name = var_name + '_array' + + # Check if array exists + if array_name not in self.__symbol_table: + raise RuntimeError('Array not dimensioned in line ' + str(self.__line_number)) + + self.__advance() # Past LEFTPAREN + + # Parse index expressions + indexvars = [] + self.__expr() + indexvars.append(self.__operand_stack.pop()) + + while self.__token.category == Token.COMMA: + self.__advance() # Past comma + self.__expr() + indexvars.append(self.__operand_stack.pop()) + + self.__consume(Token.RIGHTPAREN) + + return { + 'type': 'array', + 'name': var_name, + 'array_name': array_name, + 'indices': indexvars, + 'is_string': is_string + } + else: + # Simple variable target + return { + 'type': 'simple', + 'name': var_name, + 'is_string': is_string + } + + def __assign_to_target(self, target, value): + """Assigns a value to a target (simple variable or array element) + + :param target: Target dictionary from __parse_variable_target + :param value: Value to assign + """ + # Type checking + if target['is_string'] and not isinstance(value, str): + raise ValueError('Non-string input provided to a string variable in line ' + + str(self.__line_number)) + elif not target['is_string'] and isinstance(value, str): + raise ValueError('Non-numeric input provided to a numeric variable in line ' + + str(self.__line_number)) + + if target['type'] == 'simple': + self.__symbol_table[target['name']] = value + else: # array + BASICarray = self.__symbol_table[target['array_name']] + self.__assign_array_val(BASICarray, target['indices'], value) + def __compoundstmt(self): """Parses compound statements, specifically if-then-else and @@ -1084,6 +1235,12 @@ def __compoundstmt(self): elif self.__token.category == Token.ON: return self.__ongosubstmt() + elif self.__token.category == Token.WHILE: + return self.__whilestmt() + + elif self.__token.category == Token.WEND: + return self.__wendstmt() + def __ifstmt(self): """Parses if-then-else statements @@ -1107,11 +1264,12 @@ def __ifstmt(self): return FlowSignal(ftype=FlowSignal.EXECUTE) else: self.__expr() + target = self.__operand_stack.pop() # Jump if the expression evaluated to True if saveval: # Set up and return the flow signal - return FlowSignal(ftarget=self.__operand_stack.pop()) + return FlowSignal(ftarget=target) # advance to ELSE while self.__tokenindex < len(self.__tokenlist) and self.__token.category != Token.ELSE: @@ -1249,6 +1407,42 @@ def __nextstmt(self): return FlowSignal(ftype=FlowSignal.LOOP_REPEAT,floop_var=loop_variable) + def __whilestmt(self): + """Parses WHILE loops + + :return: The FlowSignal to indicate that + a WHILE loop start has been processed + + """ + + self.__advance() # Advance past WHILE token + self.__logexpr() # Evaluate the condition + + # Save result of expression + condition_result = self.__operand_stack.pop() + + # Check if the condition is true + if condition_result: + # Condition is true, execute the loop body + return FlowSignal(ftype=FlowSignal.WHILE_BEGIN) + else: + # Condition is false, skip the loop + return FlowSignal(ftype=FlowSignal.WHILE_SKIP) + + def __wendstmt(self): + """Processes a WEND statement that terminates + a WHILE loop + + :return: A FlowSignal indicating that the loop + should repeat (return to WHILE) + + """ + + self.__advance() # Advance past WEND token + + # Return signal to repeat the WHILE loop + return FlowSignal(ftype=FlowSignal.WHILE_REPEAT) + def __ongosubstmt(self): """Process the ON-GOSUB statement diff --git a/basictoken.py b/basictoken.py index 7aba639..0f66bfd 100644 --- a/basictoken.py +++ b/basictoken.py @@ -119,6 +119,9 @@ class BASICToken: SEMICOLON = 86 # SEMICOLON LEFT = 87 # LEFT$ function RIGHT = 88 # RIGHT$ function + WHILE = 89 # WHILE keyword + WEND = 90 # WEND keyword + RENUMBER = 91 # RENUMBER command # Displayable names for each token category catnames = ['EOF', 'LET', 'LIST', 'PRINT', 'RUN', @@ -137,7 +140,8 @@ class BASICToken: 'MAX', 'MIN', 'INSTR', 'AND', 'OR', 'NOT', 'PI', 'RNDINT', 'OPEN', 'HASH', 'CLOSE', 'FSEEK', 'APPEND', 'OUTPUT', 'RESTORE', 'RNDINT', 'TAB', 'SEMICOLON', - 'LEFT', 'RIGHT'] + 'LEFT', 'RIGHT', + 'WHILE', 'WEND', 'RENUMBER'] smalltokens = {'=': ASSIGNOP, '(': LEFTPAREN, ')': RIGHTPAREN, '+': PLUS, '-': MINUS, '*': TIMES, '/': DIVIDE, @@ -174,7 +178,8 @@ class BASICToken: 'CLOSE': CLOSE, 'FSEEK': FSEEK, 'APPEND': APPEND, 'OUTPUT':OUTPUT, 'RESTORE': RESTORE, 'TAB': TAB, - 'LEFT$': LEFT, 'RIGHT$': RIGHT} + 'LEFT$': LEFT, 'RIGHT$': RIGHT,'RENUMBER': RENUMBER, + 'WHILE': WHILE, 'WEND': WEND} # Functions @@ -198,3 +203,4 @@ def pretty_print(self): def print_lexeme(self): print(self.lexeme, end=' ') + diff --git a/examples/Pneuma.bas b/examples/Pneuma.bas new file mode 100644 index 0000000..0d342fc --- /dev/null +++ b/examples/Pneuma.bas @@ -0,0 +1,585 @@ +10 REM Pneuma - A space adventure +20 REM ========== backstory and instructions ========== +30 PRINT "********************************" : PRINT +35 PRINT " Pneuma - A space adventure" : PRINT +40 PRINT "********************************": PRINT +45 PRINT "To get instructions, type 'help'" +50 PRINT +60 PRINT "You wake up in your bunk, in the sleeping quarters of the starship Pneuma. You can't" +65 PRINT "remember much. You went to bed feeling sick and after a feverish few hours tossing and" +70 PRINT "turning, feeling like you were burning up, you eventually fell asleep." : PRINT +75 PRINT "Now, your sheets and night clothes are damp with sweat, and you have a raging thirst." +80 PRINT "You have a sore throat and the mother of all headaches, like your brain has been boiling" +85 PRINT "in your skull. In fact, you're no longer sure exactly where you are ... you seem to be" +90 PRINT "suffering from some sort of amnesia ...": PRINT +95 REM set up environment +100 GOSUB 2700 +500 REM setup room descriptions +510 GOSUB 3000 +520 REM setup up interative descriptions +530 GOSUB 5000 +540 REM setup dialogue +550 GOSUB 7000 +700 REM ========== main loop ========== +701 IF WPL <> 99 THEN GOSUB 8400 : REM wraith-hound movement +702 REM show room details +703 PRINT "You are in the " ; LO$ ( PL ) : PRINT +705 GOSUB 4010 : REM print room description +706 GOSUB 8000 : REM tracker info if carried +707 IF WPL <> 99 THEN GOSUB 8200 : REM wraith-hound proximity check +709 IF WPL = PL THEN GOSUB 11000 : REM fight wraith-hound +710 INPUT "What now? " ; I$ +715 PRINT +716 MOVE = 1 +720 IF LEFT$ ( LOWER$ ( I$ ) , 4 ) = "get " THEN MOVE = 0 : GOSUB 1400 +730 IF LEFT$ ( LOWER$ ( I$ ) , 5 ) = "take " THEN MOVE = 0 : GOSUB 1700 +740 IF LEFT$ ( LOWER$ ( I$ ) , 5 ) = "drop " THEN MOVE = 0 : GOSUB 2000 +750 IF LEFT$ ( LOWER$ ( I$ ) , 8 ) = "examine " THEN MOVE = 0 : GOSUB 2300 +760 IF LEFT$ ( LOWER$ ( I$ ) , 4 ) = "look" THEN MOVE = 0 : GOSUB 4010 +765 IF LEFT$ ( LOWER$ ( I$ ) , 4 ) = "help" THEN MOVE = 0 : GOSUB 4130 +767 IF LEFT$ ( LOWER$ ( I$ ) , 4 ) = "use " THEN MOVE = 0 : GOSUB 9000 +770 IF LOWER$ ( I$ ) = "i" OR LOWER$ ( I$ ) = "inventory" THEN MOVE = 0 : GOSUB 1000 +775 IF LEFT$ ( LOWER$ ( I$ ) , 8 ) = "talk to " THEN MOVE = 0 : GOSUB 2630 +780 IF LEFT$ ( LOWER$ ( I$ ) , 1 ) = "q" THEN GOSUB 2600 +785 IF LEFT$ ( LOWER$ ( I$ ) , 3 ) = "go " THEN GOSUB 1100 +790 IF LOWER$ ( I$ ) = "f" OR LOWER$ ( I$ ) = "forward" THEN GOSUB 1200 +800 IF LOWER$ ( I$ ) = "a" OR LOWER$ ( I$ ) = "aft" THEN GOSUB 1200 +810 IF LOWER$ ( I$ ) = "p" OR LOWER$ ( I$ ) = "port" THEN GOSUB 1200 +820 IF LOWER$ ( I$ ) = "s" OR LOWER$ ( I$ ) = "starboard" THEN GOSUB 1200 +830 IF LOWER$ ( I$ ) = "u" OR LOWER$ ( I$ ) = "up" THEN GOSUB 1200 +840 IF LOWER$ ( I$ ) = "d" OR LOWER$ ( I$ ) = "down" THEN GOSUB 1200 +895 IF MOVE = 1 THEN GOTO 700 ELSE GOTO 710 +900 STOP +995 REM ========== actions ========== +1000 REM list the player's inventory +1005 PRINT "You have the following items:" : PRINT +1010 FOR I = 0 TO OC - 1 +1020 IF OL ( I ) = 0 THEN PRINT OB$ ( I ) +1030 NEXT I +1035 PRINT +1040 RETURN +1100 REM fully written out move (e.g. 'go aft') +1110 D$ = MID$ ( LOWER$(I$) , 4 , 1 ) +1120 GOSUB 1300 +1130 RETURN +1200 REM abbreviated move (e.g. 'a' or 'aft') +1210 D$ = LOWER$( I$ ) +1220 GOSUB 1300 +1230 RETURN +1300 REM go to the player's new location (PL) +1310 IF D$ = "f" THEN NPL = VAL ( MID$ ( EX$ ( PL ) , 1 , 2 ) ) +1320 IF D$ = "a" THEN NPL = VAL ( MID$ ( EX$ ( PL ) , 3 , 2 ) ) +1330 IF D$ = "p" THEN NPL = VAL ( MID$ ( EX$ ( PL ) , 5 , 2 ) ) +1340 IF D$ = "s" THEN NPL = VAL ( MID$ ( EX$ ( PL ) , 7 , 2 ) ) +1345 IF D$ = "u" THEN NPL = VAL ( MID$ ( EX$ ( PL ) , 9 , 2 ) ) +1350 IF D$ = "d" THEN NPL = VAL ( MID$ ( EX$ ( PL ) , 11 , 2 ) ) +1355 IF NPL = 0 THEN PRINT "You can't go that way." : PRINT ELSE PL = NPL +1360 RETURN +1400 REM get command +1405 F=-1: R$="" +1410 R$ = MID$(LOWER$(I$), 5) : REM R$ is the requested object +1420 REM get the object ID +1430 FOR I= 0 TO OC-1 +1440 IF OB$(I) = R$ THEN F=I : REM object exists +1450 NEXT I +1460 REM can't find the item? +1470 IF F=-1 THEN PRINT "You can't take that." : PRINT : RETURN +1480 IF OL(F) <> PL THEN PRINT "That item doesn't appear to be around here." : PRINT : RETURN +1490 IF OL(F)=0 THEN PRINT "You already have that item." : PRINT: RETURN +1520 OL(F)=0 : REM add the item to the inventory +1530 PRINT "You've picked up ";OB$(F); "." : PRINT +1540 RETURN +1700 REM take command +1710 F=-1: R$="" +1720 R$ = MID$(LOWER$(I$), 6) : REM R$ is the requested object +1730 GOTO 1420 : REM use the same logic as the get command +2000 REM drop command +2010 F=-1: R$="" +2020 R$ = MID$(LOWER$(I$), 6) : REM R$ is the requested object +2030 FOR I= 0 TO OC-1 +2040 IF OB$(I) = R$ THEN F=I : REM object exists +2050 NEXT I +2060 REM can't find it? +2070 IF F=-1 THEN PRINT "You don't have that." : PRINT: RETURN +2080 IF OL(F) <> 0 THEN PRINT "You aren't carrying that." : PRINT: RETURN +2090 OL(F) = PL : PRINT "You've dropped ";OB$(F); ".": PRINT: REM add the item to the current room +2095 IF F = SUIT THEN SUIT_WORN = 0 +2110 RETURN +2300 REM examine command +2310 F=-1 : R$="" +2320 R$ = MID$(LOWER$(I$), 9) : REM R$ is the object to examine +2330 FOR I = 0 TO IC-1 +2340 IF IO$(I) = R$ THEN F=I : REM object exists +2350 NEXT I +2360 REM can't find it? +2370 IF F=-1 THEN PRINT "You can't examine that." : PRINT : RETURN +2380 IF IL(F) <> PL THEN PRINT "There isn't one of these here." : PRINT : RETURN +2390 GOSUB 6000 : REM print result of examination +2400 RETURN +2600 REM quit command +2610 PRINT "Farewell spacefarer ..." +2620 STOP +2630 REM talk to command +2635 F=-1 : R$ = "" +2640 R$ = MID$(LOWER$(I$), 9) : REM R$ is the person to talk to +2645 FOR I = 0 TO PC-1 +2650 IF P$(I) = R$ THEN F=I : REM person exists +2655 NEXT I +2660 REM can't find them? +2665 IF F=-1 THEN PRINT "You've not met them." : PRINT : RETURN +2670 IF PLOC(F) <> PL THEN PRINT "They aren't here." : PRINT : RETURN +2675 GOSUB 7500 : REM print dialogue +2695 RETURN +2700 REM ========== set up environment =========== +2705 RC = 18 : REM room count +2710 DIM LO$ ( RC ) +2715 INV = 0 : LO$ ( INV ) = "Inventory" +2720 GAL = 1 : LO$ ( GAL ) = "Galley" +2725 REC = 2 : LO$ ( REC ) = "Recreation/Dining Room" +2730 ARM = 3 : LO$ ( ARM ) = "Armoury" +2735 BDG = 4 : LO$ ( BDG ) = "Bridge" +2740 SLP = 5 : LO$ ( SLP ) = "Sleeping Quarters" +2745 MED = 6 : LO$ ( MED ) = "Medical Centre" +2750 GYM = 7 : LO$ ( GYM ) = "Gymnasium" +2755 LAC = 8 : LO$ ( LAC ) = "Lower Aft Corridor" +2760 ENG = 9 : LO$ ( ENG ) = "Engine Room" +2765 STO = 10 : LO$ ( STO ) = "Storeroom" +2770 MEN = 11 : LO$ ( MEN ) = "Menagerie" +2775 LAB = 12 : LO$ ( LAB ) = "Laboratory" +2780 LFC = 13 : LO$ ( LFC ) = "Lower Forward Corridor" +2785 POD = 14 : LO$ ( POD ) = "Pod Bay" +2790 AMC = 15 : LO$ ( AMC ) = "Aft Main Corridor" +2795 MMC = 16 : LO$ ( MMC ) = "Mid Main Corridor" +2800 FMC = 17 : LO$ ( FMC ) = "Forward Main Corridor" +2805 REM encoded room exits, two digits per direction f, a, p, s, u, d +2810 DIM EX$ ( RC ) +2815 EX$ ( GAL ) = "020000150008" +2820 EX$ ( REC ) = "000100160000" +2825 EX$ ( ARM ) = "000000170000" +2830 EX$ ( BDG ) = "001700000000" +2835 EX$ ( SLP ) = "000015000000" +2840 EX$ ( MED ) = "070016000000" +2845 EX$ ( GYM ) = "000617000013" +2850 EX$ ( LAC ) = "100000090100" +2855 EX$ ( ENG ) = "000008000000" +2860 EX$ ( STO ) = "120800110000" +2865 EX$ ( MEN ) = "000010000000" +2870 EX$ ( LAB ) = "001000130000" +2875 EX$ ( LFC ) = "140012000700" +2880 EX$ ( POD ) = "001300000000" +2890 EX$ ( AMC ) = "160001050000" +2895 EX$ ( MMC ) = "171502060000" +2900 EX$ ( FMC ) = "041603070000" +2905 OC = 5 : REM object count +2910 DIM OB$ ( OC ) +2915 PULSE = 0 : OB$ ( PULSE ) = "pulse rifle" +2920 SUIT = 1 : OB$ ( SUIT ) = "space suit" +2922 KNIFE = 2 : OB$ ( KNIFE ) = "knife" +2924 TRACKER = 3 : OB$ ( TRACKER ) = "tracker" +2926 SYRINGE = 4 : OB$ ( SYRINGE ) = "syringe" +2930 REM object locations +2932 REM location 0 = player's inventory +2934 DIM OL ( OC ) +2936 OL ( PULSE ) = ARM +2937 OL ( SUIT ) = POD +2939 OL ( KNIFE ) = GAL +2940 OL ( TRACKER ) = GYM +2941 OL ( SYRINGE ) = MED +2942 IC = 5 : REM interactive object count +2944 DIM IO$ (IC) +2946 MEDLOG = 0 : IO$ ( MEDLOG ) = "medical log" +2948 PORTHOLE = 1 : IO$ (PORTHOLE) = "porthole" +2950 CONSOLE = 2 : IO$(CONSOLE) = "console" +2952 ENGINE = 3 : IO$(ENGINE)= "engine control" +2954 TERMINAL = 4 : IO$(TERMINAL) = "library terminal" +2960 REM interative object locations +2962 DIM IL ( IC ) +2964 IL ( MEDLOG) = MED +2966 IL (PORTHOLE) = POD +2968 IL (CONSOLE) = BDG +2970 IL (ENGINE) = ENG +2971 IL (TERMINAL) = REC +2972 PC = 3 : REM person count +2974 DIM P$ ( PC ) +2976 CHEF = 0 : P$ ( CHEF ) = "chef" +2978 RUNNER = 1 : P$ ( RUNNER ) = "runner" +2980 PILOT = 2 : P$ ( PILOT ) = "pilot" +2981 REM person locations +2983 DIM PLOC ( PC ) +2985 PLOC ( CHEF ) = GAL +2987 PLOC ( RUNNER ) = GYM +2989 PLOC ( PILOT ) = BDG +2990 PL = SLP : REM initial player location +2992 WPL = MEN : REM wraith-hound initial location +2994 SUIT_WORN = 0 : REM is space suit worn? +2995 SHUTDOWN = 0 : REM are engines shut down? +2999 RETURN +3000 REM ========== room descriptions ========== +3010 DIM RD$ ( RC, 5 ) +3015 REM inventory +3020 DATA "", "", "", "", "" +3025 REM galley +3030 DATA "The galley contains gleaming, stainless steel cupboards along the aft wall. A food" +3040 DATA "preparation surface is on the port wall, currently covered in rotting food. A chef" +3050 DATA "stands at the work surface, methodically chopping food even though everything has" +3060 DATA "already been thoroughly diced. There are doors in the starboard and forward walls and" +3070 DATA "a stairway leads downwards in the far corner." +3075 REM recreation room +3080 DATA "Space is clearly at a premium in this ship. The room doubles as both a dining and" +3090 DATA "recreation area. Long tables for dining are located on the port side, while couches" +3100 DATA "and low tables are scattered around the remaining space. A library terminal is switched" +3105 DATA "on in the corner. There are doors in the aft and starboard walls.", "" +3120 REM armoury +3130 DATA "Locked cabinets line the starboard wall. Each cabinet has a prominently displayed" +3140 DATA "notice on its door reading 'Weapons to be removed only when authorised by the Chief" +3150 DATA "Security Officer'. However, the door to one cupboard has been prized open, it is" +3160 DATA "warped and bent. This cupboard appears to be empty. The only exit is a starboard door.", "" +3170 REM bridge +3180 DATA "The bridge is the heart of the ship. A vast array of glowing screens and switches fill" +3190 DATA "every surface. On the screens are complex graphics providing detailed information about" +3200 DATA "the status of every system on the ship. Many of them are showing red warning symbols." +3210 DATA "There is a console directly in front of you, a pilot gripping the throttle." +3220 DATA "An aft exit leads back into the main corridor." +3225 REM sleeping quarters +3230 DATA "The sleeping quarters is filled with bunks, one up, one down. Several of the bunks" +3240 DATA "contain sleeping forms, some gently shoring. The room has a partition to separate" +3250 DATA "male and female bunks. Against the forward wall are two corresponding sets of heads." +3260 DATA "The room is messy, with discarded personal items everywhere ... on bunks, on the floor." +3270 DATA "There is a door in the port wall." +3275 REM medical centre +3280 DATA "The medical centre looks relatively normal, but there is evidence of discarded items" +3290 DATA "lying around the room. Blood filled syringes are scattered on a workbench, as well as" +3300 DATA "some bloodied bandanges. The words 'I'm losing myself' are scrawled messily in blood on" +3310 DATA "one wall. In the corner you can see a terminal. On the terminal screen is a portion" +3320 DATA "of the medical log. Exits lead port and forward." +3325 REM gymnasium +3330 DATA "The gymnasium is full of exercise equipment. A female runner is sprinting furiously on a" +3340 DATA "treadmill. She looks exhausted and emaciated, but she keeps running at top speed, almost at" +3350 DATA "a sprint. Her eyes remain fixed on the treadmill console. There are aft and port exits," +3360 DATA "as well as a stairwell leading to the lower deck in the far corner.", "" +3370 REM lower aft corridor +3380 DATA "This is a featureless, utilitarian corridor. A stairwell leads upwards. There are also" +3390 DATA "exits leading starboard and forward.", "", "", "" +3400 REM engine room +3430 DATA "The engine room is characterised by a continual rumble, as though incredible energies are" +3440 DATA "barely being contained. There is an engine control in the far corner, festooned with" +3450 DATA "switches and engine readouts. A single exit leads out into the corridor.", "", "" +3460 REM storeroom +3480 DATA "The storeroom is full of crates, most neatly stacked, but with some scattered across the" +3490 DATA "floor, their contents spilling out. Along the port wall is a door marked 'Test specimens'." +3500 DATA "From behind the door, strange animal noises are audible ... snuffling sounds and the" +3510 DATA "occasional primate shriek. A dead man wearing a scuffed and torn lab coat is lying face down" +3520 DATA "in front of the specimen door. Two other exits lead forward and aft." +3525 REM menagerie +3530 DATA "The room is a hellhole. Cages stand open, while various animals roam about: chimpanzees," +3540 DATA "dogs, and rats. Some of the rats are dead, having been savaged and eviscerated. The floor" +3550 DATA "and walls are smeared with animal faeces, and the smell is almost overpowering. A capsule," +3552 DATA "its door ajar, is marked 'BIOWEAPON CONTAINMENT'. The capsule is empty." +3555 DATA "A single door to port leads back into the storeroom." +3560 REM laboratory +3570 DATA "The laboratory is full of scientific equipment, chemical glassware, electronic analysers," +3580 DATA "fume cupboards, and two couches. The place looks disorded, like the rest of the ship, the" +3590 DATA "result of frenetic activity. A number of experiments seem to be in progress, with logbooks" +3600 DATA "and tablets covered in dense calculations and notes. Whatever has been happening in here," +3610 DATA "it has been done with extreme urgency. Exits lead aft and to starboard." +3615 REM lower forward corridor +3620 DATA "This is a featureless, utilitarian corridor. A stairwell leads upwards. There are also" +3630 DATA "doors leading to port and forward.", "", "", "" +3660 REM pod bay +3670 DATA "You are in a large room, with a row of spacesuits hanging on the port wall. At the forward" +3680 DATA "end of the room is a small, two seater vehicle, capable of operating in space outside the" +3690 DATA "main ship for limited periods. In front of the small ship is the pod bay door, leading out" +3700 DATA "into space. There is a porthole on the far wall. There is a single exit leading aft.", "" +3710 REM aft main corridor +3720 DATA "The main corridor stretches away from you towards the front of the ship. It is featureless" +3730 DATA "and utilitarian. The lighting is dim. You can see doors either side of you, to port and" +3740 DATA "to starboard.", "", "" +3760 REM mid main corridor +3770 DATA "The main corridor stretches away from you both forward and aft. It is featureless" +3780 DATA "and utilitarian. The lighting is dim. You can see doors either side of you, to port and" +3790 DATA "to starboard.", "", "" +3810 REM forward main corridor +3820 DATA "The main corridor stretches away from you towards the rear of the ship. It is featureless" +3830 DATA "and utilitarian. The lighting is dim. You can see doors either side of you, to port and" +3840 DATA "to starboard, as well as a third door leading forward.", "", "" +3860 REM assign to room descriptions +3870 FOR ROOM = 0 to RC-1 +3880 FOR I = 0 TO 4 +3885 READ DESC$ : RD$ (ROOM, I) = DESC$ +3890 NEXT I +3900 NEXT ROOM +4000 RETURN +4010 REM ========== print room description ========== +4020 FOR LINE = 0 TO 4 +4030 IF RD$(PL, LINE) <> "" THEN PRINT RD$(PL, LINE) +4040 NEXT LINE +4055 GOSUB 4070 +4060 RETURN +4070 REM print objects +4080 FOR I = 0 TO OC-1 +4090 IF OL ( I ) = PL THEN PRINT : PRINT "You can see: ";OB$ ( I ); "." +4100 NEXT I +4110 PRINT +4115 IF PL = LAB THEN GOSUB 9700 +4120 RETURN +4130 REM ========== print help ========== +4140 PRINT "For movement, try [go] a[ft], f[orward], p[ort], s[tarboard], u[p] or d[own]." +4150 PRINT "For actions, try get, take, drop, examine, look, use, i[nventory], q[uit]." +4155 PRINT "To examine, pick up, use or drop items, refer to them exactly as they are described." +4157 PRINT +4160 RETURN +5000 REM ========== interactive item descriptions ========== +5010 DIM ID$(IC, 20) +5020 REM medical log +5030 DATA "The last few entries of the medical log are still visible on the screen:", " " +5040 DATA "'2142-6-13: Three days since chimpanzee Nova was innoculated with agent #53." +5050 DATA " However, subject Nova showing no signs of adaptation to planetary destination" +5060 DATA " atmosphere. In fact, she appears listless, though punctuated with periods of" +5070 DATA " extreme agression.", " " +5090 DATA " 2142-6-14: This morning, Dr Pearson was bitten by Nova. Very quickly he exhibited" +5100 DATA " signs of a high fever and is now resting in his quarters.", " " +5120 DATA " 2142-6-15: After a brief period of catatonia, Pearson got up on his own, returned" +5130 DATA " to the medical bay and began to compulsively prepare more agent #53 samples. He" +5140 DATA " is otherwise uncommunicative.", " " +5160 DATA " 2142-6-17: Pearson's compulsive behaviour continues unabated. We have a hypothesis" +5170 DATA " for what the virus, which we have now designated HO-1, does to the brain. However," +5180 DATA " we are struggling to isolate Pearson and may have to seal the medical bay.", " " +5200 DATA " 2142-6-17: Hollow (HO-1) confirmed as airborne, and other cases appearing around the ship." +5210 DATA " Shipwide lockdown declared but crew cohesion and discipline already breaking down.'" +5215 REM porthole +5220 DATA "Looking through the porthole, you seen a spacesuit clad figure floating outside." +5230 DATA "Although he is tethered to an anchor point on the ship's hull, he is otherwise floating" +5240 DATA "freely, his arms and legs splayed out to his sides.", " " +5260 DATA "Peering at the figure more closely, as he slowly rotates, a light from the ship" +5270 DATA "briefly illuminates his face. He's clearly dead, his oxygen ran out some time ago." +5280 DATA "His expression is frozen in a rictus of pain. He was screaming almost until the end ..." +5290 DATA "", "", "", "", "", "", "", "", "", "", "", "", "" +5300 REM console +5310 DATA "The console shows the state of the ship's engines. The pilot" +5320 DATA "appears to have the throttle jammed wide open with his right arm. The muscles in his" +5340 DATA "forearm are taught, there's no way he's going to release the throttle. He left hand" +5350 DATA "works its way around the console switches. After a few moments you realise that he is" +5360 DATA "executing the same sequence of switches over and over again.", " " +5370 DATA "His eyes are glazed, mentally he is somewhere else. He is" +5380 DATA "mumbling to himself but you cannot make out the words, although he appears to be" +5390 DATA "reciting some sort of launch checklist.", " " +5392 DATA "One screen shows the mission parameters:" +5394 DATA " 1. Explore potentially habitable worlds" +5396 DATA " 2. Conduct research to develop gene therapies to aid adaptation to planetary conditions" +5398 DATA " 3. Conduct research to develop custom biologic weapons systems, to protect colonists", " " +5400 DATA "If the engines are online, the ship will be careering through space at maximum" +5410 DATA "speed. Rescue will be impossible unless the engines are offline." +5420 DATA "", "", "" +5430 REM engine control +5440 DATA "The control panel is grubby, smeared with oil and grime. This is clearly" +5450 DATA "the engineering heart of the ship. Most of the readouts mean nothing to you," +5460 DATA "whatever your duties were on this ship, you were clearly not a warp engineer.", " " +5470 DATA "Many of the lights on the front panel are flashing red. Not all is well with" +5480 DATA "the Pneuma. Even to your untrained eye, it's obvious that the ship's engines appear to" +5490 DATA "be on the point of burnout, having been run at full capacity for many hours.", " " +5500 DATA "To the top left of the panel, a screen reads:", " " +5510 DATA "'WARNING: CORE BREACH IMMINENT'", " " +5520 DATA "Just below the screen is a large red button, shielded by a cover that can be" +5530 DATA "flipped aside.", "", "", "", "", "", "" +5540 REM library terminal +5550 DATA "A ancient text is open in the terminal: 'The Origin of Consciousness in the Breakdown" +5560 DATA "of the Bicameral Mind by Julian Jaynes, 1976.'", "", "", "", "", "", "", "", "", "" +5570 DATA "", "", "", "", "", "", "", "", "" +5940 FOR OBJECT = 0 TO IC-1 +5950 FOR I = 0 TO 19 +5960 READ DESC$ : ID$ (OBJECT, I) = DESC$ +5970 NEXT I +5980 NEXT OBJECT +5990 RETURN +6000 REM ========== print interative object description ========== +6010 FOR LINE = 0 TO 19 +6020 IF ID$(F, LINE) <> "" THEN PRINT ID$(F, LINE) +6030 NEXT LINE +6035 PRINT +6040 RETURN +7000 REM ========== character speech ========== +7010 REM chef +7015 DIM PD$(PC, 4) +7020 DATA "Carrots, potatoes, I'm going to make a nice beef stew." +7030 DATA "This stew is going to be delicious." +7060 REM runner +7070 DATA "24:50, that's my new personal best. Not much further to go!" +7080 DATA "24:50, that's my new personal best. Not much further to go!" +7110 REM pilot +7120 DATA "Main engines online, supercruise on my mark." +7130 DATA "Five ... four ... three ... two ... one ... mark!" +7140 FOR PERSON = 0 TO PC-1 +7150 FOR I = 0 TO 1 +7160 READ DESC$ : PD$ (PERSON, I) = DESC$ +7170 NEXT I +7180 PD$(PERSON, 2) = PD$(PERSON, 0) +7190 PD$(PERSON, 3) = PD$(PERSON, 1) +7200 NEXT PERSON +7210 RETURN +7500 REM ========== print character speech ========== +7510 FOR LINE = 0 TO 4 +7520 PRINT PD$(F, LINE) +7530 NEXT LINE +7535 PRINT +7550 RETURN +8000 REM ========== wraith-hound tracking ========== +8010 IF OL ( TRACKER ) <> INV THEN RETURN +8015 IF WPL = 99 THEN PRINT "Tracking: Wraith-hound terminated." : GOTO 8030 +8020 PRINT "Tracking: Wraith-hound current location is the "; LO$ ( WPL ) +8030 PRINT +8040 RETURN +8200 REM ========== wraith-hound proximity check ========== +8210 DIR$ = "" +8220 IF VAL ( MID$ ( EX$ ( PL ) , 1 , 2 ) ) = WPL THEN DIR$ = "forward of you" +8230 IF VAL ( MID$ ( EX$ ( PL ) , 3 , 2 ) ) = WPL THEN DIR$ = "aft of you" +8240 IF VAL ( MID$ ( EX$ ( PL ) , 5 , 2 ) ) = WPL THEN DIR$ = "to port" +8250 IF VAL ( MID$ ( EX$ ( PL ) , 7 , 2 ) ) = WPL THEN DIR$ = "to starboard" +8260 IF VAL ( MID$ ( EX$ ( PL ) , 9 , 2 ) ) = WPL THEN DIR$ = "above you" +8270 IF VAL ( MID$ ( EX$ ( PL ) , 11 , 2 ) ) = WPL THEN DIR$ = "below you" +8280 IF DIR$ <> "" THEN PRINT "You hear a snarling, spitting noise "; DIR$; ". The tracker is pinging ..." +8285 PRINT +8290 RETURN +8400 REM ========== wraith-hound movement ========== +8410 REM decide to move one third of the time +8420 MOVE = RNDINT(1, 3) +8430 IF MOVE <> 1 THEN RETURN +8440 REM randomly determine direction +8450 DIR = RNDINT(1, 6) +8460 IF DIR = 1 THEN INDEX = 1 +8470 IF DIR = 2 THEN INDEX = 3 +8480 IF DIR = 3 THEN INDEX = 5 +8490 IF DIR = 4 THEN INDEX = 7 +8500 IF DIR = 5 THEN INDEX = 9 +8510 IF DIR = 6 THEN INDEX = 11 +8520 NWPL = VAL ( MID$ ( EX$ ( WPL ) , INDEX , 2 ) ) : REM potential next location +8530 IF NWPL = 0 THEN GOTO 8450 : REM can't go that way +8540 WPL = NWPL +8550 RETURN +9000 REM ========== use an item ========== +9010 F=-1: R$="" +9020 R$ = MID$(LOWER$(I$), 5) : REM R$ is the requested object +9025 IF R$ <> "red button" THEN GOTO 9030 +9027 GOSUB 9500 +9028 RETURN +9030 REM get the object ID +9040 FOR I= 0 TO OC-1 +9050 IF OB$(I) = R$ THEN F=I : REM object exists +9060 NEXT I +9070 REM can't find the item? +9080 IF F=-1 THEN PRINT "You haven't seen that item." : PRINT : RETURN +9090 IF OL(F) <> 0 THEN PRINT "You haven't picked up that item." : PRINT : RETURN +9100 REM item exists and is in inventory +9110 IF F <> SYRINGE THEN GOTO 9140 +9112 IF OB$( SYRINGE ) = "blood filled syringe" THEN PRINT "The syringe is full." : PRINT : RETURN +9115 IF SUIT_WORN = 1 THEN PRINT "You can't puncture a spacesuit with a syringe!" : PRINT : RETURN +9120 OB$( SYRINGE ) = "blood filled syringe" +9130 PRINT "You use the syringe to take a blood sample from your arm." : PRINT : RETURN +9140 IF F <> TRACKER THEN GOTO 9160 +9150 PRINT "The tracker is always on." : PRINT +9160 IF F <> SUIT THEN GOTO 9200 +9170 IF OL(F) <> 0 THEN PRINT "You don't have a space suit." : PRINT : RETURN +9175 IF SUIT_WORN = 1 THEN PRINT "You're already wearing the space suit." : PRINT : RETURN +9180 SUIT_WORN = 1 +9190 PRINT "You put on the space suit." : PRINT +9400 RETURN +9500 REM special button routine +9510 IF PL <> ENG THEN PRINT "There is no red button here." : PRINT : RETURN +9520 IF SHUTDOWN = 0 THEN GOTO 9540 +9525 SHUTDOWN = 0 +9530 PRINT "Engine restart initiated ... engines online." : PRINT +9535 ID$ (ENGINE, 10) = "'WARNING: CORE BREACH IMMINENT'" : RETURN +9540 SHUTDOWN = 1 +9550 PRINT "Engine shutdown initiated ... engines offline." : PRINT +9555 ID$ (ENGINE, 10) = "'ENGINES OFFLINE'" +9560 RETURN +9700 REM ========== laboratory conversation ========== +9710 PRINT "Dr Lascoe is stood in the laboratory. He is armed. He speaks to you." : PRINT +9720 PRINT "'It's you! You were infected with HO-1, but you seem to be normal." +9730 PRINT " Hollow destroys consciousness, it shuts down all self awareness in the brain." +9740 PRINT " The patient still functions to a degree, but they're an automaton, endlessly" +9750 PRINT " running the same mental subroutines like a piece of clockwork until the body" +9760 PRINT " breaks down.'" : PRINT +9770 PRINT "'I've been hiding in here to avoid contagion. Someone released the wraith-hound" +9780 PRINT " from containment, but I've been able to keep it away with this weapon." +9790 PRINT " But if you're immune, I can develop a vaccine from your blood.'" : PRINT +9800 IF OL(SYRINGE) = 0 AND OB$(SYRINGE) = "blood filled syringe" THEN GOTO 9820 +9810 PRINT "'You need to get me a sample of your blood.'" : PRINT : RETURN +9820 PRINT "'Ah, I see you have a blood sample, excellent.'" : PRINT +9830 IF SUIT_WORN = 1 THEN GOTO 9855 +9840 PRINT " I still don't want to take the risk that you are contagious. Put on a space" +9850 PRINT " suit and then I'll let you approach me.'" : PRINT: RETURN +9855 PRINT "'Good, you're wearing a suit to avoid contaminating me.'" : PRINT +9860 IF SHUTDOWN = 1 THEN GOTO 9885 +9870 PRINT "'Rescue will be impossible while the engines are running out of control." +9880 PRINT " You'll need to take them offline.'": PRINT : RETURN +9885 PRINT "'Good job on taking the engines offline, you've saved us both!" +9890 PRINT " I'll work up the serum and we can wait for rescue.'" : PRINT +9900 GOSUB 10000 +9910 RETURN +10000 REM ========== finale ========== +10010 PRINT "Congratulations, you have save Pneuma and helped Dr Lascoe develop" +10020 PRINT "vaccine against HO-1, the Hollow virus." +10030 PRINT : PRINT " THE END" : PRINT +10040 STOP +11000 REM ========== fight ========== +11010 PRINT "The wraith-hound arrives. It is an abomination! A weapon system that" +11020 PRINT "nature would never have created unaided. Everything about it is just" +11030 PRINT "plain wrong on so many levels. One thing is clear, it is designed" +11035 PRINT "purely for hunting and killing." : PRINT +11040 INPUT "Do you r[un] or f[ight]?"; I$ +11050 PRINT +11060 IF LOWER$ ( I$ ) = "f" OR LOWER$ ( I$ ) = "fight" THEN GOTO 11190 +11065 IF LOWER$ ( I$ ) = "r" OR LOWER$ ( I$ ) = "run" THEN GOTO 11070 +11067 PRINT "Command unrecognised, you must fight!" : PRINT : GOTO 11190 +11070 REM run away to a random adjoining room +11080 DIR = RNDINT(1, 6) +11090 IF DIR = 1 THEN INDEX = 1 +11100 IF DIR = 2 THEN INDEX = 3 +11110 IF DIR = 3 THEN INDEX = 5 +11120 IF DIR = 4 THEN INDEX = 7 +11130 IF DIR = 5 THEN INDEX = 9 +11140 IF DIR = 6 THEN INDEX = 11 +11150 NPL = VAL ( MID$ ( EX$ ( PL ) , INDEX , 2 ) ) : REM potential next location +11160 IF NPL = 0 THEN GOTO 11080 : REM can't go that way +11170 PL = NPL : PRINT "You are in the " ; LO$ ( PL ) : PRINT +11175 GOSUB 4010 +11180 RETURN +11190 REM fight +11200 FEROCITY = 60 : REM ferocity factor for wraith-hound +11205 STAMINA = 50 : REM your stamina +11210 IF OL(PULSE) <> INV AND OL(KNIFE) <> INV THEN PRINT "You have no weapons, and must fight bare handed." : PRINT : GOTO 11280 +11220 IF OL(PULSE) = INV AND OL(KNIFE) <> INV THEN PRINT "Luckily, you have a pulse rifle." : PRINT: FEROCITY=50 : GOTO 11280 +11230 IF OL(PULSE) <> INV AND OL(KNIFE) = INV THEN PRINT "You only have a knife with which to fight." : PRINT : FEROCITY = 55 : GOTO 11280 +11240 INPUT "Which weapon do you choose? 1 - Pulse rifle, 2 - Knife: "; WPN +11245 PRINT +11250 IF WPN<1 OR WPN>2 THEN GOTO 11240 +11260 IF WPN=1 THEN FEROCITY = 50 +11270 IF WPN=2 THEN FEROCITY = 55 +11280 REM the battle +11290 IF RND(1)>0.5 THEN PRINT "The wraith-hound attacks!" ELSE PRINT "You attack!" +11300 PRINT +11305 GOSUB 11430 +11310 IF RND(1)>0.5 THEN PRINT "You wound the creature." : PRINT: FEROCITY = FEROCITY-5 : GOTO 11330 +11320 PRINT "The wraith-hound wounds you." : PRINT : STAMINA = STAMINA-5 +11330 GOSUB 11430 +11340 IF FEROCITY > 0 THEN GOTO 11390 +11350 PRINT "You slayed the wraith-hound!" : PRINT +11360 REM remove creature from the game +11370 WPL = 99 : REM inaccessible location +11380 RETURN +11390 IF STAMINA > 0 THEN GOTO 11420 +11400 PRINT "You were slayed by the wraith-hound! You have failed to save the Pneuma." : PRINT +11410 STOP +11420 GOTO 11280 : REM next round of the battle +11430 REM ========== delay loop ========== +11440 FOR T = 1 TO 80000 +11450 NEXT T +11460 RETURN + + + + + + + + diff --git a/examples/pi_spigot.bas b/examples/pi_spigot.bas new file mode 100644 index 0000000..a727b09 --- /dev/null +++ b/examples/pi_spigot.bas @@ -0,0 +1,22 @@ +100 REM PI SPIGOT ALGORITHM +110 INPUT "How many digits?: ";N +120 L=INT(10*N/3)+1 +130 DIM A(300) +140 FOR I=1 TO L +150 A(I)=2 +160 NEXT I +170 FOR J=1 TO N +180 Q=0 +190 FOR I=L TO 1 STEP -1 +200 X=10*A(I)+Q*I +210 D=2*I-1 +220 A(I)=X-D*INT(X/D) +230 Q=INT(X/D) +240 NEXT I +250 A(1)=Q-10*INT(Q/10) +260 PRINT STR$(INT(Q/10)); +270 IF J=1 THEN PRINT "."; +280 NEXT J +290 PRINT +300 END + diff --git a/examples/regression.bas b/examples/regression.bas index 554d08d..14ff75e 100644 --- a/examples/regression.bas +++ b/examples/regression.bas @@ -57,8 +57,22 @@ 570 PRINT "The next line should say 'Hello World!'" 580 FSEEK #2,10 590 INPUT #2,A$ -600 PRINT A$ -610 OPEN "NOFILE.X7Z" FOR INPUT AS #2 ELSE 620 +595 PRINT A$ +600 CLOSE #2 +601 OPEN "REGRESSION.TXT" FOR APPEND AS #2 +602 PRINT "3 text lines starting with 0,T,S and ending with !,g,e should follow:" +603 PRINT #2,"Should be a seperate line" +604 CLOSE #2 +605 OPEN "REGRESSION.TXT" FOR INPUT AS #2 +606 FOR I = 1 TO 2 +607 INPUT #2,A$:PRINT A$ +608 NEXT I +609 INPUT #2,A$ +610 FOR I = 1 TO 25 +611 PRINT MID$(A$,I,1); +612 NEXT I +613 PRINT:CLOSE #2 +614 OPEN "NOFILE.X7Z" FOR INPUT AS #2 ELSE 620 615 PRINT "***This Message should NOT be Displayed***" 620 N = 0 630 I = 7 diff --git a/examples/while_tests.bas b/examples/while_tests.bas new file mode 100644 index 0000000..34fa98f --- /dev/null +++ b/examples/while_tests.bas @@ -0,0 +1,160 @@ +1 REM =============================================== +2 REM WHILE-WEND LOOP COMPREHENSIVE TEST SUITE +3 REM =============================================== +4 REM +5 REM This program tests all aspects of WHILE-WEND +6 REM loop functionality in PyBasic +7 REM =============================================== +8 REM +10 PRINT "=== WHILE-WEND Loop Test Suite ===" +20 PRINT "" +21 REM +22 REM =============================================== +23 REM Test 1: Basic WHILE Loop +24 REM =============================================== +30 PRINT "Test 1: Basic WHILE Loop" +40 PRINT "Expected: Count 1 through 5" +50 PRINT "------------------------" +60 LET I = 1 +70 WHILE I <= 5 +80 PRINT "Count: "; I +90 LET I = I + 1 +100 WEND +110 PRINT "Basic WHILE loop completed" +120 PRINT "" +121 REM +122 REM =============================================== +123 REM Test 2: WHILE Loop with False Condition +124 REM =============================================== +130 PRINT "Test 2: WHILE Loop Skip Test" +140 PRINT "Expected: Only skip message, no loop output" +150 PRINT "----------------------------------------" +160 LET X = 10 +170 WHILE X < 5 +180 PRINT "This should NOT print!" +190 WEND +200 PRINT "WHILE loop was correctly skipped" +210 PRINT "" +211 REM +212 REM =============================================== +213 REM Test 3: Nested WHILE Loops +214 REM =============================================== +220 PRINT "Test 3: Nested WHILE Loops" +230 PRINT "Expected: Outer 1-3, Inner 1-2 for each" +240 PRINT "-------------------------------------" +250 LET I = 1 +260 WHILE I <= 3 +270 PRINT "Outer loop: "; I +280 LET J = 1 +290 WHILE J <= 2 +300 PRINT " Inner loop: "; J +310 LET J = J + 1 +320 WEND +330 LET I = I + 1 +340 WEND +350 PRINT "Nested WHILE loops completed" +360 PRINT "" +361 REM +362 REM =============================================== +363 REM Test 4: WHILE with Complex Conditions +364 REM =============================================== +370 PRINT "Test 4: Complex Condition Test" +380 PRINT "Expected: A=1,B=5 through A=3,B=5" +390 PRINT "--------------------------------" +400 LET A = 1 +410 LET B = 5 +420 WHILE A < B AND A < 4 +430 PRINT "A = "; A; ", B = "; B +440 LET A = A + 1 +450 WEND +460 PRINT "Complex condition test completed" +470 PRINT "" +471 REM +472 REM =============================================== +473 REM Test 5: WHILE Loop with Countdown +474 REM =============================================== +480 PRINT "Test 5: Countdown WHILE Loop" +490 PRINT "Expected: Countdown from 5 to 1" +500 PRINT "-----------------------------" +510 LET COUNT = 5 +520 WHILE COUNT > 0 +530 PRINT "Countdown: "; COUNT +540 LET COUNT = COUNT - 1 +550 WEND +560 PRINT "Countdown completed!" +570 PRINT "" +571 REM +572 REM =============================================== +573 REM Test 6: WHILE with String Variables +574 REM =============================================== +580 PRINT "Test 6: WHILE with String Conditions" +590 PRINT "Expected: Process items until STOP" +600 PRINT "--------------------------------" +610 LET ITEM$ = "START" +620 LET COUNTER = 0 +630 WHILE ITEM$ <> "STOP" +640 LET COUNTER = COUNTER + 1 +650 PRINT "Processing item "; COUNTER +660 IF COUNTER = 3 THEN LET ITEM$ = "STOP" +670 WEND +680 PRINT "String condition test completed" +690 PRINT "" +691 REM +692 REM =============================================== +693 REM Test 7: Mixed FOR and WHILE Loops +694 REM =============================================== +700 PRINT "Test 7: Mixed FOR and WHILE Loops" +710 PRINT "Expected: FOR loop with nested WHILE" +720 PRINT "--------------------------------" +730 FOR K = 1 TO 2 +740 PRINT "FOR loop iteration: "; K +750 LET M = 1 +760 WHILE M <= 2 +770 PRINT " WHILE iteration: "; M +780 LET M = M + 1 +790 WEND +800 NEXT K +810 PRINT "Mixed loop test completed" +820 PRINT "" +821 REM +822 REM =============================================== +823 REM Test 8: WHILE with Mathematical Operations +824 REM =============================================== +830 PRINT "Test 8: WHILE with Math Operations" +840 PRINT "Expected: Powers of 2 up to 16" +850 PRINT "-----------------------------" +860 LET POWER = 1 +870 WHILE POWER <= 16 +880 PRINT "2^"; LOG(POWER)/LOG(2); " = "; POWER +890 LET POWER = POWER * 2 +900 WEND +910 PRINT "Mathematical operations test completed" +920 PRINT "" +921 REM +922 REM =============================================== +923 REM Final Summary +924 REM =============================================== +930 PRINT "=======================================" +940 PRINT "ALL WHILE-WEND TESTS COMPLETED!" +950 PRINT "If you see this message, all tests" +960 PRINT "have executed successfully!" +970 PRINT "=======================================" +975 REM +976 REM =============================================== +977 REM Test 9: WHILE-WEND Validation Tests +978 REM =============================================== +980 PRINT "Test 9: WHILE-WEND Validation" +985 PRINT "Expected: Tests that require proper structure" +990 PRINT "--------------------------------------------" +1000 PRINT "Note: Missing WEND validation occurs at load time" +1010 PRINT "GOTO breaking WHILE loops causes runtime errors" +1020 PRINT "This ensures proper WHILE-WEND structure" +1030 PRINT "Validation tests completed" +1040 PRINT "" +1050 PRINT "=======================================" +1060 PRINT "ALL WHILE-WEND TESTS COMPLETED!" +1070 PRINT "VALIDATION: Missing WEND detected at load" +1080 PRINT "VALIDATION: GOTO in WHILE causes error" +1090 PRINT "All features working correctly!" +1100 PRINT "=======================================" +1110 END \ No newline at end of file diff --git a/flowsignal.py b/flowsignal.py index b966c1d..65f7bae 100644 --- a/flowsignal.py +++ b/flowsignal.py @@ -21,12 +21,14 @@ return address need to be added to the return stack. >>> flowsignal = FlowSignal(ftype=FlowSignal.RETURN) ->>> print(flowsignal.ftarget) --1 +>>> flowsignal.ftarget is None +True +>>> flowsignal.ftype +5 >>> flowsignal = FlowSignal(ftarget=100, ftype=FlowSignal.SIMPLE_JUMP) ->>> print(flowsignal.ftarget) +>>> flowsignal.ftarget 100 ->>> print(flowsignal.ftype) +>>> flowsignal.ftype 0 """ @@ -76,6 +78,18 @@ class FlowSignal: # Indicates that a conditional result block should be executed EXECUTE = 7 + # Indicates the start of a WHILE loop where the condition + # is true and the loop body should be executed + WHILE_BEGIN = 8 + + # Indicates the end of a WHILE loop and that execution should + # return to the beginning of the loop to re-evaluate the condition + WHILE_REPEAT = 9 + + # Indicates that a WHILE loop should be skipped because + # the condition is false + WHILE_SKIP = 10 + def __init__(self, ftarget=None, ftype=SIMPLE_JUMP, floop_var=None): """Creates a new FlowSignal for a branch. If the jump target is supplied, then the branch is assumed to be @@ -93,7 +107,8 @@ def __init__(self, ftarget=None, ftype=SIMPLE_JUMP, floop_var=None): if ftype not in [self.GOSUB, self.SIMPLE_JUMP, self.LOOP_BEGIN, self.LOOP_REPEAT, self.RETURN, - self.LOOP_SKIP, self.STOP, self.EXECUTE]: + self.LOOP_SKIP, self.STOP, self.EXECUTE, + self.WHILE_BEGIN, self.WHILE_REPEAT, self.WHILE_SKIP]: raise TypeError("Invalid flow signal type supplied: " + str(ftype)) if ftarget == None and \ @@ -102,9 +117,12 @@ def __init__(self, ftarget=None, ftype=SIMPLE_JUMP, floop_var=None): if ftarget != None and \ ftype in [self.RETURN, self.LOOP_BEGIN, self.LOOP_REPEAT, - self.STOP, self.EXECUTE]: + self.STOP, self.EXECUTE, self.WHILE_BEGIN, + self.WHILE_REPEAT]: raise TypeError("Target wrongly supplied for flow signal " + str(ftype)) self.ftype = ftype self.ftarget = ftarget - self.floop_var = floop_var \ No newline at end of file + + self.floop_var = floop_var + diff --git a/interpreter.py b/interpreter.py index caddf20..5e5ab9c 100644 --- a/interpreter.py +++ b/interpreter.py @@ -31,15 +31,13 @@ def main(): - banner = ( - """ - PPPP Y Y BBBB AAA SSSS I CCC - P P Y Y B B A A S I C - P P Y Y B B A A S I C - PPPP Y BBBB AAAAA SSSS I C - P Y B B A A S I C - P Y B B A A S I C - P Y BBBB A A SSSS I CCC + banner = (r""" + ._____________ ___________. ___ ___________ ______ + | _ .__ \ / ___ _ \ / \ / | / | + | |_) | \ \/ / | |_) | / ^ \ | (----`| | | ,----' + | ___/ \_ _/ | _ < / /_\ \ \ \ | | | | + | | | | | |_) \/ _____ \----) | | | | `----. + | _| |__| |___________/ \_________/ |____________| """) print(banner) @@ -119,6 +117,46 @@ def main(): elif tokenlist[0].category == Token.NEW: program.delete() + # Renumber the program + elif tokenlist[0].category == Token.RENUMBER: + try: + if len(tokenlist) == 1: + # RENUMBER with no arguments + program.renumber() + else: + # Parse comma-separated arguments, handling blank parameters + args = [] + current_arg = "" + + for i in range(1, len(tokenlist)): + if tokenlist[i].category == Token.COMMA: + # Process the accumulated argument + if current_arg.strip(): + args.append(int(current_arg.strip())) + else: + args.append(None) # Blank parameter + current_arg = "" + elif tokenlist[i].category == Token.UNSIGNEDINT: + current_arg += tokenlist[i].lexeme + + # Process the final argument if any + if current_arg.strip(): + args.append(int(current_arg.strip())) + elif len(tokenlist) > 1 and tokenlist[-1].category == Token.COMMA: + args.append(None) # Trailing comma means blank parameter + + # Convert args list to proper parameters for renumber() + # RENUMBER [newStart][,increment][,oldStart][,oldEnd] + new_start = args[0] if len(args) > 0 and args[0] is not None else 10 + increment = args[1] if len(args) > 1 and args[1] is not None else 10 + old_start = args[2] if len(args) > 2 and args[2] is not None else None + old_end = args[3] if len(args) > 3 and args[3] is not None else None + + program.renumber(new_start, increment, old_start, old_end) + print("Program renumbered") + except Exception as e: + print(f"RENUMBER error: {e}", file=stderr) + # Unrecognised input else: print("Unrecognised input", file=stderr) diff --git a/lexer.py b/lexer.py index bde1234..47da604 100644 --- a/lexer.py +++ b/lexer.py @@ -99,9 +99,12 @@ def tokenize(self, stmt): break # Process numbers - elif c.isdigit(): + elif c.isdigit() or c == '.': token.category = Token.UNSIGNEDINT found_point = False + if c == '.': + token.category = Token.UNSIGNEDFLOAT + found_point = True # Consume all of the digits, including any decimal point while True: @@ -112,7 +115,7 @@ def tokenize(self, stmt): # and this is not the first decimal point if not c.isdigit(): if c == '.': - if not found_point: + if found_point is False: found_point = True token.category = Token.UNSIGNEDFLOAT @@ -196,4 +199,4 @@ def __get_next_char(self): if __name__ == "__main__": import doctest - doctest.testmod() \ No newline at end of file + doctest.testmod() diff --git a/program.py b/program.py index 13d1939..03642d1 100644 --- a/program.py +++ b/program.py @@ -21,7 +21,7 @@ """ -from basictoken import BASICToken as Token +from basictoken import BASICToken as Token, BASICToken from basicparser import BASICParser from flowsignal import FlowSignal from lexer import Lexer @@ -152,6 +152,10 @@ def __init__(self): # return dictionary for loop returns self.__return_loop = {} + # WHILE loop tracking stack - separate from GOSUB return stack + # Each entry contains the line number of the WHILE statement + self.__while_stack = [] + # Setup DATA object self.__data = BASICData() @@ -171,13 +175,31 @@ def str_statement(self, line_number): statement = self.__program[line_number] if statement[0].category == Token.DATA: statement = self.__data.getTokens(line_number) - for token in statement: + + # Track operators and contexts where minus might be unary + operators_and_contexts = { + Token.PLUS, Token.MINUS, Token.TIMES, Token.DIVIDE, Token.MODULO, + Token.ASSIGNOP, Token.EQUAL, Token.NOTEQUAL, Token.GREATER, + Token.LESSER, Token.LESSEQUAL, Token.GREATEQUAL, + Token.LEFTPAREN, Token.COMMA, Token.AND, Token.OR + } + + for i, token in enumerate(statement): # Add in quotes for strings if token.category == Token.STRING: line_text += '"' + token.lexeme + '" ' - else: - line_text += token.lexeme + " " + # Check if this is a minus sign that should be treated as unary + if (token.category == Token.MINUS and + i + 1 < len(statement) and + statement[i + 1].category in {Token.UNSIGNEDINT, Token.UNSIGNEDFLOAT} and + (i == 0 or statement[i - 1].category in operators_and_contexts)): + # This is a unary minus, don't add space after it + line_text += token.lexeme + else: + # Normal token, add space after + line_text += token.lexeme + " " + line_text += "\n" return line_text @@ -292,12 +314,40 @@ def __execute(self, line_number): except RuntimeError as err: raise RuntimeError(str(err)) + def __validate_while_wend(self): + """Validate that all WHILE statements have matching WEND statements""" + while_stack = [] + line_numbers = self.line_numbers() + + for line_num in line_numbers: + statement = self.__program[line_num] + if statement and len(statement) > 0: + if statement[0].category == Token.WHILE: + while_stack.append(line_num) + elif statement[0].category == Token.WEND: + if not while_stack: + raise RuntimeError(f"WEND without matching WHILE at line {line_num}") + while_stack.pop() + + if while_stack: + raise RuntimeError(f"WHILE without matching WEND at line {while_stack[0]}") + + def __clear_while_stack_on_goto(self, current_line, target_line): + """Clear WHILE loop stack when GOTO jumps out of loops""" + # Simple solution: GOTO clears the WHILE stack completely + # This prevents infinite loops caused by corrupted loop state + # Traditional BASIC behavior - GOTO breaks loop structures + self.__while_stack.clear() + def execute(self): """Execute the program""" self.__parser = BASICParser(self.__data) self.__data.restore(0) # reset data pointer + # Validate WHILE-WEND pairs before execution + self.__validate_while_wend() + line_numbers = self.line_numbers() if len(line_numbers) > 0: @@ -318,6 +368,11 @@ def execute(self): if flowsignal: if flowsignal.ftype == FlowSignal.SIMPLE_JUMP: # GOTO or conditional branch encountered + # Clear WHILE loop stack if we're jumping out of loops + target_line = flowsignal.ftarget + current_line = self.get_next_line_number() + self.__clear_while_stack_on_goto(current_line, target_line) + try: index = line_numbers.index(flowsignal.ftarget) @@ -429,6 +484,71 @@ def execute(self): self.set_next_line_number(line_numbers[index]) + elif flowsignal.ftype == FlowSignal.WHILE_BEGIN: + # WHILE loop start encountered + # Put loop line number on the WHILE stack so + # that it can be returned to when the loop repeats + self.__while_stack.append(line_numbers[index]) + + # Continue to the next statement in the loop + index = index + 1 + + if index < len(line_numbers): + self.set_next_line_number(line_numbers[index]) + + else: + # Reached end of program + raise RuntimeError("Program terminated within a WHILE loop") + + elif flowsignal.ftype == FlowSignal.WHILE_SKIP: + # WHILE condition is false, so skip + # all statements within loop and move past the corresponding + # WEND statement + index = index + 1 + wend_count = 0 + while index < len(line_numbers): + next_line_number = line_numbers[index] + temp_tokenlist = self.__program[next_line_number] + + if temp_tokenlist[0].category == Token.WHILE: + # Found nested WHILE, increment counter + wend_count += 1 + elif temp_tokenlist[0].category == Token.WEND: + if wend_count == 0: + # Found matching WEND statement + # Move to the statement after this WEND, if there is one + index = index + 1 + if index < len(line_numbers): + next_line_number = line_numbers[index] # Statement after the WEND + self.set_next_line_number(next_line_number) + break + else: + # This WEND belongs to a nested WHILE + wend_count -= 1 + + index = index + 1 + + # Check we have not reached end of program + if index >= len(line_numbers): + # Terminate the program + break + + elif flowsignal.ftype == FlowSignal.WHILE_REPEAT: + # WHILE repeat encountered (WEND statement) + # Pop the loop start address from the WHILE stack + try: + index = line_numbers.index(self.__while_stack.pop()) + + except ValueError: + raise RuntimeError("Invalid WHILE loop exit in line " + + str(self.get_next_line_number())) + + except IndexError: + raise RuntimeError("WEND encountered without corresponding " + + "WHILE loop in line " + str(self.get_next_line_number())) + + self.set_next_line_number(line_numbers[index]) + else: index = index + 1 @@ -479,3 +599,187 @@ def set_next_line_number(self, line_number): """ self.__next_stmt = line_number + + def renumber(self, new_start=10, increment=10, old_start=None, old_end=None): + """Renumber the program according to BASIC RENUMBER command specification + + :param new_start: First line number assigned during renumbering (default: 10) + :param increment: Amount added to each successive line number (default: 10) + :param old_start: Lowest line number to renumber (default: first line) + :param old_end: Highest line number to renumber (default: last line) + """ + + # Validate parameters + if increment == 0: + raise ValueError("Increment cannot be zero") + if new_start < 1: + raise ValueError("New start line number must be >= 1") + if increment < 0: + raise ValueError("Increment must be positive") + + line_numbers = self.line_numbers() + if not line_numbers: + return # No program to renumber + + # Set defaults for old_start and old_end + if old_start is None: + old_start = line_numbers[0] + if old_end is None: + old_end = line_numbers[-1] + + # Find lines within the renumbering range + lines_to_renumber = [] + for line_num in line_numbers: + if old_start <= line_num <= old_end: + lines_to_renumber.append(line_num) + + if not lines_to_renumber: + return # No lines in range to renumber + + # Step 1: Create mapping of old line numbers to new line numbers + line_mapping = {} + new_line_num = new_start + + for old_line_num in lines_to_renumber: + line_mapping[old_line_num] = new_line_num + new_line_num += increment + + # Check for overflow (basic line numbers typically max at 65535) + if new_line_num - increment > 65535: + raise ValueError("Line numbers would exceed maximum value (65535)") + + # Check for conflicts with existing line numbers outside the range + for new_num in line_mapping.values(): + if new_num in line_numbers and new_num not in lines_to_renumber: + # Find which old line this conflicts with + for ln in line_numbers: + if ln == new_num and ln not in lines_to_renumber: + raise ValueError(f"New line number {new_num} conflicts with existing line {ln}") + + # Step 2: Update line number references in all program statements + updated_program = {} + updated_data = {} + + for line_num in line_numbers: + statement = self.__program[line_num] + + # Update line number references within the statement + updated_statement = self._update_line_references(statement, line_mapping) + + # Determine the new line number for this statement + if line_num in line_mapping: + new_line_num = line_mapping[line_num] + else: + new_line_num = line_num + + updated_program[new_line_num] = updated_statement + + # Handle DATA statements + if statement and statement[0].category == Token.DATA: + data_tokens = self.__data.getTokens(line_num) + if data_tokens: + updated_data[new_line_num] = data_tokens + + # Step 3: Replace the program with the updated version + self.__program = updated_program + + # Update DATA storage + self.__data.delete() + for line_num, data_tokens in updated_data.items(): + self.__data.addData(line_num, data_tokens) + + def _update_line_references(self, statement, line_mapping): + """Update line number references within a statement + + :param statement: List of tokens representing the statement + :param line_mapping: Dictionary mapping old line numbers to new ones + :return: Updated statement with new line number references + """ + if not statement: + return statement + + updated_statement = [] + i = 0 + + while i < len(statement): + token = statement[i] + + # Skip string literals and comments - they should not be modified + if token.category == Token.STRING: + updated_statement.append(token) + i += 1 + continue + + if token.category == Token.REM: + # Everything after REM is a comment, copy rest as-is + updated_statement.extend(statement[i:]) + break + + # Check for line number references in specific contexts + if self._is_line_number_reference(statement, i): + if token.category == Token.UNSIGNEDINT: + line_num = int(token.lexeme) + if line_num in line_mapping: + # Create new token with updated line number + new_token = BASICToken(token.column, token.category, str(line_mapping[line_num])) + updated_statement.append(new_token) + else: + updated_statement.append(token) + else: + updated_statement.append(token) + else: + updated_statement.append(token) + + i += 1 + + return updated_statement + + def _is_line_number_reference(self, statement, token_index): + """Determine if the token at the given index is a line number reference + + :param statement: List of tokens representing the statement + :param token_index: Index of the token to check + :return: True if this token is a line number reference + """ + if token_index >= len(statement): + return False + + token = statement[token_index] + if token.category != Token.UNSIGNEDINT: + return False + + # Check what comes before this number to determine context + if token_index == 0: + return False # First token is the line number itself, not a reference + + prev_token = statement[token_index - 1] + + # Direct line number references + if prev_token.category in [Token.GOTO, Token.GOSUB, Token.THEN, Token.RESTORE]: + return True + + # ON...GOTO and ON...GOSUB constructs + if prev_token.category == Token.COMMA: + # Look backward to find ON...GOTO or ON...GOSUB + for j in range(token_index - 2, -1, -1): + if statement[j].category == Token.ON: + # Check if this is followed by GOTO or GOSUB + for k in range(j + 1, token_index): + if statement[k].category in [Token.GOTO, Token.GOSUB]: + return True + break + elif statement[j].category not in [Token.UNSIGNEDINT, Token.COMMA, Token.NAME]: + break + + # Check for ON...GOTO/GOSUB patterns + if token_index >= 2: + # Look for pattern: ON GOTO/GOSUB + for j in range(token_index - 1, -1, -1): + if statement[j].category == Token.ON: + # Found ON, now look for GOTO or GOSUB before our number + for k in range(j + 1, token_index): + if statement[k].category in [Token.GOTO, Token.GOSUB]: + return True + break + + return False