Created
September 18, 2024 17:06
-
-
Save sma/8a59ca3db6eaa16b8a41735b2da4768d to your computer and use it in GitHub Desktop.
A Basic interpreter thrown together with ChatGPT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:flutter/material.dart'; | |
void main() => runApp(const BasicInterpreterApp()); | |
class BasicInterpreterApp extends StatelessWidget { | |
const BasicInterpreterApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp( | |
home: BasicInterpreterWidget(), | |
); | |
} | |
} | |
class BasicInterpreterWidget extends StatefulWidget { | |
const BasicInterpreterWidget({super.key}); | |
@override | |
State<BasicInterpreterWidget> createState() => _BasicInterpreterWidgetState(); | |
} | |
class _BasicInterpreterWidgetState extends State<BasicInterpreterWidget> { | |
final TextEditingController _controller = TextEditingController(); | |
final ScrollController _scrollController = ScrollController(); | |
String _output = ''; | |
String _inputBuffer = ''; | |
bool _isRunning = false; | |
bool _isWaitingInput = false; | |
Completer<void>? _inputCompleter; | |
final Map<int, String> _program = {}; | |
final Map<String, num> _variables = {}; | |
int _currentLineIndex = 0; | |
List<int> _sortedLineNumbers = []; | |
@override | |
void initState() { | |
super.initState(); | |
_output = 'Flutter Basic. 38911 bytes free.\nType help for help.\n'; | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
_scrollController.dispose(); | |
super.dispose(); | |
} | |
void _handleInput() async { | |
String inputLine = _controller.text.trim(); | |
_controller.clear(); | |
setState(() { | |
_output += '\n> $inputLine'; | |
}); | |
if (_isWaitingInput) { | |
_inputBuffer = inputLine; | |
_inputCompleter?.complete(); | |
_isWaitingInput = false; | |
return; | |
} | |
if (_isRunning) { | |
setState(() { | |
_output += '\nProgram is running. Please wait.'; | |
}); | |
return; | |
} | |
await _processInput(inputLine); | |
_scrollToBottom(); | |
} | |
Future<void> _processInput(String inputLine) async { | |
String lowerInput = inputLine.toLowerCase(); | |
if (lowerInput == 'new') { | |
_program.clear(); | |
setState(() { | |
_output = 'Program cleared.'; | |
}); | |
} else if (lowerInput == 'run') { | |
_isRunning = true; | |
_variables.clear(); | |
_sortedLineNumbers = _program.keys.toList()..sort(); | |
_currentLineIndex = 0; | |
await _runProgram(); | |
_isRunning = false; | |
} else if (lowerInput == 'list') { | |
_listProgram(); | |
} else if (lowerInput == 'help') { | |
_showHelp(); | |
} else if (lowerInput.startsWith('edit ')) { | |
int lineNumber = int.tryParse(lowerInput.substring(5).trim()) ?? -1; | |
if (_program.containsKey(lineNumber)) { | |
String code = _program[lineNumber]!; | |
_controller.text = '$lineNumber $code'; | |
_controller.selection = TextSelection.fromPosition( | |
TextPosition(offset: _controller.text.length), | |
); | |
} else { | |
setState(() { | |
_output += '\nLine $lineNumber does not exist.'; | |
}); | |
} | |
} else { | |
RegExp lineRegex = RegExp(r'^(\d+)\s*(.*)'); | |
Match? match = lineRegex.firstMatch(inputLine); | |
if (match != null) { | |
int lineNumber = int.parse(match.group(1)!); | |
String code = match.group(2)?.trim() ?? ''; | |
if (code.isEmpty) { | |
_program.remove(lineNumber); | |
setState(() { | |
_output += '\nDeleted line $lineNumber'; | |
}); | |
} else { | |
_program[lineNumber] = code; | |
setState(() { | |
_output += '\n$lineNumber $code'; | |
}); | |
} | |
} else { | |
setState(() { | |
_output += '\nInvalid input.'; | |
}); | |
} | |
} | |
} | |
void _showHelp() { | |
setState(() { | |
_output += '\nAvailable commands:'; | |
_output += '\n- new: Clears the current program.'; | |
_output += '\n- run: Runs the current program.'; | |
_output += '\n- list: Lists the current program.'; | |
_output += '\n- edit <line number>: Edits the specified line.'; | |
_output += '\n- help: Shows this help message.'; | |
_output += '\nTo add or modify a line, type: <line number> <code>'; | |
_output += '\nTo delete a line, type the line number without code.'; | |
_output += '\n\nSupported BASIC commands in programs:'; | |
_output += '\n- let: Assign a value to a variable (e.g., LET x = 5)'; | |
_output += '\n- print: Print text or variables (e.g., PRINT "Hello", PRINT x)'; | |
_output += '\n- input: Read a number from user input (e.g., INPUT x)'; | |
_output += '\n- if ... then ...: Conditional execution (e.g., IF x > 0 THEN PRINT x)'; | |
_output += '\n- goto: Jump to a specified line number (e.g., GOTO 10)'; | |
_output += '\n- for ... to ...: Loop from a start to end value (e.g., FOR i = 1 TO 10)'; | |
_output += '\n- next: Ends a for loop (e.g., NEXT)'; | |
_output += '\n- end: Ends the program execution.'; | |
}); | |
} | |
void _listProgram() { | |
setState(() { | |
List<int> lines = _program.keys.toList()..sort(); | |
for (var line in lines) { | |
_output += '\n$line ${_program[line]}'; | |
} | |
}); | |
} | |
Future<void> _runProgram() async { | |
while (_currentLineIndex < _sortedLineNumbers.length) { | |
int lineNumber = _sortedLineNumbers[_currentLineIndex]; | |
String? codeLine = _program[lineNumber]; | |
if (codeLine != null) { | |
bool shouldContinue = await _executeLine(codeLine); | |
if (!shouldContinue) break; | |
} | |
_currentLineIndex++; | |
} | |
} | |
Future<bool> _executeLine(String codeLine) async { | |
codeLine = codeLine.trim(); | |
if (codeLine.toLowerCase() == 'end') { | |
return false; | |
} else if (codeLine.toLowerCase().startsWith('print')) { | |
await _handlePrint(codeLine.substring(5).trim()); | |
} else if (codeLine.toLowerCase().startsWith('input')) { | |
await _handleInputCommand(codeLine.substring(5).trim()); | |
} else if (codeLine.toLowerCase().startsWith('let')) { | |
_handleLet(codeLine.substring(3).trim()); | |
} else if (codeLine.toLowerCase().startsWith('if')) { | |
await _handleIfThen(codeLine.substring(2).trim()); | |
} else if (codeLine.toLowerCase().startsWith('for')) { | |
await _handleForLoop(codeLine.substring(3).trim()); | |
} else if (codeLine.toLowerCase().startsWith('next')) { | |
// 'next' is handled in _handleForLoop | |
} else if (codeLine.toLowerCase().startsWith('goto')) { | |
await _handleGoto(codeLine.substring(4).trim()); | |
} else { | |
setState(() { | |
_output += '\nSyntax error at line ${_sortedLineNumbers[_currentLineIndex]}'; | |
}); | |
} | |
return true; | |
} | |
Future<void> _handlePrint(String expr) async { | |
expr = expr.trim(); | |
if (expr.startsWith('"') && expr.endsWith('"')) { | |
String text = expr.substring(1, expr.length - 1); | |
setState(() { | |
_output += '\n$text'; | |
}); | |
} else { | |
num value = _evaluateExpression(expr); | |
setState(() { | |
_output += '\n$value'; | |
}); | |
} | |
} | |
Future<void> _handleInputCommand(String varName) async { | |
setState(() { | |
_output += '\nEnter value for $varName: '; | |
}); | |
_isWaitingInput = true; | |
_inputCompleter = Completer<void>(); | |
await _inputCompleter!.future; | |
num? value = num.tryParse(_inputBuffer); | |
if (value == null) { | |
setState(() { | |
_output += '\nInvalid number. Using 0.'; | |
}); | |
value = 0; | |
} | |
_variables[varName] = value; | |
} | |
void _handleLet(String expr) { | |
List<String> parts = expr.split('='); | |
if (parts.length != 2) { | |
setState(() { | |
_output += '\nSyntax error in LET statement.'; | |
}); | |
return; | |
} | |
String varName = parts[0].trim(); | |
num value = _evaluateExpression(parts[1].trim()); | |
_variables[varName] = value; | |
} | |
Future<void> _handleIfThen(String expr) async { | |
RegExp thenRegex = RegExp(r'\bthen\b', caseSensitive: false); | |
List<String> parts = expr.split(thenRegex); | |
if (parts.length != 2) { | |
setState(() { | |
_output += '\nSyntax error in IF statement.'; | |
}); | |
return; | |
} | |
bool condition = _evaluateCondition(parts[0].trim()); | |
if (condition) { | |
await _executeLine(parts[1].trim()); | |
} | |
} | |
Future<void> _handleForLoop(String expr) async { | |
RegExp forRegex = RegExp(r'^(\w+)\s*=\s*(.+)\s+to\s+(.+)$', caseSensitive: false); | |
Match? match = forRegex.firstMatch(expr); | |
if (match == null) { | |
setState(() { | |
_output += '\nSyntax error in FOR statement.'; | |
}); | |
return; | |
} | |
String varName = match.group(1)!; | |
num startValue = _evaluateExpression(match.group(2)!); | |
num endValue = _evaluateExpression(match.group(3)!); | |
_variables[varName] = startValue; | |
int loopStartLineIndex = _currentLineIndex; | |
while (_variables[varName]! <= endValue) { | |
_currentLineIndex++; | |
while (_currentLineIndex < _sortedLineNumbers.length && | |
!_program[_sortedLineNumbers[_currentLineIndex]]!.toLowerCase().startsWith('next')) { | |
String? codeLine = _program[_sortedLineNumbers[_currentLineIndex]]; | |
if (codeLine != null) { | |
bool shouldContinue = await _executeLine(codeLine); | |
if (!shouldContinue) return; | |
} | |
_currentLineIndex++; | |
} | |
_variables[varName] = _variables[varName]! + 1; | |
if (_variables[varName]! <= endValue) { | |
_currentLineIndex = loopStartLineIndex; | |
} | |
} | |
// Skip the 'next' line | |
} | |
Future<void> _handleGoto(String expr) async { | |
int targetLine = int.tryParse(expr) ?? -1; | |
if (_program.containsKey(targetLine)) { | |
_currentLineIndex = _sortedLineNumbers.indexOf(targetLine) - 1; | |
} else { | |
setState(() { | |
_output += '\nLine $targetLine does not exist.'; | |
}); | |
} | |
} | |
// Declare these variables at the class level within your state class | |
late String _expr; | |
late int _pos; | |
num _evaluateExpression(String expr) { | |
expr = expr.trim(); | |
_expr = expr; | |
_pos = 0; | |
try { | |
num value = _parseExpression(); | |
return value; | |
} catch (e) { | |
setState(() { | |
_output += '\nError evaluating expression: $expr'; | |
}); | |
return 0; | |
} | |
} | |
void _skipWhitespace() { | |
while (_pos < _expr.length && _expr[_pos] == ' ') { | |
_pos++; | |
} | |
} | |
num _parseExpression() { | |
num value = _parseTerm(); | |
while (true) { | |
_skipWhitespace(); | |
if (_pos >= _expr.length) break; | |
String op = _expr[_pos]; | |
if (op == '+') { | |
_pos++; | |
num term = _parseTerm(); | |
value += term; | |
} else if (op == '-') { | |
_pos++; | |
num term = _parseTerm(); | |
value -= term; | |
} else { | |
break; | |
} | |
} | |
return value; | |
} | |
num _parseTerm() { | |
num value = _parseFactor(); | |
while (true) { | |
_skipWhitespace(); | |
if (_pos >= _expr.length) break; | |
String op = _expr[_pos]; | |
if (op == '*') { | |
_pos++; | |
num factor = _parseFactor(); | |
value *= factor; | |
} else if (op == '/') { | |
_pos++; | |
num factor = _parseFactor(); | |
value /= factor; | |
} else { | |
break; | |
} | |
} | |
return value; | |
} | |
num _parseFactor() { | |
_skipWhitespace(); | |
if (_pos >= _expr.length) { | |
throw Exception('Unexpected end of expression'); | |
} | |
String currentChar = _expr[_pos]; | |
if (currentChar == '(') { | |
_pos++; | |
num value = _parseExpression(); | |
_skipWhitespace(); | |
if (_pos >= _expr.length || _expr[_pos] != ')') { | |
throw Exception('Expected closing parenthesis'); | |
} | |
_pos++; | |
return value; | |
} else if (currentChar == '+' || currentChar == '-') { | |
// Handle unary plus and minus | |
String op = currentChar; | |
_pos++; | |
num value = _parseFactor(); | |
return op == '-' ? -value : value; | |
} else if (isDigit(currentChar) || currentChar == '.') { | |
return _parseNumber(); | |
} else if (isLetter(currentChar)) { | |
return _parseVariable(); | |
} else { | |
throw Exception('Unexpected character: $currentChar'); | |
} | |
} | |
num _parseNumber() { | |
int start = _pos; | |
while (_pos < _expr.length && (isDigit(_expr[_pos]) || _expr[_pos] == '.')) { | |
_pos++; | |
} | |
String numberString = _expr.substring(start, _pos); | |
num? value = num.tryParse(numberString); | |
if (value == null) { | |
throw Exception('Invalid number: $numberString'); | |
} | |
return value; | |
} | |
num _parseVariable() { | |
int start = _pos; | |
while (_pos < _expr.length && isLetterOrDigit(_expr[_pos])) { | |
_pos++; | |
} | |
String varName = _expr.substring(start, _pos); | |
if (_variables.containsKey(varName)) { | |
return _variables[varName]!; | |
} else { | |
throw Exception('Undefined variable: $varName'); | |
} | |
} | |
bool isDigit(String s) { | |
return s.codeUnitAt(0) >= '0'.codeUnitAt(0) && s.codeUnitAt(0) <= '9'.codeUnitAt(0); | |
} | |
bool isLetter(String s) { | |
int codeUnit = s.codeUnitAt(0); | |
return (codeUnit >= 'a'.codeUnitAt(0) && codeUnit <= 'z'.codeUnitAt(0)) || | |
(codeUnit >= 'A'.codeUnitAt(0) && codeUnit <= 'Z'.codeUnitAt(0)); | |
} | |
bool isLetterOrDigit(String s) { | |
return isLetter(s) || isDigit(s); | |
} | |
bool _evaluateCondition(String condition) { | |
RegExp condRegex = RegExp(r'^(.+)\s*(=|<>|<|>|<=|>=)\s*(.+)$'); | |
Match? match = condRegex.firstMatch(condition); | |
if (match == null) { | |
setState(() { | |
_output += '\nInvalid condition: $condition'; | |
}); | |
return false; | |
} | |
num left = _evaluateExpression(match.group(1)!.trim()); | |
num right = _evaluateExpression(match.group(3)!.trim()); | |
String op = match.group(2)!; | |
switch (op) { | |
case '=': | |
return left == right; | |
case '<>': | |
return left != right; | |
case '<': | |
return left < right; | |
case '>': | |
return left > right; | |
case '<=': | |
return left <= right; | |
case '>=': | |
return left >= right; | |
default: | |
return false; | |
} | |
} | |
void _scrollToBottom() { | |
WidgetsBinding.instance.addPostFrameCallback((_) { | |
if (_scrollController.hasClients) { | |
_scrollController.jumpTo(_scrollController.position.maxScrollExtent); | |
} | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
backgroundColor: Colors.black, | |
body: SafeArea( | |
child: Container( | |
padding: const EdgeInsets.all(8.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.stretch, | |
children: [ | |
Expanded( | |
child: SingleChildScrollView( | |
controller: _scrollController, | |
child: SelectableText( | |
_output, | |
style: const TextStyle( | |
color: Colors.green, | |
fontFamily: 'Courier', | |
fontSize: 16, | |
), | |
textAlign: TextAlign.left, | |
), | |
), | |
), | |
TextField( | |
controller: _controller, | |
style: const TextStyle( | |
color: Colors.green, | |
fontFamily: 'Courier', | |
fontSize: 16, | |
), | |
cursorColor: Colors.green, | |
autofocus: true, | |
decoration: const InputDecoration( | |
border: InputBorder.none, | |
fillColor: Colors.black, | |
filled: true, | |
), | |
onEditingComplete: _handleInput, | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment