Add Befunge-93
This commit is contained in:
		
							
								
								
									
										3
									
								
								engines/befunge93/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								engines/befunge93/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| # Befunge-93 | ||||
|  | ||||
| - Interactive input is not supported yet, so currenty division-by-zero throws a runtime error. | ||||
							
								
								
									
										79
									
								
								engines/befunge93/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								engines/befunge93/constants.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | ||||
| import { MonacoTokensProvider } from "../types"; | ||||
|  | ||||
| export type Bfg93RS = { | ||||
|   stack: number[]; | ||||
|   direction: Bfg93Direction; | ||||
|   strMode: boolean; | ||||
| }; | ||||
|  | ||||
| /** Direction of program counter */ | ||||
| export enum Bfg93Direction { | ||||
|   UP = "up", | ||||
|   DOWN = "down", | ||||
|   LEFT = "left", | ||||
|   RIGHT = "right", | ||||
| } | ||||
|  | ||||
| /** Allowed operations in Befunge */ | ||||
| export enum Bfg93Op { | ||||
|   NOOP = " ", | ||||
|   ADD = "+", | ||||
|   SUBTRACT = "-", | ||||
|   MULTIPLY = "*", | ||||
|   DIVIDE = "/", | ||||
|   MODULO = "%", | ||||
|   NOT = "!", | ||||
|   GREATER = "`", | ||||
|   RIGHT = ">", | ||||
|   LEFT = "<", | ||||
|   UP = "^", | ||||
|   DOWN = "v", | ||||
|   RANDOM = "?", | ||||
|   H_IF = "_", | ||||
|   V_IF = "|", | ||||
|   TOGGLE_STR = '"', | ||||
|   DUPLICATE = ":", | ||||
|   SWAP = "\\", | ||||
|   POP_DELETE = "$", | ||||
|   POP_OUTINT = ".", | ||||
|   POP_OUTCHAR = ",", | ||||
|   BRIDGE = "#", | ||||
|   GET_DATA = "g", | ||||
|   PUT_DATA = "p", | ||||
|   STDIN_INT = "&", | ||||
|   STDIN_CHAR = "~", | ||||
|   END = "@", | ||||
|   PUSH_0 = "0", | ||||
|   PUSH_1 = "1", | ||||
|   PUSH_2 = "2", | ||||
|   PUSH_3 = "3", | ||||
|   PUSH_4 = "4", | ||||
|   PUSH_5 = "5", | ||||
|   PUSH_6 = "6", | ||||
|   PUSH_7 = "7", | ||||
|   PUSH_8 = "8", | ||||
|   PUSH_9 = "9", | ||||
| } | ||||
|  | ||||
| /** Sample program printing "Hello world" */ | ||||
| export const sampleProgram = [ | ||||
|   `"!dlroW ,olleH">:v`, | ||||
|   `               |,<`, | ||||
|   `               @`, | ||||
| ].join("\n"); | ||||
|  | ||||
| /** Tokens provider */ | ||||
| export const editorTokensProvider: MonacoTokensProvider = { | ||||
|   tokenizer: { | ||||
|     root: [ | ||||
|       [/[\>\^<v\?]/, "red"], | ||||
|       [/[\+\-\*\/%!`]/, "orange"], | ||||
|       [/[|_]/, "blue"], | ||||
|       [/[":\\#]/, "green"], | ||||
|       [/[\$\.,]/, "violet"], | ||||
|       [/[gp@]/, "indigo"], | ||||
|       [/[&~0-9]/, "turquoise"], | ||||
|     ], | ||||
|   }, | ||||
|   defaultToken: "comment", | ||||
| }; | ||||
							
								
								
									
										4
									
								
								engines/befunge93/engine.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								engines/befunge93/engine.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| import { setupWorker } from "../setup-worker"; | ||||
| import Befunge93LanguageEngine from "./runtime"; | ||||
|  | ||||
| setupWorker(new Befunge93LanguageEngine()); | ||||
							
								
								
									
										11
									
								
								engines/befunge93/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								engines/befunge93/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| import { Renderer } from "./renderer"; | ||||
| import { LanguageProvider } from "../types"; | ||||
| import { Bfg93RS, sampleProgram, editorTokensProvider } from "./constants"; | ||||
|  | ||||
| const provider: LanguageProvider<Bfg93RS> = { | ||||
|   Renderer, | ||||
|   sampleProgram, | ||||
|   editorTokensProvider, | ||||
| }; | ||||
|  | ||||
| export default provider; | ||||
							
								
								
									
										46
									
								
								engines/befunge93/input-stream.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								engines/befunge93/input-stream.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| import { RuntimeError } from "../worker-errors"; | ||||
|  | ||||
| /** | ||||
|  * A barebones input stream implementation for consuming integers and characters from a string. | ||||
|  */ | ||||
| export default class InputStream { | ||||
|   private _text: string; | ||||
|  | ||||
|   /** Create a new input stream loaded with the given input */ | ||||
|   constructor(text: string) { | ||||
|     this._text = text; | ||||
|   } | ||||
|  | ||||
|   /** Remove leading whitespace from the current input stream */ | ||||
|   private exhaustLeadingWhitespace(): void { | ||||
|     const firstChar = this._text.trim()[0]; | ||||
|     const posn = this._text.search(firstChar); | ||||
|     this._text = this._text.slice(posn); | ||||
|   } | ||||
|  | ||||
|   /** Parse input stream for an integer */ | ||||
|   getNumber(): number { | ||||
|     this.exhaustLeadingWhitespace(); | ||||
|     // The extra whitespace differentiates whether string is empty or all numbers. | ||||
|     if (this._text === "") throw new RuntimeError("Unexpected end of input"); | ||||
|     let posn = this._text.search(/[^0-9]/); | ||||
|     if (posn === 0) | ||||
|       throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`); | ||||
|     if (posn === -1) posn = this._text.length; | ||||
|     // Consume and parse numeric part | ||||
|     const numStr = this._text.slice(0, posn); | ||||
|     this._text = this._text.slice(posn); | ||||
|     return parseInt(numStr, 10); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Parse input stream for the first character, and return its ASCII code. | ||||
|    * If end of input, returns -1. | ||||
|    */ | ||||
|   getChar(): number { | ||||
|     if (this._text.length === 0) return -1; | ||||
|     const char = this._text[0]; | ||||
|     this._text = this._text.slice(1); | ||||
|     return char.charCodeAt(0); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										80
									
								
								engines/befunge93/renderer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								engines/befunge93/renderer.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | ||||
| import { Card, Colors, Icon, IconName } from "@blueprintjs/core"; | ||||
| import { RendererProps } from "../types"; | ||||
| import { Bfg93Direction, Bfg93RS } from "./constants"; | ||||
|  | ||||
| /** Common border color for dark and light, using transparency */ | ||||
| export const BorderColor = Colors.GRAY3 + "55"; | ||||
|  | ||||
| const styles = { | ||||
|   placeholderDiv: { | ||||
|     height: "100%", | ||||
|     width: "100%", | ||||
|     display: "flex", | ||||
|     justifyContent: "center", | ||||
|     alignItems: "center", | ||||
|     fontSize: "1.2em", | ||||
|   }, | ||||
|   rootContainer: { | ||||
|     height: "100%", | ||||
|     display: "flex", | ||||
|     flexDirection: "column" as "column", | ||||
|   }, | ||||
|   dirnContainer: { | ||||
|     borderBottom: "1px solid " + BorderColor, | ||||
|     padding: "5px 10px", | ||||
|   }, | ||||
|   stackContainer: { | ||||
|     padding: 10, | ||||
|     height: "100%", | ||||
|     display: "flex", | ||||
|     flexWrap: "wrap" as "wrap", | ||||
|     alignContent: "flex-start", | ||||
|     overflowY: "auto" as "auto", | ||||
|   }, | ||||
|   stackItem: { | ||||
|     // Sizing | ||||
|     width: "10%", | ||||
|     height: "40px", | ||||
|     margin: "5px 0.25%", | ||||
|     // Center-align values | ||||
|     display: "flex", | ||||
|     justifyContent: "center", | ||||
|     alignItems: "center", | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const DirectionIcons: { [k: string]: IconName } = { | ||||
|   [Bfg93Direction.RIGHT]: "arrow-right", | ||||
|   [Bfg93Direction.LEFT]: "arrow-left", | ||||
|   [Bfg93Direction.UP]: "arrow-up", | ||||
|   [Bfg93Direction.DOWN]: "arrow-down", | ||||
| }; | ||||
|  | ||||
| const StackItem = ({ value }: { value: number }) => { | ||||
|   const cellStyle = { ...styles.stackItem }; | ||||
|   return <Card style={cellStyle}>{value}</Card>; | ||||
| }; | ||||
|  | ||||
| export const Renderer = ({ state }: RendererProps<Bfg93RS>) => { | ||||
|   if (state == null) | ||||
|     return <div style={styles.placeholderDiv}>Run some code...</div>; | ||||
|  | ||||
|   return ( | ||||
|     <div style={styles.rootContainer}> | ||||
|       <div style={styles.dirnContainer}> | ||||
|         <span style={{ fontWeight: "bold", marginRight: 5 }}>Direction: </span> | ||||
|         <Icon icon={DirectionIcons[state.direction]} /> | ||||
|         {/* <span style={{ marginLeft: 10 }} /> */} | ||||
|         <span style={{ marginLeft: 30, fontWeight: "bold", marginRight: 5 }}> | ||||
|           String mode:{" "} | ||||
|         </span> | ||||
|         {state.strMode ? "ON" : "OFF"} | ||||
|       </div> | ||||
|       <div style={styles.stackContainer}> | ||||
|         {state.stack.map((value, idx) => ( | ||||
|           <StackItem key={idx} value={value} /> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										407
									
								
								engines/befunge93/runtime.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										407
									
								
								engines/befunge93/runtime.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,407 @@ | ||||
| import InputStream from "./input-stream"; | ||||
| import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types"; | ||||
| import { ParseError, RuntimeError } from "../worker-errors"; | ||||
| import { Bfg93RS, Bfg93Op, Bfg93Direction } from "./constants"; | ||||
|  | ||||
| const ROWSIZE = 80; // Maximum size of a single grid row | ||||
| const COLSIZE = 25; // Maximum size of a single grid column | ||||
|  | ||||
| /** Program counter is coordinates in 2D grid. */ | ||||
| type PC = { | ||||
|   x: number; // 0-indexed, goes rightwards | ||||
|   y: number; // 0-indexed, goes downwards | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Defines bounds of the used portion of the grid. So, if the code | ||||
|  * only occupies top-left 30x20 square, all items in array `x` are < 30, | ||||
|  * and all items in array `y` are < 20. | ||||
|  * | ||||
|  * - `bounds.x[10]`: highest index used on 11th row of grid | ||||
|  * - `bounds.y[5]`: highest index used on 6th column of grid | ||||
|  */ | ||||
| type CodeBounds = { | ||||
|   x: number[]; | ||||
|   y: number[]; | ||||
| }; | ||||
|  | ||||
| // Default values for internal states | ||||
| // Factories are used to create new objects on reset | ||||
| const DEFAULT_AST = (): string[] => []; | ||||
| const DEFAULT_PC = () => ({ x: -1, y: -1 }); | ||||
| const DEFAULT_STACK = (): number[] => []; | ||||
| const DEFAULT_DIRN = Bfg93Direction.RIGHT; | ||||
| const DEFAULT_STR_MODE = false; | ||||
| const DEFAULT_BOUNDS = (): CodeBounds => ({ | ||||
|   x: [], | ||||
|   y: [], | ||||
| }); | ||||
|  | ||||
| // List of characters representing valid Befunge-93 ops | ||||
| const OP_CHARS = Object.values(Bfg93Op); | ||||
|  | ||||
| export default class Befunge93LanguageEngine | ||||
|   implements LanguageEngine<Bfg93RS> | ||||
| { | ||||
|   private _ast: string[] = DEFAULT_AST(); | ||||
|   private _stack: number[] = DEFAULT_STACK(); | ||||
|   private _pc: PC = DEFAULT_PC(); | ||||
|   private _dirn: Bfg93Direction = DEFAULT_DIRN; | ||||
|   private _strmode: boolean = DEFAULT_STR_MODE; | ||||
|   private _bounds: CodeBounds = DEFAULT_BOUNDS(); | ||||
|   private _input: InputStream = new InputStream(""); | ||||
|  | ||||
|   resetState() { | ||||
|     this._ast = DEFAULT_AST(); | ||||
|     this._stack = DEFAULT_STACK(); | ||||
|     this._pc = DEFAULT_PC(); | ||||
|     this._dirn = DEFAULT_DIRN; | ||||
|     this._strmode = DEFAULT_STR_MODE; | ||||
|     this._bounds = DEFAULT_BOUNDS(); | ||||
|     this._input = new InputStream(""); | ||||
|   } | ||||
|  | ||||
|   validateCode(code: string) { | ||||
|     this.parseCode(code); | ||||
|   } | ||||
|  | ||||
|   prepare(code: string, input: string) { | ||||
|     this._ast = this.parseCode(code); | ||||
|     this._input = new InputStream(input); | ||||
|   } | ||||
|  | ||||
|   executeStep(): StepExecutionResult<Bfg93RS> { | ||||
|     // Execute and update program counter | ||||
|     let output: string | undefined = undefined; | ||||
|     let end: boolean = false; | ||||
|     if (this._pc.x === -1 && this._pc.y === -1) { | ||||
|       this._pc = { x: 0, y: 0 }; | ||||
|     } else { | ||||
|       const result = this.processOp(); | ||||
|       output = result.output; | ||||
|       end = !!result.end; | ||||
|     } | ||||
|  | ||||
|     // Prepare location of next step | ||||
|     let nextStepLocation: DocumentRange | null = null; | ||||
|     if (!end) nextStepLocation = this.toRange(this._pc.y, this._pc.x); | ||||
|  | ||||
|     // Prepare and return execution result | ||||
|     const rendererState: Bfg93RS = { | ||||
|       stack: this._stack, | ||||
|       direction: this._dirn, | ||||
|       strMode: this._strmode, | ||||
|     }; | ||||
|     return { rendererState, nextStepLocation, output }; | ||||
|   } | ||||
|  | ||||
|   private parseCode(code: string) { | ||||
|     // A Befunge program can contain any character in the program, so the only | ||||
|     // validation to do is ensure program is within 80x25 bounds. | ||||
|  | ||||
|     // Validate that program is within the 80x25 bounds | ||||
|     const lines = code.split("\n"); | ||||
|     if (lines.length > COLSIZE) | ||||
|       throw new ParseError(`Code is longer than ${COLSIZE} lines`, { | ||||
|         line: COLSIZE, | ||||
|       }); | ||||
|     lines.forEach((line, idx) => { | ||||
|       if (line.length > ROWSIZE) | ||||
|         throw new ParseError(`Line is longer than ${ROWSIZE} characters`, { | ||||
|           line: idx, | ||||
|           charRange: { start: ROWSIZE }, | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     // Global bounds for each axis | ||||
|     const maxX = Math.max(...lines.map((line) => line.length - 1)); | ||||
|     const maxY = lines.length - 1; | ||||
|  | ||||
|     // Define bounds for each line and column | ||||
|     for (let i = 0; i < COLSIZE; ++i) | ||||
|       this._bounds.x[i] = lines[i]?.length - 1 || -1; | ||||
|     for (let j = 0; j < ROWSIZE; ++j) this._bounds.y[j] = j <= maxX ? maxY : -1; | ||||
|  | ||||
|     // Pad the program to size 80x25 for execution | ||||
|     const grid = lines.map((line) => line.padEnd(80, " ")); | ||||
|     grid.push(...new Array(25 - lines.length).fill(" ".repeat(80))); | ||||
|     return grid; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Process the instruction at the current program grid pointer. | ||||
|    * Also updates stack and pointer states. | ||||
|    * @returns String to append to output, if any | ||||
|    */ | ||||
|   private processOp(): { output?: string; end?: boolean } { | ||||
|     const char = this.getGridCell(this._pc.x, this._pc.y); | ||||
|     if (this._strmode && char !== '"') { | ||||
|       // Push character to string and return; | ||||
|       this._stack.push(char.charCodeAt(0)); | ||||
|       this.updatePointer(); | ||||
|       return {}; | ||||
|     } | ||||
|  | ||||
|     let output: string | undefined = undefined; | ||||
|     let end: boolean = false; | ||||
|  | ||||
|     const op = this.charToOp(char); | ||||
|     if (!op) throw new RuntimeError("Invalid instruction"); | ||||
|     switch (op) { | ||||
|       case Bfg93Op.NOOP: { | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.ADD: { | ||||
|         const a = this.popStack(); | ||||
|         const b = this.popStack(); | ||||
|         this.pushStack(a + b); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.SUBTRACT: { | ||||
|         const a = this.popStack(); | ||||
|         const b = this.popStack(); | ||||
|         this.pushStack(b - a); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.MULTIPLY: { | ||||
|         const a = this.popStack(); | ||||
|         const b = this.popStack(); | ||||
|         this.pushStack(a * b); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.DIVIDE: { | ||||
|         const a = this.popStack(); | ||||
|         const b = this.popStack(); | ||||
|         if (a === 0) throw new RuntimeError("cannot divide by zero"); | ||||
|         this.pushStack(Math.floor(b / a)); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.MODULO: { | ||||
|         const a = this.popStack(); | ||||
|         const b = this.popStack(); | ||||
|         this.pushStack(b % a); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.NOT: { | ||||
|         const val = this.popStack(); | ||||
|         this.pushStack(val === 0 ? 1 : 0); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.GREATER: { | ||||
|         const a = this.popStack(); | ||||
|         const b = this.popStack(); | ||||
|         this.pushStack(b > a ? 1 : 0); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.RIGHT: { | ||||
|         this._dirn = Bfg93Direction.RIGHT; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.LEFT: { | ||||
|         this._dirn = Bfg93Direction.LEFT; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.UP: { | ||||
|         this._dirn = Bfg93Direction.UP; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.DOWN: { | ||||
|         this._dirn = Bfg93Direction.DOWN; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.RANDOM: { | ||||
|         const rand = Math.floor(Math.random() * 4); | ||||
|         if (rand === 0) this._dirn = Bfg93Direction.RIGHT; | ||||
|         else if (rand === 1) this._dirn = Bfg93Direction.LEFT; | ||||
|         else if (rand === 2) this._dirn = Bfg93Direction.UP; | ||||
|         else this._dirn = Bfg93Direction.DOWN; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.H_IF: { | ||||
|         const val = this.popStack(); | ||||
|         if (val === 0) this._dirn = Bfg93Direction.RIGHT; | ||||
|         else this._dirn = Bfg93Direction.LEFT; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.V_IF: { | ||||
|         const val = this.popStack(); | ||||
|         if (val === 0) this._dirn = Bfg93Direction.DOWN; | ||||
|         else this._dirn = Bfg93Direction.UP; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.TOGGLE_STR: { | ||||
|         this._strmode = !this._strmode; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.DUPLICATE: { | ||||
|         const val = this.popStack(); | ||||
|         this.pushStack(val); | ||||
|         this.pushStack(val); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.SWAP: { | ||||
|         const top = this.popStack(); | ||||
|         const other = this.popStack(); | ||||
|         this.pushStack(top); | ||||
|         this.pushStack(other); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.POP_DELETE: { | ||||
|         this.popStack(); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.POP_OUTINT: { | ||||
|         const int = this.popStack(); | ||||
|         output = int.toString() + " "; | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.POP_OUTCHAR: { | ||||
|         const charCode = this.popStack(); | ||||
|         output = String.fromCharCode(charCode); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.BRIDGE: { | ||||
|         this.updatePointer(); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.GET_DATA: { | ||||
|         const y = this.popStack(); | ||||
|         const x = this.popStack(); | ||||
|         const char = this.getGridCell(x, y); | ||||
|         this.pushStack(char.charCodeAt(0)); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.PUT_DATA: { | ||||
|         const y = this.popStack(); | ||||
|         const x = this.popStack(); | ||||
|         const charCode = this.popStack(); | ||||
|         this.setGridCell(x, y, charCode); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.STDIN_INT: { | ||||
|         this.pushStack(this._input.getNumber()); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.STDIN_CHAR: { | ||||
|         const charCode = this._input.getChar(); | ||||
|         this.pushStack(charCode); | ||||
|         break; | ||||
|       } | ||||
|       case Bfg93Op.END: { | ||||
|         end = true; | ||||
|         break; | ||||
|       } | ||||
|       default: { | ||||
|         this.pushStack(parseInt(op, 10)); | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Update grid pointer and return | ||||
|     this.updatePointer(); | ||||
|     return { output, end }; | ||||
|   } | ||||
|  | ||||
|   /** Push a number onto the stack */ | ||||
|   private pushStack(num: number): void { | ||||
|     this._stack.push(num); | ||||
|   } | ||||
|  | ||||
|   /** Pop a number from stack. If empty stack, returns 0 */ | ||||
|   private popStack(): number { | ||||
|     if (this._stack.length === 0) return 0; | ||||
|     else return this._stack.pop()!; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get character at position (x, y) of program grid. | ||||
|    * Throws RuntimeError if (x, y) is out of bounds. | ||||
|    */ | ||||
|   private getGridCell(x: number, y: number): string { | ||||
|     if (!this.isInGrid(x, y)) | ||||
|       throw new RuntimeError("Coordinates out of bounds"); | ||||
|     else return this._ast[y][x]; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set cell at (x, y) of program grid to character with given ASCII value. | ||||
|    * Throws if (x, y) is out of bounds | ||||
|    */ | ||||
|   private setGridCell(x: number, y: number, asciiVal: number): void { | ||||
|     if (!this.isInGrid(x, y)) | ||||
|       throw new RuntimeError("Coordinates out of bound"); | ||||
|  | ||||
|     // Change character at position (x, y) | ||||
|     this._ast[y] = | ||||
|       this._ast[y].slice(0, x) + | ||||
|       String.fromCharCode(asciiVal) + | ||||
|       this._ast[y].slice(x + 1); | ||||
|  | ||||
|     // Update grid bounds | ||||
|     this._bounds.x[y] = Math.max(this._bounds.x[y], x); | ||||
|     this._bounds.y[x] = Math.max(this._bounds.y[x], y); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Update program grid pointer according to currently set direction. | ||||
|    * Throws RuntimeError if pointer lands outside 80x25 grid. | ||||
|    */ | ||||
|   private updatePointer(): void { | ||||
|     // Update pointer | ||||
|     if (this._dirn === Bfg93Direction.RIGHT) this._pc.x += 1; | ||||
|     else if (this._dirn === Bfg93Direction.LEFT) this._pc.x -= 1; | ||||
|     else if (this._dirn === Bfg93Direction.UP) this._pc.y -= 1; | ||||
|     else if (this._dirn === Bfg93Direction.DOWN) this._pc.y += 1; | ||||
|     else throw new Error("Unknown direction"); | ||||
|  | ||||
|     // Check pointer position and wrap if necessary | ||||
|     this.wrapPointer(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Wraps the pointer around the program bounds. Note that program bounds are | ||||
|    * not 80x25 - they are the bounds of the used parts of grid. | ||||
|    * | ||||
|    * Assumes that only one of x and y-coordinates is out of bounds. | ||||
|    */ | ||||
|   private wrapPointer(): void { | ||||
|     if (this._strmode) { | ||||
|       // String mode: just wrap the pointer around the 80x25 grid | ||||
|       this._pc.x = (this._pc.x + ROWSIZE) % ROWSIZE; | ||||
|       this._pc.y = (this._pc.y + COLSIZE) % COLSIZE; | ||||
|     } else if ( | ||||
|       this._dirn === Bfg93Direction.LEFT || | ||||
|       this._dirn === Bfg93Direction.RIGHT | ||||
|     ) { | ||||
|       // Wrap pointer around code bounds in horizontal direction (along x-axis) | ||||
|       if (this._pc.x < 0) this._pc.x = this._bounds.x[this._pc.y]; | ||||
|       else if (this._pc.x > this._bounds.x[this._pc.y]) this._pc.x = 0; | ||||
|     } else { | ||||
|       // Wrap pointer around code bounds in vertical direction (along y-axis) | ||||
|       if (this._pc.y < 0) this._pc.y = this._bounds.y[this._pc.x]; | ||||
|       else if (this._pc.y > this._bounds.y[this._pc.x]) this._pc.y = 0; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Cast the given character to corresponding Befunge-93 op. | ||||
|    * If character is invalid op, returns null. | ||||
|    * @param char Character to cast to Befunge-93 op | ||||
|    * @returns Corresponding Befunge-93 op, or null. | ||||
|    */ | ||||
|   private charToOp(char: string): Bfg93Op | null { | ||||
|     if (char.length !== 1) throw new Error(`'${char}' not a character`); | ||||
|     if (!OP_CHARS.includes(char as Bfg93Op)) return null; | ||||
|     else return char as Bfg93Op; | ||||
|   } | ||||
|  | ||||
|   /** Convert 2D coordinates to DocumentRange */ | ||||
|   private toRange(line: number, char: number): DocumentRange { | ||||
|     return { line, charRange: { start: char, end: char + 1 } }; | ||||
|   } | ||||
|  | ||||
|   /** Check if given coordinates lies inside 80x25 grid */ | ||||
|   private isInGrid(x: number, y: number): boolean { | ||||
|     return x >= 0 && x < ROWSIZE && y >= 0 && y < COLSIZE; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										96
									
								
								engines/befunge93/tests/index.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								engines/befunge93/tests/index.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| import { readTestProgram, executeProgram } from "../../test-utils"; | ||||
| import { Bfg93Direction } from "../constants"; | ||||
| import Engine from "../runtime"; | ||||
|  | ||||
| /** | ||||
|  * All test programs are picked up from https://esolangs.org/wiki/Befunge, | ||||
|  * except the modifications mentioned alongside each test. | ||||
|  */ | ||||
|  | ||||
| /** Relative path to directory of sample programs */ | ||||
| const DIRNAME = __dirname + "/samples"; | ||||
|  | ||||
| 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.charCodeAt(result.output.length - 1)).toBe(0); | ||||
|     expect(result.output.slice(0, -1)).toBe("Hello, World!"); | ||||
|     expect(result.rendererState.direction).toBe(Bfg93Direction.DOWN); | ||||
|     expect(result.rendererState.stack.length).toBe(0); | ||||
|   }); | ||||
|  | ||||
|   // cat program | ||||
|   test("cat program", async () => { | ||||
|     const input = "abcd efgh\nijkl mnop\n"; | ||||
|     const code = readTestProgram(DIRNAME, "cat"); | ||||
|     const result = await executeProgram(new Engine(), code, input); | ||||
|     expect(result.output).toBe(input); | ||||
|     expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT); | ||||
|     expect(result.rendererState.stack).toEqual([-1]); | ||||
|   }); | ||||
|  | ||||
|   // Random DNA printer | ||||
|   test("random DNA", async () => { | ||||
|     const code = readTestProgram(DIRNAME, "dna"); | ||||
|     const result = await executeProgram(new Engine(), code); | ||||
|     // program prints "\r\n" at the end of output | ||||
|     expect(result.output.length).toBe(56 + 2); | ||||
|     expect(result.output.trim().search(/[^ATGC]/)).toBe(-1); | ||||
|     expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT); | ||||
|     expect(result.rendererState.stack).toEqual([0]); | ||||
|   }); | ||||
|  | ||||
|   // Factorial program | ||||
|   test("factorial", async () => { | ||||
|     const code = readTestProgram(DIRNAME, "factorial"); | ||||
|     const result = await executeProgram(new Engine(), code, "5"); | ||||
|     expect(result.output).toBe("120 "); | ||||
|     expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT); | ||||
|     expect(result.rendererState.stack.length).toBe(0); | ||||
|   }); | ||||
|  | ||||
|   // Sieve of Eratosthenes - prints prime nums upto 36 | ||||
|   // (original prints up to 80, shortened here for testing purposes) | ||||
|   test("sieve of eratosthenes", async () => { | ||||
|     const code = readTestProgram(DIRNAME, "prime-sieve"); | ||||
|     const result = await executeProgram(new Engine(), code); | ||||
|     const outputNums = result.output | ||||
|       .trim() | ||||
|       .split(" ") | ||||
|       .map((a) => parseInt(a, 10)); | ||||
|     const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]; | ||||
|     expect(outputNums).toEqual(primes); | ||||
|     expect(result.rendererState.direction).toBe(Bfg93Direction.DOWN); | ||||
|     expect(result.rendererState.stack).toEqual([37]); | ||||
|   }); | ||||
|  | ||||
|   // Quine 1 - simple single-line quine | ||||
|   test("simple singleline quine", async () => { | ||||
|     const code = readTestProgram(DIRNAME, "quine1"); | ||||
|     const result = await executeProgram(new Engine(), code); | ||||
|     expect(result.output).toBe(code); | ||||
|     expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT); | ||||
|     expect(result.rendererState.stack).toEqual([44]); | ||||
|   }); | ||||
|  | ||||
|   // Quine 2 - multiline quine | ||||
|   test("multiline quine", async () => { | ||||
|     const code = readTestProgram(DIRNAME, "quine2"); | ||||
|     const result = await executeProgram(new Engine(), code); | ||||
|     // Output has an extra space at the end - verified on tio.run | ||||
|     expect(result.output).toBe(code + " "); | ||||
|     expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT); | ||||
|     expect(result.rendererState.stack).toEqual([0]); | ||||
|   }); | ||||
|  | ||||
|   // Quine 3 - quine without using "g" | ||||
|   test("quine without using 'g'", async () => { | ||||
|     const code = readTestProgram(DIRNAME, "quine3"); | ||||
|     const result = await executeProgram(new Engine(), code); | ||||
|     expect(result.output).toBe(code); | ||||
|     expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT); | ||||
|     expect(result.rendererState.stack).toEqual([0]); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										1
									
								
								engines/befunge93/tests/samples/cat.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								engines/befunge93/tests/samples/cat.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| ~:1+!#@_, | ||||
							
								
								
									
										8
									
								
								engines/befunge93/tests/samples/dna.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								engines/befunge93/tests/samples/dna.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| 7^DN>vA | ||||
| v_#v? v | ||||
| 7^<"""" | ||||
| 3  ACGT | ||||
| 90!"""" | ||||
| 4*:>>>v | ||||
| +8^-1,< | ||||
| > ,+,@) | ||||
							
								
								
									
										2
									
								
								engines/befunge93/tests/samples/factorial.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								engines/befunge93/tests/samples/factorial.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| &>:1-:v v *_$.@  | ||||
|  ^    _$>\:^ | ||||
							
								
								
									
										3
									
								
								engines/befunge93/tests/samples/helloworld.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								engines/befunge93/tests/samples/helloworld.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| "!dlroW ,olleH">:v | ||||
|                |,< | ||||
|                @ | ||||
							
								
								
									
										4
									
								
								engines/befunge93/tests/samples/prime-sieve.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								engines/befunge93/tests/samples/prime-sieve.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| 2>:3g" "-!v\  g30          < | ||||
|  |!`"$":+1_:.:03p>03g+:"$"`| | ||||
|  @               ^  p3\" ":< | ||||
| 2 234567890123456789012345678901234567890123456789012345678901234567890123456789 | ||||
							
								
								
									
										1
									
								
								engines/befunge93/tests/samples/quine1.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								engines/befunge93/tests/samples/quine1.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| 01->1# +# :# 0# g# ,# :# 5# 8# *# 4# +# -# _@ | ||||
							
								
								
									
										2
									
								
								engines/befunge93/tests/samples/quine2.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								engines/befunge93/tests/samples/quine2.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| 0 v | ||||
|  "<@_ #! #: #,<*2-1*92,*84,*25,+*92*4*55.0                                       | ||||
							
								
								
									
										1
									
								
								engines/befunge93/tests/samples/quine3.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								engines/befunge93/tests/samples/quine3.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <@,+2*48_,#! #:<,_$#-:#*8#4<8" | ||||
| @ -153,7 +153,7 @@ class ExecutionController<RS> { | ||||
|           if (isRuntimeError(error)) { | ||||
|             this._isPaused = true; | ||||
|             resolve({ | ||||
|               result: this._result!, | ||||
|               result: { ...this._result!, output: undefined }, | ||||
|               error: serializeRuntimeError(error), | ||||
|             }); | ||||
|           } else reject(error); | ||||
|  | ||||
							
								
								
									
										26
									
								
								pages/ide/befunge93.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								pages/ide/befunge93.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| import React from "react"; | ||||
| import { NextPage } from "next"; | ||||
| import Head from "next/head"; | ||||
| import { Mainframe } from "../../ui/Mainframe"; | ||||
| import { Header } from "../../ui/header"; | ||||
| import LangProvider from "../../engines/befunge93"; | ||||
| const LANG_ID = "befunge93"; | ||||
| const LANG_NAME = "Befunge-93"; | ||||
|  | ||||
| const IDE: NextPage = () => { | ||||
|   return ( | ||||
|     <> | ||||
|       <Head> | ||||
|         <title>{LANG_NAME} | Esolang Park</title> | ||||
|       </Head> | ||||
|       <div style={{ height: "100%", display: "flex", flexDirection: "column" }}> | ||||
|         <Header langName={LANG_NAME} /> | ||||
|         <div style={{ flexGrow: 1 }}> | ||||
|           <Mainframe langName={LANG_ID} provider={LangProvider} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default IDE; | ||||
		Reference in New Issue
	
	Block a user
	 Nilay Majorwar
					Nilay Majorwar