├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── languages ├── befunge93 │ ├── README.md │ ├── constants.ts │ ├── engine.ts │ ├── index.ts │ ├── input-stream.ts │ ├── renderer.tsx │ ├── runtime.ts │ └── tests │ │ ├── index.test.ts │ │ └── samples │ │ ├── cat.txt │ │ ├── dna.txt │ │ ├── factorial.txt │ │ ├── helloworld.txt │ │ ├── prime-sieve.txt │ │ ├── quine1.txt │ │ ├── quine2.txt │ │ └── quine3.txt ├── brainfuck │ ├── README.md │ ├── common.ts │ ├── engine.ts │ ├── index.ts │ ├── renderer.tsx │ ├── runtime.ts │ └── tests │ │ ├── cat.txt │ │ ├── cellsize.txt │ │ ├── helloworld-subzero.txt │ │ ├── helloworld.txt │ │ └── index.test.ts ├── chef │ ├── README.md │ ├── constants.ts │ ├── engine.ts │ ├── index.ts │ ├── parser │ │ ├── constants.ts │ │ ├── core.ts │ │ ├── index.ts │ │ └── regex.ts │ ├── renderer │ │ ├── bowl-dish-columns.tsx │ │ ├── index.tsx │ │ ├── ingredients-pane.tsx │ │ ├── kitchen-display.tsx │ │ └── utils.tsx │ ├── runtime │ │ ├── index.ts │ │ ├── input-stream.ts │ │ └── kitchen.ts │ ├── tests │ │ ├── index.test.ts │ │ ├── parser-core.test.ts │ │ ├── parser-index.test.ts │ │ └── samples │ │ │ ├── fibonacci-fromage.txt │ │ │ ├── hello-world-cake.txt │ │ │ └── hello-world-souffle.txt │ └── types.ts ├── deadfish │ ├── README.md │ ├── constants.ts │ ├── engine.ts │ ├── index.ts │ ├── renderer.tsx │ ├── runtime.ts │ └── tests │ │ ├── 288.txt │ │ ├── helloworld.txt │ │ ├── index.test.ts │ │ ├── zero1.txt │ │ └── zero2.txt ├── engine-utils.ts ├── execution-controller.ts ├── setup-worker.ts ├── shakespeare │ ├── README.md │ ├── common.ts │ ├── engine.ts │ ├── index.ts │ ├── input-stream.ts │ ├── parser │ │ ├── constants.ts │ │ ├── cst.d.ts │ │ ├── generate-cst-types.ts │ │ ├── index.ts │ │ ├── parser.ts │ │ ├── tokens.ts │ │ ├── visitor-types.d.ts │ │ └── visitor.ts │ ├── renderer │ │ ├── character-row.tsx │ │ ├── index.tsx │ │ └── topbar.tsx │ ├── runtime.ts │ └── tests │ │ ├── helloworld.txt │ │ ├── index.test.ts │ │ ├── primes.txt │ │ └── reverse.txt ├── test-utils.ts ├── types.ts ├── ui-utils.tsx ├── worker-constants.ts └── worker-errors.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── ide │ ├── befunge93.tsx │ ├── brainfuck.tsx │ ├── chef.tsx │ ├── deadfish.tsx │ └── shakespeare.tsx ├── index.tsx └── languages.json ├── public ├── favicon.ico └── vercel.svg ├── scripts ├── add-new-language.js └── new-lang-template │ ├── README.md │ ├── common.ts │ ├── engine.ts │ ├── ide-page.tsx │ ├── index.ts │ ├── renderer.tsx │ └── runtime.ts ├── styles ├── editor.css ├── globals.css └── mosaic.scss ├── tsconfig.json ├── ui ├── MainLayout.tsx ├── Mainframe.tsx ├── assets │ ├── guide-breakpoints.png │ ├── guide-exec-controls.png │ ├── guide-info-btn.png │ ├── guide-syntax-check.png │ └── logo.png ├── code-editor │ ├── index.tsx │ ├── monaco-utils.ts │ ├── themes │ │ ├── dark.json │ │ └── light.json │ ├── use-code-validator.ts │ ├── use-editor-breakpoints.ts │ └── use-editor-lang-config.ts ├── custom-icons.tsx ├── execution-controls.tsx ├── features-guide.tsx ├── header.tsx ├── input-editor.tsx ├── output-viewer.tsx ├── providers │ ├── dark-mode-provider.tsx │ ├── error-boundary-provider.tsx │ ├── features-guide-provider.tsx │ └── index.tsx ├── renderer-wrapper.tsx └── use-exec-controller.ts ├── worker-pack ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | # bundled worker scripts 40 | public/workers 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nilay Majorwar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Esolang Park 2 | 3 | Esolang Park is an **online interpreter and debugger** catered for **esoteric programming languages**. Think Repl.it, but a more simplified version for esoteric languages, with a visual debugger catered to each language, that runs on your own device. 4 | 5 | The goal of Esolang Park is to be a platform for esolang enthusiasts to test and debug their code more easily, as well as for other people to discover and play around with esoteric languages without leaving the browser. 6 | 7 | For every esolang, Esolang Park provides the following features: 8 | 9 | - Powerful Monaco code editor 10 | - Syntax highlighting 11 | - Live syntax checking in the editor 12 | - Set breakpoints in your code 13 | - Pause and step through code execution at any time 14 | - Adjust the speed of execution 15 | - View the runtime's internal state during execution 16 | 17 | Code for the core app is **completely decoupled** from the esolang runtime - adding the runtime for a new esoteric language is just writing code that implements a standard API. This allows developers to create a development environment for an esolang without worrying about the common details of text editor, error handling, debugging and input-output. 18 | 19 | ## Building the app 20 | 21 | Esolang Park is a [Next.js](https://nextjs.org) application built with TypeScript. To run Esolang Park locally, you need to have a modern version of [Node.js](https://nodejs.org) installed. This project uses [Yarn](https://yarnpkg.com/) as its package manager, so you need that too. 22 | 23 | Once you've cloned the repository locally, run the following commands and then navigate to http://localhost:3000 on your browser: 24 | 25 | ```sh 26 | yarn # Install project dependencies 27 | yarn build:worker # Build the esolang worker scripts 28 | yarn dev # Run the development server 29 | ``` 30 | 31 | If you're interested in modifying the source code of the language providers, run `yarn dev:worker` in a separate terminal window. This starts the webpack watcher on language provider source code. 32 | 33 | ## Adding new esolangs 34 | 35 | If you want to add new esolangs to the project, create an issue. Or you can also implement it yourself, if you're comfortable with TypeScript and a bit of React. The wiki contains a guide to implementing a language provider for Esolang Park. 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require("next/jest"); 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: "./", 6 | }); 7 | 8 | // Add any custom config to be passed to Jest 9 | const customJestConfig = { 10 | // setupFilesAfterEnv: ["/jest.setup.js"], 11 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 12 | // moduleDirectories: ["node_modules", "/"], 13 | testEnvironment: "jest-environment-node", 14 | }; 15 | 16 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 17 | module.exports = createJestConfig(customJestConfig); 18 | -------------------------------------------------------------------------------- /languages/befunge93/README.md: -------------------------------------------------------------------------------- 1 | # Befunge-93 2 | 3 | Befunge-93 is a two-dimensional esolang created by Chris Pressey, with the goal of being as difficult to compile 4 | as possible. The program is a finite 2D grid of operations, and execution can proceed in any of the four main 5 | directions. This makes watching the execution of a Befunge-93 program particularly satisfying. 6 | 7 | Memory in Befunge-93 is a stack of integers. In addition, Befunge-93 programs can be self-modifying - there are 8 | operations that allow accessing and editing the content of the source code at runtime. This means you can also store 9 | runtime data in the source code itself, if space permits. 10 | 11 | The [esolang wiki page](https://esolangs.org/wiki/Befunge) contains the complete language-specification, along with 12 | some interesting sample programs. 13 | 14 | ## Notes for the user 15 | 16 | - The program grid is padded with space to size 80x25 before execution. 17 | - Interactive input is not supported in Esolang Park, so currently division-by-zero throws a runtime error. 18 | - `g` and `p` calls that access coordinates outside of the 80x25 grid result in a runtime error. 19 | - Due to being two-dimensional, the breakpoint functionality is rather uncomfortable to use in Befunge-93. 20 | 21 | ## Implementation details 22 | 23 | - To avoid long no-op stretches, the engine keeps track of the bounds of the user's source code. If the pointer reaches 24 | the end of the _set_ portion of a horizontal line, it immediately loops over to the opposite end of the line. The same 25 | happens in vertical lines. Try executing `>>>>` to visualise this. 26 | 27 | Note that this does not happen in string mode. In string mode the pointer loops over the full grid, pushing ` ` (space) 28 | characters onto the stack. 29 | 30 | ## Possible improvements 31 | 32 | - There seems to be some minor issue in the initial grid padding. Some lines get padded to 81 characters instead 33 | of 80. This is only cosmetic though - the program executes correctly. 34 | -------------------------------------------------------------------------------- /languages/befunge93/constants.ts: -------------------------------------------------------------------------------- 1 | import { MonacoTokensProvider } from "../types"; 2 | 3 | export type Bfg93RS = { 4 | stack: number[]; 5 | direction: Bfg93Direction; 6 | strMode: boolean; 7 | }; 8 | 9 | /** Direction of program counter */ 10 | export enum Bfg93Direction { 11 | UP = "up", 12 | DOWN = "down", 13 | LEFT = "left", 14 | RIGHT = "right", 15 | } 16 | 17 | /** Allowed operations in Befunge */ 18 | export enum Bfg93Op { 19 | NOOP = " ", 20 | ADD = "+", 21 | SUBTRACT = "-", 22 | MULTIPLY = "*", 23 | DIVIDE = "/", 24 | MODULO = "%", 25 | NOT = "!", 26 | GREATER = "`", 27 | RIGHT = ">", 28 | LEFT = "<", 29 | UP = "^", 30 | DOWN = "v", 31 | RANDOM = "?", 32 | H_IF = "_", 33 | V_IF = "|", 34 | TOGGLE_STR = '"', 35 | DUPLICATE = ":", 36 | SWAP = "\\", 37 | POP_DELETE = "$", 38 | POP_OUTINT = ".", 39 | POP_OUTCHAR = ",", 40 | BRIDGE = "#", 41 | GET_DATA = "g", 42 | PUT_DATA = "p", 43 | STDIN_INT = "&", 44 | STDIN_CHAR = "~", 45 | END = "@", 46 | PUSH_0 = "0", 47 | PUSH_1 = "1", 48 | PUSH_2 = "2", 49 | PUSH_3 = "3", 50 | PUSH_4 = "4", 51 | PUSH_5 = "5", 52 | PUSH_6 = "6", 53 | PUSH_7 = "7", 54 | PUSH_8 = "8", 55 | PUSH_9 = "9", 56 | } 57 | 58 | /** Sample program printing "Hello world" */ 59 | export const sampleProgram = [ 60 | `"!dlroW ,olleH">:v`, 61 | ` |,<`, 62 | ` @`, 63 | ].join("\n"); 64 | 65 | /** Tokens provider */ 66 | export const editorTokensProvider: MonacoTokensProvider = { 67 | tokenizer: { 68 | root: [ 69 | [/[\>\^ = { 6 | Renderer, 7 | sampleProgram, 8 | editorTokensProvider, 9 | }; 10 | 11 | export default provider; 12 | -------------------------------------------------------------------------------- /languages/befunge93/input-stream.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeError } from "../worker-errors"; 2 | 3 | /** 4 | * A barebones input stream implementation for consuming integers and characters from a string. 5 | */ 6 | export default class InputStream { 7 | private _text: string; 8 | 9 | /** Create a new input stream loaded with the given input */ 10 | constructor(text: string) { 11 | this._text = text; 12 | } 13 | 14 | /** Remove leading whitespace from the current input stream */ 15 | private exhaustLeadingWhitespace(): void { 16 | const firstChar = this._text.trim()[0]; 17 | const posn = this._text.search(firstChar); 18 | this._text = this._text.slice(posn); 19 | } 20 | 21 | /** Parse input stream for an integer */ 22 | getNumber(): number { 23 | this.exhaustLeadingWhitespace(); 24 | // The extra whitespace differentiates whether string is empty or all numbers. 25 | if (this._text === "") throw new RuntimeError("Unexpected end of input"); 26 | let posn = this._text.search(/[^0-9]/); 27 | if (posn === 0) 28 | throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`); 29 | if (posn === -1) posn = this._text.length; 30 | // Consume and parse numeric part 31 | const numStr = this._text.slice(0, posn); 32 | this._text = this._text.slice(posn); 33 | return parseInt(numStr, 10); 34 | } 35 | 36 | /** 37 | * Parse input stream for the first character, and return its ASCII code. 38 | * If end of input, returns -1. 39 | */ 40 | getChar(): number { 41 | if (this._text.length === 0) return -1; 42 | const char = this._text[0]; 43 | this._text = this._text.slice(1); 44 | return char.charCodeAt(0); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /languages/befunge93/renderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Colors, Icon, IconName } from "@blueprintjs/core"; 3 | import { RendererProps } from "../types"; 4 | import { Box } from "../ui-utils"; 5 | import { Bfg93Direction, Bfg93RS } from "./constants"; 6 | 7 | /** Common border color for dark and light, using transparency */ 8 | export const BorderColor = Colors.GRAY3 + "55"; 9 | 10 | // Parameters for cell sizing, balanced to span the full row width 11 | // Constraint: `(width% + 2 * margin%) * ROWSIZE = 100%` 12 | const ROWSIZE = 8; 13 | const CELL_WIDTH = "12%"; 14 | const CELL_MARGIN = "5px 0.25%"; 15 | 16 | const styles = { 17 | placeholderDiv: { 18 | height: "100%", 19 | width: "100%", 20 | display: "flex", 21 | justifyContent: "center", 22 | alignItems: "center", 23 | fontSize: "1.2em", 24 | }, 25 | rootContainer: { 26 | height: "100%", 27 | display: "flex", 28 | flexDirection: "column" as "column", 29 | }, 30 | dirnContainer: { 31 | borderBottom: "1px solid " + BorderColor, 32 | padding: "5px 10px", 33 | }, 34 | stackContainer: { 35 | padding: 10, 36 | height: "100%", 37 | display: "flex", 38 | flexWrap: "wrap" as "wrap", 39 | alignContent: "flex-start", 40 | overflowY: "auto" as "auto", 41 | }, 42 | stackItem: { 43 | // Sizing 44 | width: CELL_WIDTH, 45 | margin: CELL_MARGIN, 46 | height: "50px", 47 | // Center-align values 48 | display: "flex", 49 | justifyContent: "center", 50 | alignItems: "center", 51 | }, 52 | }; 53 | 54 | const DirectionIcons: { [k: string]: IconName } = { 55 | [Bfg93Direction.RIGHT]: "arrow-right", 56 | [Bfg93Direction.LEFT]: "arrow-left", 57 | [Bfg93Direction.UP]: "arrow-up", 58 | [Bfg93Direction.DOWN]: "arrow-down", 59 | }; 60 | 61 | /** Component for displaying a single stack item */ 62 | const StackItem = React.memo(({ value }: { value: number }) => { 63 | return {value}; 64 | }); 65 | 66 | export const Renderer = ({ state }: RendererProps) => { 67 | if (state == null) 68 | return
Run some code...
; 69 | 70 | return ( 71 |
72 |
73 | Direction: 74 | 75 | 76 | String mode:{" "} 77 | 78 | {state.strMode ? "ON" : "OFF"} 79 |
80 |
81 | {state.stack.map((value, idx) => ( 82 | 83 | ))} 84 |
85 |
86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /languages/befunge93/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { readTestProgram, executeProgram } from "../../test-utils"; 2 | import { Bfg93Direction } from "../constants"; 3 | import Engine from "../runtime"; 4 | 5 | /** 6 | * All test programs are picked up from https://esolangs.org/wiki/Befunge, 7 | * except the modifications mentioned alongside each test. 8 | */ 9 | 10 | /** Relative path to directory of sample programs */ 11 | const DIRNAME = __dirname + "/samples"; 12 | 13 | describe("Test programs", () => { 14 | // Standard hello-world program 15 | test("hello world", async () => { 16 | const code = readTestProgram(DIRNAME, "helloworld"); 17 | const result = await executeProgram(new Engine(), code); 18 | expect(result.output.charCodeAt(result.output.length - 1)).toBe(0); 19 | expect(result.output.slice(0, -1)).toBe("Hello, World!"); 20 | expect(result.rendererState.direction).toBe(Bfg93Direction.DOWN); 21 | expect(result.rendererState.stack.length).toBe(0); 22 | }); 23 | 24 | // cat program 25 | test("cat program", async () => { 26 | const input = "abcd efgh\nijkl mnop\n"; 27 | const code = readTestProgram(DIRNAME, "cat"); 28 | const result = await executeProgram(new Engine(), code, input); 29 | expect(result.output).toBe(input); 30 | expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT); 31 | expect(result.rendererState.stack).toEqual([-1]); 32 | }); 33 | 34 | // Random DNA printer 35 | test("random DNA", async () => { 36 | const code = readTestProgram(DIRNAME, "dna"); 37 | const result = await executeProgram(new Engine(), code); 38 | // program prints "\r\n" at the end of output 39 | expect(result.output.length).toBe(56 + 2); 40 | expect(result.output.trim().search(/[^ATGC]/)).toBe(-1); 41 | expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT); 42 | expect(result.rendererState.stack).toEqual([0]); 43 | }); 44 | 45 | // Factorial program 46 | test("factorial", async () => { 47 | const code = readTestProgram(DIRNAME, "factorial"); 48 | const result = await executeProgram(new Engine(), code, "5"); 49 | expect(result.output).toBe("120 "); 50 | expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT); 51 | expect(result.rendererState.stack.length).toBe(0); 52 | }); 53 | 54 | // Sieve of Eratosthenes - prints prime nums upto 36 55 | // (original prints up to 80, shortened here for testing purposes) 56 | test("sieve of eratosthenes", async () => { 57 | const code = readTestProgram(DIRNAME, "prime-sieve"); 58 | const result = await executeProgram(new Engine(), code); 59 | const outputNums = result.output 60 | .trim() 61 | .split(" ") 62 | .map((a) => parseInt(a, 10)); 63 | const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]; 64 | expect(outputNums).toEqual(primes); 65 | expect(result.rendererState.direction).toBe(Bfg93Direction.DOWN); 66 | expect(result.rendererState.stack).toEqual([37]); 67 | }); 68 | 69 | // Quine 1 - simple single-line quine 70 | test("simple singleline quine", async () => { 71 | const code = readTestProgram(DIRNAME, "quine1"); 72 | const result = await executeProgram(new Engine(), code); 73 | expect(result.output).toBe(code); 74 | expect(result.rendererState.direction).toBe(Bfg93Direction.RIGHT); 75 | expect(result.rendererState.stack).toEqual([44]); 76 | }); 77 | 78 | // Quine 2 - multiline quine 79 | test("multiline quine", async () => { 80 | const code = readTestProgram(DIRNAME, "quine2"); 81 | const result = await executeProgram(new Engine(), code); 82 | // Output has an extra space at the end - verified on tio.run 83 | expect(result.output).toBe(code + " "); 84 | expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT); 85 | expect(result.rendererState.stack).toEqual([0]); 86 | }); 87 | 88 | // Quine 3 - quine without using "g" 89 | test("quine without using 'g'", async () => { 90 | const code = readTestProgram(DIRNAME, "quine3"); 91 | const result = await executeProgram(new Engine(), code); 92 | expect(result.output).toBe(code); 93 | expect(result.rendererState.direction).toBe(Bfg93Direction.LEFT); 94 | expect(result.rendererState.stack).toEqual([0]); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /languages/befunge93/tests/samples/cat.txt: -------------------------------------------------------------------------------- 1 | ~:1+!#@_, -------------------------------------------------------------------------------- /languages/befunge93/tests/samples/dna.txt: -------------------------------------------------------------------------------- 1 | 7^DN>vA 2 | v_#v? v 3 | 7^<"""" 4 | 3 ACGT 5 | 90!"""" 6 | 4*:>>>v 7 | +8^-1,< 8 | > ,+,@) -------------------------------------------------------------------------------- /languages/befunge93/tests/samples/factorial.txt: -------------------------------------------------------------------------------- 1 | &>:1-:v v *_$.@ 2 | ^ _$>\:^ -------------------------------------------------------------------------------- /languages/befunge93/tests/samples/helloworld.txt: -------------------------------------------------------------------------------- 1 | "!dlroW ,olleH">:v 2 | |,< 3 | @ -------------------------------------------------------------------------------- /languages/befunge93/tests/samples/prime-sieve.txt: -------------------------------------------------------------------------------- 1 | 2>:3g" "-!v\ g30 < 2 | |!`"$":+1_:.:03p>03g+:"$"`| 3 | @ ^ p3\" ":< 4 | 2 234567890123456789012345678901234567890123456789012345678901234567890123456789 -------------------------------------------------------------------------------- /languages/befunge93/tests/samples/quine1.txt: -------------------------------------------------------------------------------- 1 | 01->1# +# :# 0# g# ,# :# 5# 8# *# 4# +# -# _@ -------------------------------------------------------------------------------- /languages/befunge93/tests/samples/quine2.txt: -------------------------------------------------------------------------------- 1 | 0 v 2 | "<@_ #! #: #,<*2-1*92,*84,*25,+*92*4*55.0 -------------------------------------------------------------------------------- /languages/befunge93/tests/samples/quine3.txt: -------------------------------------------------------------------------------- 1 | <@,+2*48_,#! #:<,_$#-:#*8#4<8" -------------------------------------------------------------------------------- /languages/brainfuck/README.md: -------------------------------------------------------------------------------- 1 | # Brainfuck 2 | 3 | Brainfuck is perhaps the most popular esoteric programming language, created by Urban Müller in 1993. 4 | It is Turing-complete and has 8 instructions which operate on a linear array of cells storing integer values. 5 | The [esolangs wiki page](https://esolangs.org/wiki/Brainfuck) contains the language specification and some 6 | sample programs. 7 | 8 | Note that brainfuck has minor variants which primarily differ in the workings of the cell array. You may find 9 | many brainfuck programs which don't work correctly on Esolang Park. 10 | 11 | ## Notes for the user 12 | 13 | - The cell array is semi-infinite. There is no cell to the left of the initial cell, and trying to go left 14 | anyway will result in a runtime error. The right side of the cell array is unbounded. 15 | - Cell size is 8 bits, and allows values in the range `[-128, 127]`. Values loop over to the other side on reaching the bounds. 16 | - The usual ASCII value `10` is designated for newlines. 17 | - The value `0` is returned in input (`,`) operations on reaching `EOF`. 18 | 19 | ## Possible improvements 20 | 21 | - The renderer currently uses Blueprint's `Card` component. This leads to performance issues when the stack grows large. 22 | Usage of this component should be replaced by a simple custom component to drastically improve performance. Look at the 23 | `SimpleTag` component used in the Shakespeare renderer for an example. 24 | -------------------------------------------------------------------------------- /languages/brainfuck/common.ts: -------------------------------------------------------------------------------- 1 | import { MonacoTokensProvider } from "../types"; 2 | 3 | export type BFRS = { 4 | tape: { [k: number]: number }; 5 | pointer: number; 6 | }; 7 | 8 | export enum BF_OP { 9 | LEFT = "<", 10 | RIGHT = ">", 11 | INCR = "+", 12 | DECR = "-", 13 | OUT = ".", 14 | IN = ",", 15 | LOOPIN = "[", 16 | LOOPOUT = "]", 17 | } 18 | 19 | /** A single instruction of the program */ 20 | export type BFInstruction = { 21 | /** Type of instruction */ 22 | type: BF_OP; 23 | /** Used for location of opposite end of loops */ 24 | param?: number; 25 | }; 26 | 27 | /** A single element of the program's AST */ 28 | export type BFAstStep = { 29 | instr: BFInstruction; 30 | location: { line: number; char: number }; 31 | }; 32 | 33 | /** Sample program printing "Hello World!" */ 34 | export const sampleProgram = [ 35 | "+++++ +++ Set Cell #0 to 8", 36 | "[", 37 | " >++++ Add 4 to Cell #1; this will always set Cell #1 to 4", 38 | " [ as the cell will be cleared by the loop", 39 | " >++ Add 4*2 to Cell #2", 40 | " >+++ Add 4*3 to Cell #3", 41 | " >+++ Add 4*3 to Cell #4", 42 | " >+ Add 4 to Cell #5", 43 | " <<<<- Decrement the loop counter in Cell #1", 44 | " ] Loop till Cell #1 is zero", 45 | " >+ Add 1 to Cell #2", 46 | " >+ Add 1 to Cell #3", 47 | " >- Subtract 1 from Cell #4", 48 | " >>+ Add 1 to Cell #6", 49 | " [<] Move back to the first zero cell you find; this will", 50 | " be Cell #1 which was cleared by the previous loop", 51 | " <- Decrement the loop Counter in Cell #0", 52 | "] Loop till Cell #0 is zero", 53 | "", 54 | "The result of this is:", 55 | "Cell No : 0 1 2 3 4 5 6", 56 | "Contents: 0 0 72 104 88 32 8", 57 | "Pointer : ^", 58 | "", 59 | ">>. Cell #2 has value 72 which is 'H'", 60 | ">---. Subtract 3 from Cell #3 to get 101 which is 'e'", 61 | "+++++ ++..+++. Likewise for 'llo' from Cell #3", 62 | ">>. Cell #5 is 32 for the space", 63 | "<-. Subtract 1 from Cell #4 for 87 to give a 'W'", 64 | "<. Cell #3 was set to 'o' from the end of 'Hello'", 65 | "+++.----- -.----- ---. Cell #3 for 'rl' and 'd'", 66 | ">>+. Add 1 to Cell #5 gives us an exclamation point", 67 | ">++. And finally a newline from Cell #6", 68 | ].join("\n"); 69 | 70 | /** Tokens provider */ 71 | export const editorTokensProvider: MonacoTokensProvider = { 72 | tokenizer: { 73 | root: [ 74 | [/[-\+]/, ""], 75 | [/[<>]/, "tag"], 76 | [/[\[\]]/, "keyword"], 77 | [/[\,\.]/, "identifier"], 78 | ], 79 | }, 80 | defaultToken: "comment", 81 | }; 82 | 83 | /** Serialize tape from object format into linear array */ 84 | export const serializeTapeMap = ( 85 | tape: BFRS["tape"], 86 | minCells: number = 0 87 | ): number[] => { 88 | const cellIdxs = Object.keys(tape).map((s) => parseInt(s, 10)); 89 | const maxCellIdx = Math.max(minCells - 1, ...cellIdxs); 90 | const linearTape: number[] = Array(maxCellIdx + 1).fill(0); 91 | cellIdxs.forEach((i) => (linearTape[i] = tape[i] || 0)); 92 | return linearTape; 93 | }; 94 | -------------------------------------------------------------------------------- /languages/brainfuck/engine.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from "../setup-worker"; 2 | import BrainfuckLanguageEngine from "./runtime"; 3 | 4 | setupWorker(new BrainfuckLanguageEngine()); 5 | -------------------------------------------------------------------------------- /languages/brainfuck/index.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from "./renderer"; 2 | import { LanguageProvider } from "../types"; 3 | import { BFRS, sampleProgram, editorTokensProvider } from "./common"; 4 | 5 | const provider: LanguageProvider = { 6 | Renderer, 7 | sampleProgram, 8 | editorTokensProvider, 9 | }; 10 | 11 | export default provider; 12 | -------------------------------------------------------------------------------- /languages/brainfuck/renderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { RendererProps } from "../types"; 3 | import { Box } from "../ui-utils"; 4 | import { BFRS, serializeTapeMap } from "./common"; 5 | 6 | /** Number of cells shown in a single row */ 7 | const ROWSIZE = 8; 8 | 9 | // Parameters for cell sizing, balanced to span the full row width 10 | // Constraint: `(width% + 2 * margin%) * ROWSIZE = 100%` 11 | const CELL_WIDTH = "12%"; 12 | const CELL_MARGIN = "5px 0.25%"; 13 | 14 | const styles: { [k: string]: React.CSSProperties } = { 15 | container: { 16 | padding: 10, 17 | height: "100%", 18 | display: "flex", 19 | flexWrap: "wrap", 20 | alignContent: "flex-start", 21 | overflowY: "auto", 22 | }, 23 | cell: { 24 | // Sizing 25 | width: CELL_WIDTH, 26 | margin: CELL_MARGIN, 27 | height: "50px", 28 | // Center-align values 29 | display: "flex", 30 | justifyContent: "center", 31 | alignItems: "center", 32 | }, 33 | }; 34 | 35 | /** Component for displaying a single tape cell */ 36 | const Cell = React.memo( 37 | ({ value, active }: { value: number; active: boolean }) => { 38 | return ( 39 | 43 | {value} 44 | 45 | ); 46 | } 47 | ); 48 | 49 | /** Renderer for Brainfuck */ 50 | export const Renderer = ({ state }: RendererProps) => { 51 | return ( 52 |
53 | {serializeTapeMap(state?.tape || {}, 2 * ROWSIZE).map((num, i) => ( 54 | 55 | ))} 56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /languages/brainfuck/tests/cat.txt: -------------------------------------------------------------------------------- 1 | ,[.,] -------------------------------------------------------------------------------- /languages/brainfuck/tests/cellsize.txt: -------------------------------------------------------------------------------- 1 | Calculate the value 256 and test if it's zero 2 | If the interpreter errors on overflow this is where it'll happen 3 | ++++++++[>++++++++<-]>[<++++>-] 4 | +<[>-< 5 | Not zero so multiply by 256 again to get 65536 6 | [>++++<-]>[<++++++++>-]<[>++++++++<-] 7 | +>[> 8 | # Print "32" 9 | ++++++++++[>+++++<-]>+.-.[-]< 10 | <[-]<->] <[>> 11 | # Print "16" 12 | +++++++[>+++++++<-]>.+++++.[-]< 13 | <<-]] >[> 14 | # Print "8" 15 | ++++++++[>+++++++<-]>.[-]< 16 | <-]< 17 | # Print " bit cells" 18 | +++++++++++[>+++>+++++++++>+++++++++>+<<<<-]>-.>-.+++++++.+++++++++++.<. 19 | >>.++.+++++++..<-.>>- -------------------------------------------------------------------------------- /languages/brainfuck/tests/helloworld-subzero.txt: -------------------------------------------------------------------------------- 1 | >++++++++[-<+++++++++>]<.>>+>-[+]++>++>+++[>[->+++<<+++>]<<]>-----.>-> 2 | +++..+++.>-.<<+[>[+>+]>>]<--------------.>>.+++.------.--------.>+.>+. -------------------------------------------------------------------------------- /languages/brainfuck/tests/helloworld.txt: -------------------------------------------------------------------------------- 1 | +++++ +++ Set Cell #0 to 8 2 | [ 3 | >++++ Add 4 to Cell #1; this will always set Cell #1 to 4 4 | [ as the cell will be cleared by the loop 5 | >++ Add 4*2 to Cell #2 6 | >+++ Add 4*3 to Cell #3 7 | >+++ Add 4*3 to Cell #4 8 | >+ Add 4 to Cell #5 9 | <<<<- Decrement the loop counter in Cell #1 10 | ] Loop till Cell #1 is zero 11 | >+ Add 1 to Cell #2 12 | >+ Add 1 to Cell #3 13 | >- Subtract 1 from Cell #4 14 | >>+ Add 1 to Cell #6 15 | [<] Move back to the first zero cell you find; this will 16 | be Cell #1 which was cleared by the previous loop 17 | <- Decrement the loop Counter in Cell #0 18 | ] Loop till Cell #0 is zero 19 | 20 | The result of this is: 21 | Cell No : 0 1 2 3 4 5 6 22 | Contents: 0 0 72 104 88 32 8 23 | Pointer : ^ 24 | 25 | >>. Cell #2 has value 72 which is 'H' 26 | >---. Subtract 3 from Cell #3 to get 101 which is 'e' 27 | +++++ ++..+++. Likewise for 'llo' from Cell #3 28 | >>. Cell #5 is 32 for the space 29 | <-. Subtract 1 from Cell #4 for 87 to give a 'W' 30 | <. Cell #3 was set to 'o' from the end of 'Hello' 31 | +++.----- -.----- ---. Cell #3 for 'rl' and 'd' 32 | >>+. Add 1 to Cell #5 gives us an exclamation point 33 | >++. And finally a newline from Cell #6 -------------------------------------------------------------------------------- /languages/brainfuck/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { readTestProgram, executeProgram } from "../../test-utils"; 2 | import { BFRS, serializeTapeMap } from "../common"; 3 | import Engine from "../runtime"; 4 | 5 | /** 6 | * All test programs are picked up from https://esolangs.org/wiki/Brainfuck. 7 | * - Cell cleanup code at end of cell size program is not included. 8 | */ 9 | 10 | /** 11 | * Check if actual cell array matches expected cell array. 12 | * Expected cell array must exclude trailing zeros. 13 | * @param cellsMap Map of cell index to value, as provided in execution result. 14 | * @param expected Array of expected cell values, without trailing zeros. 15 | */ 16 | const expectCellsToBe = (cellsMap: BFRS["tape"], expected: number[]) => { 17 | const cells = serializeTapeMap(cellsMap); 18 | expect(cells.length).toBeGreaterThanOrEqual(expected.length); 19 | cells.forEach((value, idx) => { 20 | if (idx < expected.length) expect(value).toBe(expected[idx]); 21 | else expect(value).toBe(0); 22 | }); 23 | }; 24 | 25 | describe("Test programs", () => { 26 | // Standard hello-world program 27 | test("hello world", async () => { 28 | const code = readTestProgram(__dirname, "helloworld"); 29 | const result = await executeProgram(new Engine(), code); 30 | expect(result.output).toBe("Hello World!\n"); 31 | expect(result.rendererState.pointer).toBe(6); 32 | const expectedCells = [0, 0, 72, 100, 87, 33, 10]; 33 | expectCellsToBe(result.rendererState.tape, expectedCells); 34 | }); 35 | 36 | // Hello-world program using subzero cell values 37 | test("hello world with subzero cell values", async () => { 38 | const code = readTestProgram(__dirname, "helloworld-subzero"); 39 | const result = await executeProgram(new Engine(), code); 40 | expect(result.output).toBe("Hello World!\n"); 41 | expect(result.rendererState.pointer).toBe(6); 42 | const expectedCells = [72, 0, 87, 0, 100, 33, 10]; 43 | expectCellsToBe(result.rendererState.tape, expectedCells); 44 | }); 45 | 46 | // cat program 47 | test("cat program", async () => { 48 | const code = readTestProgram(__dirname, "cat"); 49 | const result = await executeProgram(new Engine(), code, "foo \n bar"); 50 | expect(result.output).toBe("foo \n bar"); 51 | expect(result.rendererState.pointer).toBe(0); 52 | expectCellsToBe(result.rendererState.tape, []); 53 | }); 54 | 55 | // Program to calculate cell size 56 | test("cell size calculator", async () => { 57 | const code = readTestProgram(__dirname, "cellsize"); 58 | const result = await executeProgram(new Engine(), code); 59 | expect(result.output).toBe("8 bit cells"); 60 | expect(result.rendererState.pointer).toBe(4); 61 | expectCellsToBe(result.rendererState.tape, [0, 32, 115, 108, 10]); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /languages/chef/README.md: -------------------------------------------------------------------------------- 1 | # Chef 2 | 3 | Chef is a stack-based Turing-complete esolang created by David Morgan-Mar in 2002, in which programs read 4 | like cooking recipes. An important guideline while writing Chef programs/recipes is for the recipe to be easy 5 | to prepare and delicious. The complete language specification is available on the 6 | [official Chef homepage](https://www.dangermouse.net/esoteric/chef.html). 7 | 8 | ## Notes for the user 9 | 10 | - Ingredient names are case-sensitive and must contain only letters and spaces. 11 | 12 | - Auxiliary recipe names are case-sensitive. If the recipe title is `Chocolate Sauce`, calling instruction must be `Serve with Chocolate Sauce` and not `Serve with chocolate sauce`. 13 | 14 | - Each method instruction must end with a period. 15 | 16 | - Method instructions can be located on separate lines, the same line or a mix of both. The following is a valid Chef recipe method: 17 | 18 | ``` 19 | Put salt into the mixing bowl. Mix well. 20 | Fold sugar into the mixing bowl. Clean the mixing bowl. 21 | ``` 22 | 23 | Note that a single method instruction cannot roll over to the next line, though. 24 | 25 | - The Chef language involves usage of present and past forms of verbs: 26 | ``` 27 | Blend the sugar. 28 | 29 | Shake the mixture until blended. 30 | ``` 31 | The Esolang Park interpreter cannot convert verbs between the two forms, so we adopt the following convention: the past form of the verb is the same as the present form of the verb. So the above example must be changed to the following for Esolang Park: 32 | ``` 33 | Blend the sugar. 34 | 35 | Shake the mixture until blend. 36 | ``` 37 | 38 | ## Implementation details 39 | 40 | - The parser is hand-built and uses regular expressions for basically everything. 41 | - The engine is split in two classes: `Kitchen` handles kitchen-related operations while the main class 42 | handles control-flow operations and manages the call-stack. 43 | 44 | ## Possible improvements 45 | 46 | - Allow case-insensitive usage of auxiliary recipe names. 47 | 48 | - Chef syntax is flat and simple, easily representable by regular expressions. This means that syntax highlighting for 49 | Chef can easily be made perfect by slightly modifying and reusing parser regexes for the Monarch tokenizer. Currently 50 | the method part of syntax highlighting just highlights the known keywords. 51 | 52 | - We need to find a sensible solution for the verb tense issue mentioned above. Currently, any Chef program involving loops 53 | requires modifications to be run in Esolang Park. Importing a very lightweight English verb-dictionary and only allowing 54 | verbs from this dictionary is a possible way to resolve this. 55 | -------------------------------------------------------------------------------- /languages/chef/constants.ts: -------------------------------------------------------------------------------- 1 | import { MonacoTokensProvider } from "../types"; 2 | 3 | /** Error thrown on malformed syntax. Caught and converted into ParseError higher up */ 4 | export class SyntaxError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = "SyntaxError"; 8 | } 9 | } 10 | 11 | /** Check if an error is instance of SyntaxError */ 12 | export const isSyntaxError = (error: any): error is SyntaxError => { 13 | return error instanceof SyntaxError || error.name === "SyntaxError"; 14 | }; 15 | 16 | /** Sample Hello World program for Chef */ 17 | export const sampleProgram = [ 18 | "Hello World Souffle.", 19 | "", 20 | 'This recipe prints the immortal words "Hello world!", in a basically ', 21 | "brute force way. It also makes a lot of food for one person.", 22 | "", 23 | "Ingredients.", 24 | "72 g haricot beans", 25 | "101 eggs", 26 | "108 g lard", 27 | "111 cups oil", 28 | "32 zucchinis", 29 | "119 ml water", 30 | "114 g red salmon", 31 | "100 g dijon mustard", 32 | "33 potatoes", 33 | "", 34 | "Method.", 35 | "Put potatoes into the mixing bowl.", 36 | "Put dijon mustard into the mixing bowl.", 37 | "Put lard into the mixing bowl.", 38 | "Put red salmon into the mixing bowl.", 39 | "Put oil into the mixing bowl.", 40 | "Put water into the mixing bowl.", 41 | "Put zucchinis into the mixing bowl.", 42 | "Put oil into the mixing bowl.", 43 | "Put lard into the mixing bowl.", 44 | "Put lard into the mixing bowl.", 45 | "Put eggs into the mixing bowl.", 46 | "Put haricot beans into the mixing bowl.", 47 | "Liquefy contents of the mixing bowl.", 48 | "Pour contents of the mixing bowl into the baking dish.", 49 | "", 50 | "Serves 1.", 51 | ].join("\n"); 52 | 53 | /** Syntax highlighting provider */ 54 | export const editorTokensProvider: MonacoTokensProvider = { 55 | tokenizer: { 56 | root: [ 57 | [/^\s*$/, { token: "" }], 58 | [/^.+$/, { token: "variable.function", next: "@recipe" }], 59 | ], 60 | recipe: [ 61 | [/^\s*Ingredients\.\s*$/, { token: "annotation", next: "@ingredients" }], 62 | [/^\s*Method\.\s*$/, { token: "annotation", next: "@method" }], 63 | [ 64 | /(^\s*)(Serves )(\d+)(\.\s*$)/, 65 | ["", "", "number", { token: "", next: "@popall" }] as any, 66 | ], 67 | [/^.+$/, { token: "comment" }], 68 | ], 69 | ingredients: [ 70 | [/\d+/, "number"], 71 | [/ (g|kg|pinch(?:es)?|ml|l|dash(?:es)?) /, "type"], 72 | [/ ((heaped|level) )?(cups?|teaspoons?|tablespoons?) /, "type"], 73 | [/^\s*$/, { token: "", next: "@pop" }], 74 | ], 75 | method: [ 76 | [/mixing bowl/, "tag"], 77 | [/baking dish/, "meta"], 78 | [ 79 | /(^|\.\s*)(Take|Put|Fold|Add|Remove|Combine|Divide|Liquefy|Stir|Mix|Clean|Pour|Set aside|Serve with|Refrigerate)($| )/, 80 | ["", "keyword", ""], 81 | ], 82 | [/^\s*$/, { token: "", next: "@pop" }], 83 | ], 84 | }, 85 | defaultToken: "", 86 | }; 87 | -------------------------------------------------------------------------------- /languages/chef/engine.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from "../setup-worker"; 2 | import ChefRuntime from "./runtime"; 3 | 4 | setupWorker(new ChefRuntime()); 5 | -------------------------------------------------------------------------------- /languages/chef/index.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from "./renderer"; 2 | import { LanguageProvider } from "../types"; 3 | import { ChefRS } from "./types"; 4 | import { sampleProgram, editorTokensProvider } from "./constants"; 5 | 6 | const provider: LanguageProvider = { 7 | Renderer, 8 | sampleProgram, 9 | editorTokensProvider, 10 | }; 11 | 12 | export default provider; 13 | -------------------------------------------------------------------------------- /languages/chef/parser/constants.ts: -------------------------------------------------------------------------------- 1 | import { ChefArithmeticOp } from "../types"; 2 | 3 | /** Ingredient measures considered as dry */ 4 | export const DryMeasures = ["g", "kg", "pinch", "pinches"]; 5 | 6 | /** Ingredient measures considered as liquid */ 7 | export const LiquidMeasures = ["ml", "l", "dash", "dashes"]; 8 | 9 | /** Ingredient measures that may be dry or liquid */ 10 | export const UnknownMeasures = [ 11 | "cup", 12 | "cups", 13 | "teaspoon", 14 | "teaspoons", 15 | "tablespoon", 16 | "tablespoons", 17 | ]; 18 | 19 | /** Types of measures - irrelevant to execution */ 20 | export const MeasureTypes = ["heaped", "level"]; 21 | 22 | /** A map from arithmetic instruction verbs to op codes */ 23 | export const ArithmeticCodes: { [k: string]: ChefArithmeticOp["code"] } = { 24 | Add: "ADD", 25 | Remove: "SUBTRACT", 26 | Combine: "MULTIPLY", 27 | Divide: "DIVIDE", 28 | }; 29 | 30 | /** Placeholder value for loop jump addresses */ 31 | export const JumpAddressPlaceholder = -1; 32 | -------------------------------------------------------------------------------- /languages/chef/parser/regex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For each regular expression below: 3 | * - Doc comments include the details of each capture group in the regex. 4 | * - Regex comments provide a little overview of the regex, where 5 | * [...] denotes optional clause, <...> denotes capture group. 6 | */ 7 | 8 | /** 9 | * Regular expression for `Take ingredient from refrigerator` op 10 | * Capture groups: 11 | * 1. **Ingredient name**: string with letters and spaces 12 | */ 13 | export const TakeFromFridgeRegex = 14 | /** */ 15 | /^Take ([a-zA-Z ]+?) from(?: the)? refrigerator$/; 16 | 17 | /** 18 | * Regular expression for `Put ingredient into nth bowl` op 19 | * Capture groups: 20 | * 1. **Ingredient name**: string with letters and spaces 21 | * 2. **Mixing bowl index** (optional): integer 22 | */ 23 | export const PutInBowlRegex = 24 | /** [ ] */ 25 | /^Put(?: the)? ([a-zA-Z ]+?) into(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/; 26 | 27 | /** 28 | * Regular expression for `Fold ingredient into nth bowl` op 29 | * Capture groups: 30 | * 1. **Ingredient name**: string with letters and spaces 31 | * 2. **Mixing bowl index** (optional): integer 32 | */ 33 | export const FoldIntoBowlRegex = 34 | /** [ ] */ 35 | /^Fold(?: the)? ([a-zA-Z ]+?) into(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/; 36 | 37 | /** 38 | * Regular expression to match the four main arithmetic operations in Chef. 39 | * Capture groups: 40 | * 1. **Operation name**: `"Add" | "Remove" | "Combine" | "Divide"` 41 | * 2. **Ingredient name**: string with letters and spaces 42 | * 3. **Proverb** (optional): `"to" | "into" | "from"` 43 | * 4. **Mixing bowl index** (optional): integer 44 | */ 45 | export const ArithmeticOpRegex = 46 | /** [ [ ] ] */ 47 | /^(Add|Remove|Combine|Divide) ([a-zA-Z ]+?)(?: (to|into|from)(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl)?$/; 48 | 49 | /** 50 | * Regular expression for the `Add dry ingredients ...` op. 51 | * Capture groups: 52 | * 1. **Mixing bowl index** (optional): integer 53 | */ 54 | export const AddDryIngsOpRegex = 55 | /** [ [ ] ] */ 56 | /^Add dry ingredients(?: to(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl)?$/; 57 | 58 | /** 59 | * Regular expression for the `Liquefy contents` op 60 | * Capture groups: 61 | * 1. **Mixing bowl index** (optional): integer 62 | */ 63 | export const LiquefyBowlRegex = 64 | /** [ ] */ 65 | /^Liquefy(?: the)? contents of the(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/; 66 | 67 | /** 68 | * Regular expression for the `Liquefy ingredient` op 69 | * Capture groups: 70 | * 1. **Ingredient name**: string with letters and spaces 71 | */ 72 | export const LiquefyIngRegex = 73 | /** */ 74 | /^Liquefy(?: the)? ([a-zA-Z ]+?)$/; 75 | 76 | /** 77 | * Regular expression to match the `Stir for minutes` op. 78 | * Capture groups: 79 | * 1. **Mixing bowl index** (optional): integer 80 | * 2. **Number of mins**: integer 81 | */ 82 | export const StirBowlRegex = 83 | /** [ [ ]? ]? */ 84 | /^Stir(?: the(?: (\d+)(?:nd|rd|th|st))? mixing bowl)? for (\d+) minutes?$/; 85 | 86 | /** 87 | * Regular expression to match the `Stir into [nth] mixing bowl` op. 88 | * Capture groups: 89 | * 1. **Ingredient name**: string with letters and spaces 90 | * 2. **Mixing bowl index** (optional): integer 91 | */ 92 | export const StirIngredientRegex = 93 | /** [ ] */ 94 | /^Stir ([a-zA-Z ]+?) into the(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/; 95 | 96 | /** 97 | * Regular expression to match the `Mix [the [nth] mixing bowl] well` op. 98 | * Capture groups: 99 | * 1. **Mixing bowl index** (optional): integer 100 | */ 101 | export const MixBowlRegex = 102 | /** [ [ ]? ]? */ 103 | /^Mix(?: the(?: (\d+)(?:nd|rd|th|st))? mixing bowl)? well$/; 104 | 105 | /** 106 | * Regular expression for the `Clean bowl` op 107 | * Capture groups: 108 | * 1. **Mixing bowl index** (optional): integer 109 | */ 110 | export const CleanBowlRegex = 111 | /** [ ] */ 112 | /^Clean(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/; 113 | 114 | /** 115 | * Regular expression to match the `Pour ...` op. 116 | * Capture groups: 117 | * 1. **Mixing bowl index** (optional): integer 118 | * 2. **Baking dish index** (optional): integer 119 | */ 120 | export const PourBowlRegex = 121 | /** [ ]? [ ]? */ 122 | /^Pour contents of the(?: (\d+)(?:nd|rd|th|st))? mixing bowl into the(?: (\d+)(?:nd|rd|th|st))? baking dish$/; 123 | 124 | /** 125 | * Regular expression to match the `Serve with` op. 126 | * Capture groups: 127 | * 1. **Name of aux recipe**: string with alphanumerics and spaces 128 | */ 129 | export const ServeWithRegex = 130 | /** */ 131 | /^Serve with ([a-zA-Z0-9 ]+)$/; 132 | 133 | /** 134 | * Regular expression to match the `Refrigerate` op. 135 | * Capture groups: 136 | * 1. **Number of hours** (optional): integer 137 | */ 138 | export const RefrigerateRegex = 139 | /** [ ] */ 140 | /^Refrigerate(?: for (\d+) hours?)?$/; 141 | 142 | /** 143 | * Regular expression to match the `Verb the ingredient` op. 144 | * Capture groups: 145 | * 1. **Verb**: string with letters 146 | * 2. **Ingredient name**: string with letters and spaces 147 | */ 148 | export const LoopOpenerRegex = 149 | /** */ 150 | /^([a-zA-Z]+?)(?: the)? ([a-zA-Z ]+)$/; 151 | 152 | /** 153 | * Regular expression to match the `Verb [the ing] until verbed` op. 154 | * Capture groups: 155 | * 1. **Ingredient name** (optional): string with letters and spaces 156 | * 2. **Matched verb**: string with letters 157 | */ 158 | export const LoopEnderRegex = 159 | /** Verb [ ] */ 160 | /^(?:[a-zA-Z]+?)(?: the)?(?: ([a-zA-Z ]+?))? until ([a-zA-Z]+)$/; 161 | -------------------------------------------------------------------------------- /languages/chef/renderer/bowl-dish-columns.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { MixingBowl, StackItem } from "../types"; 3 | import { ItemTypeIcons } from "./utils"; 4 | 5 | const styles = { 6 | cellContainer: { 7 | display: "flex", 8 | justifyContent: "space-between", 9 | margin: "2px 0", 10 | }, 11 | listContainer: { 12 | width: "80%", 13 | marginTop: 5, 14 | marginLeft: "auto", 15 | marginRight: "auto", 16 | }, 17 | stackContainer: { 18 | overflowY: "auto" as "auto", 19 | }, 20 | columnContainer: { 21 | height: "100%", 22 | textAlign: "center" as "center", 23 | margin: "0 10px", 24 | display: "flex", 25 | flexDirection: "column" as "column", 26 | }, 27 | stackMarker: { 28 | height: "0.7em", 29 | borderRadius: 5, 30 | }, 31 | stackHeader: { 32 | fontWeight: "bold", 33 | margin: "5px 0", 34 | }, 35 | }; 36 | 37 | /** Displays a single item of a bowl or dish, along with type */ 38 | const StackItemCell = ({ item }: { item: StackItem }) => { 39 | return ( 40 |
41 | {ItemTypeIcons[item.type]} 42 | 43 | {item.value.toString()} 44 | 45 |
46 | ); 47 | }; 48 | 49 | /** Displays a list of bowl/dish items in reverse order */ 50 | const StackItemList = ({ items }: { items: StackItem[] }) => { 51 | return ( 52 |
53 | {items.map((item, idx) => ( 54 | 55 | ))} 56 |
57 | ); 58 | }; 59 | 60 | /** Displays a mixing bowl in a vertical strip */ 61 | export const MixingBowlColumn = ({ 62 | bowl, 63 | index, 64 | }: { 65 | bowl: MixingBowl; 66 | index: number; 67 | }) => { 68 | return ( 69 |
70 |
Bowl {index + 1}
71 |
72 |
75 | 76 |
77 |
78 | ); 79 | }; 80 | 81 | /** Displays a baking dish in a vertical strip */ 82 | export const BakingDishColumn = ({ 83 | dish, 84 | index, 85 | }: { 86 | dish: MixingBowl; 87 | index: number; 88 | }) => { 89 | return ( 90 |
91 |
Dish {index + 1}
92 |
93 |
96 | 97 |
98 |
99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /languages/chef/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumbs } from "@blueprintjs/core"; 2 | import * as React from "react"; 3 | import { RendererProps } from "../../types"; 4 | import { ChefRS } from "../types"; 5 | import { KitchenDisplay } from "./kitchen-display"; 6 | import { BorderColor } from "./utils"; 7 | 8 | const styles = { 9 | placeholderDiv: { 10 | height: "100%", 11 | width: "100%", 12 | display: "flex", 13 | justifyContent: "center", 14 | alignItems: "center", 15 | fontSize: "1.2em", 16 | }, 17 | rootContainer: { 18 | height: "100%", 19 | display: "flex", 20 | flexDirection: "column" as "column", 21 | }, 22 | callStackContainer: { 23 | borderBottom: "1px solid " + BorderColor, 24 | padding: "5px 10px", 25 | }, 26 | kitchenContainer: { 27 | flex: 1, 28 | minHeight: 0, 29 | }, 30 | }; 31 | 32 | export const Renderer = ({ state }: RendererProps) => { 33 | if (state == null) 34 | return ( 35 |
Run some code to see the kitchen!
36 | ); 37 | 38 | const crumbs = state.stack.map((name) => ({ text: name })); 39 | 40 | return ( 41 |
42 |
43 | 44 |
45 |
46 | 47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /languages/chef/renderer/ingredients-pane.tsx: -------------------------------------------------------------------------------- 1 | import { IngredientBox, IngredientItem } from "../types"; 2 | import { ItemTypeIcons } from "./utils"; 3 | 4 | const styles = { 5 | paneHeader: { 6 | fontSize: "1.1em", 7 | fontWeight: "bold", 8 | marginBottom: 15, 9 | }, 10 | paneContainer: { 11 | height: "100%", 12 | padding: 10, 13 | }, 14 | rowItemContainer: { 15 | display: "flex", 16 | justifyContent: "space-between", 17 | alignItems: "center", 18 | margin: "3px 0", 19 | }, 20 | rowItemRight: { 21 | display: "flex", 22 | alignItems: "center", 23 | }, 24 | }; 25 | 26 | /** Displays a single ingredient item's name, type and value */ 27 | const IngredientPaneRow = ({ 28 | name, 29 | item, 30 | }: { 31 | name: string; 32 | item: IngredientItem; 33 | }) => { 34 | return ( 35 |
36 | {name} 37 | 38 | {item.value == null ? "-" : item.value.toString()} 39 | 40 | {ItemTypeIcons[item.type]} 41 | 42 |
43 | ); 44 | }; 45 | 46 | /** Displays list of ingredients under an "Ingredients" header */ 47 | export const IngredientsPane = ({ box }: { box: IngredientBox }) => { 48 | return ( 49 |
50 |
Ingredients
51 | {Object.keys(box).map((name) => ( 52 | 53 | ))} 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /languages/chef/renderer/kitchen-display.tsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "@blueprintjs/core"; 2 | import { ChefRS } from "../types"; 3 | import { BakingDishColumn, MixingBowlColumn } from "./bowl-dish-columns"; 4 | import { IngredientsPane } from "./ingredients-pane"; 5 | import { BorderColor } from "./utils"; 6 | 7 | const styles = { 8 | ingredientsPane: { 9 | width: 200, 10 | flexShrink: 0, 11 | overflowY: "auto" as "auto", 12 | borderRight: "1px solid " + BorderColor, 13 | }, 14 | stacksPane: { 15 | padding: 5, 16 | flexGrow: 1, 17 | display: "flex", 18 | height: "100%", 19 | overflowX: "auto" as "auto", 20 | }, 21 | stackColumn: { 22 | width: 125, 23 | flexShrink: 0, 24 | }, 25 | }; 26 | 27 | export const KitchenDisplay = ({ 28 | state, 29 | }: { 30 | state: ChefRS["currentKitchen"]; 31 | }) => { 32 | return ( 33 |
34 |
35 | 36 |
37 |
38 | {Object.keys(state!.bowls).map((bowlId) => ( 39 |
40 | 44 |
45 | ))} 46 | {Object.keys(state!.dishes).map((dishId) => ( 47 |
48 | 52 |
53 | ))} 54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /languages/chef/renderer/utils.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@blueprintjs/core"; 2 | import { Colors } from "@blueprintjs/core"; 3 | 4 | /** Common border color for dark and light, using transparency */ 5 | export const BorderColor = Colors.GRAY3 + "55"; 6 | 7 | /** Map from item type to corresponding icon */ 8 | export const ItemTypeIcons: { [k: string]: React.ReactNode } = { 9 | dry: , 10 | liquid: , 11 | unknown: , 12 | }; 13 | -------------------------------------------------------------------------------- /languages/chef/runtime/input-stream.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeError } from "../../worker-errors"; 2 | 3 | /** 4 | * A barebones input stream implementation for consuming integers from a string. 5 | */ 6 | export default class InputStream { 7 | private _text: string; 8 | 9 | /** Create a new input stream loaded with the given input */ 10 | constructor(text: string) { 11 | this._text = text; 12 | } 13 | 14 | /** Remove leading whitespace from the current input stream */ 15 | private exhaustLeadingWhitespace(): void { 16 | const firstChar = this._text.trim()[0]; 17 | const posn = this._text.search(firstChar); 18 | this._text = this._text.slice(posn); 19 | } 20 | 21 | /** Parse input stream for an integer */ 22 | getNumber(): number { 23 | this.exhaustLeadingWhitespace(); 24 | // The extra whitespace differentiates whether string is empty or all numbers. 25 | if (this._text === "") throw new RuntimeError("Unexpected end of input"); 26 | let posn = this._text.search(/[^0-9]/); 27 | if (posn === 0) 28 | throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`); 29 | if (posn === -1) posn = this._text.length; 30 | // Consume and parse numeric part 31 | const numStr = this._text.slice(0, posn); 32 | this._text = this._text.slice(posn); 33 | return parseInt(numStr, 10); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /languages/chef/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { executeProgram, readTestProgram } from "../../test-utils"; 2 | import ChefRuntime from "../runtime"; 3 | 4 | /** Absolute path to directory of sample programs */ 5 | const DIRNAME = __dirname + "/samples"; 6 | 7 | describe("Test programs", () => { 8 | test("Hello World Souffle", async () => { 9 | const code = readTestProgram(DIRNAME, "hello-world-souffle"); 10 | const result = await executeProgram(new ChefRuntime(), code); 11 | expect(result.output).toBe("Hello world!"); 12 | }); 13 | 14 | test("Fibonacci Du Fromage", async () => { 15 | const code = readTestProgram(DIRNAME, "fibonacci-fromage"); 16 | const result = await executeProgram(new ChefRuntime(), code, "10"); 17 | expect(result.output).toBe(" 1 1 2 3 5 8 13 21 34 55"); 18 | }); 19 | 20 | test("Hello World Cake with Chocolate Sauce", async () => { 21 | const code = readTestProgram(DIRNAME, "hello-world-cake"); 22 | const result = await executeProgram(new ChefRuntime(), code); 23 | expect(result.output).toBe("Hello world!"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /languages/chef/tests/samples/fibonacci-fromage.txt: -------------------------------------------------------------------------------- 1 | Fibonacci Du Fromage. 2 | 3 | ==== Source: https://github.com/joostrijneveld/Chef-Interpreter/blob/master/ChefInterpreter/FibonacciDuFromage ==== 4 | An improvement on the Fibonacci with Caramel Sauce recipe. Much less for the sweettooths, much more correct. 5 | 6 | Ingredients. 7 | 5 g numbers 8 | 1 g cheese 9 | 10 | Method. 11 | Take numbers from refrigerator. 12 | Put cheese into mixing bowl. 13 | Put cheese into mixing bowl. 14 | Put numbers into 2nd mixing bowl. 15 | Remove cheese from 2nd mixing bowl. 16 | Remove cheese from 2nd mixing bowl. 17 | Fold numbers into 2nd mixing bowl. 18 | Put numbers into 2nd mixing bowl. 19 | Calculate the numbers. 20 | Serve with salt and pepper. 21 | Ponder the numbers until calculated. 22 | Add cheese to 2nd mixing bowl. 23 | Add cheese to 2nd mixing bowl. 24 | Fold numbers into 2nd mixing bowl. 25 | Move the numbers. 26 | Fold cheese into mixing bowl. 27 | Put cheese into 2nd mixing bowl. 28 | Transfer the numbers until moved. 29 | Pour contents of the 2nd mixing bowl into the baking dish. 30 | 31 | Serves 1. 32 | 33 | salt and pepper. 34 | 35 | Ingredients. 36 | 1 g salt 37 | 1 g pepper 38 | 39 | Method. 40 | Fold salt into mixing bowl. 41 | Fold pepper into mixing bowl. 42 | Clean mixing bowl. 43 | Put salt into mixing bowl. 44 | Add pepper. -------------------------------------------------------------------------------- /languages/chef/tests/samples/hello-world-cake.txt: -------------------------------------------------------------------------------- 1 | Hello World Cake with Chocolate sauce. 2 | 3 | ==== Source: Mike Worth, http://www.mike-worth.com/2013/03/31/baking-a-hello-world-cake/ ==== 4 | This prints hello world, while being tastier than Hello World Souffle. The main 5 | chef makes a " world!" cake, which he puts in the baking dish. When he gets the 6 | sous chef to make the "Hello" chocolate sauce, it gets put into the baking dish 7 | and then the whole thing is printed when he refrigerates the sauce. When 8 | actually cooking, I'm interpreting the chocolate sauce baking dish to be 9 | separate from the cake one and Liquify to mean either melt or blend depending on 10 | context. 11 | 12 | Ingredients. 13 | 33 g chocolate chips 14 | 100 g butter 15 | 54 ml double cream 16 | 2 pinches baking powder 17 | 114 g sugar 18 | 111 ml beaten eggs 19 | 119 g flour 20 | 32 g cocoa powder 21 | 0 g cake mixture 22 | 23 | Cooking time: 25 minutes. 24 | 25 | Pre-heat oven to 180 degrees Celsius. 26 | 27 | Method. 28 | Put chocolate chips into the mixing bowl. 29 | Put butter into the mixing bowl. 30 | Put sugar into the mixing bowl. 31 | Put beaten eggs into the mixing bowl. 32 | Put flour into the mixing bowl. 33 | Put baking powder into the mixing bowl. 34 | Put cocoa powder into the mixing bowl. 35 | Stir the mixing bowl for 1 minute. 36 | Combine double cream into the mixing bowl. 37 | Stir the mixing bowl for 4 minutes. 38 | Liquefy the contents of the mixing bowl. 39 | Pour contents of the mixing bowl into the baking dish. 40 | bake the cake mixture. 41 | Wait until baked. 42 | Serve with chocolate sauce. 43 | 44 | chocolate sauce. 45 | 46 | Ingredients. 47 | 111 g sugar 48 | 108 ml hot water 49 | 108 ml heated double cream 50 | 101 g dark chocolate 51 | 72 g milk chocolate 52 | 53 | Method. 54 | Clean the mixing bowl. 55 | Put sugar into the mixing bowl. 56 | Put hot water into the mixing bowl. 57 | Put heated double cream into the mixing bowl. 58 | dissolve the sugar. 59 | agitate the sugar until dissolved. 60 | Liquefy the dark chocolate. 61 | Put dark chocolate into the mixing bowl. 62 | Liquefy the milk chocolate. 63 | Put milk chocolate into the mixing bowl. 64 | Liquefy contents of the mixing bowl. 65 | Pour contents of the mixing bowl into the baking dish. 66 | Refrigerate for 1 hour. -------------------------------------------------------------------------------- /languages/chef/tests/samples/hello-world-souffle.txt: -------------------------------------------------------------------------------- 1 | Hello World Souffle. 2 | 3 | ==== Source: David Morgan-Mar, https://www.dangermouse.net/esoteric/chef_hello.html ==== 4 | This recipe prints the immortal words "Hello world!", in a basically brute force way. It also makes a lot of food for one person. 5 | 6 | Ingredients. 7 | 72 g haricot beans 8 | 101 eggs 9 | 108 g lard 10 | 111 cups oil 11 | 32 zucchinis 12 | 119 ml water 13 | 114 g red salmon 14 | 100 g dijon mustard 15 | 33 potatoes 16 | 17 | Method. 18 | Put potatoes into the mixing bowl. Put dijon mustard into the mixing bowl. Put lard into the mixing bowl. Put red salmon into the mixing bowl. Put oil into the mixing bowl. Put water into the mixing bowl. Put zucchinis into the mixing bowl. Put oil into the mixing bowl. Put lard into the mixing bowl. Put lard into the mixing bowl. Put eggs into the mixing bowl. Put haricot beans into the mixing bowl. Liquefy contents of the mixing bowl. Pour contents of the mixing bowl into the baking dish. 19 | 20 | Serves 1. -------------------------------------------------------------------------------- /languages/deadfish/README.md: -------------------------------------------------------------------------------- 1 | # Deadfish 2 | 3 | Deadfish is a dead-simple esolang created by Jonathan Todd Skinner. Four straightforward commands, no conditionals, 4 | no loops. It was the second language to be added to Esolang Park simply because I was feeling a bit lazy. The 5 | [esolangs wiki page](https://esolangs.org/wiki/Deadfish) is the only resource used for this implementation. 6 | 7 | In all honesty, Esolang Park does not do Deadfish justice. Deadfish is designed for writing highly interactive 8 | programs, but due to the lack of support for interactive input a lot of Deadfish's power is taken away. 9 | -------------------------------------------------------------------------------- /languages/deadfish/constants.ts: -------------------------------------------------------------------------------- 1 | import { MonacoTokensProvider } from "../types"; 2 | 3 | export type DFRS = { 4 | value: number; 5 | }; 6 | 7 | export enum DF_OP { 8 | INCR = "i", 9 | DECR = "d", 10 | SQ = "s", 11 | OUT = "o", 12 | } 13 | 14 | /** A single element of the program's AST */ 15 | export type DFAstStep = { 16 | instr: DF_OP; 17 | location: { line: number; char: number }; 18 | }; 19 | 20 | /** Sample program printing "Hello world" */ 21 | export const sampleProgram = [ 22 | "iisiiiisiiiiiiiio", 23 | "iiiiiiiiiiiiiiiiiiiiiiiiiiiiio", 24 | "iiiiiiioo", 25 | "iiio", 26 | "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddo", 27 | "dddddddddddddddddddddsddo", 28 | "ddddddddo", 29 | "iiio", 30 | "ddddddo", 31 | "ddddddddo", 32 | ].join("\n"); 33 | 34 | /** Tokens provider */ 35 | export const editorTokensProvider: MonacoTokensProvider = { 36 | tokenizer: { 37 | root: [ 38 | [/i/, "identifier"], 39 | [/d/, "variable"], 40 | [/s/, "meta"], 41 | [/o/, "tag"], 42 | ], 43 | }, 44 | defaultToken: "comment", 45 | }; 46 | -------------------------------------------------------------------------------- /languages/deadfish/engine.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from "../setup-worker"; 2 | import DeadfishLanguageEngine from "./runtime"; 3 | 4 | setupWorker(new DeadfishLanguageEngine()); 5 | -------------------------------------------------------------------------------- /languages/deadfish/index.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from "./renderer"; 2 | import { LanguageProvider } from "../types"; 3 | import { DFRS, sampleProgram, editorTokensProvider } from "./constants"; 4 | 5 | const provider: LanguageProvider = { 6 | Renderer, 7 | sampleProgram, 8 | editorTokensProvider, 9 | }; 10 | 11 | export default provider; 12 | -------------------------------------------------------------------------------- /languages/deadfish/renderer.tsx: -------------------------------------------------------------------------------- 1 | import { RendererProps } from "../types"; 2 | import { DFRS } from "./constants"; 3 | 4 | const styles = { 5 | container: { 6 | width: "100%", 7 | height: "100%", 8 | display: "flex", 9 | alignItems: "center", 10 | justifyContent: "center", 11 | }, 12 | text: { 13 | fontSize: "4em", 14 | }, 15 | }; 16 | 17 | export const Renderer = ({ state }: RendererProps) => { 18 | const value = state == null ? 0 : state.value; 19 | return ( 20 |
21 |

{value}

22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /languages/deadfish/runtime.ts: -------------------------------------------------------------------------------- 1 | import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types"; 2 | import { DFAstStep, DFRS, DF_OP } from "./constants"; 3 | 4 | // Default values for internal states 5 | // Factories are used to create new objects on reset 6 | const DEFAULT_AST = (): DFAstStep[] => []; 7 | const DEFAULT_PC = -1; 8 | const DEFAULT_VALUE = 0; 9 | 10 | // Instruction characters 11 | const OP_CHARS = Object.values(DF_OP); 12 | 13 | export default class DeadfishLanguageEngine implements LanguageEngine { 14 | private _ast: DFAstStep[] = DEFAULT_AST(); 15 | private _value: number = DEFAULT_VALUE; 16 | private _pc: number = DEFAULT_PC; 17 | 18 | resetState() { 19 | this._ast = DEFAULT_AST(); 20 | this._value = DEFAULT_VALUE; 21 | this._pc = DEFAULT_PC; 22 | } 23 | 24 | validateCode(code: string) { 25 | this.parseCode(code); 26 | } 27 | 28 | prepare(code: string, _input: string) { 29 | this._ast = this.parseCode(code); 30 | } 31 | 32 | executeStep(): StepExecutionResult { 33 | // Execute and update program counter 34 | let output: string | undefined = undefined; 35 | if (this._pc !== -1) { 36 | const astStep = this._ast[this._pc]; 37 | output = this.processOp(astStep.instr); 38 | } 39 | this._pc += 1; 40 | 41 | // Prepare location of next step 42 | let nextStepLocation: DocumentRange | null = null; 43 | if (this._pc < this._ast.length) { 44 | const { line, char } = this._ast[this._pc].location; 45 | nextStepLocation = { startLine: line, startCol: char, endCol: char + 1 }; 46 | } 47 | 48 | // Prepare and return execution result 49 | const rendererState = { value: this._value }; 50 | return { rendererState, nextStepLocation, output }; 51 | } 52 | 53 | private parseCode(code: string) { 54 | const ast: DFAstStep[] = []; 55 | 56 | // For each line... 57 | code.split("\n").forEach((line, lIdx) => { 58 | // For each character of this line... 59 | line.split("").forEach((char, cIdx) => { 60 | if (OP_CHARS.includes(char as DF_OP)) { 61 | ast.push({ 62 | instr: char as DF_OP, 63 | location: { line: lIdx, char: cIdx }, 64 | }); 65 | } 66 | }); 67 | }); 68 | 69 | return ast; 70 | } 71 | 72 | /** 73 | * Process the given instruction and return string to push to output if any. 74 | * 75 | * @param instr Instruction to apply 76 | * @returns String to append to output, if any 77 | */ 78 | private processOp(instr: DF_OP): string | undefined { 79 | if (this._value === -1 || this._value === 256) this._value = 0; 80 | if (instr === DF_OP.INCR) ++this._value; 81 | else if (instr === DF_OP.DECR) --this._value; 82 | else if (instr === DF_OP.SQ) this._value = this._value * this._value; 83 | else if (instr === DF_OP.OUT) return this._value.toString(); 84 | else throw new Error("Invalid instruction"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /languages/deadfish/tests/288.txt: -------------------------------------------------------------------------------- 1 | diissisdo -------------------------------------------------------------------------------- /languages/deadfish/tests/helloworld.txt: -------------------------------------------------------------------------------- 1 | iisiiiisiiiiiiiioiiiiiiiiiiiiiiiiiiiiiiiiiiiiioiiiiiiiooiiio 2 | dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddo 3 | dddddddddddddddddddddsddoddddddddoiiioddddddoddddddddo -------------------------------------------------------------------------------- /languages/deadfish/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { readTestProgram, executeProgram } from "../../test-utils"; 2 | import Engine from "../runtime"; 3 | 4 | /** 5 | * All test programs are picked up from https://esolangs.org/wiki/Deadfish. 6 | */ 7 | 8 | describe("Test programs", () => { 9 | // Standard hello-world program 10 | test("hello world", async () => { 11 | const code = readTestProgram(__dirname, "helloworld"); 12 | const result = await executeProgram(new Engine(), code); 13 | expect(result.output).toBe("7210110810811132119111114108100"); 14 | expect(result.rendererState.value).toBe(100); 15 | }); 16 | 17 | // Test program 1, output 0 18 | test("output zero (1)", async () => { 19 | const code = readTestProgram(__dirname, "zero1"); 20 | const result = await executeProgram(new Engine(), code); 21 | expect(result.output).toBe("0"); 22 | expect(result.rendererState.value).toBe(0); 23 | }); 24 | 25 | // Test program 2, output 0 26 | test("output zero (2)", async () => { 27 | const code = readTestProgram(__dirname, "zero2"); 28 | const result = await executeProgram(new Engine(), code); 29 | expect(result.output).toBe("0"); 30 | expect(result.rendererState.value).toBe(0); 31 | }); 32 | 33 | // Test program 3, output 288 34 | test("output 288", async () => { 35 | const code = readTestProgram(__dirname, "288"); 36 | const result = await executeProgram(new Engine(), code); 37 | expect(result.output).toBe("288"); 38 | expect(result.rendererState.value).toBe(288); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /languages/deadfish/tests/zero1.txt: -------------------------------------------------------------------------------- 1 | iissso -------------------------------------------------------------------------------- /languages/deadfish/tests/zero2.txt: -------------------------------------------------------------------------------- 1 | iissisdddddddddddddddddddddddddddddddddo -------------------------------------------------------------------------------- /languages/engine-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For given ASCII code, returns character that is safe to insert into code. 3 | * 4 | * This is useful for self-modifying programs that may insert non-printable characters into 5 | * the source code at runtime. Characters like `\n`, `\r` and `Tab` distort the grid visually 6 | * in the code editor. This function replaces such characters with safely printable alts. Other 7 | * control characters will be safely rendered by the code editor. 8 | * 9 | * @param asciiVal ASCII value to get safe character for 10 | * @returns Character safe to print without distorting code 11 | */ 12 | export const toSafePrintableChar = (asciiVal: number): string => { 13 | // "\n" -> "⤶" 14 | if (asciiVal === 10) return "\u21b5"; 15 | // "\r" -> "␍" 16 | else if (asciiVal === 13) return "\u240d"; 17 | // Tab -> "⇆" 18 | else if (asciiVal === 9) return "\u21c6"; 19 | else return String.fromCharCode(asciiVal); 20 | }; 21 | -------------------------------------------------------------------------------- /languages/setup-worker.ts: -------------------------------------------------------------------------------- 1 | import ExecutionController from "./execution-controller"; 2 | import { LanguageEngine, StepExecutionResult } from "./types"; 3 | import * as E from "./worker-errors"; 4 | import * as C from "./worker-constants"; 5 | 6 | /** Create a worker response for acknowledgement */ 7 | const ackMessage = ( 8 | ackType: A, 9 | error?: C.WorkerAckError[A] 10 | ): C.WorkerResponseData => ({ 11 | type: "ack", 12 | data: ackType, 13 | error, 14 | }); 15 | 16 | /** Create a worker response for code validation result */ 17 | const validationMessage = ( 18 | error?: E.WorkerParseError 19 | ): C.WorkerResponseData => ({ type: "validate", error }); 20 | 21 | /** Create a worker response for execution result */ 22 | const resultMessage = ( 23 | result: StepExecutionResult, 24 | error?: E.WorkerRuntimeError 25 | ): C.WorkerResponseData => ({ 26 | type: "result", 27 | data: result, 28 | error, 29 | }); 30 | 31 | /** Create a worker response for unexpected errors */ 32 | const errorMessage = ( 33 | error: E.WorkerError 34 | ): C.WorkerResponseData => ({ type: "error", error }); 35 | 36 | /** Initialize the execution controller */ 37 | const initController = () => { 38 | postMessage(ackMessage("init")); 39 | }; 40 | 41 | /** 42 | * Reset the state of the controller and engine, to 43 | * prepare for execution of a new program. 44 | */ 45 | const resetController = (controller: ExecutionController) => { 46 | controller.resetState(); 47 | postMessage(ackMessage("reset")); 48 | }; 49 | 50 | /** 51 | * Load program code into the engine. 52 | * @param code Code content of the program 53 | */ 54 | const prepare = ( 55 | controller: ExecutionController, 56 | { code, input }: { code: string; input: string } 57 | ) => { 58 | try { 59 | controller.prepare(code, input); 60 | postMessage(ackMessage("prepare")); 61 | } catch (error) { 62 | if (E.isParseError(error)) 63 | postMessage(ackMessage("prepare", E.serializeParseError(error))); 64 | else throw error; 65 | } 66 | }; 67 | 68 | /** 69 | * Update debugging breakpoints 70 | * @param points List of line numbers having breakpoints 71 | */ 72 | const updateBreakpoints = ( 73 | controller: ExecutionController, 74 | points: number[] 75 | ) => { 76 | controller.updateBreakpoints(points); 77 | postMessage(ackMessage("bp-update")); 78 | }; 79 | 80 | /** Validate the user's program syntax */ 81 | const validateCode = ( 82 | controller: ExecutionController, 83 | code: string 84 | ) => { 85 | const error = controller.validateCode(code); 86 | postMessage(validationMessage(error)); 87 | }; 88 | 89 | /** 90 | * Execute the entire program loaded on engine, 91 | * and return result of execution. 92 | */ 93 | const execute = async ( 94 | controller: ExecutionController, 95 | interval: number 96 | ) => { 97 | const { result, error } = await controller.executeAll({ 98 | interval, 99 | onResult: (res) => postMessage(resultMessage(res)), 100 | }); 101 | if (error) postMessage(resultMessage(result, error)); 102 | }; 103 | 104 | /** Trigger pause in program execution */ 105 | const pauseExecution = async (controller: ExecutionController) => { 106 | await controller.pauseExecution(); 107 | postMessage(ackMessage("pause")); 108 | }; 109 | 110 | /** Run a single execution step */ 111 | const executeStep = (controller: ExecutionController) => { 112 | const { result, error } = controller.executeStep(); 113 | postMessage(resultMessage(result, error)); 114 | }; 115 | 116 | /** 117 | * Create an execution controller worker script with the given engine. 118 | * @param engine Language engine to create worker for 119 | */ 120 | export const setupWorker = (engine: LanguageEngine) => { 121 | const controller = new ExecutionController(engine); 122 | 123 | addEventListener("message", async (ev: MessageEvent) => { 124 | try { 125 | if (ev.data.type === "Init") return initController(); 126 | if (ev.data.type === "Reset") return resetController(controller); 127 | if (ev.data.type === "Prepare") 128 | return prepare(controller, ev.data.params); 129 | if (ev.data.type === "ValidateCode") 130 | return validateCode(controller, ev.data.params.code); 131 | if (ev.data.type === "Execute") 132 | return await execute(controller, ev.data.params.interval); 133 | if (ev.data.type === "Pause") return await pauseExecution(controller); 134 | if (ev.data.type === "ExecuteStep") return executeStep(controller); 135 | if (ev.data.type === "UpdateBreakpoints") 136 | return updateBreakpoints(controller, ev.data.params.points); 137 | } catch (error) { 138 | // Error here indicates an implementation bug 139 | const err = error as Error; 140 | postMessage(errorMessage(E.serializeError(err))); 141 | return; 142 | } 143 | throw new Error("Invalid worker message type"); 144 | }); 145 | }; 146 | -------------------------------------------------------------------------------- /languages/shakespeare/README.md: -------------------------------------------------------------------------------- 1 | # Shakespeare 2 | 3 | Shakespeare is a programming language that reads like a Shakespearean play, usually involving characters 4 | appreciating and (rather roughly) criticizing other characters. It was created by Karl Wiberg and Jon Åslund 5 | as part of a university course assignment in 2001. 6 | 7 | The original resource page is [hosted on Sourceforge](http://shakespearelang.sourceforge.net). It contains the 8 | [language specification PDF](http://shakespearelang.sourceforge.net/report/shakespeare.pdf) and a Shakespeare-to-C 9 | transpiler written with Flex and Bison in C, along with links to some other related resources. 10 | 11 | The specification and the transpiler source code are the only references used while implementing the Shakespeare 12 | language for Esolang Park. 13 | 14 | ## Notes for the user 15 | 16 | - It is not necessary for questions to immediately be followed by conditionals. Conditionals 17 | must immediately follow a question though - a runtime error is thrown otherwise. 18 | 19 | - Empty acts and scenes are invalid. An act must contain at least one scene, and a scene must contain 20 | at least one dialogue set or entry-exit clause. 21 | 22 | ## Implementation details 23 | 24 | - The parser is implemented with [Chevrotain](https://chevrotain.io). 25 | - The engine is quite small in size, and is a single file (`runtime.ts`). 26 | - The renderer uses a custom component instead of Blueprint's `Tag` component for much better performance. 27 | 28 | ## Possible improvements 29 | 30 | - Currently, the check that conditionals must be preceded immediately by a question is done at runtime. It is a static 31 | check and should be done while parsing the program instead. 32 | 33 | - Syntax highlighting doesn't work for multi-word keywords that break into two lines (`... summer's \n day ...`). 34 | -------------------------------------------------------------------------------- /languages/shakespeare/common.ts: -------------------------------------------------------------------------------- 1 | import { MonacoTokensProvider } from "../types"; 2 | import * as R from "./parser/constants"; 3 | 4 | /** Runtime value of a character */ 5 | export type CharacterValue = { 6 | value: number; 7 | stack: number[]; 8 | }; 9 | 10 | /** Bag of characters declared in the program */ 11 | export type CharacterBag = { [name: string]: CharacterValue }; 12 | 13 | /** Type of props passed to renderer */ 14 | export type RS = { 15 | currentSpeaker: string | null; 16 | charactersOnStage: string[]; 17 | characterBag: CharacterBag; 18 | questionState: boolean | null; 19 | }; 20 | 21 | /** Sample program */ 22 | export const sampleProgram = `The Infamous Hello World Program. 23 | 24 | Romeo, a young man with a remarkable patience. 25 | Juliet, a likewise young woman of remarkable grace. 26 | Ophelia, a remarkable woman much in dispute with Hamlet. 27 | Hamlet, the flatterer of Andersen Insulting A/S. 28 | 29 | 30 | Act I: Hamlet's insults and flattery. 31 | 32 | Scene I: The insulting of Romeo. 33 | 34 | [Enter Hamlet and Romeo] 35 | 36 | Hamlet: 37 | You lying stupid fatherless big smelly half-witted coward! 38 | You are as stupid as the difference between a handsome rich brave 39 | hero and thyself! Speak your mind! 40 | 41 | You are as brave as the sum of your fat little stuffed misused dusty 42 | old rotten codpiece and a beautiful fair warm peaceful sunny summer's day. 43 | You are as healthy as the difference between the sum of the sweetest 44 | reddest rose and my father and yourself! Speak your mind! 45 | 46 | You are as cowardly as the sum of yourself and the difference 47 | between a big mighty proud kingdom and a horse. Speak your mind. 48 | 49 | Speak your mind! 50 | 51 | [Exit Romeo] 52 | 53 | Scene II: The praising of Juliet. 54 | 55 | [Enter Juliet] 56 | 57 | Hamlet: 58 | Thou art as sweet as the sum of the sum of Romeo and his horse and his 59 | black cat! Speak thy mind! 60 | 61 | [Exit Juliet] 62 | 63 | Scene III: The praising of Ophelia. 64 | 65 | [Enter Ophelia] 66 | 67 | Hamlet: 68 | Thou art as lovely as the product of a large rural town and my amazing 69 | bottomless embroidered purse. Speak thy mind! 70 | 71 | Thou art as loving as the product of the bluest clearest sweetest sky 72 | and the sum of a squirrel and a white horse. Thou art as beautiful as 73 | the difference between Juliet and thyself. Speak thy mind! 74 | 75 | [Exeunt Ophelia and Hamlet] 76 | 77 | 78 | Act II: Behind Hamlet's back. 79 | 80 | Scene I: Romeo and Juliet's conversation. 81 | 82 | [Enter Romeo and Juliet] 83 | 84 | Romeo: 85 | Speak your mind. You are as worried as the sum of yourself and the 86 | difference between my small smooth hamster and my nose. Speak your mind! 87 | 88 | Juliet: 89 | Speak YOUR mind! You are as bad as Hamlet! You are as small as the 90 | difference between the square of the difference between my little pony 91 | and your big hairy hound and the cube of your sorry little 92 | codpiece. Speak your mind! 93 | 94 | [Exit Romeo] 95 | 96 | Scene II: Juliet and Ophelia's conversation. 97 | 98 | [Enter Ophelia] 99 | 100 | Juliet: 101 | Thou art as good as the quotient between Romeo and the sum of a small 102 | furry animal and a leech. Speak your mind! 103 | 104 | Ophelia: 105 | Thou art as disgusting as the quotient between Romeo and twice the 106 | difference between a mistletoe and an oozing infected blister! 107 | Speak your mind! 108 | 109 | [Exeunt] 110 | 111 | `; 112 | 113 | /** Syntax highlighting */ 114 | export const editorTokensProvider: MonacoTokensProvider = { 115 | ignoreCase: true, 116 | tokenizer: { 117 | /** Program title */ 118 | root: [[/[^\.]+\./, { token: "meta", next: "introduction" }]], 119 | /** Character introductions */ 120 | introduction: [ 121 | { include: "whitespace" }, 122 | [ 123 | `${R.CharacterRegex.source}(,\\s*)([^\\.]+\\.\\s*\\n?)`, 124 | ["tag", "", "comment"], 125 | ], 126 | [ 127 | /(act)(\s+)([IVXLCDM]+)(:\s+)([^\.]+\.)/, 128 | // prettier-ignore 129 | ["keyword", "", "constant", "", { token: "comment", next: "actSection" }] as any, 130 | ], 131 | ], 132 | /** A single act of the play */ 133 | actSection: [ 134 | { include: "whitespace" }, 135 | [/(?=act\b)/, { token: "", next: "@pop" }], 136 | [ 137 | /(scene)(\s+)([IVXLCDM]+)(:\s+)(.+?(?:\.|\!|\?))/, 138 | // prettier-ignore 139 | ["constant.function", "", "constant", "", {token: "comment", next: "sceneSection"}] as any, 140 | ], 141 | ], 142 | /** A single scene of an act in the play */ 143 | sceneSection: [ 144 | { include: "whitespace" }, 145 | [/\[/, { token: "", next: "entryExitClause" }], 146 | [/(?=act\b)/, { token: "", next: "@pop" }], 147 | [/(?=scene\b)/, { token: "", next: "@pop" }], 148 | { include: "dialogue" }, 149 | ], 150 | /** Dialogues spoken by characters */ 151 | dialogue: [ 152 | { include: "whitespace" }, 153 | [`${R.CharacterRegex.source}:`, "tag"], 154 | [R.CharacterRegex, "tag"], 155 | [/nothing|zero/, "constant"], 156 | [/(remember|recall)\b/, "keyword"], 157 | [/(myself|i|me|thyself|yourself|thee|thou|you)\b/, "variable"], 158 | [/speak (thine|thy|your) mind\b/, "constant.function"], 159 | [/open (thine|thy|your) heart\b/, "constant.function"], 160 | [/listen to (thine|thy|your) heart\b/, "constant.function"], 161 | [/open (thine|thy|your) mind\b/, "constant.function"], 162 | [ 163 | /(punier|smaller|worse|better|bigger|fresher|friendlier|nicer|jollier)\b/, 164 | "attribute", 165 | ], 166 | [/(more|less)\b/, "attribute"], 167 | [/if (so|not),/, "keyword"], 168 | [ 169 | /((?:let us|we shall|we must) (?:return|proceed) to (?:act|scene) )([IVXLCDM]+)/, 170 | ["annotation", "constant"], 171 | ], 172 | [ 173 | /(sum|difference|product|quotient|remainder|factorial|square|root|cube|twice)\b/, 174 | "operators", 175 | ], 176 | [R.PositiveNounRegex, "constant"], 177 | [R.NeutralNounRegex, "constant"], 178 | [R.NegativeNounRegex, "constant"], 179 | [R.PositiveAdjectiveRegex, "attribute"], 180 | [R.NeutralAdjectiveRegex, "attribute"], 181 | [R.NegativeAdjectiveRegex, "attribute"], 182 | ], 183 | /** Clause for entry/exit of character(s) */ 184 | entryExitClause: [ 185 | { include: "whitespace" }, 186 | [/enter|exit|exeunt/, "keyword"], 187 | [/]/, { token: "", next: "@pop" }], 188 | [R.CharacterRegex, "tag"], 189 | [/and\b|,/, ""], 190 | ], 191 | /** Utility: skip across whitespace and line breaks */ 192 | whitespace: [[/[\s\n]+/, ""]], 193 | }, 194 | defaultToken: "", 195 | }; 196 | -------------------------------------------------------------------------------- /languages/shakespeare/engine.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from "../setup-worker"; 2 | import XYZLanguageEngine from "./runtime"; 3 | 4 | setupWorker(new XYZLanguageEngine()); 5 | -------------------------------------------------------------------------------- /languages/shakespeare/index.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from "./renderer"; 2 | import { LanguageProvider } from "../types"; 3 | import { RS, sampleProgram, editorTokensProvider } from "./common"; 4 | 5 | const provider: LanguageProvider = { 6 | Renderer, 7 | sampleProgram, 8 | editorTokensProvider, 9 | }; 10 | 11 | export default provider; 12 | -------------------------------------------------------------------------------- /languages/shakespeare/input-stream.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeError } from "../worker-errors"; 2 | 3 | /** 4 | * A barebones input stream implementation for consuming integers and characters from a string. 5 | */ 6 | export default class InputStream { 7 | private _text: string; 8 | 9 | /** Create a new input stream loaded with the given input */ 10 | constructor(text: string) { 11 | this._text = text; 12 | } 13 | 14 | /** Remove leading whitespace from the current input stream */ 15 | private exhaustLeadingWhitespace(): void { 16 | const firstChar = this._text.trim()[0]; 17 | const posn = this._text.search(firstChar); 18 | this._text = this._text.slice(posn); 19 | } 20 | 21 | /** Parse input stream for an integer */ 22 | getNumber(): number { 23 | this.exhaustLeadingWhitespace(); 24 | // The extra whitespace differentiates whether string is empty or all numbers. 25 | if (this._text === "") throw new RuntimeError("Unexpected end of input"); 26 | let posn = this._text.search(/[^0-9]/); 27 | if (posn === 0) 28 | throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`); 29 | if (posn === -1) posn = this._text.length; 30 | // Consume and parse numeric part 31 | const numStr = this._text.slice(0, posn); 32 | this._text = this._text.slice(posn); 33 | return parseInt(numStr, 10); 34 | } 35 | 36 | /** 37 | * Parse input stream for the first character, and return its ASCII code. 38 | * If end of input, returns -1. 39 | */ 40 | getChar(): number { 41 | if (this._text.length === 0) return -1; 42 | const char = this._text[0]; 43 | this._text = this._text.slice(1); 44 | return char.charCodeAt(0); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /languages/shakespeare/parser/constants.ts: -------------------------------------------------------------------------------- 1 | /** Regex bit to accommodate line breaks in multi-word keywords */ 2 | const br = /(?: |\s*\n\s*)/.source; 3 | 4 | /** Utility to create regex from list of words */ 5 | const makeRegex = (words: string[], flags?: string) => { 6 | const fixedWords = words.map((w) => w.replace(/ /g, br)); 7 | const pattern = "(" + fixedWords.join("|") + ")\\b"; 8 | return new RegExp(pattern, flags); 9 | }; 10 | 11 | // prettier-ignore 12 | const CHARACTERS = ["Achilles", "Adonis", "Adriana", "Aegeon", "Aemilia", "Agamemnon", "Agrippa", "Ajax", "Alonso", "Andromache", 13 | "Angelo", "Antiochus", "Antonio", "Arthur", "Autolycus", "Balthazar", "Banquo", "Beatrice", "Benedick", "Benvolio", "Bianca", 14 | "Brabantio", "Brutus", "Capulet", "Cassandra", "Cassius", "Christopher Sly", "Cicero", "Claudio", "Claudius", "Cleopatra", 15 | "Cordelia", "Cornelius", "Cressida", "Cymberline", "Demetrius", "Desdemona", "Dionyza", "Doctor Caius", "Dogberry", "Don John", 16 | "Don Pedro", "Donalbain", "Dorcas", "Duncan", "Egeus", "Emilia", "Escalus", "Falstaff", "Fenton", "Ferdinand", "Ford", "Fortinbras", 17 | "Francisca", "Friar John", "Friar Laurence", "Gertrude", "Goneril", "Hamlet", "Hecate", "Hector", "Helen", "Helena", "Hermia", 18 | "Hermonie", "Hippolyta", "Horatio", "Imogen", "Isabella", "John of Gaunt", "John of Lancaster", "Julia", "Juliet", "Julius Caesar", 19 | "King Henry", "King John", "King Lear", "King Richard", "Lady Capulet", "Lady Macbeth", "Lady Macduff", "Lady Montague", "Lennox", 20 | "Leonato", "Luciana", "Lucio", "Lychorida", "Lysander", "Macbeth", "Macduff", "Malcolm", "Mariana", "Mark Antony", "Mercutio", 21 | "Miranda", "Mistress Ford", "Mistress Overdone", "Mistress Page", "Montague", "Mopsa", "Oberon", "Octavia", "Octavius Caesar", 22 | "Olivia", "Ophelia", "Orlando", "Orsino", "Othello", "Page", "Pantino", "Paris", "Pericles", "Pinch", "Polonius", "Pompeius", 23 | "Portia", "Priam", "Prince Henry", "Prospero", "Proteus", "Publius", "Puck", "Queen Elinor", "Regan", "Robin", "Romeo", "Rosalind", 24 | "Sebastian", "Shallow", "Shylock", "Slender", "Solinus", "Stephano", "Thaisa", "The Abbot of Westminster", "The Apothecary", 25 | "The Archbishop of Canterbury", "The Duke of Milan", "The Duke of Venice", "The Ghost", "Theseus", "Thurio", "Timon", "Titania", 26 | "Titus", "Troilus", "Tybalt", "Ulysses", "Valentine", "Venus", "Vincentio", "Viola"] 27 | 28 | /** Regex that matches an identified character name */ 29 | export const CharacterRegex = makeRegex(CHARACTERS, "i"); 30 | 31 | // prettier-ignore 32 | const NEGATIVE_ADJECTIVE = ["bad","cowardly","cursed","damned","dirty","disgusting", "distasteful","dusty","evil","fat-kidneyed", 33 | "fat","fatherless","foul","hairy","half-witted", "horrible","horrid","infected","lying","miserable","misused", 34 | "oozing","rotten","rotten","smelly","snotty","sorry", "stinking","stuffed","stupid","vile","villainous","worried"] 35 | 36 | /** Regex that matches a negative adjective */ 37 | export const NegativeAdjectiveRegex = makeRegex(NEGATIVE_ADJECTIVE, "i"); 38 | 39 | // prettier-ignore 40 | const NEGATIVE_NOUNS = ["Hell","Microsoft","bastard","beggar","blister","codpiece","coward","curse","death","devil","draught", 41 | "famine","flirt-gill","goat","hate","hog","hound","leech","lie","pig","plague","starvation","toad","war","wolf"]; 42 | 43 | /** Regex that matches a negative noun */ 44 | export const NegativeNounRegex = makeRegex(NEGATIVE_NOUNS, "i"); 45 | 46 | // prettier-ignore 47 | const NEUTRAL_ADJECTIVES = ["big","black","blue","bluest","bottomless","furry","green","hard","huge","large","little", 48 | "normal","old","purple","red","rural","small","tiny","white","yellow"]; 49 | 50 | /** Regex that matches a neutral adjective */ 51 | export const NeutralAdjectiveRegex = makeRegex(NEUTRAL_ADJECTIVES, "i"); 52 | 53 | // prettier-ignore 54 | const NEUTRAL_NOUNS = ["animal","aunt","brother","cat","chihuahua","cousin","cow","daughter","door","face","father", 55 | "fellow","granddaughter","grandfather","grandmother","grandson","hair","hamster","horse","lamp","lantern","mistletoe", 56 | "moon","morning","mother","nephew","niece","nose","purse","road","roman","sister","sky","son","squirrel","stone wall", 57 | "thing","town","tree","uncle","wind"] 58 | 59 | /** Regex that matches a neutral noun */ 60 | export const NeutralNounRegex = makeRegex(NEUTRAL_NOUNS, "i"); 61 | 62 | // prettier-ignore 63 | const POSITIVE_ADJECTIVES = ["amazing","beautiful","blossoming","bold","brave","charming","clearest","cunning","cute", 64 | "delicious","embroidered","fair","fine","gentle","golden","good","handsome","happy","healthy","honest","lovely","loving", 65 | "mighty","noble","peaceful","pretty","prompt","proud","reddest","rich","smooth","sunny","sweet","sweetest","trustworthy","warm"] 66 | 67 | /** Regex that matches a positive adjective */ 68 | export const PositiveAdjectiveRegex = makeRegex(POSITIVE_ADJECTIVES, "i"); 69 | 70 | // prettier-ignore 71 | const POSITIVE_NOUN = ["Heaven","King","Lord","angel","flower","happiness","joy","plum","summer's day","hero","rose","kingdom","pony"] 72 | 73 | /** Regex that matches a positive noun */ 74 | export const PositiveNounRegex = makeRegex(POSITIVE_NOUN, "i"); 75 | -------------------------------------------------------------------------------- /languages/shakespeare/parser/generate-cst-types.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "fs"; 2 | import { resolve } from "path"; 3 | import { generateCstDts } from "chevrotain"; 4 | import { ShakespeareParser } from "./parser"; 5 | 6 | /** 7 | * This script generates CST types for the Shakespeare parser. 8 | * To run: `yarn ts-node $(pwd)/generate-cst-types.ts` in this directory. 9 | * 10 | * The `$(pwd)` makes the path absolute. Due to some reason, relative paths with ts-node 11 | * aren't working on my side. 12 | */ 13 | 14 | const productions = new ShakespeareParser().getGAstProductions(); 15 | const dtsString = generateCstDts(productions); 16 | 17 | const dtsPath = resolve(__dirname, "./cst.d.ts"); 18 | writeFileSync(dtsPath, dtsString); 19 | -------------------------------------------------------------------------------- /languages/shakespeare/parser/index.ts: -------------------------------------------------------------------------------- 1 | import { CstNode, IToken, Lexer } from "chevrotain"; 2 | import { DocumentRange } from "../../types"; 3 | import { ParseError } from "../../worker-errors"; 4 | import { ShakespeareParser } from "./parser"; 5 | import { AllTokens } from "./tokens"; 6 | import { ShakespeareVisitor } from "./visitor"; 7 | import { Program } from "./visitor-types"; 8 | 9 | export class Parser { 10 | private readonly _lexer: Lexer = new Lexer(AllTokens); 11 | private readonly _parser: ShakespeareParser = new ShakespeareParser(); 12 | private readonly _visitor: ShakespeareVisitor = new ShakespeareVisitor(); 13 | 14 | public parse(text: string): Program { 15 | const tokens = this.runLexer(text); 16 | const cst = this.runParser(tokens); 17 | return this.runVisitor(cst); 18 | } 19 | 20 | private runLexer(text: string): IToken[] { 21 | const { tokens, errors } = this._lexer.tokenize(text); 22 | if (errors.length > 0) { 23 | const error = errors[0]; 24 | throw new ParseError(error.message, { 25 | startLine: error.line ? error.line - 1 : 0, 26 | startCol: error.column && error.column - 1, 27 | endCol: error.column && error.column + error.length - 1, 28 | }); 29 | } 30 | return tokens; 31 | } 32 | 33 | private runParser(tokens: IToken[]): CstNode { 34 | this._parser.input = tokens; 35 | const parseResult = this._parser.program(); 36 | if (this._parser.errors.length > 0) { 37 | const error = this._parser.errors[0]; 38 | throw new ParseError(error.message, this.getRange(error.token)); 39 | } 40 | 41 | return parseResult; 42 | } 43 | 44 | private runVisitor(cst: CstNode): Program { 45 | return this._visitor.visit(cst); 46 | } 47 | 48 | private getRange(token: IToken): DocumentRange { 49 | const startLine = (token.startLine || 1) - 1; 50 | const startCol = token.startColumn && token.startColumn - 1; 51 | const endCol = token.endColumn; 52 | return { startLine, startCol, endCol }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /languages/shakespeare/renderer/character-row.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "../../ui-utils"; 2 | import { CharacterValue } from "../common"; 3 | 4 | type Props = { 5 | name: string; 6 | value: CharacterValue; 7 | }; 8 | 9 | export const CharacterRow = (props: Props) => { 10 | const { name, value } = props; 11 | 12 | return ( 13 |
14 |
15 | {name}:{" "} 16 |
{value.value}
17 | {value.stack.map((v, i) => ( 18 | {v} 19 | ))} 20 |
21 |
22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /languages/shakespeare/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "@blueprintjs/core"; 2 | import { RendererProps } from "../../types"; 3 | import { RS } from "../common"; 4 | import { CharacterRow } from "./character-row"; 5 | import { TopBar } from "./topbar"; 6 | 7 | /** Common border color for dark and light, using transparency */ 8 | export const BorderColor = Colors.GRAY3 + "55"; 9 | 10 | const styles = { 11 | placeholderDiv: { 12 | height: "100%", 13 | width: "100%", 14 | display: "flex", 15 | justifyContent: "center", 16 | alignItems: "center", 17 | fontSize: "1.2em", 18 | }, 19 | rootContainer: { 20 | height: "100%", 21 | display: "flex", 22 | flexDirection: "column" as "column", 23 | }, 24 | topBarContainer: { 25 | borderBottom: "1px solid " + BorderColor, 26 | padding: 10, 27 | }, 28 | mainContainer: { 29 | flex: 1, 30 | minHeight: 0, 31 | overflowY: "auto" as "auto", 32 | }, 33 | }; 34 | 35 | export const Renderer = ({ state }: RendererProps) => { 36 | if (state == null) 37 | return ( 38 |
Run some code to see the stage!
39 | ); 40 | 41 | return ( 42 |
43 |
44 | 49 |
50 |
51 | {Object.keys(state.characterBag).map((name) => ( 52 | 57 | ))} 58 |
59 |
60 | ); 61 | 62 | return null; 63 | }; 64 | -------------------------------------------------------------------------------- /languages/shakespeare/renderer/topbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text } from "@blueprintjs/core"; 3 | import { Box } from "../../ui-utils"; 4 | 5 | const styles = { 6 | charChip: { 7 | margin: "0 5px", 8 | }, 9 | questionText: { 10 | marginLeft: 30, 11 | marginRight: 10, 12 | }, 13 | }; 14 | 15 | type Props = { 16 | charactersOnStage: string[]; 17 | currSpeaker: string | null; 18 | questionState: boolean | null; 19 | }; 20 | 21 | export const TopBar = (props: Props) => { 22 | const { charactersOnStage, currSpeaker, questionState } = props; 23 | 24 | const characterChips = 25 | charactersOnStage.length === 0 ? ( 26 | The stage is empty 27 | ) : ( 28 | charactersOnStage.map((character) => ( 29 | 33 | {character} 34 | 35 | )) 36 | ); 37 | 38 | return ( 39 |
40 | {characterChips} 41 | {questionState != null && ( 42 | <> 43 | 44 | Answer to question: 45 | 46 | 47 | {questionState ? "yes" : "no"} 48 | 49 | 50 | )} 51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /languages/shakespeare/tests/helloworld.txt: -------------------------------------------------------------------------------- 1 | The Infamous Hello World Program. 2 | 3 | Romeo, a young man with a remarkable patience. 4 | Juliet, a likewise young woman of remarkable grace. 5 | Ophelia, a remarkable woman much in dispute with Hamlet. 6 | Hamlet, the flatterer of Andersen Insulting A/S. 7 | 8 | 9 | Act I: Hamlet's insults and flattery. 10 | 11 | Scene I: The insulting of Romeo. 12 | 13 | [Enter Hamlet and Romeo] 14 | 15 | Hamlet: 16 | You lying stupid fatherless big smelly half-witted coward! 17 | You are as stupid as the difference between a handsome rich brave 18 | hero and thyself! Speak your mind! 19 | 20 | You are as brave as the sum of your fat little stuffed misused dusty 21 | old rotten codpiece and a beautiful fair warm peaceful sunny summer's 22 | day. You are as healthy as the difference between the sum of the 23 | sweetest reddest rose and my father and yourself! Speak your mind! 24 | 25 | You are as cowardly as the sum of yourself and the difference 26 | between a big mighty proud kingdom and a horse. Speak your mind. 27 | 28 | Speak your mind! 29 | 30 | [Exit Romeo] 31 | 32 | Scene II: The praising of Juliet. 33 | 34 | [Enter Juliet] 35 | 36 | Hamlet: 37 | Thou art as sweet as the sum of the sum of Romeo and his horse and his 38 | black cat! Speak thy mind! 39 | 40 | [Exit Juliet] 41 | 42 | Scene III: The praising of Ophelia. 43 | 44 | [Enter Ophelia] 45 | 46 | Hamlet: 47 | Thou art as lovely as the product of a large rural town and my amazing 48 | bottomless embroidered purse. Speak thy mind! 49 | 50 | Thou art as loving as the product of the bluest clearest sweetest sky 51 | and the sum of a squirrel and a white horse. Thou art as beautiful as 52 | the difference between Juliet and thyself. Speak thy mind! 53 | 54 | [Exeunt Ophelia and Hamlet] 55 | 56 | 57 | Act II: Behind Hamlet's back. 58 | 59 | Scene I: Romeo and Juliet's conversation. 60 | 61 | [Enter Romeo and Juliet] 62 | 63 | Romeo: 64 | Speak your mind. You are as worried as the sum of yourself and the 65 | difference between my small smooth hamster and my nose. Speak your 66 | mind! 67 | 68 | Juliet: 69 | Speak YOUR mind! You are as bad as Hamlet! You are as small as the 70 | difference between the square of the difference between my little pony 71 | and your big hairy hound and the cube of your sorry little 72 | codpiece. Speak your mind! 73 | 74 | [Exit Romeo] 75 | 76 | Scene II: Juliet and Ophelia's conversation. 77 | 78 | [Enter Ophelia] 79 | 80 | Juliet: 81 | Thou art as good as the quotient between Romeo and the sum of a small 82 | furry animal and a leech. Speak your mind! 83 | 84 | Ophelia: 85 | Thou art as disgusting as the quotient between Romeo and twice the 86 | difference between a mistletoe and an oozing infected blister! Speak 87 | your mind! 88 | 89 | [Exeunt] 90 | -------------------------------------------------------------------------------- /languages/shakespeare/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { executeProgram, readTestProgram } from "../../test-utils"; 2 | import Engine from "../runtime"; 3 | 4 | describe("Test programs", () => { 5 | test("Hello World", async () => { 6 | const code = readTestProgram(__dirname, "helloworld"); 7 | const result = await executeProgram(new Engine(), code); 8 | expect(result.output).toBe("Hello World!\n"); 9 | }); 10 | 11 | test("Prime Numbers", async () => { 12 | const code = readTestProgram(__dirname, "primes"); 13 | const result = await executeProgram(new Engine(), code, "15"); 14 | expect(result.output).toBe(">2 3 5 7 11 13 "); 15 | }); 16 | 17 | test("Reverse cat", async () => { 18 | const code = readTestProgram(__dirname, "reverse"); 19 | const input = "abcd efgh\nijkl mnop\n"; 20 | const expectedOutput = input.split("").reverse().join(""); 21 | const result = await executeProgram(new Engine(), code, input); 22 | expect(result.output).toBe(expectedOutput); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /languages/shakespeare/tests/primes.txt: -------------------------------------------------------------------------------- 1 | === Modification in Act II, Scene III, Romeo's line === 2 | === Numbers separated by space instead of newline === 3 | Prime Number Computation in Copenhagen. 4 | 5 | Romeo, a young man of Verona. 6 | Juliet, a young woman. 7 | Hamlet, a temporary variable from Denmark. 8 | The Ghost, a limiting factor (and by a remarkable coincidence also Hamlet's father). 9 | 10 | 11 | Act I: Interview with the other side. 12 | 13 | Scene I: At the last hour before dawn. 14 | 15 | [Enter the Ghost and Juliet] 16 | 17 | The Ghost: 18 | You pretty little warm thing! Thou art as prompt as the difference 19 | between the square of thyself and your golden hair. Speak your mind. 20 | 21 | Juliet: 22 | Listen to your heart! 23 | 24 | [Exit the Ghost] 25 | 26 | [Enter Romeo] 27 | 28 | Juliet: 29 | Thou art as sweet as a sunny summer's day! 30 | 31 | 32 | Act II: Determining divisibility. 33 | 34 | Scene I: A private conversation. 35 | 36 | Juliet: 37 | Art thou more cunning than the Ghost? 38 | 39 | Romeo: 40 | If so, let us proceed to scene V. 41 | 42 | [Exit Romeo] 43 | 44 | [Enter Hamlet] 45 | 46 | Juliet: 47 | You are as villainous as the square root of Romeo! 48 | 49 | Hamlet: 50 | You are as lovely as a red rose. 51 | 52 | Scene II: Questions and the consequences thereof. 53 | 54 | Juliet: 55 | Am I better than you? 56 | 57 | Hamlet: 58 | If so, let us proceed to scene III. 59 | 60 | Juliet: 61 | Is the remainder of the quotient between Romeo and me as good as 62 | nothing? 63 | 64 | Hamlet: 65 | If so, let us proceed to scene IV. 66 | Thou art as bold as the sum of thyself and a roman. 67 | 68 | Juliet: 69 | Let us return to scene II. 70 | 71 | Scene III: Romeo must die! 72 | 73 | [Exit Hamlet] 74 | 75 | [Enter Romeo] 76 | 77 | Juliet: 78 | Open your heart. 79 | 80 | [Exit Juliet] 81 | 82 | [Enter Hamlet] 83 | 84 | Romeo: 85 | Thou art as rotten as the difference between nothing and a vile 86 | disgusting snotty stinking half-witted hog. Speak your mind! 87 | 88 | [Exit Romeo] 89 | 90 | [Enter Juliet] 91 | 92 | Scene IV: One small dog at a time. 93 | 94 | [Exit Hamlet] 95 | 96 | [Enter Romeo] 97 | 98 | Juliet: 99 | Thou art as handsome as the sum of thyself and my chihuahua! 100 | Let us return to scene I. 101 | 102 | Scene V: Fin. 103 | 104 | [Exeunt] 105 | -------------------------------------------------------------------------------- /languages/shakespeare/tests/reverse.txt: -------------------------------------------------------------------------------- 1 | Outputting Input Reversedly. 2 | 3 | Othello, a stacky man. 4 | Lady Macbeth, who pushes him around till he pops. 5 | 6 | 7 | Act I: The one and only. 8 | 9 | Scene I: In the beginning, there was nothing. 10 | 11 | [Enter Othello and Lady Macbeth] 12 | 13 | Othello: 14 | You are nothing! 15 | 16 | Scene II: Pushing to the very end. 17 | 18 | Lady Macbeth: 19 | Open your mind! Remember yourself. 20 | 21 | Othello: 22 | You are as hard as the sum of yourself and a stone wall. Am I as 23 | horrid as a flirt-gill? 24 | 25 | Lady Macbeth: 26 | If not, let us return to scene II. Recall your imminent death! 27 | 28 | Othello: 29 | You are as small as the difference between yourself and a hair! 30 | 31 | Scene III: Once you pop, you can't stop! 32 | 33 | Lady Macbeth: 34 | Recall your unhappy childhood. Speak your mind! 35 | 36 | Othello: 37 | You are as vile as the sum of yourself and a toad! Are you better 38 | than nothing? 39 | 40 | Lady Macbeth: 41 | If so, let us return to scene III. 42 | 43 | Scene IV: The end. 44 | 45 | [Exeunt] 46 | -------------------------------------------------------------------------------- /languages/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { resolve } from "path"; 3 | import ExecutionController from "./execution-controller"; 4 | import { LanguageEngine } from "./types"; 5 | 6 | /** 7 | * Read the entire contents of a test program. 8 | * @param dirname Absolute path to directory containing program 9 | * @param name Name of TXT file, without extension 10 | * @returns Contents of file, as a \n-delimited string 11 | */ 12 | export const readTestProgram = (dirname: string, name: string): string => { 13 | const path = resolve(dirname, name + ".txt"); 14 | return readFileSync(path, { encoding: "utf8" }).toString(); 15 | }; 16 | 17 | /** 18 | * Run code on language engine and return final result. 19 | * If error thrown, promise rejects with the error. 20 | * @param engine Engine to use for execution 21 | * @param code Source code to execute 22 | * @param input STDIN input for execution 23 | * @returns Final execution result object 24 | */ 25 | export const executeProgram = async ( 26 | engine: LanguageEngine, 27 | code: string, 28 | input: string = "" 29 | ): Promise<{ output: string; rendererState: T }> => { 30 | const controller = new ExecutionController(engine); 31 | controller.prepare(code, input); 32 | return new Promise(async (resolve, reject) => { 33 | try { 34 | let output = ""; 35 | const { error } = await controller.executeAll({ 36 | interval: 0, 37 | onResult: (res) => { 38 | if (res.output) output += res.output; 39 | if (res.nextStepLocation == null) { 40 | resolve({ output, rendererState: res.rendererState }); 41 | } 42 | }, 43 | }); 44 | if (error) reject(error); 45 | } catch (error) { 46 | reject(error); 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /languages/types.ts: -------------------------------------------------------------------------------- 1 | import monaco from "monaco-editor"; 2 | import React from "react"; 3 | 4 | /** 5 | * Type denoting a contiguous range of text in document. 6 | * All fields must be zero-indexed. 7 | */ 8 | export type DocumentRange = { 9 | /** Line number on which the range starts */ 10 | startLine: number; 11 | /** 12 | * Column number on which the range starts. 13 | * Omit to make the range start at the beginning of the line. 14 | */ 15 | startCol?: number; 16 | /** 17 | * Line number on which the range ends. 18 | * Omit to make the range end on the starting line. 19 | */ 20 | endLine?: number; 21 | /** 22 | * Column number on which the range ends. 23 | * Omit to make the range end at the end of the line. 24 | */ 25 | endCol?: number; 26 | }; 27 | 28 | /** Type denoting a document edit */ 29 | export type DocumentEdit = { 30 | /** Range to replace with the given text. Keep empty to insert text */ 31 | range: DocumentRange; 32 | /** Text to replace the given range with */ 33 | text: string; 34 | }; 35 | 36 | /** Source code token provider for the language, specific to Monaco */ 37 | export type MonacoTokensProvider = monaco.languages.IMonarchLanguage; 38 | 39 | /** Type alias for props passed to renderer */ 40 | export type RendererProps = { state: RS | null }; 41 | 42 | /** 43 | * Type alias for the result of engine executing a single step. 44 | */ 45 | export type StepExecutionResult = { 46 | /** New props to be passed to the renderer */ 47 | rendererState: RS; 48 | 49 | /** String to write to program output */ 50 | output?: string; 51 | 52 | /** Self-modifying programs: edit to apply on code */ 53 | codeEdits?: DocumentEdit[]; 54 | 55 | /** 56 | * Used to highlight next line to be executed in the editor. 57 | * Passing `null` indicates reaching the end of program. 58 | */ 59 | nextStepLocation: DocumentRange | null; 60 | 61 | /** Signal if execution has been paused */ 62 | signal?: "paused"; 63 | }; 64 | 65 | /** 66 | * Language engine is responsible for providing 67 | * execution and debugging API to the platform. 68 | */ 69 | export interface LanguageEngine { 70 | /** Validate the syntax of the given code. Throw ParseError if any */ 71 | validateCode: (code: string) => void; 72 | 73 | /** Load code and user input into the engine and prepare for execution */ 74 | prepare: (code: string, input: string) => void; 75 | 76 | /** Perform a single step of code execution */ 77 | executeStep: () => StepExecutionResult; 78 | 79 | /** Reset all state to prepare for new cycle */ 80 | resetState: () => void; 81 | } 82 | 83 | /** 84 | * Language provider provides all language-specific 85 | * functionality to the platform. 86 | */ 87 | export interface LanguageProvider { 88 | /** Monaco-specific tokenizer for syntax highlighting */ 89 | editorTokensProvider?: MonacoTokensProvider; 90 | 91 | /** Monaco-specific autocomplete provider */ 92 | autocompleteProvider?: any; 93 | 94 | /** Sample code sample for the language */ 95 | sampleProgram: string; 96 | 97 | /** React component for visualising runtime state */ 98 | Renderer: React.FC>; 99 | } 100 | -------------------------------------------------------------------------------- /languages/ui-utils.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from "react"; 2 | import { Colors } from "@blueprintjs/core"; 3 | import { useDarkMode } from "../ui/providers/dark-mode-provider"; 4 | 5 | const backgroundColorsLight = { 6 | success: Colors.GREEN5, 7 | danger: Colors.RED5, 8 | plain: Colors.LIGHT_GRAY1, 9 | active: Colors.GRAY4, 10 | }; 11 | 12 | const backgroundColorsDark = { 13 | success: Colors.GREEN1, 14 | danger: Colors.RED1, 15 | plain: Colors.DARK_GRAY5, 16 | active: Colors.GRAY1, 17 | }; 18 | 19 | const foregroundColorsLight = { 20 | success: Colors.GREEN1, 21 | danger: Colors.RED1, 22 | plain: Colors.DARK_GRAY1, 23 | active: Colors.DARK_GRAY1, 24 | }; 25 | 26 | const foregroundColorsDark = { 27 | success: Colors.GREEN5, 28 | danger: Colors.RED5, 29 | plain: Colors.LIGHT_GRAY5, 30 | active: Colors.LIGHT_GRAY5, 31 | }; 32 | 33 | /** 34 | * Utility component for rendering a simple general-purpose stylable box. Useful 35 | * for performance-critical components in visualisation renderers. 36 | */ 37 | export const Box = (props: { 38 | children: React.ReactNode; 39 | intent?: "plain" | "success" | "danger" | "active"; 40 | style?: CSSProperties; 41 | }) => { 42 | const { isDark } = useDarkMode(); 43 | const intent = props.intent == null ? "plain" : props.intent; 44 | const backgroundMap = isDark ? backgroundColorsDark : backgroundColorsLight; 45 | const foregroundMap = isDark ? foregroundColorsDark : foregroundColorsLight; 46 | 47 | return ( 48 | 59 | {props.children} 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /languages/worker-constants.ts: -------------------------------------------------------------------------------- 1 | import { StepExecutionResult } from "./types"; 2 | import * as E from "./worker-errors"; 3 | 4 | /** Types of requests the worker handles */ 5 | export type WorkerRequestData = 6 | | { 7 | type: "Init"; 8 | params?: null; 9 | } 10 | | { 11 | type: "Reset"; 12 | params?: null; 13 | } 14 | | { 15 | type: "Prepare"; 16 | params: { code: string; input: string }; 17 | } 18 | | { 19 | type: "UpdateBreakpoints"; 20 | params: { points: number[] }; 21 | } 22 | | { 23 | type: "ValidateCode"; 24 | params: { code: string }; 25 | } 26 | | { 27 | type: "Execute"; 28 | params: { interval: number }; 29 | } 30 | | { 31 | type: "ExecuteStep"; 32 | params?: null; 33 | } 34 | | { 35 | type: "Pause"; 36 | params?: null; 37 | }; 38 | 39 | /** Kinds of acknowledgement responses the worker can send */ 40 | export type WorkerAckType = 41 | | "init" // on initialization 42 | | "reset" // on state reset 43 | | "bp-update" // on updating breakpoints 44 | | "prepare" // on preparing for execution 45 | | "pause"; // on pausing execution 46 | 47 | /** Errors associated with each response ack type */ 48 | export type WorkerAckError = { 49 | init: undefined; 50 | reset: undefined; 51 | "bp-update": undefined; 52 | prepare: E.WorkerParseError; 53 | pause: undefined; 54 | }; 55 | 56 | /** Types of responses the worker can send */ 57 | export type WorkerResponseData = 58 | /** Ack for one-off requests, optionally containing error occured (if any) */ 59 | | { 60 | type: "ack"; 61 | data: A; 62 | error?: WorkerAckError[A]; 63 | } 64 | /** Result of code validation, containing parsing error (if any) */ 65 | | { 66 | type: "validate"; 67 | error?: E.WorkerParseError; 68 | } 69 | /** Response containing step execution result, and runtime error (if any) */ 70 | | { 71 | type: "result"; 72 | data: StepExecutionResult; 73 | error?: E.WorkerRuntimeError; 74 | } 75 | /** Response indicating a bug in worker/engine logic */ 76 | | { type: "error"; error: E.WorkerError }; 77 | -------------------------------------------------------------------------------- /languages/worker-errors.ts: -------------------------------------------------------------------------------- 1 | import { DocumentRange } from "./types"; 2 | 3 | /** 4 | * Special error class, to be thrown when encountering a 5 | * syntax error while parsing a program. 6 | */ 7 | export class ParseError extends Error { 8 | /** Location of syntax error in the program */ 9 | range: DocumentRange; 10 | 11 | /** 12 | * Create an instance of ParseError 13 | * @param message Error message 14 | * @param range Location of syntactically incorrect code 15 | */ 16 | constructor(message: string, range: DocumentRange) { 17 | super(message); 18 | this.name = "ParseError"; 19 | this.range = range; 20 | } 21 | } 22 | 23 | /** 24 | * Special error class, to be thrown when encountering an error 25 | * at runtime that is indicative of a bug in the user's program. 26 | */ 27 | export class RuntimeError extends Error { 28 | /** 29 | * Create an instance of RuntimeError 30 | * @param message Error message 31 | */ 32 | constructor(message: string) { 33 | super(message); 34 | this.name = "RuntimeError"; 35 | } 36 | } 37 | 38 | /** Check if an error object is instance of a ParseError */ 39 | export const isParseError = (error: any): error is ParseError => { 40 | return error instanceof ParseError || error.name === "ParseError"; 41 | }; 42 | 43 | /** Check if an error object is instance of a RuntimeError */ 44 | export const isRuntimeError = (error: any): error is RuntimeError => { 45 | return error instanceof RuntimeError || error.name === "RuntimeError"; 46 | }; 47 | 48 | /** 49 | * Error sent by worker in case of parsing error. 50 | * Not for use by language providers. 51 | */ 52 | export type WorkerParseError = { 53 | name: "ParseError"; 54 | message: string; 55 | range: DocumentRange; 56 | }; 57 | 58 | /** 59 | * Error sent by worker in case error at runtime. 60 | * Not for use by language providers. 61 | */ 62 | export type WorkerRuntimeError = { 63 | name: "RuntimeError"; 64 | message: string; 65 | }; 66 | 67 | /** 68 | * Error sent by worker indicating an implementation bug. 69 | * Not for use by language providers. 70 | */ 71 | export type WorkerError = { 72 | name: string; 73 | message: string; 74 | stack?: string; 75 | }; 76 | 77 | /** 78 | * Serialize a RuntimeError instance into a plain object. 79 | * Not for use by language providers. 80 | */ 81 | export const serializeRuntimeError = ( 82 | error: RuntimeError 83 | ): WorkerRuntimeError => { 84 | return { name: "RuntimeError", message: error.message }; 85 | }; 86 | 87 | /** 88 | * Serialize a ParseError instance into a plain object. 89 | * Not for use by language providers. 90 | */ 91 | export const serializeParseError = (error: ParseError): WorkerParseError => { 92 | return { name: "ParseError", message: error.message, range: error.range }; 93 | }; 94 | 95 | /** 96 | * Serialize an arbitrary error into a plain object. 97 | * Not for use by language providers. 98 | */ 99 | export const serializeError = (error: Error): WorkerError => { 100 | return { name: error.name, message: error.message, stack: error.stack }; 101 | }; 102 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esolang-park", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "start": "next start", 7 | "lint": "next lint", 8 | "test": "jest", 9 | "build": "yarn run build:worker && next build", 10 | "dev:worker": "webpack -c worker-pack/webpack.dev.js --watch", 11 | "build:worker": "webpack -c worker-pack/webpack.prod.js", 12 | "add-new-language": "node scripts/add-new-language.js" 13 | }, 14 | "dependencies": { 15 | "@blueprintjs/core": "^4.0.0-beta.12", 16 | "@monaco-editor/react": "^4.3.1", 17 | "chevrotain": "^10.0.0", 18 | "monaco-editor": "^0.30.1", 19 | "next": "12.0.7", 20 | "react": "17.0.2", 21 | "react-dom": "17.0.2", 22 | "react-mosaic-component": "^5.0.0", 23 | "sass": "^1.49.7" 24 | }, 25 | "devDependencies": { 26 | "@types/jest": "^27.4.0", 27 | "@types/node": "16.11.11", 28 | "@types/react": "17.0.37", 29 | "eslint": "8.4.0", 30 | "eslint-config-next": "12.0.7", 31 | "jest": "^27.4.7", 32 | "peggy": "^1.2.0", 33 | "ts-loader": "^9.2.6", 34 | "ts-node": "^10.5.0", 35 | "typescript": "4.5.2", 36 | "webpack": "^5.65.0", 37 | "webpack-cli": "^4.9.1", 38 | "webpack-merge": "^5.8.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../styles/globals.css"; 3 | import "../styles/editor.css"; 4 | import "../styles/mosaic.scss"; 5 | import "@blueprintjs/core/lib/css/blueprint.css"; 6 | import "@blueprintjs/icons/lib/css/blueprint-icons.css"; 7 | import "react-mosaic-component/react-mosaic-component.css"; 8 | import type { AppProps } from "next/app"; 9 | import { Providers } from "../ui/providers"; 10 | import { NextPage } from "next"; 11 | 12 | /** Type for pages that use a custom layout */ 13 | export type NextPageWithLayout = NextPage & { 14 | getLayout?: (page: React.ReactNode) => React.ReactNode; 15 | }; 16 | 17 | /** AppProps type but extended for custom layouts */ 18 | type AppPropsWithLayout = AppProps & { 19 | Component: NextPageWithLayout; 20 | }; 21 | 22 | function MyApp({ Component, pageProps }: AppPropsWithLayout) { 23 | // Use the layout defined at the page level, if available 24 | const getLayout = 25 | Component.getLayout ?? ((page) => {page}); 26 | return getLayout(); 27 | } 28 | 29 | export default MyApp; 30 | -------------------------------------------------------------------------------- /pages/ide/befunge93.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPage } from "next"; 3 | import Head from "next/head"; 4 | import { Mainframe } from "../../ui/Mainframe"; 5 | import LangProvider from "../../languages/befunge93"; 6 | const LANG_ID = "befunge93"; 7 | const LANG_NAME = "Befunge-93"; 8 | 9 | const IDE: NextPage = () => { 10 | return ( 11 | <> 12 | 13 | {LANG_NAME} | Esolang Park 14 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default IDE; 25 | -------------------------------------------------------------------------------- /pages/ide/brainfuck.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPage } from "next"; 3 | import Head from "next/head"; 4 | import { Mainframe } from "../../ui/Mainframe"; 5 | import LangProvider from "../../languages/brainfuck"; 6 | const LANG_ID = "brainfuck"; 7 | const LANG_NAME = "Brainfuck"; 8 | 9 | const IDE: NextPage = () => { 10 | return ( 11 | <> 12 | 13 | {LANG_NAME} | Esolang Park 14 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default IDE; 25 | -------------------------------------------------------------------------------- /pages/ide/chef.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPage } from "next"; 3 | import Head from "next/head"; 4 | import { Mainframe } from "../../ui/Mainframe"; 5 | import LangProvider from "../../languages/chef"; 6 | const LANG_ID = "chef"; 7 | const LANG_NAME = "Chef"; 8 | 9 | const IDE: NextPage = () => { 10 | return ( 11 | <> 12 | 13 | {LANG_NAME} | Esolang Park 14 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default IDE; 25 | -------------------------------------------------------------------------------- /pages/ide/deadfish.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPage } from "next"; 3 | import Head from "next/head"; 4 | import { Mainframe } from "../../ui/Mainframe"; 5 | import LangProvider from "../../languages/deadfish"; 6 | const LANG_ID = "deadfish"; 7 | const LANG_NAME = "Deadfish"; 8 | 9 | const IDE: NextPage = () => { 10 | return ( 11 | <> 12 | 13 | {LANG_NAME} | Esolang Park 14 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default IDE; 25 | -------------------------------------------------------------------------------- /pages/ide/shakespeare.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPage } from "next"; 3 | import Head from "next/head"; 4 | import { Mainframe } from "../../ui/Mainframe"; 5 | import LangProvider from "../../languages/shakespeare"; 6 | const LANG_ID = "shakespeare"; 7 | const LANG_NAME = "Shakespeare"; 8 | 9 | const IDE: NextPage = () => { 10 | return ( 11 | <> 12 | 13 | {LANG_NAME} | Esolang Park 14 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default IDE; 25 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPageWithLayout } from "./_app"; 3 | import Head from "next/head"; 4 | import logoImg from "../ui/assets/logo.png"; 5 | import Image from "next/image"; 6 | import { Button, Card, Colors, Icon, Text } from "@blueprintjs/core"; 7 | import Link from "next/link"; 8 | import { useDarkMode } from "../ui/providers/dark-mode-provider"; 9 | import LANGUAGES from "./languages.json"; 10 | import { GitHubIcon } from "../ui/custom-icons"; 11 | import { Providers } from "../ui/providers"; 12 | 13 | const REPO_URL = "https://github.com/nilaymaj/esolang-park"; 14 | const WIKI_URL = REPO_URL + "/wiki"; 15 | const GUIDE_URL = REPO_URL + "/wiki/LP-Getting-Started"; 16 | const ISSUE_URL = REPO_URL + "/issues/new"; 17 | 18 | const styles = { 19 | topPanel: { 20 | position: "absolute" as "absolute", 21 | top: 0, 22 | right: 0, 23 | padding: 10, 24 | }, 25 | rootContainer: { 26 | height: "100%", 27 | display: "flex", 28 | flexDirection: "column" as "column", 29 | alignItems: "center", 30 | justifyContent: "center", 31 | padding: "10%", 32 | textAlign: "center" as "center", 33 | }, 34 | headerContainer: { 35 | display: "flex", 36 | alignItems: "center", 37 | }, 38 | langsContainer: { 39 | marginTop: 30, 40 | width: "100%", 41 | display: "flex", 42 | flexWrap: "wrap" as "wrap", 43 | alignContent: "flex-start", 44 | justifyContent: "center", 45 | }, 46 | langCard: { 47 | minWidth: 200, 48 | textAlign: "center" as "center", 49 | margin: 20, 50 | padding: "30px 0", 51 | }, 52 | }; 53 | 54 | const Index: NextPageWithLayout = () => { 55 | const DarkMode = useDarkMode(); 56 | const backgroundColor = DarkMode.isDark ? Colors.DARK_GRAY3 : Colors.WHITE; 57 | 58 | return ( 59 | <> 60 | 61 | Esolang Park 62 | 63 | {/* Buttons in the top-right */} 64 |
65 |
78 | {/* Container for center content */} 79 |
80 | {/* Project heading */} 81 |
82 |
83 | logo 84 |
85 | 86 |

Esolang Park

87 |
88 |
89 | 90 |

An online visual debugger for esoteric languages

91 |
92 | {/* Language cards */} 93 |
94 | {LANGUAGES.map(({ display, id }) => ( 95 | 96 | 97 | 98 | {display} 99 | 100 | 101 | 102 | ))} 103 |
104 | {/* "More esolangs" section */} 105 | 106 |

107 | Need support for your favorite esolang? Submit an{" "} 108 | issue on GitHub (or{" "} 109 | implement it yourself!) 110 |

111 |
112 |
113 | 114 | ); 115 | }; 116 | 117 | // Feature guide should not be shown on the home page 118 | Index.getLayout = function getLayout(page: React.ReactNode) { 119 | return {page}; 120 | }; 121 | 122 | export default Index; 123 | -------------------------------------------------------------------------------- /pages/languages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "display": "Befunge-93", 4 | "id": "befunge93" 5 | }, 6 | { 7 | "display": "Brainfuck", 8 | "id": "brainfuck" 9 | }, 10 | { 11 | "display": "Chef", 12 | "id": "chef" 13 | }, 14 | { 15 | "display": "Deadfish", 16 | "id": "deadfish" 17 | }, 18 | { 19 | "display": "Shakespeare", 20 | "id": "shakespeare" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaymaj/esolang-park/5d4921432a4a6322450223aea244c5c60e2b9fd9/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /scripts/add-new-language.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const args = process.argv; 5 | 6 | const langId = args[2]; 7 | const langName = args[3]; 8 | if (!langId || !langName) { 9 | console.log( 10 | `Usage: npm run add-new-language 11 | yarn run add-new-language ` 12 | ); 13 | process.exit(1); 14 | } 15 | 16 | // Check if language provider directory already exists 17 | const dir = path.resolve(__dirname, "../languages/", langId); 18 | if (fs.existsSync(dir)) { 19 | console.log(`Language ID '${langId}' already exists.`); 20 | process.exit(0); 21 | } 22 | fs.mkdirSync(dir); 23 | 24 | /** 25 | * Remove the "// @ts-nocheck" line at the top of given file contents. 26 | * Returns null if file doesn't start with the comment. 27 | * @param {string} contents String containing contents, lines separated by "\n" 28 | * @returns Contents with first line removed 29 | */ 30 | const cropFirstLine = (contents) => { 31 | const lines = contents.split("\n"); 32 | const firstLine = lines.shift(); 33 | if (firstLine !== "// @ts-nocheck") return null; 34 | else return lines.join("\n"); 35 | }; 36 | 37 | /** 38 | * Copy a file from source path to destination. 39 | * Also removes first line from file, which contains the "@ts-nocheck" comment 40 | * @param {string} src Absolute path to source file 41 | * @param {string} dest Absolute path to destination 42 | */ 43 | const copyFile = (src, dest) => { 44 | const rawContents = fs.readFileSync(src).toString(); 45 | const destContents = cropFirstLine(rawContents); 46 | if (!destContents) { 47 | console.error(`Template file '${src}' doesn't have @ts-nocheck comment`); 48 | process.exit(1); 49 | } 50 | fs.writeFileSync(dest, destContents); 51 | }; 52 | 53 | { 54 | // Copy language provider template files (except the README) 55 | ["index.ts", "common.ts", "runtime.ts", "engine.ts", "renderer.tsx"].forEach( 56 | (filename) => { 57 | const srcPath = path.resolve(__dirname, `new-lang-template/${filename}`); 58 | const destPath = path.resolve(dir, filename); 59 | copyFile(srcPath, destPath); 60 | } 61 | ); 62 | } 63 | 64 | { 65 | // Copy the language provider README file 66 | const src = path.resolve(__dirname, "./new-lang-template/README.md"); 67 | const dest = path.resolve(dir, "README.md"); 68 | const contents = fs.readFileSync(src).toString(); 69 | const finalContents = contents.replace("$LANG_NAME", langName); 70 | fs.writeFileSync(dest, finalContents); 71 | } 72 | 73 | { 74 | // Generate Next.js page 75 | const src = path.resolve(__dirname, "./new-lang-template/ide-page.tsx"); 76 | const dest = path.resolve(__dirname, `../pages/ide/${langId}.tsx`); 77 | const contents = cropFirstLine(fs.readFileSync(src).toString()); 78 | const finalContents = contents 79 | .replace(/\$LANG_ID/g, langId) 80 | .replace(/\$LANG_NAME/g, langName); 81 | fs.writeFileSync(dest, finalContents); 82 | } 83 | 84 | { 85 | // Add entry to `pages/languages.json` 86 | const jsonPath = path.resolve(__dirname, "../pages/languages.json"); 87 | const contents = JSON.parse(fs.readFileSync(jsonPath).toString()); 88 | if (!Array.isArray(contents)) { 89 | console.error("languages.json is malformed, please check its contents"); 90 | process.exit(1); 91 | } 92 | const existingIdx = contents.findIndex((c) => c.id === langId); 93 | if (existingIdx !== -1) { 94 | console.error("languages.json already contains entry."); 95 | process.exit(1); 96 | } 97 | const newContents = [...contents, { display: langName, id: langId }]; 98 | fs.writeFileSync(jsonPath, JSON.stringify(newContents, undefined, 2)); 99 | } 100 | 101 | // Print success message 102 | console.log(`Done! Created files for language '${langId}'`); 103 | -------------------------------------------------------------------------------- /scripts/new-lang-template/README.md: -------------------------------------------------------------------------------- 1 | # $LANG_NAME 2 | 3 | Add a short summary about the language, including link(s) to the language specification and sample programs. 4 | 5 | ## Notes for the user 6 | 7 | - Add details and quirks (if any) about the Esolang Park implementation of this esolang. 8 | - These are details that the user may want to know while executing or debugging programs on the editor. 9 | 10 | ## Implementation details 11 | 12 | - Add notes that a contributor may want to read while looking at the source code of the language provider. 13 | - This includes any third-party libraries you've used, something you've done for performance, or just the 14 | main decisions you took while implementing the language provider. 15 | - If the source code is simple enough, you can simply omit this section. 16 | 17 | ## Possible improvements 18 | 19 | - Add any bugs or suboptimalities in the language provider that a contributor can work on. 20 | - (this section doesn't mean you can add a half-baked interpreter and leave, though.) 21 | -------------------------------------------------------------------------------- /scripts/new-lang-template/common.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { MonacoTokensProvider } from "../types"; 3 | 4 | /** Type of props passed to renderer */ 5 | export type RS = { 6 | value: number; 7 | }; 8 | 9 | /** Sample program */ 10 | export const sampleProgram = `Program line 1 11 | Program line 2 12 | Program line 3`; 13 | 14 | /** Syntax highlighting */ 15 | export const editorTokensProvider: MonacoTokensProvider = { 16 | tokenizer: { 17 | root: [ 18 | [/i/, "variable"], 19 | [/d/, "keyword"], 20 | [/s/, "constant"], 21 | [/o/, "operator"], 22 | ], 23 | }, 24 | defaultToken: "comment", 25 | }; 26 | -------------------------------------------------------------------------------- /scripts/new-lang-template/engine.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { setupWorker } from "../setup-worker"; 3 | import XYZLanguageEngine from "./runtime"; 4 | 5 | setupWorker(new XYZLanguageEngine()); 6 | -------------------------------------------------------------------------------- /scripts/new-lang-template/ide-page.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React from "react"; 3 | import { NextPage } from "next"; 4 | import Head from "next/head"; 5 | import { Mainframe } from "../../ui/Mainframe"; 6 | import LangProvider from "../../languages/$LANG_ID"; 7 | const LANG_ID = "$LANG_ID"; 8 | const LANG_NAME = "$LANG_NAME"; 9 | 10 | const IDE: NextPage = () => { 11 | return ( 12 | <> 13 | 14 | {LANG_NAME} | Esolang Park 15 | 16 | 21 | 22 | ); 23 | }; 24 | 25 | export default IDE; 26 | -------------------------------------------------------------------------------- /scripts/new-lang-template/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { Renderer } from "./renderer"; 3 | import { LanguageProvider } from "../types"; 4 | import { RS, sampleProgram, editorTokensProvider } from "./common"; 5 | 6 | const provider: LanguageProvider = { 7 | Renderer, 8 | sampleProgram, 9 | editorTokensProvider, 10 | }; 11 | 12 | export default provider; 13 | -------------------------------------------------------------------------------- /scripts/new-lang-template/renderer.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { RendererProps } from "../types"; 3 | import { RS } from "./common"; 4 | 5 | export const Renderer = ({ state }: RendererProps) => { 6 | return state == null ? null :

state.value

; 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/new-lang-template/runtime.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { LanguageEngine, StepExecutionResult } from "../types"; 3 | import { RS } from "./common"; 4 | 5 | export default class XYZLanguageEngine implements LanguageEngine { 6 | resetState() { 7 | // TODO: Unimplemented 8 | } 9 | 10 | validateCode(code: string) { 11 | // TODO: Unimplemented 12 | } 13 | 14 | prepare(code: string, input: string) { 15 | // TODO: Unimplemented 16 | } 17 | 18 | executeStep(): StepExecutionResult { 19 | // TODO: Unimplemented 20 | return { rendererState: { value: 0 }, nextStepLocation: { startLine: 0 } }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /styles/editor.css: -------------------------------------------------------------------------------- 1 | .code-highlight { 2 | background-color: #ffff0055; 3 | } 4 | 5 | .breakpoint-glyph { 6 | box-sizing: border-box; 7 | padding: 6%; /* Make the dot smaller in size */ 8 | margin-top: 2px; /* Fix dot appearing slightly above baseline */ 9 | border-radius: 50%; 10 | background-clip: content-box; 11 | } 12 | 13 | .breakpoint-glyph.solid { 14 | background-color: #ff5555 !important; 15 | } 16 | 17 | .breakpoint-glyph.hint { 18 | background-color: #ff555555; 19 | } 20 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "@blueprintjs/core/lib/css/blueprint.css"; 2 | @import "@blueprintjs/icons/lib/css/blueprint-icons.css"; 3 | @import "react-mosaic-component/react-mosaic-component.css"; 4 | 5 | html, 6 | body, 7 | #__next { 8 | padding: 0; 9 | margin: 0; 10 | width: 100%; 11 | height: 100vh; 12 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 13 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 14 | } 15 | 16 | a { 17 | color: inherit; 18 | text-decoration: none; 19 | } 20 | 21 | * { 22 | box-sizing: border-box; 23 | } 24 | 25 | .esolang-notes-hint { 26 | transition: all 0.1s; 27 | transform-origin: 100% 50%; 28 | } 29 | 30 | .esolang-notes-hint.hide { 31 | transform: scaleX(0); 32 | } 33 | -------------------------------------------------------------------------------- /styles/mosaic.scss: -------------------------------------------------------------------------------- 1 | @import "../node_modules/@blueprintjs/core/lib/scss/variables.scss"; 2 | 3 | .mosaic.mosaic-custom-theme { 4 | background: $gray4; 5 | 6 | .mosaic-zero-state { 7 | background: $light-gray3; 8 | border-radius: $pt-border-radius; 9 | box-shadow: $pt-elevation-shadow-0; 10 | 11 | .default-zero-state-icon { 12 | font-size: 120px; 13 | } 14 | } 15 | 16 | .mosaic-split:hover { 17 | background: none; 18 | .mosaic-split-line { 19 | box-shadow: 0 0 0 1px $blue4; 20 | } 21 | } 22 | 23 | &.mosaic-drop-target, 24 | .mosaic-drop-target { 25 | .drop-target-container .drop-target { 26 | background: fade($blue5, 20%); 27 | border: 2px solid $blue4; 28 | transition: opacity 100ms; 29 | border-radius: $pt-border-radius; 30 | } 31 | } 32 | 33 | .mosaic-window, 34 | .mosaic-preview { 35 | box-shadow: $pt-elevation-shadow-0; 36 | border-radius: $pt-border-radius; 37 | 38 | .mosaic-window-toolbar { 39 | box-shadow: 0 1px 1px $pt-divider-black; 40 | border-top-right-radius: $pt-border-radius; 41 | border-top-left-radius: $pt-border-radius; 42 | 43 | &.draggable:hover { 44 | .mosaic-window-title { 45 | color: $black; 46 | } 47 | background: linear-gradient(to bottom, $white, $light-gray5); 48 | } 49 | } 50 | 51 | .mosaic-window-title { 52 | font-weight: 600; 53 | color: $dark-gray5; 54 | } 55 | 56 | .mosaic-window-controls { 57 | .separator { 58 | border-left: 1px solid $light-gray2; 59 | } 60 | .bp4-button { 61 | &, 62 | &:before { 63 | color: $gray2; 64 | } 65 | } 66 | } 67 | 68 | .default-preview-icon { 69 | font-size: 72px; 70 | } 71 | 72 | .mosaic-window-body { 73 | border-top-width: 0; 74 | background: $pt-app-background-color; 75 | border-bottom-right-radius: $pt-border-radius; 76 | border-bottom-left-radius: $pt-border-radius; 77 | } 78 | 79 | .mosaic-window-additional-actions-bar { 80 | transition: height 250ms; 81 | box-shadow: 0 1px 1px $pt-divider-black; 82 | 83 | .bp4-button { 84 | &, 85 | &:before { 86 | color: $gray2; 87 | } 88 | } 89 | } 90 | 91 | &.additional-controls-open { 92 | .mosaic-window-toolbar { 93 | box-shadow: 0 1px 0 $pt-elevation-shadow-0; 94 | } 95 | } 96 | 97 | .mosaic-preview { 98 | border: 1px solid $gray3; 99 | 100 | h4 { 101 | color: $dark-gray5; 102 | } 103 | } 104 | } 105 | 106 | &.bp4-dark { 107 | background: $dark-gray2; 108 | 109 | .mosaic-zero-state { 110 | background: $dark-gray4; 111 | box-shadow: $pt-dark-elevation-shadow-0; 112 | } 113 | 114 | .mosaic-split:hover .mosaic-split-line { 115 | box-shadow: 0 0 0 1px $blue3; 116 | } 117 | 118 | &.mosaic-drop-target, 119 | .mosaic-drop-target { 120 | .drop-target-container .drop-target { 121 | background: fade($blue2, 20%); 122 | border-color: $blue3; 123 | } 124 | } 125 | 126 | .mosaic-window-toolbar, 127 | .mosaic-window-additional-actions-bar { 128 | background: $dark-gray4; 129 | box-shadow: 0 1px 1px $pt-dark-divider-black; 130 | } 131 | 132 | .mosaic-window, 133 | .mosaic-preview { 134 | box-shadow: $pt-dark-elevation-shadow-0; 135 | 136 | .mosaic-window-toolbar.draggable:hover { 137 | .mosaic-window-title { 138 | color: $white; 139 | } 140 | background: linear-gradient(to bottom, $dark-gray5, $dark-gray4); 141 | } 142 | 143 | .mosaic-window-title { 144 | color: $light-gray2; 145 | } 146 | 147 | .mosaic-window-controls { 148 | .separator { 149 | border-color: $gray1; 150 | } 151 | .bp4-button { 152 | &, 153 | &:before { 154 | color: $gray4; 155 | } 156 | } 157 | } 158 | 159 | .mosaic-window-body { 160 | background: $pt-dark-app-background-color; 161 | } 162 | 163 | .mosaic-window-additional-actions-bar { 164 | .bp4-button { 165 | &, 166 | &:before { 167 | color: $gray5; 168 | } 169 | } 170 | } 171 | 172 | &.additional-controls-open { 173 | .mosaic-window-toolbar { 174 | box-shadow: $pt-dark-elevation-shadow-0; 175 | } 176 | } 177 | 178 | .mosaic-preview { 179 | border-color: $gray1; 180 | 181 | h4 { 182 | color: $light-gray4; 183 | } 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules", "scripts/*/*.ts", "scripts/*/*.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /ui/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Mosaic, MosaicNode, MosaicWindow } from "react-mosaic-component"; 3 | import { Header } from "./header"; 4 | import { useDarkMode } from "./providers/dark-mode-provider"; 5 | 6 | // IDs of windows in the mosaic layout 7 | type WINDOW_ID = "editor" | "renderer" | "input" | "output"; 8 | 9 | const WindowTitles = { 10 | editor: "Code Editor", 11 | renderer: "Visualization", 12 | input: "User Input", 13 | output: "Execution Output", 14 | }; 15 | 16 | /** 17 | * Default layout of the mosaic. This must be defined outside of 18 | * the React component to persist layout adjustments by the user. 19 | */ 20 | const INITIAL_LAYOUT: MosaicNode = { 21 | direction: "row", 22 | first: "editor", 23 | second: { 24 | direction: "column", 25 | first: "renderer", 26 | second: { 27 | direction: "row", 28 | first: "input", 29 | second: "output", 30 | }, 31 | }, 32 | }; 33 | 34 | type Props = { 35 | langId: string; 36 | langName: string; 37 | renderEditor: () => React.ReactNode; 38 | renderRenderer: () => React.ReactNode; 39 | renderInput: () => React.ReactNode; 40 | renderOutput: () => React.ReactNode; 41 | renderExecControls: () => React.ReactNode; 42 | }; 43 | 44 | export const MainLayout = (props: Props) => { 45 | const { isDark } = useDarkMode(); 46 | const mosaicClass = "mosaic-custom-theme" + (isDark ? " bp4-dark" : ""); 47 | 48 | const MOSAIC_MAP = { 49 | editor: props.renderEditor, 50 | renderer: props.renderRenderer, 51 | input: props.renderInput, 52 | output: props.renderOutput, 53 | }; 54 | 55 | return ( 56 |
57 |
62 |
63 | 64 | className={mosaicClass} 65 | initialValue={INITIAL_LAYOUT} 66 | renderTile={(windowId, path) => ( 67 | 68 | path={path} 69 | title={WindowTitles[windowId]} 70 | toolbarControls={} 71 | > 72 | {MOSAIC_MAP[windowId]()} 73 | 74 | )} 75 | /> 76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /ui/Mainframe.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CodeEditor, CodeEditorRef } from "../ui/code-editor"; 3 | import { InputEditor, InputEditorRef } from "../ui/input-editor"; 4 | import { MainLayout } from "../ui/MainLayout"; 5 | import { useExecController } from "../ui/use-exec-controller"; 6 | import { LanguageProvider, StepExecutionResult } from "../languages/types"; 7 | import { OutputViewer, OutputViewerRef } from "../ui/output-viewer"; 8 | import { ExecutionControls } from "./execution-controls"; 9 | import { RendererRef, RendererWrapper } from "./renderer-wrapper"; 10 | import { WorkerRuntimeError } from "../languages/worker-errors"; 11 | 12 | type Props = { 13 | langId: string; 14 | langName: string; 15 | provider: LanguageProvider; 16 | }; 17 | 18 | /** 19 | * React component that contains and controls the entire IDE. 20 | * 21 | * For performance reasons, Mainframe makes spare use of state hooks. This 22 | * component is rather expensive to render, and will block the main thread on 23 | * small execution intervals if rendered on every execution. All state management 24 | * is delegated to imperatively controlled child components. 25 | */ 26 | export const Mainframe = (props: Props) => { 27 | // Language provider and engine 28 | const providerRef = React.useRef(props.provider); 29 | const execController = useExecController(props.langId); 30 | 31 | // Refs for controlling UI components 32 | const codeEditorRef = React.useRef(null); 33 | const inputEditorRef = React.useRef(null); 34 | const outputEditorRef = React.useRef(null); 35 | const rendererRef = React.useRef>(null); 36 | 37 | // Interval of execution 38 | const [execInterval, setExecInterval] = React.useState(20); 39 | 40 | /** Utility that updates UI with the provided execution result */ 41 | const updateWithResult = ( 42 | result: StepExecutionResult, 43 | error?: WorkerRuntimeError 44 | ) => { 45 | rendererRef.current!.updateState(result.rendererState); 46 | codeEditorRef.current!.updateHighlights(result.nextStepLocation); 47 | outputEditorRef.current!.append(result.output); 48 | 49 | // Self-modifying programs: update code 50 | if (result.codeEdits != null) 51 | codeEditorRef.current!.editCode(result.codeEdits); 52 | 53 | // End of program: reset code to original version 54 | if (!result.nextStepLocation) codeEditorRef.current!.endExecutionMode(); 55 | 56 | // RuntimeError: print error to output 57 | if (error) outputEditorRef.current!.setError(error); 58 | }; 59 | 60 | /** Reset and begin a new execution */ 61 | const runProgram = async () => { 62 | // Check if controller is free for execution 63 | const readyStates = ["empty", "done"]; 64 | if (!readyStates.includes(execController.state)) { 65 | console.error(`Controller not ready: state is ${execController.state}`); 66 | return; 67 | } 68 | 69 | // Reset any existing execution state 70 | outputEditorRef.current!.reset(); 71 | await execController.resetState(); 72 | const error = await execController.prepare( 73 | codeEditorRef.current!.getCode(), 74 | inputEditorRef.current!.getValue() 75 | ); 76 | 77 | // Check for ParseError, else begin execution 78 | if (error) outputEditorRef.current!.setError(error); 79 | else { 80 | codeEditorRef.current!.startExecutionMode(); 81 | await execController.execute(updateWithResult, execInterval); 82 | } 83 | }; 84 | 85 | /** Pause the ongoing execution */ 86 | const pauseExecution = async () => { 87 | // Check if controller is indeed executing code 88 | if (execController.state !== "processing") { 89 | console.error("Controller not processing any code"); 90 | return; 91 | } 92 | await execController.pauseExecution(); 93 | }; 94 | 95 | /** Run a single step of execution */ 96 | const executeStep = async () => { 97 | // Check if controller is paused 98 | if (execController.state !== "paused") { 99 | console.error("Controller not paused"); 100 | return; 101 | } 102 | 103 | // Run and update execution states 104 | const response = await execController.executeStep(); 105 | updateWithResult(response.result, response.error); 106 | }; 107 | 108 | /** Resume the currently paused execution */ 109 | const resumeExecution = async () => { 110 | // Check if controller is indeed paused 111 | if (execController.state !== "paused") { 112 | console.error("Controller is not paused"); 113 | return; 114 | } 115 | 116 | // Begin execution 117 | await execController.execute(updateWithResult, execInterval); 118 | }; 119 | 120 | /** Stop the currently active execution */ 121 | const stopExecution = async () => { 122 | // Check if controller has execution 123 | if (!["paused", "processing", "error"].includes(execController.state)) { 124 | console.error("No active execution in controller"); 125 | return; 126 | } 127 | 128 | // If currently processing, pause execution loop first 129 | if (execController.state === "processing") 130 | await execController.pauseExecution(); 131 | 132 | // Reset all execution states 133 | await execController.resetState(); 134 | rendererRef.current!.updateState(null); 135 | codeEditorRef.current!.updateHighlights(null); 136 | codeEditorRef.current!.endExecutionMode(); 137 | }; 138 | 139 | /** Translate execution controller state to debug controls state */ 140 | const getDebugState = () => { 141 | const currState = execController.state; 142 | if (currState === "processing") return "running"; 143 | else if (currState === "paused") return "paused"; 144 | else if (currState === "error") return "error"; 145 | else return "off"; 146 | }; 147 | 148 | return ( 149 | ( 153 | 160 | execController.updateBreakpoints(newPoints) 161 | } 162 | /> 163 | )} 164 | renderRenderer={() => ( 165 | 169 | )} 170 | renderInput={() => ( 171 | 175 | )} 176 | renderOutput={() => } 177 | renderExecControls={() => ( 178 | 187 | )} 188 | /> 189 | ); 190 | }; 191 | -------------------------------------------------------------------------------- /ui/assets/guide-breakpoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaymaj/esolang-park/5d4921432a4a6322450223aea244c5c60e2b9fd9/ui/assets/guide-breakpoints.png -------------------------------------------------------------------------------- /ui/assets/guide-exec-controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaymaj/esolang-park/5d4921432a4a6322450223aea244c5c60e2b9fd9/ui/assets/guide-exec-controls.png -------------------------------------------------------------------------------- /ui/assets/guide-info-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaymaj/esolang-park/5d4921432a4a6322450223aea244c5c60e2b9fd9/ui/assets/guide-info-btn.png -------------------------------------------------------------------------------- /ui/assets/guide-syntax-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaymaj/esolang-park/5d4921432a4a6322450223aea244c5c60e2b9fd9/ui/assets/guide-syntax-check.png -------------------------------------------------------------------------------- /ui/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaymaj/esolang-park/5d4921432a4a6322450223aea244c5c60e2b9fd9/ui/assets/logo.png -------------------------------------------------------------------------------- /ui/code-editor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Editor from "@monaco-editor/react"; 3 | import { useEditorLanguageConfig } from "./use-editor-lang-config"; 4 | import { 5 | DocumentEdit, 6 | DocumentRange, 7 | MonacoTokensProvider, 8 | } from "../../languages/types"; 9 | import { 10 | createHighlightRange, 11 | createMonacoDocEdit, 12 | EditorInstance, 13 | MonacoInstance, 14 | } from "./monaco-utils"; 15 | import { useEditorBreakpoints } from "./use-editor-breakpoints"; 16 | import darkTheme from "./themes/dark.json"; 17 | import lightTheme from "./themes/light.json"; 18 | import { useDarkMode } from "../providers/dark-mode-provider"; 19 | import { WorkerParseError } from "../../languages/worker-errors"; 20 | import { useCodeValidator } from "./use-code-validator"; 21 | 22 | /** Keeps track of user's original program and modifications done to it */ 23 | type ChangeTrackerValue = { 24 | /** The user's original program code */ 25 | original: string; 26 | /** Tracks if code was modified during execution */ 27 | changed: boolean; 28 | }; 29 | 30 | // Interface for interacting with the editor 31 | export interface CodeEditorRef { 32 | /** Get the current text content of the editor */ 33 | getCode: () => string; 34 | /** Update value of code */ 35 | editCode: (edits: DocumentEdit[]) => void; 36 | /** Update code highlights */ 37 | updateHighlights: (highlights: DocumentRange | null) => void; 38 | /** Start execution mode - readonly editor with modifyable contents */ 39 | startExecutionMode: () => void; 40 | /** End execution mode - reset contents and readonly state */ 41 | endExecutionMode: () => void; 42 | } 43 | 44 | type Props = { 45 | /** ID of the active language */ 46 | languageId: string; 47 | /** Default code to display in editor */ 48 | defaultValue: string; 49 | /** Tokens provider for the language */ 50 | tokensProvider?: MonacoTokensProvider; 51 | /** Callback to validate code syntax */ 52 | onValidateCode: (code: string) => Promise; 53 | /** Callback to update debugging breakpoints */ 54 | onUpdateBreakpoints: (newBreakpoints: number[]) => void; 55 | }; 56 | 57 | /** 58 | * Wrapper around the Monaco editor that reveals 59 | * only the required functionality to the parent container. 60 | */ 61 | const CodeEditorComponent = (props: Props, ref: React.Ref) => { 62 | const [editor, setEditor] = React.useState(null); 63 | const [monaco, setMonaco] = React.useState(null); 64 | const [readOnly, setReadOnly] = React.useState(false); 65 | const highlightRange = React.useRef([]); 66 | const { isDark } = useDarkMode(); 67 | 68 | // Code modification tracker, used in execution mode 69 | const changeTracker = React.useRef({ 70 | original: "", 71 | changed: false, 72 | }); 73 | 74 | // Breakpoints 75 | useEditorBreakpoints({ 76 | editor, 77 | monaco, 78 | onUpdateBreakpoints: props.onUpdateBreakpoints, 79 | }); 80 | 81 | // Language config 82 | useEditorLanguageConfig({ 83 | languageId: props.languageId, 84 | tokensProvider: props.tokensProvider, 85 | }); 86 | 87 | // Code validation 88 | useCodeValidator({ 89 | editor, 90 | monaco, 91 | onValidateCode: props.onValidateCode, 92 | }); 93 | 94 | /** Update code highlights */ 95 | const updateHighlights = React.useCallback( 96 | (hl: DocumentRange | null) => { 97 | if (!editor) return; 98 | 99 | // Remove previous highlights 100 | const prevRange = highlightRange.current; 101 | editor.deltaDecorations(prevRange, []); 102 | 103 | // Add new highlights 104 | if (!hl) return; 105 | const newRange = createHighlightRange(monaco!, hl); 106 | const rangeStr = editor.deltaDecorations([], [newRange]); 107 | highlightRange.current = rangeStr; 108 | }, 109 | [editor] 110 | ); 111 | 112 | // Provide handle to parent for accessing editor contents 113 | React.useImperativeHandle( 114 | ref, 115 | () => ({ 116 | getCode: () => editor!.getValue(), 117 | editCode: (edits) => { 118 | changeTracker.current.changed = true; 119 | const monacoEdits = edits.map(createMonacoDocEdit); 120 | editor!.getModel()!.applyEdits(monacoEdits); 121 | }, 122 | updateHighlights, 123 | startExecutionMode: () => { 124 | changeTracker.current.original = editor!.getValue(); 125 | changeTracker.current.changed = false; 126 | setReadOnly(true); 127 | }, 128 | endExecutionMode: () => { 129 | setReadOnly(false); 130 | if (changeTracker.current.changed) { 131 | editor!.getModel()!.setValue(changeTracker.current.original); 132 | changeTracker.current.changed = false; 133 | } 134 | }, 135 | }), 136 | [editor] 137 | ); 138 | 139 | return ( 140 | { 145 | monaco.editor.defineTheme("ep-dark", darkTheme as any); 146 | monaco.editor.defineTheme("ep-light", lightTheme as any); 147 | }} 148 | onMount={(editor, monaco) => { 149 | if (!editor || !monaco) throw new Error("Error in initializing editor"); 150 | setEditor(editor); 151 | setMonaco(monaco); 152 | }} 153 | options={{ 154 | minimap: { enabled: false }, 155 | glyphMargin: true, 156 | readOnly: readOnly, 157 | // Self-modifying programs may add control characters to the code. 158 | // This option ensures such characters are properly displayed. 159 | renderControlCharacters: true, 160 | fixedOverflowWidgets: true, 161 | }} 162 | /> 163 | ); 164 | }; 165 | 166 | export const CodeEditor = React.forwardRef(CodeEditorComponent); 167 | -------------------------------------------------------------------------------- /ui/code-editor/monaco-utils.ts: -------------------------------------------------------------------------------- 1 | import monaco from "monaco-editor"; 2 | import { DocumentEdit, DocumentRange } from "../../languages/types"; 3 | import { WorkerParseError } from "../../languages/worker-errors"; 4 | 5 | /** Type alias for an instance of Monaco editor */ 6 | export type EditorInstance = monaco.editor.IStandaloneCodeEditor; 7 | 8 | /** Type alias for the Monaco global */ 9 | export type MonacoInstance = typeof monaco; 10 | 11 | /** Type alias for Monaco mouse events */ 12 | export type MonacoMouseEvent = monaco.editor.IEditorMouseEvent; 13 | 14 | /** Type alias for Monaco mouse-leave event */ 15 | export type MonacoMouseLeaveEvent = monaco.editor.IPartialEditorMouseEvent; 16 | 17 | /** Type alias for Monaco decoration object */ 18 | export type MonacoDecoration = monaco.editor.IModelDeltaDecoration; 19 | 20 | /** Create Monaco decoration range object for text highlighting */ 21 | export const createHighlightRange = ( 22 | monacoInstance: MonacoInstance, 23 | highlights: DocumentRange 24 | ): MonacoDecoration => { 25 | const location = get1IndexedLocation(highlights); 26 | let { startLine, endLine, startCol, endCol } = location; 27 | const range = new monacoInstance.Range( 28 | startLine, 29 | startCol == null ? 1 : startCol, 30 | endLine == null ? startLine : endLine, 31 | endCol == null ? Infinity : endCol 32 | ); 33 | // const isWholeLine = startCol == null && endCol == null; 34 | return { range, options: { inlineClassName: "code-highlight" } }; 35 | }; 36 | 37 | /** Create Monaco decoration range object from highlights */ 38 | export const createBreakpointRange = ( 39 | monacoInstance: MonacoInstance, 40 | lineNum: number, 41 | hint?: boolean 42 | ): MonacoDecoration => { 43 | const range = new monacoInstance.Range(lineNum, 0, lineNum, 1000); 44 | const className = "breakpoint-glyph " + (hint ? "hint" : "solid"); 45 | return { range, options: { glyphMarginClassName: className } }; 46 | }; 47 | 48 | /** Create Monaco syntax-error marker from message and document range */ 49 | export const createValidationMarker = ( 50 | monacoInstance: MonacoInstance, 51 | error: WorkerParseError, 52 | range: DocumentRange 53 | ): monaco.editor.IMarkerData => { 54 | const location = get1IndexedLocation(range); 55 | const { startLine, endLine, startCol, endCol } = location; 56 | return { 57 | startLineNumber: startLine, 58 | endLineNumber: endLine == null ? startLine : endLine, 59 | startColumn: startCol == null ? 1 : startCol, 60 | endColumn: endCol == null ? Infinity : endCol, 61 | severity: monacoInstance.MarkerSeverity.Error, 62 | message: error.message, 63 | source: error.name, 64 | }; 65 | }; 66 | 67 | /** 68 | * Convert a DocumentEdit instance to Monaco edit object format. 69 | * @param edit DocumentEdit to convert to Monaco format 70 | * @returns Instance of Monaco's edit object 71 | */ 72 | export const createMonacoDocEdit = ( 73 | edit: DocumentEdit 74 | ): monaco.editor.IIdentifiedSingleEditOperation => { 75 | const location = get1IndexedLocation(edit.range); 76 | const { startLine, endLine, startCol, endCol } = location; 77 | return { 78 | text: edit.text, 79 | range: { 80 | startLineNumber: startLine, 81 | endLineNumber: endLine == null ? startLine : endLine, 82 | startColumn: startCol == null ? 1 : startCol, 83 | endColumn: endCol == null ? Infinity : endCol, 84 | }, 85 | }; 86 | }; 87 | 88 | /** 89 | * Convert a DocumentRange to use 1-indexed values. Used since language engines 90 | * use 0-indexed ranges but Monaco requires 1-indexed ranges. 91 | * @param range DocumentRange to convert to 1-indexed 92 | * @returns DocumentRange that uses 1-indexed values 93 | */ 94 | const get1IndexedLocation = (range: DocumentRange): DocumentRange => { 95 | return { 96 | startLine: range.startLine + 1, 97 | startCol: range.startCol == null ? 1 : range.startCol + 1, 98 | endLine: range.endLine == null ? range.startLine + 1 : range.endLine + 1, 99 | endCol: range.endCol == null ? Infinity : range.endCol + 1, 100 | }; 101 | }; 102 | -------------------------------------------------------------------------------- /ui/code-editor/themes/dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherit": false, 3 | "base": "vs-dark", 4 | "colors": { 5 | "focusBorder": "#2D72D2", 6 | "foreground": "#F6F7F9", 7 | "button.background": "#2D72D2", 8 | "button.foreground": "#ffffff", 9 | "dropdown.background": "#4b5360", 10 | "input.background": "#4b5360", 11 | "inputOption.activeBorder": "#2D72D2", 12 | "list.activeSelectionBackground": "#2D72D280", 13 | "list.activeSelectionForeground": "#FFFFFF", 14 | "list.dropBackground": "#2D72D280", 15 | "list.focusBackground": "#2D72D280", 16 | "list.focusForeground": "#FFFFFF", 17 | "list.highlightForeground": "#2D72D2", 18 | "list.hoverBackground": "#FFFFFF1a", 19 | "list.inactiveSelectionBackground": "#FFFFFF33", 20 | "activityBar.background": "#414854", 21 | "activityBar.dropBackground": "#2D72D280", 22 | "activityBarBadge.background": "#2D72D2", 23 | "activityBarBadge.foreground": "#ffffff", 24 | "badge.background": "#2D72D2", 25 | "badge.foreground": "#ffffff", 26 | "sideBar.background": "#383e48", 27 | "sideBarSectionHeader.background": "#414854", 28 | "editorGroup.dropBackground": "#2D72D280", 29 | "editorGroup.focusedEmptyBorder": "#2D72D2", 30 | "editorGroupHeader.tabsBackground": "#383e48", 31 | "tab.border": "#00000033", 32 | "tab.activeBorder": "#2D72D2", 33 | "tab.inactiveBackground": "#414854", 34 | "tab.activeModifiedBorder": "#2D72D2", 35 | "tab.inactiveModifiedBorder": "#1b447e", 36 | "tab.unfocusedActiveModifiedBorder": "#245ba8", 37 | "tab.unfocusedInactiveModifiedBorder": "#1b447e", 38 | "editor.background": "#2F343C", 39 | "editor.foreground": "#F6F7F9", 40 | "editorLineNumber.foreground": "#FFFFFF4d", 41 | "editorLineNumber.activeForeground": "#F6F7F9", 42 | "editor.lineHighlightBorder": "#ffffff00", 43 | "editor.lineHighlightBackground": "#FFFFFF0a", 44 | "editor.rangeHighlightBackground": "#FFFFFF0d", 45 | "editorWidget.background": "#383e48", 46 | "editorHoverWidget.background": "#383e48", 47 | "editorMarkerNavigation.background": "#383e48", 48 | "peekView.border": "#2D72D2", 49 | "peekViewEditor.background": "#252930", 50 | "peekViewResult.background": "#383e48", 51 | "peekViewTitle.background": "#2F343C", 52 | "panel.background": "#383e48", 53 | "panel.border": "#FFFFFF1a", 54 | "panelTitle.activeBorder": "#F6F7F980", 55 | "panelTitle.inactiveForeground": "#F6F7F980", 56 | "statusBar.background": "#252930", 57 | "statusBar.debuggingBackground": "#2D72D2", 58 | "statusBar.debuggingForeground": "#ffffff", 59 | "statusBar.noFolderBackground": "#252930", 60 | "statusBarItem.activeBackground": "#2D72D280", 61 | "statusBarItem.hoverBackground": "#FFFFFF1a", 62 | "statusBarItem.remoteBackground": "#2D72D2", 63 | "statusBarItem.remoteForeground": "#ffffff", 64 | "titleBar.activeBackground": "#252930", 65 | "pickerGroup.border": "#FFFFFF1a", 66 | "debugToolBar.background": "#414854", 67 | "editorBracketHighlight.foreground1": "#388eff", 68 | "editorBracketHighlight.foreground2": "#ff5257", 69 | "editorBracketHighlight.foreground3": "#2ba665", 70 | "editorBracketHighlight.foreground4": "#fff099", 71 | "selection.background": "#2D72D2" 72 | }, 73 | "rules": [ 74 | { "token": "", "foreground": "F6F7F9" }, 75 | { "token": "invalid", "foreground": "ff0000" }, 76 | { "token": "emphasis", "fontStyle": "italic" }, 77 | { "token": "strong", "fontStyle": "bold" }, 78 | { "token": "identifier", "foreground": "EC9A3C", "note": "orange" }, 79 | { "token": "variable", "foreground": "EF596F", "note": "red" }, 80 | { "token": "variable.function", "foreground": "61AFEF", "note": "blue" }, 81 | { "token": "constant", "foreground": "F0B726", "note": "gold" }, 82 | { "token": "constant.function", "foreground": "BDADFF", "note": "indigo" }, 83 | { "token": "number", "foreground": "F0B726", "note": "gold" }, 84 | { "token": "annotation", "foreground": "13C9BA", "note": "turquoise" }, 85 | { "token": "type", "foreground": "EC9A3C", "note": "orange" }, 86 | { "token": "delimiter", "foreground": "F6F7F9", "note": "white" }, 87 | { "token": "tag", "foreground": "43BF4D", "note": "forest" }, 88 | { "token": "meta", "foreground": "61AFEF", "note": "blue" }, 89 | { "token": "key", "foreground": "EF596F", "note": "red" }, 90 | { "token": "operators", "foreground": "3FA6DA", "note": "cerulean" }, 91 | { "token": "attribute", "foreground": "EC9A3C", "note": "orange" }, 92 | { "token": "string", "foreground": "72CA9B", "note": "green" }, 93 | { "token": "keyword", "foreground": "D55FDE", "note": "violet" }, 94 | { "token": "comment", "foreground": "#8F99A8", "fontStyle": "italic" } 95 | ], 96 | "encodedTokensColors": [] 97 | } 98 | -------------------------------------------------------------------------------- /ui/code-editor/themes/light.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherit": false, 3 | "base": "vs-dark", 4 | "colors": { 5 | "focusBorder": "#0E5A8A", 6 | "foreground": "#202B33", 7 | "button.background": "#0E5A8A", 8 | "button.foreground": "#ffffff", 9 | "dropdown.background": "#ffffff", 10 | "input.background": "#ffffff", 11 | "inputOption.activeBorder": "#0E5A8A", 12 | "list.activeSelectionBackground": "#0E5A8A80", 13 | "list.activeSelectionForeground": "#FFFFFF", 14 | "list.dropBackground": "#0E5A8A80", 15 | "list.focusBackground": "#0E5A8A80", 16 | "list.focusForeground": "#FFFFFF", 17 | "list.highlightForeground": "#0E5A8A", 18 | "list.hoverBackground": "#FFFFFF1a", 19 | "list.inactiveSelectionBackground": "#FFFFFF33", 20 | "activityBar.background": "#ffffff", 21 | "activityBar.dropBackground": "#0E5A8A80", 22 | "activityBarBadge.background": "#0E5A8A", 23 | "activityBarBadge.foreground": "#ffffff", 24 | "badge.background": "#0E5A8A", 25 | "badge.foreground": "#ffffff", 26 | "sideBar.background": "#ffffff", 27 | "sideBarSectionHeader.background": "#ffffff", 28 | "editorGroup.dropBackground": "#0E5A8A80", 29 | "editorGroup.focusedEmptyBorder": "#0E5A8A", 30 | "editorGroupHeader.tabsBackground": "#ffffff", 31 | "tab.border": "#00000033", 32 | "tab.activeBorder": "#0E5A8A", 33 | "tab.inactiveBackground": "#ffffff", 34 | "tab.activeModifiedBorder": "#0E5A8A", 35 | "tab.inactiveModifiedBorder": "#083652", 36 | "tab.unfocusedActiveModifiedBorder": "#0b486e", 37 | "tab.unfocusedInactiveModifiedBorder": "#083652", 38 | "editor.background": "#f5f8fa", 39 | "editor.foreground": "#202B33", 40 | "editor.selectionBackground": "#91d5ff", 41 | "editor.findMatchBackground": "#91d5ff", 42 | "editor.findMatchBorder": "#57bfff", 43 | "editor.findMatchHighlightBackground": "#cee2ed", 44 | "editorLineNumber.foreground": "#8A9BA8", 45 | "editorLineNumber.activeForeground": "#000000", 46 | "editor.lineHighlightBorder": "#D8E1E8", 47 | "editor.rangeHighlightBackground": "#ffffff", 48 | "editorWidget.background": "#ffffff", 49 | "editorHoverWidget.background": "#ffffff", 50 | "editorMarkerNavigation.background": "#ffffff", 51 | "peekView.border": "#0E5A8A", 52 | "peekViewEditor.background": "#c4c6c8", 53 | "peekViewResult.background": "#ffffff", 54 | "peekViewTitle.background": "#f5f8fa", 55 | "panel.background": "#ffffff", 56 | "panel.border": "#FFFFFF1a", 57 | "panelTitle.activeBorder": "#202B3380", 58 | "panelTitle.inactiveForeground": "#202B3380", 59 | "statusBar.background": "#c4c6c8", 60 | "statusBar.debuggingBackground": "#0E5A8A", 61 | "statusBar.debuggingForeground": "#ffffff", 62 | "statusBar.noFolderBackground": "#c4c6c8", 63 | "statusBarItem.activeBackground": "#0E5A8A80", 64 | "statusBarItem.hoverBackground": "#FFFFFF1a", 65 | "statusBarItem.remoteBackground": "#0E5A8A", 66 | "statusBarItem.remoteForeground": "#ffffff", 67 | "titleBar.activeBackground": "#c4c6c8", 68 | "pickerGroup.border": "#FFFFFF1a", 69 | "debugToolBar.background": "#ffffff", 70 | "editorBracketHighlight.foreground1": "#1170ac", 71 | "editorBracketHighlight.foreground2": "#d23434", 72 | "editorBracketHighlight.foreground3": "#0c7f50", 73 | "editorBracketHighlight.foreground4": "#ffdf7f", 74 | "selection.background": "#0E5A8A" 75 | }, 76 | "rules": [ 77 | { "token": "", "foreground": "252A31" }, 78 | { "token": "invalid", "foreground": "ff0000" }, 79 | { "token": "emphasis", "fontStyle": "italic" }, 80 | { "token": "strong", "fontStyle": "bold" }, 81 | { "token": "identifier", "foreground": "C87619", "note": "orange" }, 82 | { "token": "variable", "foreground": "CD4246", "note": "red" }, 83 | { "token": "variable.function", "foreground": "2D72D2", "note": "blue" }, 84 | { "token": "constant", "foreground": "D1980B", "note": "gold" }, 85 | { "token": "constant.function", "foreground": "634DBF", "note": "indigo" }, 86 | { "token": "number", "foreground": "D1980B", "note": "gold" }, 87 | { "token": "annotation", "foreground": "00A396", "note": "turquoise" }, 88 | { "token": "type", "foreground": "C87619", "note": "orange" }, 89 | { "token": "delimiter", "foreground": "202B33", "note": "white" }, 90 | { "token": "tag", "foreground": "29A634", "note": "forest" }, 91 | { "token": "meta", "foreground": "2D72D2", "note": "blue" }, 92 | { "token": "key", "foreground": "CD4246", "note": "red" }, 93 | { "token": "operators", "foreground": "147EB3", "note": "cerulean" }, 94 | { "token": "attribute", "foreground": "C87619", "note": "orange" }, 95 | { "token": "string", "foreground": "238551", "note": "green" }, 96 | { "token": "keyword", "foreground": "9D3F9D", "note": "violet" }, 97 | { "token": "comment", "foreground": "5F6B7C", "fontStyle": "italic" } 98 | ], 99 | "encodedTokensColors": [] 100 | } 101 | -------------------------------------------------------------------------------- /ui/code-editor/use-code-validator.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { WorkerParseError } from "../../languages/worker-errors"; 3 | import { 4 | createValidationMarker, 5 | EditorInstance, 6 | MonacoInstance, 7 | } from "./monaco-utils"; 8 | 9 | /** Constant denoting "owner" of syntax error markers */ 10 | const MARKER_OWNER = "code-validation"; 11 | 12 | /** Delay between user's last edit and sending validation request */ 13 | const VALIDATE_DELAY = 500; 14 | 15 | type Args = { 16 | editor: EditorInstance | null; 17 | monaco: MonacoInstance | null; 18 | onValidateCode: (code: string) => Promise; 19 | }; 20 | 21 | /** 22 | * React hook that sets up code validation lifecycle on the Monaco editor. 23 | * Code validation is done a fixed delay after user's last edit, and markers 24 | * for indicating syntax error are added to the editor. 25 | */ 26 | export const useCodeValidator = ({ editor, monaco, onValidateCode }: Args) => { 27 | const timeoutRef = React.useRef(null); 28 | 29 | const runValidator = async () => { 30 | if (!editor || !monaco) return; 31 | const error = await onValidateCode(editor.getValue()); 32 | if (error) 33 | monaco.editor.setModelMarkers(editor.getModel()!, MARKER_OWNER, [ 34 | createValidationMarker(monaco, error, error.range), 35 | ]); 36 | }; 37 | 38 | React.useEffect(() => { 39 | if (!editor || !monaco) return; 40 | const disposer = editor.getModel()!.onDidChangeContent(() => { 41 | monaco.editor.setModelMarkers(editor.getModel()!, MARKER_OWNER, []); 42 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 43 | timeoutRef.current = setTimeout(runValidator, VALIDATE_DELAY); 44 | }); 45 | return () => disposer.dispose(); 46 | }, [editor, monaco]); 47 | }; 48 | -------------------------------------------------------------------------------- /ui/code-editor/use-editor-breakpoints.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | createBreakpointRange, 4 | EditorInstance, 5 | MonacoInstance, 6 | MonacoMouseEvent, 7 | MonacoMouseLeaveEvent, 8 | } from "./monaco-utils"; 9 | 10 | type BreakpointsMap = { [k: number]: string[] }; 11 | type HoverBreakpoint = { 12 | lineNum: number; 13 | decorRanges: string[]; 14 | }; 15 | 16 | type Args = { 17 | editor: EditorInstance | null; 18 | monaco: MonacoInstance | null; 19 | onUpdateBreakpoints: (newBreakpoints: number[]) => void; 20 | }; 21 | 22 | export const useEditorBreakpoints = ({ 23 | editor, 24 | monaco, 25 | onUpdateBreakpoints, 26 | }: Args) => { 27 | const breakpoints = React.useRef({}); 28 | const hoverBreakpoint = React.useRef(null); 29 | 30 | // Mouse clicks -> add or remove breakpoint 31 | React.useEffect(() => { 32 | if (!editor || !monaco) return; 33 | const disposer = editor.onMouseDown((e: MonacoMouseEvent) => { 34 | // Check if click is in glyph display channel 35 | const glyphMarginType = monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN; 36 | const isGlyphMargin = e.target.type === glyphMarginType; 37 | if (!isGlyphMargin) return; 38 | 39 | const lineNum = e.target.position!.lineNumber; 40 | const existingRange = breakpoints.current[lineNum]; 41 | if (existingRange) { 42 | // Already has breakpoint - remove it 43 | editor.deltaDecorations(existingRange, []); 44 | delete breakpoints.current[lineNum]; 45 | } else { 46 | // Add breakpoint to this line 47 | const range = createBreakpointRange(monaco, lineNum); 48 | const newRangeStr = editor.deltaDecorations([], [range]); 49 | breakpoints.current[lineNum] = newRangeStr; 50 | } 51 | 52 | // Update breakpoints to parent 53 | const bpLineNumStrs = Object.keys(breakpoints.current); 54 | const bpLineNums = bpLineNumStrs.map( 55 | (numStr) => parseInt(numStr, 10) - 1 56 | ); 57 | onUpdateBreakpoints(bpLineNums); 58 | }); 59 | return () => disposer.dispose(); 60 | }, [editor, monaco]); 61 | 62 | // Mouse enter -> show semi-transparent breakpoint icon 63 | React.useEffect(() => { 64 | if (!editor || !monaco) return; 65 | const disposer = editor.onMouseMove((e: MonacoMouseEvent) => { 66 | // Check if click is in glyph display channel 67 | const glyphMarginType = monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN; 68 | const isGlyphMargin = e.target.type === glyphMarginType; 69 | 70 | // If mouse goes out of glyph channel... 71 | if (!isGlyphMargin) { 72 | if (hoverBreakpoint.current) { 73 | editor.deltaDecorations(hoverBreakpoint.current.decorRanges, []); 74 | hoverBreakpoint.current = null; 75 | } 76 | return; 77 | } 78 | 79 | // Check if hover is in already hinted line 80 | const hoverLineNum = e.target.position!.lineNumber; 81 | if (hoverLineNum === hoverBreakpoint.current?.lineNum) return; 82 | 83 | // Add hover decoration to newly hovered line 84 | const range = createBreakpointRange(monaco, hoverLineNum, true); 85 | const newHoverRangeStr = editor.deltaDecorations([], [range]); 86 | 87 | // Remove existing breakpoint hover 88 | if (hoverBreakpoint.current) 89 | editor.deltaDecorations(hoverBreakpoint.current.decorRanges, []); 90 | 91 | // Set hover breakpoint state to new one 92 | hoverBreakpoint.current = { 93 | lineNum: hoverLineNum, 94 | decorRanges: newHoverRangeStr, 95 | }; 96 | 97 | // If breakpoint already on line, ignore 98 | const lineNum = e.target.position!.lineNumber; 99 | const existingRange = breakpoints.current[lineNum]; 100 | if (existingRange) return; 101 | }); 102 | return () => disposer.dispose(); 103 | }, [editor, monaco]); 104 | 105 | // Mouse leaves editor -> remove hover breakpoint hint 106 | React.useEffect(() => { 107 | if (!editor) return; 108 | const disposer = editor.onMouseLeave((_: MonacoMouseLeaveEvent) => { 109 | if (!hoverBreakpoint.current) return; 110 | editor.deltaDecorations(hoverBreakpoint.current.decorRanges, []); 111 | hoverBreakpoint.current = null; 112 | }); 113 | return () => disposer.dispose(); 114 | }, [editor]); 115 | 116 | return breakpoints; 117 | }; 118 | -------------------------------------------------------------------------------- /ui/code-editor/use-editor-lang-config.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMonaco } from "@monaco-editor/react"; 3 | import { MonacoTokensProvider } from "../../languages/types"; 4 | 5 | type ConfigParams = { 6 | languageId: string; 7 | tokensProvider?: MonacoTokensProvider; 8 | }; 9 | 10 | /** Add custom language and relevant providers to Monaco */ 11 | export const useEditorLanguageConfig = (params: ConfigParams) => { 12 | const monaco = useMonaco(); 13 | 14 | React.useEffect(() => { 15 | if (!monaco) return; 16 | 17 | // Register language 18 | monaco.languages.register({ id: params.languageId }); 19 | 20 | // If provided, register token provider for language 21 | if (params.tokensProvider) { 22 | monaco.languages.setMonarchTokensProvider( 23 | params.languageId, 24 | params.tokensProvider 25 | ); 26 | } 27 | }, [monaco]); 28 | }; 29 | -------------------------------------------------------------------------------- /ui/custom-icons.tsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "@blueprintjs/core"; 2 | import { useDarkMode } from "./providers/dark-mode-provider"; 3 | 4 | export const GitHubIcon = () => { 5 | const { isDark } = useDarkMode(); 6 | const color = isDark ? Colors.GRAY4 : Colors.GRAY1; 7 | 8 | return ( 9 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /ui/execution-controls.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | ButtonGroup, 4 | Icon, 5 | NumericInput, 6 | Spinner, 7 | Tag, 8 | } from "@blueprintjs/core"; 9 | 10 | const styles = { 11 | container: { 12 | display: "flex", 13 | alignItems: "center", 14 | paddingRight: 5, 15 | marginRight: -15, 16 | }, 17 | inputWrapper: { 18 | /** 19 | * As of Feb'22, NumericInput doesn't have `small` prop yet, 20 | * so we instead use `transform` to hack up a slightly smaller input. 21 | */ 22 | transform: "scale(0.9)", 23 | marginLeft: 10, 24 | }, 25 | input: { 26 | width: 125, 27 | }, 28 | }; 29 | 30 | /** Possible states of the debug controls component */ 31 | type DebugControlsState = "off" | "running" | "paused" | "error"; 32 | 33 | /** Input field for changing execution interval */ 34 | const IntervalInput = (props: { 35 | disabled: boolean; 36 | onChange: (v: number) => void; 37 | }) => { 38 | return ( 39 |
40 | props.onChange(v)} 51 | rightElement={ms} 52 | allowNumericCharactersOnly 53 | /> 54 |
55 | ); 56 | }; 57 | 58 | /** Button for starting code execution */ 59 | const RunButton = ({ onClick }: { onClick: () => void }) => ( 60 | 69 | ); 70 | 71 | /** Button group for debugging controls */ 72 | const DebugControls = (props: { 73 | state: DebugControlsState; 74 | onPause: () => void; 75 | onResume: () => void; 76 | onStep: () => void; 77 | onStop: () => void; 78 | }) => { 79 | const paused = props.state === "paused" || props.state === "error"; 80 | const pauseDisabled = props.state === "error"; 81 | const stepDisabled = ["off", "running", "error"].includes(props.state); 82 | 83 | return ( 84 | 85 | 59 | 60 | 61 | {props.langName} 62 | 63 | 64 | ); 65 | 66 | const controlsSection = ( 67 |
{props.renderExecControls()}
68 | ); 69 | 70 | const infoSection = ( 71 |
81 | 82 | setShowNotesHint(false)} 86 | title="View the notes for this esolang" 87 | > 88 |
106 | ); 107 | 108 | return ( 109 |
110 | 118 | {brandSection} 119 | {controlsSection} 120 | {infoSection} 121 | 122 |
123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /ui/input-editor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextArea } from "@blueprintjs/core"; 3 | 4 | // Interface for interacting with the editor 5 | export interface InputEditorRef { 6 | /** 7 | * Get the current text content of the editor. 8 | */ 9 | getValue: () => string; 10 | } 11 | 12 | /** 13 | * A very simple text editor for user input 14 | */ 15 | const InputEditorComponent = ( 16 | props: { readOnly?: boolean }, 17 | ref: React.Ref 18 | ) => { 19 | const textareaRef = React.useRef(null); 20 | 21 | React.useImperativeHandle( 22 | ref, 23 | () => ({ 24 | getValue: () => textareaRef.current!.value, 25 | }), 26 | [] 27 | ); 28 | 29 | return ( 30 |
31 |