Add unit tests for brainfuck and deadfish
This commit is contained in:
		@ -79,3 +79,12 @@ export const editorTokensProvider: MonacoTokensProvider = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  defaultToken: "comment",
 | 
					  defaultToken: "comment",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Serialize tape from object format into linear array */
 | 
				
			||||||
 | 
					export const serializeTapeMap = (tape: BFRS["tape"]): number[] => {
 | 
				
			||||||
 | 
					  const cellIdxs = Object.keys(tape).map((s) => parseInt(s, 10));
 | 
				
			||||||
 | 
					  const maxCellIdx = Math.max(15, ...cellIdxs);
 | 
				
			||||||
 | 
					  const linearTape: number[] = Array(maxCellIdx + 1).fill(0);
 | 
				
			||||||
 | 
					  cellIdxs.forEach((i) => (linearTape[i] = tape[i] || 0));
 | 
				
			||||||
 | 
					  return linearTape;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,182 +1,4 @@
 | 
				
			|||||||
import { setupWorker } from "../setup-worker";
 | 
					import { setupWorker } from "../setup-worker";
 | 
				
			||||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
 | 
					import BrainfuckLanguageEngine from "./runtime";
 | 
				
			||||||
import { BFAstStep, BFInstruction, BFRS, BF_OP } from "./constants";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Default values for internal states
 | 
					 | 
				
			||||||
// Factories are used to create new objects on reset
 | 
					 | 
				
			||||||
const DEFAULT_AST = (): BFAstStep[] => [];
 | 
					 | 
				
			||||||
const DEFAULT_PTR = 0;
 | 
					 | 
				
			||||||
const DEFAULT_PC = -1;
 | 
					 | 
				
			||||||
const DEFAULT_TAPE = (): { [k: number]: number } => ({});
 | 
					 | 
				
			||||||
const DEFAULT_INPUT: string = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Instruction characters
 | 
					 | 
				
			||||||
const OP_CHARS = Object.values(BF_OP);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BrainfuckLanguageEngine implements LanguageEngine<BFRS> {
 | 
					 | 
				
			||||||
  private _ast: BFAstStep[] = DEFAULT_AST();
 | 
					 | 
				
			||||||
  private _ptr: number = DEFAULT_PTR;
 | 
					 | 
				
			||||||
  private _tape: { [k: number]: number } = DEFAULT_TAPE();
 | 
					 | 
				
			||||||
  private _input: string = DEFAULT_INPUT;
 | 
					 | 
				
			||||||
  private _pc: number = DEFAULT_PC;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  resetState() {
 | 
					 | 
				
			||||||
    this._ast = DEFAULT_AST();
 | 
					 | 
				
			||||||
    this._ptr = DEFAULT_PTR;
 | 
					 | 
				
			||||||
    this._tape = DEFAULT_TAPE();
 | 
					 | 
				
			||||||
    this._input = DEFAULT_INPUT;
 | 
					 | 
				
			||||||
    this._pc = DEFAULT_PC;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  prepare(code: string, input: string) {
 | 
					 | 
				
			||||||
    this._input = input;
 | 
					 | 
				
			||||||
    this._ast = this.parseCode(code);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  executeStep(): StepExecutionResult<BFRS> {
 | 
					 | 
				
			||||||
    // Execute and update program counter
 | 
					 | 
				
			||||||
    let output: string | undefined = undefined;
 | 
					 | 
				
			||||||
    if (this._pc !== -1) {
 | 
					 | 
				
			||||||
      const astStep = this._ast[this._pc];
 | 
					 | 
				
			||||||
      const opResult = this.processOp(astStep.instr);
 | 
					 | 
				
			||||||
      this._pc = opResult?.newPc == null ? this._pc + 1 : opResult.newPc;
 | 
					 | 
				
			||||||
      output = opResult?.output;
 | 
					 | 
				
			||||||
    } else this._pc += 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Prepare location of next step
 | 
					 | 
				
			||||||
    let nextStepLocation: DocumentRange | null = null;
 | 
					 | 
				
			||||||
    if (this._pc < this._ast.length) {
 | 
					 | 
				
			||||||
      const { line, char } = this._ast[this._pc].location;
 | 
					 | 
				
			||||||
      const charRange = { start: char, end: char + 1 };
 | 
					 | 
				
			||||||
      nextStepLocation = { line, charRange };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Prepare and return execution result
 | 
					 | 
				
			||||||
    const rendererState = { tape: this._tape, pointer: this._ptr };
 | 
					 | 
				
			||||||
    return { rendererState, nextStepLocation, output };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private parseCode(code: string) {
 | 
					 | 
				
			||||||
    const ast: BFAstStep[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Stack to maintain loop counts. Element of stack denotes
 | 
					 | 
				
			||||||
    // program counter for loop-opening instruction.
 | 
					 | 
				
			||||||
    const loopStack: number[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // For each line...
 | 
					 | 
				
			||||||
    code.split("\n").forEach((line, lIdx) => {
 | 
					 | 
				
			||||||
      // For each character of this line...
 | 
					 | 
				
			||||||
      line.split("").forEach((char, cIdx) => {
 | 
					 | 
				
			||||||
        // Ignore if the character is not an operation
 | 
					 | 
				
			||||||
        if (!OP_CHARS.includes(char as BF_OP)) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Update loop-tracking stack if it's a loop-char
 | 
					 | 
				
			||||||
        let jumpTarget = undefined;
 | 
					 | 
				
			||||||
        if (char === BF_OP.LOOPIN) {
 | 
					 | 
				
			||||||
          // Push loop start into stack
 | 
					 | 
				
			||||||
          // Opposite end location will be added at loop close
 | 
					 | 
				
			||||||
          loopStack.push(ast.length);
 | 
					 | 
				
			||||||
        } else if (char === BF_OP.LOOPOUT) {
 | 
					 | 
				
			||||||
          // Get location of loop-opener
 | 
					 | 
				
			||||||
          jumpTarget = loopStack.pop()!;
 | 
					 | 
				
			||||||
          // Add closing end location to loop-opener
 | 
					 | 
				
			||||||
          ast[jumpTarget].instr.param = ast.length;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Add instruction to AST
 | 
					 | 
				
			||||||
        ast.push({
 | 
					 | 
				
			||||||
          instr: { type: char as BF_OP, param: jumpTarget },
 | 
					 | 
				
			||||||
          location: { line: lIdx + 1, char: cIdx + 1 },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return ast;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Process the given instruction and return the updated program counter and
 | 
					 | 
				
			||||||
   * any output to send to stdout.
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * If program counter is not returned, counter should be incremented by 1.
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * @param instr Instruction to apply
 | 
					 | 
				
			||||||
   * @returns Optional fields for new program counter and step output
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  private processOp(
 | 
					 | 
				
			||||||
    instr: BFInstruction
 | 
					 | 
				
			||||||
  ): { newPc?: number; output?: string } | void {
 | 
					 | 
				
			||||||
    // Pointer-shift operations
 | 
					 | 
				
			||||||
    if (instr.type === BF_OP.LEFT) this.decrementPtr();
 | 
					 | 
				
			||||||
    else if (instr.type === BF_OP.RIGHT) this.incrementPtr();
 | 
					 | 
				
			||||||
    // Current cell modifiers
 | 
					 | 
				
			||||||
    else if (instr.type === BF_OP.INCR) this.incrementCell(this._ptr);
 | 
					 | 
				
			||||||
    else if (instr.type === BF_OP.DECR) this.decrementCell(this._ptr);
 | 
					 | 
				
			||||||
    // Input and output
 | 
					 | 
				
			||||||
    else if (instr.type === BF_OP.OUT) return { output: this.outputChar() };
 | 
					 | 
				
			||||||
    else if (instr.type === BF_OP.IN) this.inputChar();
 | 
					 | 
				
			||||||
    // Looping
 | 
					 | 
				
			||||||
    else if (instr.type === BF_OP.LOOPIN) {
 | 
					 | 
				
			||||||
      // Conditionally jump past loop-closer
 | 
					 | 
				
			||||||
      if (this.getCell(this._ptr) !== 0) return;
 | 
					 | 
				
			||||||
      return { newPc: instr.param! + 1 };
 | 
					 | 
				
			||||||
    } else if (instr.type === BF_OP.LOOPOUT) {
 | 
					 | 
				
			||||||
      // Conditionally jump to loop-opener
 | 
					 | 
				
			||||||
      if (this.getCell(this._ptr) === 0) return;
 | 
					 | 
				
			||||||
      return { newPc: instr.param };
 | 
					 | 
				
			||||||
    } else throw new Error("Unexpected instruction type");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** Output character from current cell */
 | 
					 | 
				
			||||||
  private outputChar(): string {
 | 
					 | 
				
			||||||
    const code = this._tape[this._ptr];
 | 
					 | 
				
			||||||
    return String.fromCharCode(code);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** Input character into current cell */
 | 
					 | 
				
			||||||
  private inputChar(): void {
 | 
					 | 
				
			||||||
    if (this._input.length === 0) {
 | 
					 | 
				
			||||||
      // EOF is treated as a zero
 | 
					 | 
				
			||||||
      this._tape[this._ptr] = 0;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      // Pop first char of input and set to cell
 | 
					 | 
				
			||||||
      this._tape[this._ptr] = this._input.charCodeAt(0);
 | 
					 | 
				
			||||||
      this._input = this._input.slice(1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** Get value of tape cell. Initializes cell if first use */
 | 
					 | 
				
			||||||
  private getCell(cellId: number): number {
 | 
					 | 
				
			||||||
    if (!this._tape[cellId]) this._tape[cellId] = 0;
 | 
					 | 
				
			||||||
    return this._tape[cellId];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** Increment tape cell at specified location */
 | 
					 | 
				
			||||||
  private incrementCell(cellId: number): void {
 | 
					 | 
				
			||||||
    if (!this._tape[cellId]) this._tape[cellId] = 0;
 | 
					 | 
				
			||||||
    this._tape[cellId] += 1;
 | 
					 | 
				
			||||||
    if (this._tape[cellId] === 128) this._tape[cellId] = -128;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** Decrement tape cell at specified location */
 | 
					 | 
				
			||||||
  private decrementCell(cellId: number): void {
 | 
					 | 
				
			||||||
    if (!this._tape[cellId]) this._tape[cellId] = 0;
 | 
					 | 
				
			||||||
    this._tape[cellId] -= 1;
 | 
					 | 
				
			||||||
    if (this._tape[cellId] === -129) this._tape[cellId] = 127;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** Move the tape pointer one cell to the right */
 | 
					 | 
				
			||||||
  private incrementPtr(): void {
 | 
					 | 
				
			||||||
    this._ptr += 1;
 | 
					 | 
				
			||||||
    this.getCell(this._ptr); // Init cell if required
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** Move the tape pointer one cell to the left */
 | 
					 | 
				
			||||||
  private decrementPtr(): void {
 | 
					 | 
				
			||||||
    if (this._ptr <= 0) throw new Error("Ptr out of bounds");
 | 
					 | 
				
			||||||
    this._ptr -= 1;
 | 
					 | 
				
			||||||
    this.getCell(this._ptr); // Init cell if required
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
setupWorker(new BrainfuckLanguageEngine());
 | 
					setupWorker(new BrainfuckLanguageEngine());
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { Renderer } from "./renderer";
 | 
					import { Renderer } from "./renderer";
 | 
				
			||||||
import { LanguageProvider } from "../types";
 | 
					import { LanguageProvider } from "../types";
 | 
				
			||||||
import { BFRS, sampleProgram, editorTokensProvider } from "./constants";
 | 
					import { BFRS, sampleProgram, editorTokensProvider } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const provider: LanguageProvider<BFRS> = {
 | 
					const provider: LanguageProvider<BFRS> = {
 | 
				
			||||||
  Renderer,
 | 
					  Renderer,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { Card, Colors } from "@blueprintjs/core";
 | 
				
			|||||||
import { CSSProperties } from "react";
 | 
					import { CSSProperties } from "react";
 | 
				
			||||||
import { useDarkMode } from "../../ui/providers/dark-mode-provider";
 | 
					import { useDarkMode } from "../../ui/providers/dark-mode-provider";
 | 
				
			||||||
import { RendererProps } from "../types";
 | 
					import { RendererProps } from "../types";
 | 
				
			||||||
import { BFRS } from "./constants";
 | 
					import { BFRS, serializeTapeMap } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Colors used as background of active cells
 | 
					// Colors used as background of active cells
 | 
				
			||||||
const darkActiveBG = Colors.DARK_GRAY2;
 | 
					const darkActiveBG = Colors.DARK_GRAY2;
 | 
				
			||||||
@ -47,18 +47,9 @@ const Cell = ({ value, active }: { value: number; active: boolean }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/** Renderer for Brainfuck */
 | 
					/** Renderer for Brainfuck */
 | 
				
			||||||
export const Renderer = ({ state }: RendererProps<BFRS>) => {
 | 
					export const Renderer = ({ state }: RendererProps<BFRS>) => {
 | 
				
			||||||
  /** Serialize tape from object format into linear array */
 | 
					 | 
				
			||||||
  const serializeTapeObj = (tape: BFRS["tape"]) => {
 | 
					 | 
				
			||||||
    const cellIdxs = Object.keys(tape).map((s) => parseInt(s, 10));
 | 
					 | 
				
			||||||
    const maxCellIdx = Math.max(15, ...cellIdxs);
 | 
					 | 
				
			||||||
    const linearTape = Array(maxCellIdx + 1).fill(0);
 | 
					 | 
				
			||||||
    cellIdxs.forEach((i) => (linearTape[i] = tape[i] || 0));
 | 
					 | 
				
			||||||
    return linearTape;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div style={styles.container}>
 | 
					    <div style={styles.container}>
 | 
				
			||||||
      {serializeTapeObj(state?.tape || {}).map((num, i) => (
 | 
					      {serializeTapeMap(state?.tape || {}).map((num, i) => (
 | 
				
			||||||
        <Cell value={num} key={i} active={(state?.pointer || 0) === i} />
 | 
					        <Cell value={num} key={i} active={(state?.pointer || 0) === i} />
 | 
				
			||||||
      ))}
 | 
					      ))}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										179
									
								
								engines/brainfuck/runtime.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								engines/brainfuck/runtime.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,179 @@
 | 
				
			|||||||
 | 
					import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
 | 
				
			||||||
 | 
					import { BFAstStep, BFInstruction, BFRS, BF_OP } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Default values for internal states
 | 
				
			||||||
 | 
					// Factories are used to create new objects on reset
 | 
				
			||||||
 | 
					const DEFAULT_AST = (): BFAstStep[] => [];
 | 
				
			||||||
 | 
					const DEFAULT_PTR = 0;
 | 
				
			||||||
 | 
					const DEFAULT_PC = -1;
 | 
				
			||||||
 | 
					const DEFAULT_TAPE = (): { [k: number]: number } => ({});
 | 
				
			||||||
 | 
					const DEFAULT_INPUT: string = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Instruction characters
 | 
				
			||||||
 | 
					const OP_CHARS = Object.values(BF_OP);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class BrainfuckLanguageEngine implements LanguageEngine<BFRS> {
 | 
				
			||||||
 | 
					  private _ast: BFAstStep[] = DEFAULT_AST();
 | 
				
			||||||
 | 
					  private _ptr: number = DEFAULT_PTR;
 | 
				
			||||||
 | 
					  private _tape: { [k: number]: number } = DEFAULT_TAPE();
 | 
				
			||||||
 | 
					  private _input: string = DEFAULT_INPUT;
 | 
				
			||||||
 | 
					  private _pc: number = DEFAULT_PC;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  resetState() {
 | 
				
			||||||
 | 
					    this._ast = DEFAULT_AST();
 | 
				
			||||||
 | 
					    this._ptr = DEFAULT_PTR;
 | 
				
			||||||
 | 
					    this._tape = DEFAULT_TAPE();
 | 
				
			||||||
 | 
					    this._input = DEFAULT_INPUT;
 | 
				
			||||||
 | 
					    this._pc = DEFAULT_PC;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  prepare(code: string, input: string) {
 | 
				
			||||||
 | 
					    this._input = input;
 | 
				
			||||||
 | 
					    this._ast = this.parseCode(code);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  executeStep(): StepExecutionResult<BFRS> {
 | 
				
			||||||
 | 
					    // Execute and update program counter
 | 
				
			||||||
 | 
					    let output: string | undefined = undefined;
 | 
				
			||||||
 | 
					    if (this._pc !== -1) {
 | 
				
			||||||
 | 
					      const astStep = this._ast[this._pc];
 | 
				
			||||||
 | 
					      const opResult = this.processOp(astStep.instr);
 | 
				
			||||||
 | 
					      this._pc = opResult?.newPc == null ? this._pc + 1 : opResult.newPc;
 | 
				
			||||||
 | 
					      output = opResult?.output;
 | 
				
			||||||
 | 
					    } else this._pc += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepare location of next step
 | 
				
			||||||
 | 
					    let nextStepLocation: DocumentRange | null = null;
 | 
				
			||||||
 | 
					    if (this._pc < this._ast.length) {
 | 
				
			||||||
 | 
					      const { line, char } = this._ast[this._pc].location;
 | 
				
			||||||
 | 
					      const charRange = { start: char, end: char + 1 };
 | 
				
			||||||
 | 
					      nextStepLocation = { line, charRange };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepare and return execution result
 | 
				
			||||||
 | 
					    const rendererState = { tape: this._tape, pointer: this._ptr };
 | 
				
			||||||
 | 
					    return { rendererState, nextStepLocation, output };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private parseCode(code: string) {
 | 
				
			||||||
 | 
					    const ast: BFAstStep[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Stack to maintain loop counts. Element of stack denotes
 | 
				
			||||||
 | 
					    // program counter for loop-opening instruction.
 | 
				
			||||||
 | 
					    const loopStack: number[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // For each line...
 | 
				
			||||||
 | 
					    code.split("\n").forEach((line, lIdx) => {
 | 
				
			||||||
 | 
					      // For each character of this line...
 | 
				
			||||||
 | 
					      line.split("").forEach((char, cIdx) => {
 | 
				
			||||||
 | 
					        // Ignore if the character is not an operation
 | 
				
			||||||
 | 
					        if (!OP_CHARS.includes(char as BF_OP)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update loop-tracking stack if it's a loop-char
 | 
				
			||||||
 | 
					        let jumpTarget = undefined;
 | 
				
			||||||
 | 
					        if (char === BF_OP.LOOPIN) {
 | 
				
			||||||
 | 
					          // Push loop start into stack
 | 
				
			||||||
 | 
					          // Opposite end location will be added at loop close
 | 
				
			||||||
 | 
					          loopStack.push(ast.length);
 | 
				
			||||||
 | 
					        } else if (char === BF_OP.LOOPOUT) {
 | 
				
			||||||
 | 
					          // Get location of loop-opener
 | 
				
			||||||
 | 
					          jumpTarget = loopStack.pop()!;
 | 
				
			||||||
 | 
					          // Add closing end location to loop-opener
 | 
				
			||||||
 | 
					          ast[jumpTarget].instr.param = ast.length;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add instruction to AST
 | 
				
			||||||
 | 
					        ast.push({
 | 
				
			||||||
 | 
					          instr: { type: char as BF_OP, param: jumpTarget },
 | 
				
			||||||
 | 
					          location: { line: lIdx + 1, char: cIdx + 1 },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ast;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Process the given instruction and return the updated program counter and
 | 
				
			||||||
 | 
					   * any output to send to stdout.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * If program counter is not returned, counter should be incremented by 1.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param instr Instruction to apply
 | 
				
			||||||
 | 
					   * @returns Optional fields for new program counter and step output
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private processOp(
 | 
				
			||||||
 | 
					    instr: BFInstruction
 | 
				
			||||||
 | 
					  ): { newPc?: number; output?: string } | void {
 | 
				
			||||||
 | 
					    // Pointer-shift operations
 | 
				
			||||||
 | 
					    if (instr.type === BF_OP.LEFT) this.decrementPtr();
 | 
				
			||||||
 | 
					    else if (instr.type === BF_OP.RIGHT) this.incrementPtr();
 | 
				
			||||||
 | 
					    // Current cell modifiers
 | 
				
			||||||
 | 
					    else if (instr.type === BF_OP.INCR) this.incrementCell(this._ptr);
 | 
				
			||||||
 | 
					    else if (instr.type === BF_OP.DECR) this.decrementCell(this._ptr);
 | 
				
			||||||
 | 
					    // Input and output
 | 
				
			||||||
 | 
					    else if (instr.type === BF_OP.OUT) return { output: this.outputChar() };
 | 
				
			||||||
 | 
					    else if (instr.type === BF_OP.IN) this.inputChar();
 | 
				
			||||||
 | 
					    // Looping
 | 
				
			||||||
 | 
					    else if (instr.type === BF_OP.LOOPIN) {
 | 
				
			||||||
 | 
					      // Conditionally jump past loop-closer
 | 
				
			||||||
 | 
					      if (this.getCell(this._ptr) !== 0) return;
 | 
				
			||||||
 | 
					      return { newPc: instr.param! + 1 };
 | 
				
			||||||
 | 
					    } else if (instr.type === BF_OP.LOOPOUT) {
 | 
				
			||||||
 | 
					      // Conditionally jump to loop-opener
 | 
				
			||||||
 | 
					      if (this.getCell(this._ptr) === 0) return;
 | 
				
			||||||
 | 
					      return { newPc: instr.param };
 | 
				
			||||||
 | 
					    } else throw new Error("Unexpected instruction type");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** Output character from current cell */
 | 
				
			||||||
 | 
					  private outputChar(): string {
 | 
				
			||||||
 | 
					    const code = this._tape[this._ptr];
 | 
				
			||||||
 | 
					    return String.fromCharCode(code);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** Input character into current cell */
 | 
				
			||||||
 | 
					  private inputChar(): void {
 | 
				
			||||||
 | 
					    if (this._input.length === 0) {
 | 
				
			||||||
 | 
					      // EOF is treated as a zero
 | 
				
			||||||
 | 
					      this._tape[this._ptr] = 0;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Pop first char of input and set to cell
 | 
				
			||||||
 | 
					      this._tape[this._ptr] = this._input.charCodeAt(0);
 | 
				
			||||||
 | 
					      this._input = this._input.slice(1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** Get value of tape cell. Initializes cell if first use */
 | 
				
			||||||
 | 
					  private getCell(cellId: number): number {
 | 
				
			||||||
 | 
					    if (!this._tape[cellId]) this._tape[cellId] = 0;
 | 
				
			||||||
 | 
					    return this._tape[cellId];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** Increment tape cell at specified location */
 | 
				
			||||||
 | 
					  private incrementCell(cellId: number): void {
 | 
				
			||||||
 | 
					    if (!this._tape[cellId]) this._tape[cellId] = 0;
 | 
				
			||||||
 | 
					    this._tape[cellId] += 1;
 | 
				
			||||||
 | 
					    if (this._tape[cellId] === 128) this._tape[cellId] = -128;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** Decrement tape cell at specified location */
 | 
				
			||||||
 | 
					  private decrementCell(cellId: number): void {
 | 
				
			||||||
 | 
					    if (!this._tape[cellId]) this._tape[cellId] = 0;
 | 
				
			||||||
 | 
					    this._tape[cellId] -= 1;
 | 
				
			||||||
 | 
					    if (this._tape[cellId] === -129) this._tape[cellId] = 127;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** Move the tape pointer one cell to the right */
 | 
				
			||||||
 | 
					  private incrementPtr(): void {
 | 
				
			||||||
 | 
					    this._ptr += 1;
 | 
				
			||||||
 | 
					    this.getCell(this._ptr); // Init cell if required
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** Move the tape pointer one cell to the left */
 | 
				
			||||||
 | 
					  private decrementPtr(): void {
 | 
				
			||||||
 | 
					    if (this._ptr <= 0) throw new Error("Ptr out of bounds");
 | 
				
			||||||
 | 
					    this._ptr -= 1;
 | 
				
			||||||
 | 
					    this.getCell(this._ptr); // Init cell if required
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								engines/brainfuck/tests/cat.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								engines/brainfuck/tests/cat.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					,[.,]
 | 
				
			||||||
							
								
								
									
										19
									
								
								engines/brainfuck/tests/cellsize.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								engines/brainfuck/tests/cellsize.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					Calculate the value 256 and test if it's zero
 | 
				
			||||||
 | 
					If the interpreter errors on overflow this is where it'll happen
 | 
				
			||||||
 | 
					++++++++[>++++++++<-]>[<++++>-]
 | 
				
			||||||
 | 
					+<[>-<
 | 
				
			||||||
 | 
					    Not zero so multiply by 256 again to get 65536
 | 
				
			||||||
 | 
					    [>++++<-]>[<++++++++>-]<[>++++++++<-]
 | 
				
			||||||
 | 
					    +>[>
 | 
				
			||||||
 | 
					        # Print "32"
 | 
				
			||||||
 | 
					        ++++++++++[>+++++<-]>+.-.[-]<
 | 
				
			||||||
 | 
					    <[-]<->] <[>>
 | 
				
			||||||
 | 
					        # Print "16"
 | 
				
			||||||
 | 
					        +++++++[>+++++++<-]>.+++++.[-]<
 | 
				
			||||||
 | 
					<<-]] >[>
 | 
				
			||||||
 | 
					    # Print "8"
 | 
				
			||||||
 | 
					    ++++++++[>+++++++<-]>.[-]<
 | 
				
			||||||
 | 
					<-]<
 | 
				
			||||||
 | 
					# Print " bit cells"
 | 
				
			||||||
 | 
					+++++++++++[>+++>+++++++++>+++++++++>+<<<<-]>-.>-.+++++++.+++++++++++.<.
 | 
				
			||||||
 | 
					>>.++.+++++++..<-.>>-
 | 
				
			||||||
							
								
								
									
										2
									
								
								engines/brainfuck/tests/helloworld-subzero.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								engines/brainfuck/tests/helloworld-subzero.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					>++++++++[-<+++++++++>]<.>>+>-[+]++>++>+++[>[->+++<<+++>]<<]>-----.>->
 | 
				
			||||||
 | 
					+++..+++.>-.<<+[>[+>+]>>]<--------------.>>.+++.------.--------.>+.>+.
 | 
				
			||||||
							
								
								
									
										33
									
								
								engines/brainfuck/tests/helloworld.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								engines/brainfuck/tests/helloworld.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					+++++ +++               Set Cell #0 to 8
 | 
				
			||||||
 | 
					[
 | 
				
			||||||
 | 
					    >++++               Add 4 to Cell #1; this will always set Cell #1 to 4
 | 
				
			||||||
 | 
					    [                   as the cell will be cleared by the loop
 | 
				
			||||||
 | 
					        >++             Add 4*2 to Cell #2
 | 
				
			||||||
 | 
					        >+++            Add 4*3 to Cell #3
 | 
				
			||||||
 | 
					        >+++            Add 4*3 to Cell #4
 | 
				
			||||||
 | 
					        >+              Add 4 to Cell #5
 | 
				
			||||||
 | 
					        <<<<-           Decrement the loop counter in Cell #1
 | 
				
			||||||
 | 
					    ]                   Loop till Cell #1 is zero
 | 
				
			||||||
 | 
					    >+                  Add 1 to Cell #2
 | 
				
			||||||
 | 
					    >+                  Add 1 to Cell #3
 | 
				
			||||||
 | 
					    >-                  Subtract 1 from Cell #4
 | 
				
			||||||
 | 
					    >>+                 Add 1 to Cell #6
 | 
				
			||||||
 | 
					    [<]                 Move back to the first zero cell you find; this will
 | 
				
			||||||
 | 
					                        be Cell #1 which was cleared by the previous loop
 | 
				
			||||||
 | 
					    <-                  Decrement the loop Counter in Cell #0
 | 
				
			||||||
 | 
					]                       Loop till Cell #0 is zero
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The result of this is:
 | 
				
			||||||
 | 
					Cell No :   0   1   2   3   4   5   6
 | 
				
			||||||
 | 
					Contents:   0   0  72 104  88  32   8
 | 
				
			||||||
 | 
					Pointer :   ^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					>>.                     Cell #2 has value 72 which is 'H'
 | 
				
			||||||
 | 
					>---.                   Subtract 3 from Cell #3 to get 101 which is 'e'
 | 
				
			||||||
 | 
					+++++ ++..+++.          Likewise for 'llo' from Cell #3
 | 
				
			||||||
 | 
					>>.                     Cell #5 is 32 for the space
 | 
				
			||||||
 | 
					<-.                     Subtract 1 from Cell #4 for 87 to give a 'W'
 | 
				
			||||||
 | 
					<.                      Cell #3 was set to 'o' from the end of 'Hello'
 | 
				
			||||||
 | 
					+++.----- -.----- ---.  Cell #3 for 'rl' and 'd'
 | 
				
			||||||
 | 
					>>+.                    Add 1 to Cell #5 gives us an exclamation point
 | 
				
			||||||
 | 
					>++.                    And finally a newline from Cell #6
 | 
				
			||||||
							
								
								
									
										63
									
								
								engines/brainfuck/tests/index.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								engines/brainfuck/tests/index.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					import { readTestProgram, executeProgram } from "../../test-utils";
 | 
				
			||||||
 | 
					import { BFRS, serializeTapeMap } from "../common";
 | 
				
			||||||
 | 
					import Engine from "../runtime";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * All test programs are picked up from https://esolangs.org/wiki/Brainfuck.
 | 
				
			||||||
 | 
					 * - Cell cleanup code at end of cell size program is not included.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if actual cell array matches expected cell array.
 | 
				
			||||||
 | 
					 * Expected cell array must exclude trailing zeros.
 | 
				
			||||||
 | 
					 * @param cellsMap Map of cell index to value, as provided in execution result.
 | 
				
			||||||
 | 
					 * @param expected Array of expected cell values, without trailing zeros.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const expectCellsToBe = (cellsMap: BFRS["tape"], expected: number[]) => {
 | 
				
			||||||
 | 
					  const cells = serializeTapeMap(cellsMap);
 | 
				
			||||||
 | 
					  expect(cells.length).toBeGreaterThanOrEqual(expected.length);
 | 
				
			||||||
 | 
					  cells.forEach((value, idx) => {
 | 
				
			||||||
 | 
					    if (idx < expected.length) expect(value).toBe(expected[idx]);
 | 
				
			||||||
 | 
					    else expect(value).toBe(0);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("Test programs", () => {
 | 
				
			||||||
 | 
					  // Standard hello-world program
 | 
				
			||||||
 | 
					  test("hello world", async () => {
 | 
				
			||||||
 | 
					    const code = readTestProgram(__dirname, "helloworld");
 | 
				
			||||||
 | 
					    const result = await executeProgram(new Engine(), code);
 | 
				
			||||||
 | 
					    expect(result.output).toBe("Hello World!\n");
 | 
				
			||||||
 | 
					    expect(result.rendererState.pointer).toBe(6);
 | 
				
			||||||
 | 
					    const expectedCells = [0, 0, 72, 100, 87, 33, 10];
 | 
				
			||||||
 | 
					    expectCellsToBe(result.rendererState.tape, expectedCells);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Hello-world program using subzero cell values
 | 
				
			||||||
 | 
					  test("hello world with subzero cell values", async () => {
 | 
				
			||||||
 | 
					    const code = readTestProgram(__dirname, "helloworld-subzero");
 | 
				
			||||||
 | 
					    const result = await executeProgram(new Engine(), code);
 | 
				
			||||||
 | 
					    expect(result.output).toBe("Hello World!\n");
 | 
				
			||||||
 | 
					    expect(result.rendererState.pointer).toBe(6);
 | 
				
			||||||
 | 
					    const expectedCells = [72, 0, 87, 0, 100, 33, 10];
 | 
				
			||||||
 | 
					    expectCellsToBe(result.rendererState.tape, expectedCells);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // cat program
 | 
				
			||||||
 | 
					  test("cat program", async () => {
 | 
				
			||||||
 | 
					    const code = readTestProgram(__dirname, "cat");
 | 
				
			||||||
 | 
					    const result = await executeProgram(new Engine(), code, "foo \n bar");
 | 
				
			||||||
 | 
					    expect(result.output).toBe("foo \n bar");
 | 
				
			||||||
 | 
					    expect(result.rendererState.pointer).toBe(0);
 | 
				
			||||||
 | 
					    expectCellsToBe(result.rendererState.tape, []);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Program to calculate cell size
 | 
				
			||||||
 | 
					  test("cell size calculator", async () => {
 | 
				
			||||||
 | 
					    const code = readTestProgram(__dirname, "cellsize");
 | 
				
			||||||
 | 
					    const result = await executeProgram(new Engine(), code);
 | 
				
			||||||
 | 
					    expect(result.output).toBe("8 bit cells");
 | 
				
			||||||
 | 
					    expect(result.rendererState.pointer).toBe(4);
 | 
				
			||||||
 | 
					    expectCellsToBe(result.rendererState.tape, [0, 32, 115, 108, 10]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -1,87 +1,4 @@
 | 
				
			|||||||
import { setupWorker } from "../setup-worker";
 | 
					import { setupWorker } from "../setup-worker";
 | 
				
			||||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
 | 
					import DeadfishLanguageEngine from "./runtime";
 | 
				
			||||||
import { DFAstStep, DFRS, DF_OP } from "./constants";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Default values for internal states
 | 
					 | 
				
			||||||
// Factories are used to create new objects on reset
 | 
					 | 
				
			||||||
const DEFAULT_AST = (): DFAstStep[] => [];
 | 
					 | 
				
			||||||
const DEFAULT_PC = -1;
 | 
					 | 
				
			||||||
const DEFAULT_VALUE = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Instruction characters
 | 
					 | 
				
			||||||
const OP_CHARS = Object.values(DF_OP);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DeadfishLanguageEngine implements LanguageEngine<DFRS> {
 | 
					 | 
				
			||||||
  private _ast: DFAstStep[] = DEFAULT_AST();
 | 
					 | 
				
			||||||
  private _value: number = DEFAULT_VALUE;
 | 
					 | 
				
			||||||
  private _pc: number = DEFAULT_PC;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  resetState() {
 | 
					 | 
				
			||||||
    this._ast = DEFAULT_AST();
 | 
					 | 
				
			||||||
    this._value = DEFAULT_VALUE;
 | 
					 | 
				
			||||||
    this._pc = DEFAULT_PC;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  prepare(code: string, _input: string) {
 | 
					 | 
				
			||||||
    this._ast = this.parseCode(code);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  executeStep(): StepExecutionResult<DFRS> {
 | 
					 | 
				
			||||||
    // Execute and update program counter
 | 
					 | 
				
			||||||
    let output: string | undefined = undefined;
 | 
					 | 
				
			||||||
    if (this._pc !== -1) {
 | 
					 | 
				
			||||||
      const astStep = this._ast[this._pc];
 | 
					 | 
				
			||||||
      output = this.processOp(astStep.instr);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this._pc += 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Prepare location of next step
 | 
					 | 
				
			||||||
    let nextStepLocation: DocumentRange | null = null;
 | 
					 | 
				
			||||||
    if (this._pc < this._ast.length) {
 | 
					 | 
				
			||||||
      const { line, char } = this._ast[this._pc].location;
 | 
					 | 
				
			||||||
      const charRange = { start: char, end: char + 1 };
 | 
					 | 
				
			||||||
      nextStepLocation = { line, charRange };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Prepare and return execution result
 | 
					 | 
				
			||||||
    const rendererState = { value: this._value };
 | 
					 | 
				
			||||||
    return { rendererState, nextStepLocation, output };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private parseCode(code: string) {
 | 
					 | 
				
			||||||
    const ast: DFAstStep[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // For each line...
 | 
					 | 
				
			||||||
    code.split("\n").forEach((line, lIdx) => {
 | 
					 | 
				
			||||||
      // For each character of this line...
 | 
					 | 
				
			||||||
      line.split("").forEach((char, cIdx) => {
 | 
					 | 
				
			||||||
        if (OP_CHARS.includes(char as DF_OP)) {
 | 
					 | 
				
			||||||
          ast.push({
 | 
					 | 
				
			||||||
            instr: char as DF_OP,
 | 
					 | 
				
			||||||
            location: { line: lIdx + 1, char: cIdx + 1 },
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return ast;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Process the given instruction and return string to push to output if any.
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * @param instr Instruction to apply
 | 
					 | 
				
			||||||
   * @returns String to append to output, if any
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  private processOp(instr: DF_OP): string | undefined {
 | 
					 | 
				
			||||||
    if (instr === DF_OP.INCR) ++this._value;
 | 
					 | 
				
			||||||
    else if (instr === DF_OP.DECR) --this._value;
 | 
					 | 
				
			||||||
    else if (instr === DF_OP.SQ) this._value = this._value * this._value;
 | 
					 | 
				
			||||||
    else if (instr === DF_OP.OUT) return this._value.toString();
 | 
					 | 
				
			||||||
    else throw new Error("Invalid instruction");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this._value === -1 || this._value === 256) this._value = 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
setupWorker(new DeadfishLanguageEngine());
 | 
					setupWorker(new DeadfishLanguageEngine());
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										84
									
								
								engines/deadfish/runtime.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								engines/deadfish/runtime.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types";
 | 
				
			||||||
 | 
					import { DFAstStep, DFRS, DF_OP } from "./constants";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Default values for internal states
 | 
				
			||||||
 | 
					// Factories are used to create new objects on reset
 | 
				
			||||||
 | 
					const DEFAULT_AST = (): DFAstStep[] => [];
 | 
				
			||||||
 | 
					const DEFAULT_PC = -1;
 | 
				
			||||||
 | 
					const DEFAULT_VALUE = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Instruction characters
 | 
				
			||||||
 | 
					const OP_CHARS = Object.values(DF_OP);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class DeadfishLanguageEngine implements LanguageEngine<DFRS> {
 | 
				
			||||||
 | 
					  private _ast: DFAstStep[] = DEFAULT_AST();
 | 
				
			||||||
 | 
					  private _value: number = DEFAULT_VALUE;
 | 
				
			||||||
 | 
					  private _pc: number = DEFAULT_PC;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  resetState() {
 | 
				
			||||||
 | 
					    this._ast = DEFAULT_AST();
 | 
				
			||||||
 | 
					    this._value = DEFAULT_VALUE;
 | 
				
			||||||
 | 
					    this._pc = DEFAULT_PC;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  prepare(code: string, _input: string) {
 | 
				
			||||||
 | 
					    this._ast = this.parseCode(code);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  executeStep(): StepExecutionResult<DFRS> {
 | 
				
			||||||
 | 
					    // Execute and update program counter
 | 
				
			||||||
 | 
					    let output: string | undefined = undefined;
 | 
				
			||||||
 | 
					    if (this._pc !== -1) {
 | 
				
			||||||
 | 
					      const astStep = this._ast[this._pc];
 | 
				
			||||||
 | 
					      output = this.processOp(astStep.instr);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this._pc += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepare location of next step
 | 
				
			||||||
 | 
					    let nextStepLocation: DocumentRange | null = null;
 | 
				
			||||||
 | 
					    if (this._pc < this._ast.length) {
 | 
				
			||||||
 | 
					      const { line, char } = this._ast[this._pc].location;
 | 
				
			||||||
 | 
					      const charRange = { start: char, end: char + 1 };
 | 
				
			||||||
 | 
					      nextStepLocation = { line, charRange };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepare and return execution result
 | 
				
			||||||
 | 
					    const rendererState = { value: this._value };
 | 
				
			||||||
 | 
					    return { rendererState, nextStepLocation, output };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private parseCode(code: string) {
 | 
				
			||||||
 | 
					    const ast: DFAstStep[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // For each line...
 | 
				
			||||||
 | 
					    code.split("\n").forEach((line, lIdx) => {
 | 
				
			||||||
 | 
					      // For each character of this line...
 | 
				
			||||||
 | 
					      line.split("").forEach((char, cIdx) => {
 | 
				
			||||||
 | 
					        if (OP_CHARS.includes(char as DF_OP)) {
 | 
				
			||||||
 | 
					          ast.push({
 | 
				
			||||||
 | 
					            instr: char as DF_OP,
 | 
				
			||||||
 | 
					            location: { line: lIdx + 1, char: cIdx + 1 },
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ast;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Process the given instruction and return string to push to output if any.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param instr Instruction to apply
 | 
				
			||||||
 | 
					   * @returns String to append to output, if any
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private processOp(instr: DF_OP): string | undefined {
 | 
				
			||||||
 | 
					    if (instr === DF_OP.INCR) ++this._value;
 | 
				
			||||||
 | 
					    else if (instr === DF_OP.DECR) --this._value;
 | 
				
			||||||
 | 
					    else if (instr === DF_OP.SQ) this._value = this._value * this._value;
 | 
				
			||||||
 | 
					    else if (instr === DF_OP.OUT) return this._value.toString();
 | 
				
			||||||
 | 
					    else throw new Error("Invalid instruction");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this._value === -1 || this._value === 256) this._value = 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								engines/deadfish/tests/288.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								engines/deadfish/tests/288.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					diissisdo
 | 
				
			||||||
							
								
								
									
										3
									
								
								engines/deadfish/tests/helloworld.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								engines/deadfish/tests/helloworld.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					iisiiiisiiiiiiiioiiiiiiiiiiiiiiiiiiiiiiiiiiiiioiiiiiiiooiiio
 | 
				
			||||||
 | 
					dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddo
 | 
				
			||||||
 | 
					dddddddddddddddddddddsddoddddddddoiiioddddddoddddddddo
 | 
				
			||||||
							
								
								
									
										57
									
								
								engines/deadfish/tests/index.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								engines/deadfish/tests/index.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					import { readTestProgram, executeProgram } from "../../test-utils";
 | 
				
			||||||
 | 
					// import { BFRS, serializeTapeMap } from "../constants";
 | 
				
			||||||
 | 
					import Engine from "../runtime";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * All test programs are picked up from https://esolangs.org/wiki/Brainfuck.
 | 
				
			||||||
 | 
					 * - Cell cleanup code at end of cell size program is not included.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if actual cell array matches expected cell array.
 | 
				
			||||||
 | 
					 * Expected cell array must exclude trailing zeros.
 | 
				
			||||||
 | 
					 * @param cellsMap Map of cell index to value, as provided in execution result.
 | 
				
			||||||
 | 
					 * @param expected Array of expected cell values, without trailing zeros.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					// const expectCellsToBe = (cellsMap: BFRS["tape"], expected: number[]) => {
 | 
				
			||||||
 | 
					//   const cells = serializeTapeMap(cellsMap);
 | 
				
			||||||
 | 
					//   expect(cells.length).toBeGreaterThanOrEqual(expected.length);
 | 
				
			||||||
 | 
					//   cells.forEach((value, idx) => {
 | 
				
			||||||
 | 
					//     if (idx < expected.length) expect(value).toBe(expected[idx]);
 | 
				
			||||||
 | 
					//     else expect(value).toBe(0);
 | 
				
			||||||
 | 
					//   });
 | 
				
			||||||
 | 
					// };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("Test programs", () => {
 | 
				
			||||||
 | 
					  // Standard hello-world program
 | 
				
			||||||
 | 
					  test("hello world", async () => {
 | 
				
			||||||
 | 
					    const code = readTestProgram(__dirname, "helloworld");
 | 
				
			||||||
 | 
					    const result = await executeProgram(new Engine(), code);
 | 
				
			||||||
 | 
					    expect(result.output).toBe("7210110810811132119111114108100");
 | 
				
			||||||
 | 
					    expect(result.rendererState.value).toBe(100);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Test program 1, output 0
 | 
				
			||||||
 | 
					  test("output zero (1)", async () => {
 | 
				
			||||||
 | 
					    const code = readTestProgram(__dirname, "zero1");
 | 
				
			||||||
 | 
					    const result = await executeProgram(new Engine(), code);
 | 
				
			||||||
 | 
					    expect(result.output).toBe("0");
 | 
				
			||||||
 | 
					    expect(result.rendererState.value).toBe(0);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Test program 2, output 0
 | 
				
			||||||
 | 
					  test("output zero (2)", async () => {
 | 
				
			||||||
 | 
					    const code = readTestProgram(__dirname, "zero2");
 | 
				
			||||||
 | 
					    const result = await executeProgram(new Engine(), code);
 | 
				
			||||||
 | 
					    expect(result.output).toBe("0");
 | 
				
			||||||
 | 
					    expect(result.rendererState.value).toBe(0);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Test program 3, output 288
 | 
				
			||||||
 | 
					  test("output 288", async () => {
 | 
				
			||||||
 | 
					    const code = readTestProgram(__dirname, "288");
 | 
				
			||||||
 | 
					    const result = await executeProgram(new Engine(), code);
 | 
				
			||||||
 | 
					    expect(result.output).toBe("288");
 | 
				
			||||||
 | 
					    expect(result.rendererState.value).toBe(288);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										1
									
								
								engines/deadfish/tests/zero1.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								engines/deadfish/tests/zero1.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					iissso
 | 
				
			||||||
							
								
								
									
										1
									
								
								engines/deadfish/tests/zero2.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								engines/deadfish/tests/zero2.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					iissisdddddddddddddddddddddddddddddddddo
 | 
				
			||||||
		Reference in New Issue
	
	Block a user