4
\$\begingroup\$

I have this simple IDE for playing with Brainf_ck. It has no file functionality; only the code, input and output fields are present, and the "compiler" is integrated. Here is how it looks like:

Brainf_ck in action

My code is as follows:

App.java

package net.coderodde.brainfuckide;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;

/**
 * This class is the actual Brainfuck IDE application.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Nov 3, 2016)
 */
public class App 
        extends Application 
        implements CharListener, CharacterInputRequestListener {

    private static final double PREFERRED_TEXTAREA_HEIGHT = 10000.0;
    private static final Font APP_FONT = Font.font("Monospaced",
                                                   FontWeight.BOLD,
                                                   15.0);
    private static final double INITIAL_WIDTH = 500.0;
    private static final double INITIAL_HEIGHT = 400.0;
    private static final String APP_TITLE = "Brainfuck IDE - 1.6";

    private final Button runButton       = new Button("Run");
    private final TextArea codeArea      = new TextArea();
    private final TextArea outputArea    = new TextArea();
    private final Label inputPromptLabel = new Label(">>>");
    private final TextField inputField   = new TextField();
    private final HBox controlLine = new HBox();
    private final VBox mainBox = new VBox();
    private final EventHandler<KeyEvent> inputFieldHandler;
    private BlinkThread blinkThread;
    private BrainfuckVM vm;
    private boolean characterRequested;

    public App() {
        inputFieldHandler = new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                if (characterRequested) {
                    String text = inputField.getText();

                    if (text.isEmpty()) {
                        return;
                    }

                    char c = text.charAt(0);
                    text = text.substring(1);
                    inputField.setText(text);
                    characterRequested = false;
                    vm.onByteInput((byte) c);
                }
            }
        };

        inputField.setOnKeyTyped(inputFieldHandler);
    }

    @Override
    public void start(Stage primaryStage) {
        controlLine.getChildren().addAll(inputPromptLabel,
                                         inputField,
                                         runButton);
        mainBox.getChildren().addAll(codeArea,
                                     controlLine,
                                     outputArea);

        setComponentDimensions();
        setTextWrapping();
        setMiscAttributes();
        setComponentDimensions();
        setComponentFonts();
        setRunButtonListener();

        StackPane root = new StackPane();
        root.getChildren().add(mainBox);        
        Scene scene = new Scene(root, INITIAL_WIDTH, INITIAL_HEIGHT);
        primaryStage.setOnCloseRequest(e -> { onClose(); });
        primaryStage.setTitle(APP_TITLE);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    @Override
    public void acceptChar(char c) {
        outputArea.setText(outputArea.getText() + c);
    }

    @Override
    public void requestCharacterInput() {
        String text = inputField.getText();

        if (text.isEmpty()) {
            characterRequested = true;
            startInputPromptBlinking();
            return;
        }

        char c = text.charAt(0);
        text = text.substring(1);
        inputField.setText(text);
        vm.onByteInput((byte) c);
    }

    @Override
    public void stopWaitingForCharacterInput() {
        stopInputPromptBlinking();
    }

    /**
     * Launches the application.
     * 
     * @param args the command line arguments.
     */
    public static void main(String[] args) {
        launch(args);
    }    

    private void setComponentFonts() {
        codeArea.setFont(APP_FONT);
        inputPromptLabel.setFont(APP_FONT);
        inputField.setFont(APP_FONT);
        runButton.setFont(APP_FONT);
        outputArea.setFont(APP_FONT);
    }

    private void startInputPromptBlinking() {
        if (blinkThread == null) {
            blinkThread = new BlinkThread(inputPromptLabel);
            blinkThread.start();
        }
    }

    private void stopInputPromptBlinking() {
        if (blinkThread != null) {
            blinkThread.requestTermination();
            blinkThread = null;
        }
    }

    private void setComponentDimensions() {
        codeArea.setMaxHeight(PREFERRED_TEXTAREA_HEIGHT);
        codeArea.setPrefHeight(PREFERRED_TEXTAREA_HEIGHT);

        outputArea.setMaxHeight(PREFERRED_TEXTAREA_HEIGHT);
        outputArea.setPrefHeight(PREFERRED_TEXTAREA_HEIGHT);

        // Makes sure the width of the input field occupies all available width:
        HBox.setHgrow(inputField, Priority.ALWAYS);
    }

    private void setTextWrapping() {
        codeArea.wrapTextProperty().set(true);
        outputArea.wrapTextProperty().set(true);
    }

    private void setMiscAttributes() {
        inputPromptLabel.setTranslateY(5.0);
    }

    private void setRunButtonListener() {
        runButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                try {
                    vm = new BrainfuckVM(codeArea.getText(),
                                         App.this, 
                                         App.this);
                    vm.execute();
                } catch (Exception ex) {
                    showExceptionDialog(ex);
                }
            }
        });
    }

    private void showExceptionDialog(Exception ex) {
        Alert alert = new Alert(AlertType.ERROR);
        alert.setTitle("Error");
        alert.setContentText(ex.getMessage());
        alert.showAndWait();
    }

    private void onClose() {
        if (blinkThread != null) {
            blinkThread.requestTermination();

            try {
                blinkThread.join();
            } catch (InterruptedException ex) {

            }

            Platform.exit();
        }
    }
}

BrainfuckVM.java

package net.coderodde.brainfuckide;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;

public class BrainfuckVM {

    private static final int TAPE_LENGTH = 30_000;

    private final String code;
    private final byte[] tape = new byte[TAPE_LENGTH];
    private int instructionPointer; 
    private int dataPointer;
    private final CharListener charListener;
    private final CharacterInputRequestListener requestListener;

