Skip to content

Instantly share code, notes, and snippets.

@sma
Created September 18, 2024 17:06
Show Gist options
  • Save sma/8a59ca3db6eaa16b8a41735b2da4768d to your computer and use it in GitHub Desktop.
Save sma/8a59ca3db6eaa16b8a41735b2da4768d to your computer and use it in GitHub Desktop.

Revisions

  1. sma created this gist Sep 18, 2024.
    522 changes: 522 additions & 0 deletions basic.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,522 @@
    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,
    ),
    ],
    ),
    ),
    ),
    );
    }
    }