    public BrainfuckVM(String code, 
                       CharListener charListener,
                       CharacterInputRequestListener requestListener) {
        this.code = Objects.requireNonNull(code, "The code is null.");
        this.charListener =
                Objects.requireNonNull(charListener,
                                       "The char listener is null.");
        this.requestListener = 
                Objects.requireNonNull(requestListener, 
                                       "The request listener is null.");
        checkParenthesisStructure(code);
    }

    public void execute() {
        while (instructionPointer < code.length()) {
            char command = code.charAt(instructionPointer);

            switch (command) {
                case '<':
                    moveDataPointerLeft();
                    break;

                case '>':
                    moveDataPointerRight();
                    break;

                case '+':
                    incrementAtDataPointer();
                    break;

                case '-':
                    decrementAtDataPointer();
                    break;

                case '[':
                    processBlockBegin();
                    break;

                case ']':
                    processBlockEnd();
                    break;

                case '.':
                    outputCurrentDatum();
                    break;

                case ',':
                    requestListener.requestCharacterInput();
                    return;

                default:
                    // Unrecognized character, omit it:
                    ++instructionPointer;
            }
        }
    }

    private void outputCurrentDatum() {
        charListener.acceptChar((char) tape[dataPointer]);
        instructionPointer++;
    }

    public void onByteInput(byte inputByte) {
        tape[dataPointer] = inputByte;
        // 'instructionPointer' points to the ',' command. Increment it in 
        // order to start fetching commands after the ',':
        instructionPointer++; 
        requestListener.stopWaitingForCharacterInput();
        execute();
    }

    private void moveDataPointerLeft() {
        dataPointer--;
        instructionPointer++;

        if (dataPointer < 0) {
            throw new IllegalStateException(
                    "Data pointer (" + dataPointer + ") out of tape.");
        }
    }

    private void moveDataPointerRight() {
        dataPointer++;
        instructionPointer++;

        if (dataPointer >= tape.length) {
            throw new IllegalStateException(
                    "Data pointer (" + dataPointer + ") out of tape. Tape " + 
                    "length = " + tape.length + ".");
        }
    }

    private void incrementAtDataPointer() {
        instructionPointer++;
        tape[dataPointer]++;
    }

    private void decrementAtDataPointer() {
        instructionPointer++;
        tape[dataPointer]--;
    }

    private void processBlockBegin() {
        if (tape[dataPointer] != 0) {
            instructionPointer++;
            return;
        } 

        int counter = 1;
        int index;

        for (index = instructionPointer + 1; index < code.length(); ++index) {
            char currentCharacter = code.charAt(index);

            if (currentCharacter == ']') {
                counter--;

                if (counter == 0) {
                    break;
                }
            } else if (currentCharacter == '[') {
                counter++;
            }
        }

        // Move past the matching ']':
        instructionPointer = index + 1;
    }

    private void processBlockEnd() {
        if (tape[dataPointer] == 0) {
            instructionPointer++;
            return;
        }

        int counter = 1;
        int index;

        for (index = instructionPointer - 1; index >= 0; --index) {
            char currentCharacter = code.charAt(index);

            if (currentCharacter == '[') {
                counter--;

                if (counter == 0) {
                    break;
                }
            } else if (currentCharacter == ']') {
                counter++;
            }
        }

        instructionPointer = index + 1;
    }

    private void checkParenthesisStructure(String code) {
        Deque<Character> stack = new ArrayDeque<>();
        RuntimeException exception =
                new IllegalArgumentException("Bad bracket structure");

        for (char c : code.toCharArray()) {
            if (c == '[') {                
                stack.addLast(c);
            } else if (c == ']') {
                if (stack.isEmpty()) {
                    throw exception;
                }

                if (stack.getLast().equals('[')) {
                    stack.removeLast();
                } else {
                    throw exception;
                }
            }
        }

        if (!stack.isEmpty()) {
            throw exception;
        }
    }
}

BlinkThread.java

package net.coderodde.brainfuckide;

import javafx.scene.control.Label;

final class BlinkThread extends Thread {

    // 500 milliseconds:
    private static final int SLEEP_DURATION = 500;

    private final Label label;
    private volatile boolean exitRequested;

    BlinkThread(Label label) {
        this.label = label;
    }

    void requestTermination() {
        exitRequested = true;
    }

    public void run() {
        while (!exitRequested) {
            label.setVisible(true);
            sleep(SLEEP_DURATION);
            label.setVisible(false);
            sleep(SLEEP_DURATION);
        }

        // Keep the label visible:
        label.setVisible(true);
    }

    private static void sleep(int milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException ex) {

        }
    }
}

CharListener.java

package net.coderodde.brainfuckide;

public interface CharListener {

    public void acceptChar(char c);
}

CharacterInputRequestListener.java

package net.coderodde.brainfuckide;

public interface CharacterInputRequestListener {

    public void requestCharacterInput();

    public void stopWaitingForCharacterInput();
}

Critique request

I am most interested to hear the comments about these topics:

  1. Naming,
  2. Coding conventions,
  3. Overall design,
  4. Performance improvements,
  5. UI improvements.
\$\endgroup\$
  • \$\begingroup\$ Do you have any github repository for this? Would be easier to try the code that way. \$\endgroup\$ – Simon Forsberg Nov 3 '16 at 17:52
  • \$\begingroup\$ Sure. Here it is. \$\endgroup\$ – coderodde Nov 3 '16 at 17:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.