diff --git a/.firebaserc b/.firebaserc index a8dfa55..64ebe5a 100644 --- a/.firebaserc +++ b/.firebaserc @@ -5,8 +5,11 @@ "targets": { "lost-pixels-prod": { "hosting": { - "mastermind": [ + "mastermind-prod": [ "lost-pixels-mastermind" + ], + "mastermind-staging": [ + "mastermind-staging" ] } } diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..677db76 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,48 @@ +name: Dev staging pipeline + +on: + push: + branches: + - dev + paths: + - "**" + +jobs: + pipeline: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + + - uses: jetli/wasm-pack-action@v0.4.0 + + - name: Test compiler + run: cargo test + working-directory: compiler + + - name: Build compiler for WASM + run: wasm-pack build --release --target web + working-directory: compiler + + - name: Node dependencies + run: yarn install --frozen-lockfile + + - name: Build grammar + run: yarn build:grammar + + - name: Build web app + run: yarn build + + - name: Deploy to Firebase + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_LOST_PIXELS_PROD }} + projectId: lost-pixels-prod + # "target" is defined in firebase.json and .firebaserc, the latter controlling which firebase site to deploy to + target: mastermind-staging + channelId: live diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml new file mode 100644 index 0000000..df0462e --- /dev/null +++ b/.github/workflows/deploy-main.yml @@ -0,0 +1,49 @@ +name: Main branch pipeline + +on: + push: + branches: + - main + paths: + - "**" + # - "!./compiler/programs/*.mmi" + +jobs: + pipeline: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + + - uses: jetli/wasm-pack-action@v0.4.0 + + - name: Test compiler + run: cargo test + working-directory: compiler + + - name: Build compiler for WASM + run: wasm-pack build --release --target web + working-directory: compiler + + - name: Node dependencies + run: yarn install --frozen-lockfile + + - name: Build grammar + run: yarn build:grammar + + - name: Build web app + run: yarn build + + - name: Deploy to Firebase + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_LOST_PIXELS_PROD }} + projectId: lost-pixels-prod + # "target" is defined in firebase.json and .firebaserc, the latter controlling which firebase site to deploy to + target: mastermind-prod + channelId: live diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 5609865..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Test, build and deploy Mastermind Web Compiler - -on: - push: - branches: - - main - paths: - - "**" - # - "!./compiler/programs/*.mmi" - -jobs: - test_build_deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: "yarn" - cache-dependency-path: "yarn.lock" - - uses: jetli/wasm-pack-action@v0.4.0 - - run: cd compiler && cargo test - - run: cd compiler && wasm-pack build --release --target web - - run: yarn - - run: yarn build:grammar - - run: yarn build - - uses: FirebaseExtended/action-hosting-deploy@v0 - with: - repoToken: ${{ secrets.GITHUB_TOKEN }} - firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_LOST_PIXELS_PROD }} - projectId: lost-pixels-prod - target: mastermind - channelId: live diff --git a/.gitignore b/.gitignore index a547bf3..8b7e502 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,6 @@ dist-ssr *.local # Editor directories and files -.vscode/* -!.vscode/extensions.json .idea .DS_Store *.suo diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..53eeee5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "**.mmi": "plaintext", + "programs/**": "plaintext", + } +} \ No newline at end of file diff --git a/README.md b/README.md index a7e247d..c082f61 100644 --- a/README.md +++ b/README.md @@ -4,363 +4,59 @@ Mastermind is a programming language designed to compile to the esoteric languag Brainfuck is essentially a modern interpretation of the classical Turing machine. It consists of a tape of 8-bit values, with simple increment/decrement, move left/right, and control flow operations. The full language only uses 8 control characters: `+-><.,[]`. -Imagine if C was designed for computer architectures that run brainfuck, that is what Mastermind is intended to be. +Imagine if C was designed for computer architectures that run Brainfuck directly, that is what Mastermind is intended to be. -### Note +## Development and Setup -In my implementation, the Brainfuck tape extends infinitely in both directions, with 8-bit wrapping cells. +### Quickstart: -## Usage +- Install Rust/Cargo and Node/NPM. +- Install Yarn: `npm i --global yarn`. +- Run `yarn`. +- Run `yarn build:wasm`. +- Run `yarn build:grammar`. +- Run `yarn dev`, then follow the link to http://localhost:5173. -### Variables & Values +Commits to _dev_ and _main_ are published to https://staging.mastermind.lostpixels.org and https://mastermind.lostpixels.org respectively. -Variables can be defined as single cells, or as contiguous sets of cells: +### Overview: -``` -// single-cell: -let var = 56; -let c = 'g'; -let bool = true; // true/false equivalent to 1/0 -// multi-cell: -let array[4] = [1, 2, 3, 4]; -let string[5] = "hello"; -``` +This repository contains two main components: the compiler and the web IDE. There are GitHub Actions workflows which build, test, and deploy the web IDE (with bundled compiler) to Firebase Web Hosting. -If left uninitialised, a cell is assumed to have a value of 0. +#### Compiler -Expressions consist of simple adds and subtractions, arrays and strings cannot be used in expressions: +The `./compiler` subdirectory contains a Cargo (Rust) package, ensure Rust is installed. -``` -// supported: -var += 4 + (5 - 4 + (3 - 2)); -var = 'g' - 23 + true; -var = var + 5; // inefficient but supported -var += array[0] + 5; -let arr[3] = [4 + 3 - ('5' - 46), 1, 3]; +The compiler codebase has two main entrypoints: `main.rs` and `lib.rs`, for the command-line and WASM compilation targets respectively. All other Rust source files are common between compilation targets. -// NOT supported: -var += [4, 5, 7][1] + 3; -``` +Key files to look at: -##### Note: Array indices must be compile-time constant integers +- `tokeniser.rs`: tokenises the raw text files into Mastermind syntax tokens. +- `parser.rs`: parses strings of tokens into higher-level Mastermind clauses. +- `compiler.rs`: compiles the high-level clauses into a list of basic instructions akin to an intermediate representation (IR). +- `builder.rs`: takes the basic instructions from the compiler and builds the final Brainfuck program. -This is a limitation of Brainfuck, getting around this problem requires more runtime code than I want to include for the sake of optimisations. If you want to implement runtime array index access, read the section on in-line Brainfuck. +Some key commands: - +(from within the `./compiler` subdirectory) -### Input & Output +- `cargo run -- -h`: runs the command-line compiler module and displays command help information +- `cargo test`: runs the automated test suite +- `cargo build`: builds the command-line module +- `wasm-pack build`: builds the WASM module -Single bytes can be output using the `output` operator: +#### Web IDE -``` -output 'h'; -output var; -output var + 6; -output array[0]; -// the following are equivalent: -output '\n'; -output 10; -``` +The project root directory `package.json`/`yarn.lock` defines a Node package managed with Yarn. Most important commands or behaviours are defined as `npm run` or `yarn` scripts within `package.json`. -Likewise for `input`: +Ensure Node is installed, then ensure Yarn is installed with `npm i --global yarn`. -``` -input var; -input arr[2]; -``` +The web IDE is a SolidJS app using TypeScript/TSX, and Vite as a bundler. The text editing portions of the UI are provided by the _codemirror_ plugin, and syntax highlighting is defined in the included _lezer_ grammar: `./src/lexer/mastermind.grammar`. -There is also `*` spread syntax and support for strings and arrays for ease of use: +Some key commands: -``` -output "hello"; -output ['1', '2', '3']; -output *array; - -input *array; -``` - -### Loops - -The simplest is the `while` loop, which only supports cell references, not expressions: - -``` -while var { - // do stuff - // var -= 1; - // etc -} -``` - -#### Draining loops - -Next there is a very common Brainfuck pattern which I call the `drain` loop: - -``` -drain var { - -} -// shorthand for following: -while var { - // do stuff - var -= 1; -} -``` - -This destructively loops as many times as the value in the cell being referenced, this can be used with expressions: - -``` -drain 10 {} -drain var - 6 {} -``` - -There is also shorthand syntax for adding to other cells: - -``` -drain var into other_var other_var_2 *spread_array etc; - -// example of typical "for loop": -let i; -drain 10 into i { - output '0' + i; // inefficient for the example -} -// "0123456789" -// equivalent to the following: -let i; -let N = 10; -while N { - output '0' + i; -} -``` - -#### Copying loops - -Sometimes you want to loop a variable number of times but do not want to destruct the variables value, this is what the `copy` loop is for: - -``` -copy var into other_var *spread_var etc; - -// examples: -copy var { - // this will output the original var value, var times - output var; -} - -let rows = 3; -let columns = 6; -let total; -drain rows { - copy columns into total { - output '.'; - } -} -// ...... -// ...... -// ...... -``` - -### If/Else - -Note: These are currently a little bit inefficient but work for most use cases. - -If/Else statements are very simple, they check if a value is positive. If you want to invert then you can use the `not` keyword. -Examples: - -``` -if 13 { - output "13"; -} - -if var { - output "true"; -} else { - output "false"; -} - -// typical equivalence use-case: -if not var - 10 { - // == -} else { - // != -} -``` - -### Functions - -Currently functions work more like templates/macros, as they do not perform any passing by value. All functions are essentially inlined at compile time. This means multiple calls to a large function will significantly increase your compiled code size. - -For this reason, function arguments are given using `<` angled bracket `>` syntax, much like generic functions in other languages: - -``` -def quote { - output 39; // ASCII single quote - output arg; - output 39; -} - -let N = 'g'; -quote; -N += 3; -quote; -// gj -``` - -Functions have a quirk with scoping variables: currently code inside functions can access variables defined in the calling scope, I am not sure if this is erroneous or should be supported, advice is appreciated. - -### Imports - -Imports work much like the C preprocessor: - -``` -#include "other_file" -``` - -This copies the contents of "other_file" into the current file. - - - - -### In-line Brainfuck features - - - -In-line Brainfuck allows the programmer to define custom behaviour as if they were writing raw Brainfuck, much in the same way as C has in-line assembly syntax. - -``` -// This is its most basic form: -// find the next cell that equals -1 -bf { - +[->+]- -} - -// This is its more advanced form: -// input a line of lowercase letters and output the uppercase version -// this is an intentionally inefficient example -bf @3 clobbers var *spread_var etc { - ,----------[++++++++++>,----------] - <[<]> - [ - { - let g @0; - assert g unknown; - output g + ('A' - 'a'); - // embedded Mastermind! - } - > - ] - // now clear and return - <[[-]<]> -} -``` - -It is the programmer's responsibility to clear used cells and return back to the cell in which they started the in-line Brainfuck context. If the programmer does not do this, any mastermind code after the in-line Brainfuck command will likely break. - -#### Memory location specifiers - -For hand-tuning optimisations and in-line Brainfuck that reads from Mastermind variables, you can specify the location on the Brainfuck tape: - -``` -let var @3 = 4; -// compiled: >>>++++ - -bf @4 { - <><><> -} -// compiled: >>>><><><> -``` - -#### Clobbering and Assertions - -Mastermind will try to predict the value of cells at compile-time, so it can prevent unnecessary cell clean-ups and unreachable code (with optimisations turned on). If your in-line Brainfuck affects existing Mastermind variables, you should tell the compiler using the `clobbers` keyword, the syntax is similar to the `drain into` list: - -``` -bf clobbers var *spread_var other_var etc {} -``` - -The compiler will now assume nothing about the values of those variables afterwards. - -If instead you want to tell the compiler specifically that a value has become a certain value, you can use `assert`: - -``` -assert var equals 3; -// most common use cases: -assert var equals 0; -assert var unknown; -``` - -Asserting a variable as `unknown` is equivalent to clobbering - -#### Embedded Mastermind - -Embedding Mastermind into your in-line Brainfuck allows you to use Mastermind syntax features for programs within your Brainfuck, this is useful for N-length string based programs, or anything not currently possible in pure Mastermind: - -``` -let sum @0; - -bf @0 { - >> - // read input (until eof) to the tape, nullifying any spaces or newlines - // (this is probably not a good practical example, ideas are appreciated) - ,[ - { - let c @0; - assert c unknown; // needed otherwise the compiler assumes c = 0 - - if not (c - '\n') { - c = 0; - } - if not (c - ' ') { - c = 0; - } - } - >, - ] -} -``` - -Memory location specifiers are relative to the current Mastermind context. Also, top-level variables are not cleared by default in Mastermind contexts, this allows you to "leave" variables in cells for your Brainfuck to use. If you want your embedded Mastermind to clean itself up, you can simply open a scope at the top level: - -``` -bf { - ++----++[][][<><><>] // the program doesn't matter for this example - { - // variables here will not be cleared - let g @2; - assert g unknown; - { - // variables here will be cleared - let b = 32; - } - } - {{ - // self-cleaning Mastermind code here - }} -} -``` - -#### Craziness - -You can put in-line Brainfuck inside your embedded Mastermind. - -``` -bf { - ++++[ - { - let i @0; - assert i unknown; - let j @1 = i + 1; - - bf @1 { - [.+] - { - // even more layers are possible - bf { - { - output "h" - } - } - } - } - } - -] -} -``` +- `yarn`: installs npm packages +- `yarn build:wasm`: builds the compiler WASM module +- `yarn build:grammar`: compiles the lezer grammar to JS for use in codemirror +- `yarn dev`: runs the SolidJS app in a local Vite dev server +- `yarn build`: builds the SolidJS app diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock index 4e8680d..3d4fa1c 100644 --- a/compiler/Cargo.lock +++ b/compiler/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -246,6 +246,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.1" @@ -369,6 +375,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -408,6 +423,7 @@ version = "0.1.0" dependencies = [ "clap 4.4.2", "console_error_panic_hook", + "itertools", "js-sys", "regex-lite", "serde", diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml index 7d5e3b6..cfbfffa 100644 --- a/compiler/Cargo.toml +++ b/compiler/Cargo.toml @@ -18,6 +18,7 @@ serde-wasm-bindgen = "0.6.3" serde_json = "1.0.108" wasm-bindgen = "0.2.89" wasm-bindgen-futures = "0.4.40" +itertools = "0.14.0" [dev-dependencies] twiggy = "0.7.0" diff --git a/compiler/programs/divisors_example.mmi b/compiler/programs/divisors_example.mmi deleted file mode 100644 index dce153e..0000000 --- a/compiler/programs/divisors_example.mmi +++ /dev/null @@ -1,79 +0,0 @@ -#include print.mmi - -let N = 100; -let number = 1; - -drain N into number { - output 'T'; - output 'h'; - output 'e'; - output ' '; - output 'f'; - output 'a'; - output 'c'; - output 't'; - output 'o'; - output 'r'; - output 's'; - output ' '; - output 'o'; - output 'f'; - output ' '; - print(number); - output ' '; - output 'a'; - output 'r'; - output 'e'; - output ' '; - output '1'; - - let nt_equal_to_one = number - 1; - if nt_equal_to_one { - - let num_copy = number - 2; - let divisor = 2; - drain num_copy into divisor { - let result; - divisible(result, number, divisor); - - if result { - output ','; - output ' '; - print(divisor); - }; - }; - - output ' '; - output 'a'; - output 'n'; - output 'd'; - output ' '; - - print(number); - }; - - output 10; -}; - -def divisible(result, dividend, divisor) { - // result = 0 if dividend % divisor = 0 - let b = dividend; - - let radix; - let iterating = true; - - while iterating { - - radix = divisor; - drain radix { - --b; - if not b { - iterating = false; - }; - }; - }; - // if there is no remainder then we are divisible - if not b { - result = true; - }; -}; diff --git a/compiler/src/brainfuck.rs b/compiler/src/brainfuck.rs index ecef7d3..eae1b5b 100644 --- a/compiler/src/brainfuck.rs +++ b/compiler/src/brainfuck.rs @@ -2,92 +2,58 @@ // this was the first thing added in this project and it has barely changed use std::{ + collections::HashMap, fmt, io::{Read, Write}, num::Wrapping, }; +use crate::macros::macros::r_panic; use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; struct Tape { - positive_array: Vec>, - negative_array: Vec>, - - head_position: i32, + memory_map: HashMap<(i32, i32), Wrapping>, + head_position: (i32, i32), } impl Tape { fn new() -> Self { Tape { - positive_array: Vec::new(), - negative_array: Vec::new(), - head_position: 0, + memory_map: HashMap::new(), + head_position: (0, 0), } } - fn get_cell(&self, index: i32) -> Result, String> { - let array; - let i: usize = (match index < 0 { - true => { - array = &self.negative_array; - -index - 1 - } - false => { - array = &self.positive_array; - index - } - }) - .try_into() - .or(Err( - "Could not read current cell due to integer error, this should never occur.", - ))?; - - Ok(match i < array.len() { - true => array[i], - false => Wrapping(0u8), - }) + fn get_cell(&self, position: (i32, i32)) -> Wrapping { + match self.memory_map.get(&position) { + Some(val) => *val, + None => Wrapping(0), + } } - fn move_head_position(&mut self, amount: i32) { - self.head_position += amount; + fn move_head_position(&mut self, amount: (i32, i32)) { + self.head_position.0 += amount.0; + self.head_position.1 += amount.1; } - fn modify_current_cell( - &mut self, - amount: Wrapping, - clear: Option, - ) -> Result<(), String> { - let array; - let i: usize = (match self.head_position < 0 { - true => { - array = &mut self.negative_array; - -self.head_position - 1 + fn increment_current_cell(&mut self, amount: Wrapping) { + let val = self.memory_map.get_mut(&self.head_position); + match val { + Some(val) => { + *val += amount; } - false => { - array = &mut self.positive_array; - self.head_position + None => { + self.memory_map.insert(self.head_position, amount); } - }) - .try_into() - .or(Err( - "Could not modify current cell due to integer error, this should never occur.", - ))?; - - if i >= array.len() { - array.resize(i + 1, Wrapping(0u8)); } - - array[i] = match clear.unwrap_or(false) { - true => amount, - false => array[i] + amount, - }; - - Ok(()) } - fn set_current_cell(&mut self, value: Wrapping) -> Result<(), String> { - Ok(self.modify_current_cell(value, Some(true))?) + fn set_current_cell(&mut self, value: Wrapping) { + self.memory_map.insert(self.head_position, value); } // TODO: simplify duplicated code? probably could use an optional mutable reference thing - fn get_current_cell(&self) -> Result, String> { - Ok(self.get_cell(self.head_position)?) + fn get_current_cell(&self) -> Wrapping { + match self.memory_map.get(&self.head_position) { + Some(val) => *val, + None => Wrapping(0), + } } } @@ -107,8 +73,8 @@ impl fmt::Display for Tape { line_3.push('|'); line_4.push('|'); - for pos in (self.head_position - 10)..(self.head_position + 10) { - let val = self.get_cell(pos).unwrap().0; + for pos in (self.head_position.1 - 10)..(self.head_position.1 + 10) { + let val = self.get_cell((pos, 0)).0; let mut dis = 32u8; if val.is_ascii_alphanumeric() || val.is_ascii_punctuation() { dis = val; @@ -125,7 +91,7 @@ impl fmt::Display for Tape { line_3.push(' '); line_3.push(dis as char); - line_4 += match pos == self.head_position { + line_4 += match pos == self.head_position.1 { true => "^^^", false => "---", }; @@ -154,7 +120,13 @@ impl fmt::Display for Tape { } } +pub struct BVMConfig { + pub enable_debug_symbols: bool, + pub enable_2d_grid: bool, +} + pub struct BVM { + config: BVMConfig, tape: Tape, program: Vec, } @@ -168,8 +140,11 @@ pub trait ByteWriter { } impl BVM { - pub fn new(program: Vec) -> Self { + const MAX_STEPS_DEFAULT: usize = (2 << 30) - 2; + + pub fn new(config: BVMConfig, program: Vec) -> Self { BVM { + config, tape: Tape::new(), program, } @@ -177,6 +152,8 @@ impl BVM { // TODO: refactor/rewrite this, can definitely be improved with async read/write traits or similar // I don't love that I duplicated this to make it work with js + // TODO: this isn't covered by unit tests + // TODO: add a maximum step count pub async fn run_async( &mut self, output_callback: &js_sys::Function, @@ -189,14 +166,14 @@ impl BVM { let mut output_bytes: Vec = Vec::new(); while pc < self.program.len() { - match self.program[pc] { - '+' => { - self.tape.modify_current_cell(Wrapping(1), None)?; - } - '-' => { - self.tape.modify_current_cell(Wrapping(-1i8 as u8), None)?; - } - ',' => { + match ( + self.program[pc], + self.config.enable_debug_symbols, + self.config.enable_2d_grid, + ) { + ('+', _, _) => self.tape.increment_current_cell(Wrapping(1)), + ('-', _, _) => self.tape.increment_current_cell(Wrapping(-1i8 as u8)), + (',', _, _) => { // https://github.com/rustwasm/wasm-bindgen/issues/2195 // let password_jsval: JsValue = func.call1(&this, &JsValue::from_bool(true))?; // let password_promise_res: Result = @@ -225,11 +202,11 @@ impl BVM { .as_f64() .expect("Could not convert js number into f64 type"); let byte: u8 = num as u8; // I have no idea if this works (TODO: test) - self.tape.set_current_cell(Wrapping(byte))?; + self.tape.set_current_cell(Wrapping(byte)); } - '.' => { + ('.', _, _) => { // TODO: handle errors - let byte = self.tape.get_current_cell()?.0; + let byte = self.tape.get_current_cell().0; let fnum: f64 = byte as f64; // I have no idea if this works (TODO: test again) output_callback .call1(&JsValue::null(), &JsValue::from_f64(fnum)) @@ -237,15 +214,15 @@ impl BVM { output_bytes.push(byte); } - '>' => { - self.tape.move_head_position(1); + ('>', _, _) => { + self.tape.move_head_position((1, 0)); } - '<' => { - self.tape.move_head_position(-1); + ('<', _, _) => { + self.tape.move_head_position((-1, 0)); } - '[' => { + ('[', _, _) => { // entering a loop - if self.tape.get_current_cell()?.0 == 0 { + if self.tape.get_current_cell().0 == 0 { // skip the loop, (advance to the corresponding closing loop brace) // TODO: make this more efficient by pre-computing a loops map let mut loop_count = 1; @@ -262,8 +239,8 @@ impl BVM { loop_stack.push(pc); } } - ']' => { - if self.tape.get_current_cell()?.0 == 0 { + (']', _, _) => { + if self.tape.get_current_cell().0 == 0 { // exit the loop loop_stack.pop(); } else { @@ -272,10 +249,18 @@ impl BVM { pc = loop_stack[loop_stack.len() - 1]; } } - // '#' => { + ('^', _, true) => self.tape.move_head_position((0, 1)), + ('v', _, true) => self.tape.move_head_position((0, -1)), + ('^', _, false) => { + r_panic!("2D Brainfuck currently disabled"); + } + ('v', _, false) => { + r_panic!("2D Brainfuck currently disabled"); + } + // ('#', true, ) => { // println!("{}", self.tape); // } - // '@' => { + // ('@', true, _) => { // print!("{}", self.tape.get_current_cell().0 as i32); // } _ => (), @@ -293,37 +278,43 @@ impl BVM { Ok(unsafe { String::from_utf8_unchecked(output_bytes) }) } - pub fn run(&mut self, input: &mut impl Read, output: &mut impl Write) -> Result<(), String> { + pub fn run( + &mut self, + input: &mut impl Read, + output: &mut impl Write, + max_steps: Option, + ) -> Result<(), String> { + let mut steps = 0usize; let mut pc: usize = 0; // this could be more efficient with a pre-computed map let mut loop_stack: Vec = Vec::new(); while pc < self.program.len() { - match self.program[pc] { - '+' => { - self.tape.modify_current_cell(Wrapping(1), None)?; - } - '-' => { - self.tape.modify_current_cell(Wrapping(-1i8 as u8), None)?; - } - ',' => { + match ( + self.program[pc], + self.config.enable_debug_symbols, + self.config.enable_2d_grid, + ) { + ('+', _, _) => self.tape.increment_current_cell(Wrapping(1)), + ('-', _, _) => self.tape.increment_current_cell(Wrapping(-1i8 as u8)), + (',', _, _) => { let mut buf = [0; 1]; let _ = input.read_exact(&mut buf); - self.tape.set_current_cell(Wrapping(buf[0]))?; + self.tape.set_current_cell(Wrapping(buf[0])); } - '.' => { - let buf = [self.tape.get_current_cell()?.0]; + ('.', _, _) => { + let buf = [self.tape.get_current_cell().0]; let _ = output.write(&buf); } - '>' => { - self.tape.move_head_position(1); + ('>', _, _) => { + self.tape.move_head_position((1, 0)); } - '<' => { - self.tape.move_head_position(-1); + ('<', _, _) => { + self.tape.move_head_position((-1, 0)); } - '[' => { + ('[', _, _) => { // entering a loop - if self.tape.get_current_cell()?.0 == 0 { + if self.tape.get_current_cell().0 == 0 { // skip the loop, (advance to the corresponding closing loop brace) // TODO: make this more efficient by pre-computing a loops map let mut loop_count = 1; @@ -340,8 +331,8 @@ impl BVM { loop_stack.push(pc); } } - ']' => { - if self.tape.get_current_cell()?.0 == 0 { + (']', _, _) => { + if self.tape.get_current_cell().0 == 0 { // exit the loop loop_stack.pop(); } else { @@ -350,6 +341,14 @@ impl BVM { pc = loop_stack[loop_stack.len() - 1]; } } + ('^', _, true) => self.tape.move_head_position((0, 1)), + ('v', _, true) => self.tape.move_head_position((0, -1)), + ('^', _, false) => { + r_panic!("2D Brainfuck currently disabled"); + } + ('v', _, false) => { + r_panic!("2D Brainfuck currently disabled"); + } // '#' => { // println!("{}", self.tape); // } @@ -366,6 +365,15 @@ impl BVM { // println!("{s}"); // println!("{}", self.tape); pc += 1; + + // cut the program short if it runs forever + steps += 1; + if steps > max_steps.unwrap_or(Self::MAX_STEPS_DEFAULT) { + // not sure if this should error out or just quit silently + return Err(String::from( + "Max steps reached in BVM, possibly an infinite loop.", + )); + } } Ok(()) @@ -379,25 +387,42 @@ pub mod tests { use std::io::Cursor; - pub fn run_code(code: String, input: String) -> String { - let mut bvm = BVM::new(code.chars().collect()); + pub fn run_code( + config: BVMConfig, + code: String, + input: String, + max_steps_cutoff: Option, + ) -> String { + let mut bvm = BVM::new(config, code.chars().collect()); let input_bytes: Vec = input.bytes().collect(); let mut input_stream = Cursor::new(input_bytes); let mut output_stream = Cursor::new(Vec::new()); - bvm.run(&mut input_stream, &mut output_stream).unwrap(); + bvm.run(&mut input_stream, &mut output_stream, max_steps_cutoff) + .unwrap(); // TODO: fix this unsafe stuff unsafe { String::from_utf8_unchecked(output_stream.into_inner()) } } + const BVM_CONFIG_1D: BVMConfig = BVMConfig { + enable_debug_symbols: false, + enable_2d_grid: false, + }; + const BVM_CONFIG_2D: BVMConfig = BVMConfig { + enable_debug_symbols: false, + enable_2d_grid: true, + }; #[test] fn dummy_test() { let program = String::from(""); let input = String::from(""); let desired_output = String::from(""); - assert_eq!(desired_output, run_code(program, input)) + assert_eq!( + desired_output, + run_code(BVM_CONFIG_1D, program, input, None) + ) } #[test] @@ -405,7 +430,10 @@ pub mod tests { let program = String::from("++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."); let input = String::from(""); let desired_output = String::from("Hello World!\n"); - assert_eq!(desired_output, run_code(program, input)) + assert_eq!( + desired_output, + run_code(BVM_CONFIG_1D, program, input, None) + ) } #[test] @@ -415,7 +443,10 @@ pub mod tests { ); let input = String::from(""); let desired_output = String::from("Hello, World!"); - assert_eq!(desired_output, run_code(program, input)) + assert_eq!( + desired_output, + run_code(BVM_CONFIG_1D, program, input, None) + ) } #[test] @@ -423,6 +454,112 @@ pub mod tests { let program = String::from("+++++[>+++++[>++>++>+++>+++>++++>++++<<<<<<-]<-]+++++[>>[>]<[+.<<]>[++.>>>]<[+.<]>[-.>>]<[-.<<<]>[.>]<[+.<]<-]++++++++++."); let input = String::from(""); let desired_output = String::from("eL34NfeOL454KdeJ44JOdefePK55gQ67ShfTL787KegJ77JTeghfUK88iV9:XjgYL:;:KfiJ::JYfijgZK;;k[<=]lh^L=>=KgkJ==J^gklh_K>>m`?@bnicL@A@KhmJ@@JchmnidKAA\n"); - assert_eq!(desired_output, run_code(program, input)) + assert_eq!( + desired_output, + run_code(BVM_CONFIG_1D, program, input, None) + ) + } + + #[test] + #[should_panic(expected = "2D Brainfuck currently disabled")] + fn grid_disabled_1() { + let program = String::from("++++++++[->++++++[->+>+<<]<]>>.>^+++."); + let input = String::from(""); + run_code(BVM_CONFIG_1D, program, input, None); + } + + #[test] + #[should_panic(expected = "2D Brainfuck currently disabled")] + fn grid_disabled_2() { + let program = + String::from("++++++++[->^^^+++vvvv+++[->^^^^+>+^^^^^^^^>.>vvvv+++."); + let input = String::from(""); + run_code(BVM_CONFIG_1D, program, input, None); + } + + // 2D tests: + #[test] + fn grid_regression_1() { + // hello world + let program = String::from("++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."); + let input = String::from(""); + let desired_output = String::from("Hello World!\n"); + assert_eq!( + desired_output, + run_code(BVM_CONFIG_2D, program, input, None) + ) + } + + #[test] + fn grid_regression_2() { + // random mess + let program = String::from("+++++[>+++++[>++>++>+++>+++>++++>++++<<<<<<-]<-]+++++[>>[>]<[+.<<]>[++.>>>]<[+.<]>[-.>>]<[-.<<<]>[.>]<[+.<]<-]++++++++++."); + let input = String::from(""); + let desired_output = String::from("eL34NfeOL454KdeJ44JOdefePK55gQ67ShfTL787KegJ77JTeghfUK88iV9:XjgYL:;:KfiJ::JYfijgZK;;k[<=]lh^L=>=KgkJ==J^gklh_K>>m`?@bnicL@A@KhmJ@@JchmnidKAA\n"); + assert_eq!( + desired_output, + run_code(BVM_CONFIG_2D, program, input, None) + ) + } + + #[test] + fn grid_basic_1() { + let program = String::from("++++++++[-^++++++[->+v+<^]v]>+++++^.v."); + let input = String::from(""); + let desired_output = String::from("05"); + assert_eq!( + desired_output, + run_code(BVM_CONFIG_2D, program, input, None) + ) + } + + #[test] + fn grid_mover_1() { + let program = String::from( + "-<<<<<<<<<<<<^^^^^^^^^^^^-<^++++++++[->>vv+[->v+]->v++++++<^<^+[-<^+]-<^]>>vv+[->v+]->v...", + ); + let input = String::from(""); + let desired_output = String::from("000"); + assert_eq!( + desired_output, + run_code(BVM_CONFIG_2D, program, input, None) + ) + } + + #[test] + fn grid_bfception_1() { + // run a hello world program within a 1d brainfuck interpreter implemented in 2d brainfuck + let program = String::from("-v>,[>,]^-<+[-<+]->+[-v------------------------------------------^>+]-<+[-<+]->+[-v[-^+^+vv]^[-v+^]^->+<[>-<->+<[>-<->+<[>-<->+<[>-<-------------->+<[>-<-->+<[>-<----------------------------->+<[>-<-->+<[>-[[-]<[-]vv[-]++++++v++^^^>]<[-]]>[[-]<[-]vv[-]+++++v+^^^>]<[-]]>[[-]<[-]vv[-]+++^^>]<[-]]>[[-]<[-]vv[-]++++^^>]<[-]]>[[-]<[-]vv[-]+++++++^^>]<[-]]>[[-]<[-]vv[-]++^^>]<[-]]>[[-]<[-]vv[-]++++++++^^>]<[-]]>[[-]<[-]vv[-]+^^>]+]-v-v-v-v-^^^^<+[-<+]<->v-v-v<-v->^^^^>vvv+^^^<+>+[-<->+v[-^^+^+vvv]^^[-vv+^^]^>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<[-]]>[--[+>-]+v,^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v.^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v[-v+v+^^]v[-^+v]v[[-]^^^+[-<+]-^^^+[->+]-<+[>>-[+>-]<+vv[-^^^+^+vvvv]^^^[-vvv+^^^]^->+<[>-<->+<[>-<[-]]>[->-[+>-]+^^>]<]>[->-[+>-]+^^>]vvv+[-<+]->-[+>-]+vvv]^^^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v[-v+v+^^]v[-^+v]v>+<[>-<[-]]>[-<^^^+[-<+]-^^^+[->+]-<+[>>-[+>-]>+vv[-^^^+^+vvvv]^^^[-vvv+^^^]^->+<[>-<->+<[>-<[-]]>[->-[+>-]+^^>]<]>[->-[+>-]+^^>]vvv+[-<+]->-[+>-]+vvv>]<^^^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+<<-v-^>+v+^[<+v+^>-v-^]+>-+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+>>-v-^<+v+^[>+v+^<-v-^]+<-+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v-^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v+^+[-<+]-<^^^+[->+]->-[+>-]+^^>]+]-"); + let input = String::from("++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.\n"); + let desired_output = String::from("Hello World!\n"); + assert_eq!( + desired_output, + run_code(BVM_CONFIG_2D, program, input, None) + ) + } + + #[test] + fn grid_bfception_2() { + // random mess + let program = String::from("-v>,[>,]^-<+[-<+]->+[-v------------------------------------------^>+]-<+[-<+]->+[-v[-^+^+vv]^[-v+^]^->+<[>-<->+<[>-<->+<[>-<->+<[>-<-------------->+<[>-<-->+<[>-<----------------------------->+<[>-<-->+<[>-[[-]<[-]vv[-]++++++v++^^^>]<[-]]>[[-]<[-]vv[-]+++++v+^^^>]<[-]]>[[-]<[-]vv[-]+++^^>]<[-]]>[[-]<[-]vv[-]++++^^>]<[-]]>[[-]<[-]vv[-]+++++++^^>]<[-]]>[[-]<[-]vv[-]++^^>]<[-]]>[[-]<[-]vv[-]++++++++^^>]<[-]]>[[-]<[-]vv[-]+^^>]+]-v-v-v-v-^^^^<+[-<+]<->v-v-v<-v->^^^^>vvv+^^^<+>+[-<->+v[-^^+^+vvv]^^[-vv+^^]^>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<[-]]>[--[+>-]+v,^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v.^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v[-v+v+^^]v[-^+v]v[[-]^^^+[-<+]-^^^+[->+]-<+[>>-[+>-]<+vv[-^^^+^+vvvv]^^^[-vvv+^^^]^->+<[>-<->+<[>-<[-]]>[->-[+>-]+^^>]<]>[->-[+>-]+^^>]vvv+[-<+]->-[+>-]+vvv]^^^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v[-v+v+^^]v[-^+v]v>+<[>-<[-]]>[-<^^^+[-<+]-^^^+[->+]-<+[>>-[+>-]>+vv[-^^^+^+vvvv]^^^[-vvv+^^^]^->+<[>-<->+<[>-<[-]]>[->-[+>-]+^^>]<]>[->-[+>-]+^^>]vvv+[-<+]->-[+>-]+vvv>]<^^^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+<<-v-^>+v+^[<+v+^>-v-^]+>-+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+>>-v-^<+v+^[>+v+^<-v-^]+<-+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v-^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v+^+[-<+]-<^^^+[->+]->-[+>-]+^^>]+]-"); + let input = String::from("+++++[>+++++[>++>++>+++>+++>++++>++++<<<<<<-]<-]+++++[>>[>]<[+.<<]>[++.>>>]<[+.<]>[-.>>]<[-.<<<]>[.>]<[+.<]<-]++++++++++.\n"); + let desired_output = String::from("eL34NfeOL454KdeJ44JOdefePK55gQ67ShfTL787KegJ77JTeghfUK88iV9:XjgYL:;:KfiJ::JYfijgZK;;k[<=]lh^L=>=KgkJ==J^gklh_K>>m`?@bnicL@A@KhmJ@@JchmnidKAA\n"); + assert_eq!( + desired_output, + run_code(BVM_CONFIG_2D, program, input, None) + ) + } + + #[test] + fn test_bf2d_code() { + let program = String::from( + ",.[-]+[--^-[^^+^-----vv]v--v---]^-.^^^+.^^..+++[.^]vvvv.+++.------.vv-.^^^^+.", + ); + let input = String::from(""); + let desired_output = String::from("\0Hello, World!"); + assert_eq!( + desired_output, + run_code(BVM_CONFIG_2D, program, input, None) + ) } } diff --git a/compiler/src/brainfuck_optimiser.rs b/compiler/src/brainfuck_optimiser.rs index 318e657..0072896 100644 --- a/compiler/src/brainfuck_optimiser.rs +++ b/compiler/src/brainfuck_optimiser.rs @@ -1,13 +1,13 @@ -use std::{collections::HashMap, num::Wrapping}; - use crate::builder::Opcode; +use itertools::Itertools; +use std::{collections::HashMap, num::Wrapping}; // post-compilation optimisations // simple naive brainfuck optimisations // TODO: factor in [-] into optimisations (doing) -pub fn optimise(program: Vec) -> Vec { +pub fn optimise(program: Vec, exhaustive: bool) -> Vec { let mut output = Vec::new(); // get stretch of characters to optimise (+-<>) @@ -16,12 +16,18 @@ pub fn optimise(program: Vec) -> Vec { while i < program.len() { let op = program[i]; match op { - Opcode::Add | Opcode::Subtract | Opcode::Right | Opcode::Left | Opcode::Clear => { + Opcode::Add + | Opcode::Subtract + | Opcode::Right + | Opcode::Left + | Opcode::Clear + | Opcode::Up + | Opcode::Down => { subset.push(op); } Opcode::OpenLoop | Opcode::CloseLoop | Opcode::Input | Opcode::Output => { // optimise subset and push - let optimised_subset = optimise_subset(subset); + let optimised_subset = optimise_subset(subset, exhaustive); output.extend(optimised_subset); subset = Vec::new(); @@ -34,16 +40,45 @@ pub fn optimise(program: Vec) -> Vec { output } -fn optimise_subset(run: Vec) -> Vec { +fn move_position( + mut program: Vec, + old_position: &(i32, i32), + new_position: &(i32, i32), +) -> Vec { + if old_position != new_position { + if old_position.0 < new_position.0 { + for _ in 0..(new_position.0 - old_position.0) { + program.push(Opcode::Right); + } + } else { + for _ in 0..(old_position.0 - new_position.0) { + program.push(Opcode::Left); + } + } + if old_position.1 < new_position.1 { + for _ in 0..(new_position.1 - old_position.1) { + program.push(Opcode::Up); + } + } else { + for _ in 0..(old_position.1 - new_position.1) { + program.push(Opcode::Down); + } + } + } + program +} + +fn optimise_subset(run: Vec, exhaustive: bool) -> Vec { #[derive(Clone)] enum Change { Add(Wrapping), Set(Wrapping), } - let mut tape: HashMap = HashMap::new(); - let mut head: i32 = 0; - + let mut tape: HashMap<(i32, i32), Change> = HashMap::new(); + let start = (0, 0); + let mut head = (0, 0); let mut i = 0; + //Generate a map of cells we change and how we plan to change them while i < run.len() { let op = run[i]; match op { @@ -72,150 +107,100 @@ fn optimise_subset(run: Vec) -> Vec { } } Opcode::Right => { - head += 1; + head.0 += 1; } Opcode::Left => { - head -= 1; + head.0 -= 1; + } + Opcode::Up => { + head.1 += 1; + } + Opcode::Down => { + head.1 -= 1; } _ => (), } i += 1; } - // always have a start and end cell - if !tape.contains_key(&0) { - tape.insert(0, Change::Add(Wrapping(0i8))); - } - if !tape.contains_key(&head) { - tape.insert(head, Change::Add(Wrapping(0i8))); - } - - // This whole algorithm is probably really efficient and I reckon there's almost certainly a better way - // It's also just really poorly done in general, I don't understand what everything does and I wrote the damned thing - // TODO: refactor this properly - // convert hashmap to array - // start by making a negative and positive array - let mut pos_arr = Vec::new(); - let mut neg_arr = Vec::new(); - for (cell, value) in tape.into_iter() { - let i: usize; - let arr: &mut Vec; - if cell < 0 { - i = (-(cell + 1)) as usize; - arr = &mut neg_arr; - } else { - i = cell as usize; - arr = &mut pos_arr; - } - - if i >= arr.len() { - arr.resize(i + 1, Change::Add(Wrapping(0i8))); - } - arr[i] = value; - } - let start_index = neg_arr.len(); - // now combine the two arrays - let mut tape_arr: Vec = Vec::new(); - tape_arr.extend(neg_arr.into_iter().rev()); - tape_arr.extend(pos_arr.into_iter()); - - if ((start_index) + 1) >= (tape_arr.len()) { - tape_arr.resize(start_index + 1, Change::Add(Wrapping(0i8))); - } - let final_index = ((start_index as i32) + head) as usize; - let mut output = Vec::new(); - - // Also this following algorithm for zig-zagging around the tape is pretty poor as well, there has to be a nicer way of doing it - - // if final cell is to the right of the start cell then we need to go to the left first, and vice-versa - // 1. go to the furthest point on the tape (opposite of direction to final cell) - // 2. go from that end of the tape to the opposite end of the tape - // 3. return to the final cell - let mut idx = start_index; - let d2 = final_index >= start_index; - let d1 = !d2; - let d3 = !d2; - - //1 - match d1 { - true => { - for _ in start_index..(tape_arr.len() - 1) { - output.push(Opcode::Right); - idx += 1; + if exhaustive { + //Exhaustive approach checks all permutations + let mut output_length = i32::MAX; + let mut best_permutation = Vec::new(); + for perm in tape.iter().permutations(tape.len()) { + let mut position = start; + let mut current_output_length = 0; + //Calculate the distance of this + for (cell, _) in &perm { + current_output_length += (cell.0 - position.0).abs(); + current_output_length += (cell.1 - position.1).abs(); + position = **cell; + if current_output_length > output_length { + break; + } } - } - false => { - for _ in (1..=start_index).rev() { - output.push(Opcode::Left); - idx -= 1; + if current_output_length > output_length { + continue; + } + //Add the distance to the finishing location + current_output_length += (head.0 - position.0).abs(); + current_output_length += (head.1 - position.1).abs(); + if current_output_length < output_length { + best_permutation = perm; + output_length = current_output_length; } } - } - - //2 - match d2 { - true => { - for cell in idx..tape_arr.len() { - let change = &tape_arr[cell]; - if let Change::Set(_) = change { - output.push(Opcode::Clear); - } - let (Change::Add(v) | Change::Set(v)) = change; - let v = v.0; - - for _ in 0..v.abs() { - output.push(match v > 0 { - true => Opcode::Add, - false => Opcode::Subtract, - }); - } - - if cell < (tape_arr.len() - 1) { - output.push(Opcode::Right); - idx += 1; - } + let mut position = start; + for (cell, change) in best_permutation { + output = move_position(output, &position, cell); + position = *cell; + if let Change::Set(_) = change { + output.push(Opcode::Clear); + } + let (Change::Add(v) | Change::Set(v)) = change; + let v = v.0; + for _ in 0..(v as i32).abs() { + output.push(match v == -128 || v > 0 { + true => Opcode::Add, + false => Opcode::Subtract, + }); } } - false => { - for cell in (0..=idx).rev() { - let change = &tape_arr[cell]; + output = move_position(output, &position, &head); + } else { + //Greedy approach faster for bigger datasets + let mut position = start; + //For the number of cells navigate to the nearest cell + for _ in 0..tape.len() { + if !tape.is_empty() { + let mut min_distance = i32::MAX; + let mut next_position = (0, 0); + for (cell, _value) in tape.iter() { + if (cell.0 - position.0).abs() + (cell.1 - position.1).abs() < min_distance { + min_distance = (cell.0 - position.0).abs() + (cell.1 - position.1).abs(); + next_position = *cell; + } + } + // Move to next position + output = move_position(output, &position, &next_position); + position = next_position; + //Now Update the output with correct opcodes + let change = tape.remove(&next_position).unwrap(); if let Change::Set(_) = change { output.push(Opcode::Clear); } let (Change::Add(v) | Change::Set(v)) = change; let v = v.0; - - for _ in 0..v.abs() { - output.push(match v > 0 { + for _ in 0..(v as i32).abs() { + output.push(match v == -128 || v > 0 { true => Opcode::Add, false => Opcode::Subtract, }); } - - if cell > 0 { - output.push(Opcode::Left); - idx -= 1; - } } } + output = move_position(output, &position, &head); } - - //3 - match d3 { - true => { - for _ in idx..final_index { - output.push(Opcode::Right); - idx += 1; - } - } - false => { - for _ in final_index..idx { - output.push(Opcode::Left); - idx -= 1; - } - } - } - output } @@ -226,55 +211,287 @@ mod tests { use super::*; #[test] - fn subset_equivalence_test_0() { + fn greedy_subset_equivalence_test_0() { let v = BrainfuckOpcodes::from_str("+++>><<++>--->+++<><><><><<<<<+++"); //(3) 0 0 [5] -3 3 - let o = optimise_subset(v).to_string(); - assert_eq!(o, ">>+++<---<+++++<<<+++"); + let o = optimise_subset(v, false).to_string(); + assert_eq!(o, "+++++>--->+++<<<<<+++"); } #[test] - fn program_equivalence_test_0() { + fn greedy_program_equivalence_test_0() { let v = BrainfuckOpcodes::from_str("<><><>++<+[--++>>+<<-]"); - let o: String = optimise(v).to_string(); + let o: String = optimise(v, false).to_string(); assert_eq!(o, "++<+[->>+<<]"); } #[test] - fn program_equivalence_test_1() { + fn greedy_program_equivalence_test_1() { let v = BrainfuckOpcodes::from_str( "+++++++++>>+++>---->>>++++--<--++<>++<+<->]++--->+", ); // [9] 0 (7) -4 0 0 2 // [(0)] 2 // -1 1 - let o: String = optimise(v).to_string(); + let o: String = optimise(v, false).to_string(); assert_eq!(o, "+++++++++>>+++++++>---->>>++<<<<[>++<]"); } #[test] - fn program_equivalence_test_2() { + fn greedy_program_equivalence_test_2() { let v = BrainfuckOpcodes::from_str(">><."); - let o: String = optimise(v).to_string(); + let o: String = optimise(v, false).to_string(); assert_eq!(o, ">."); } #[test] - fn subset_equivalence_test_1() { + fn greedy_subset_equivalence_test_1() { let v = BrainfuckOpcodes::from_str("+++<+++>[-]+++"); //(3) 0 0 [5] -3 3 - let o = optimise_subset(v).to_string(); - assert_eq!(o, "<+++>[-]+++"); + let o = optimise_subset(v, false).to_string(); + assert_eq!(o, "[-]+++<+++>"); } #[test] - fn subset_equivalence_test_2() { + fn greedy_subset_equivalence_test_2() { let v = BrainfuckOpcodes::from_str("+++<+++>[-]+++[-]<[-]--+>-"); //(3) 0 0 [5] -3 3 - let o = optimise_subset(v).to_string(); - assert_eq!(o, "<[-]->[-]-"); + let o = optimise_subset(v, false).to_string(); + assert_eq!(o, "[-]-<[-]->"); } #[test] - fn program_equivalence_test_3() { + fn greedy_program_equivalence_test_3() { let v = BrainfuckOpcodes::from_str( "+++++[-]+++++++++>>+++>---->>>++++--<--++<>++<+<->]++--->+", ); // [9] 0 (7) -4 0 0 2 // [(0)] 2 // -1 1 - let o: String = optimise(v).to_string(); + let o: String = optimise(v, false).to_string(); assert_eq!(o, "[-]+++++++++>>+++++++>---->>>++<<<<[[-]+>++<]"); } + + #[test] + fn greedy_two_dimensional_subset_equivalence_test_0() { + let v = BrainfuckOpcodes::from_str("+++^^vv++^---^+++v^v^v^v^vvvvv+++"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, false).to_string(); + assert_eq!(o, "+++++^---^+++vvvvv+++"); + } + + #[test] + fn greedy_two_dimensional_program_equivalence_test_0() { + let v = BrainfuckOpcodes::from_str("v^v^v^++v+[--++^^+vv-]"); + let o: String = optimise(v, false).to_string(); + assert_eq!(o, "++v+[-^^+vv]"); + } + + #[test] + fn greedy_two_dimensional_program_equivalence_test_1() { + let v = BrainfuckOpcodes::from_str( + "+++++++++^^+++^----^^^++++--v--++vvhellov++++[-v+^^++v+v-^]++---^+", + ); // [9] 0 (7) -4 0 0 2 // [(0)] 2 // -1 1 + let o: String = optimise(v, false).to_string(); + assert_eq!(o, "+++++++++^^+++++++^----^^^++vvvv[^++v]"); + } + + #[test] + fn greedy_two_dimensional_program_equivalence_test_2() { + let v = BrainfuckOpcodes::from_str("^^v."); + let o: String = optimise(v, false).to_string(); + assert_eq!(o, "^."); + } + + #[test] + fn greedy_two_dimensional_subset_equivalence_test_1() { + let v = BrainfuckOpcodes::from_str("+++v+++^[-]+++"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, false).to_string(); + assert_eq!(o, "[-]+++v+++^"); + } + + #[test] + fn greedy_two_dimensional_subset_equivalence_test_2() { + let v = BrainfuckOpcodes::from_str("+++v+++^[-]+++[-]v[-]--+^-"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, false).to_string(); + assert_eq!(o, "[-]-v[-]-^"); + } + + #[test] + fn greedy_two_dimensional_program_equivalence_test_3() { + let v = BrainfuckOpcodes::from_str( + "+++++[-]+++++++++^^+++^----^^^++++--v--++vvhellov++++[[-]v+^^++v+v-^]++---^+", + ); // [9] 0 (7) -4 0 0 2 // [(0)] 2 // -1 1 + let o: String = optimise(v, false).to_string(); + assert_eq!(o, "[-]+++++++++^^+++++++^----^^^++vvvv[[-]+^++v]"); + } + + #[test] + #[ignore] + fn exhaustive_subset_equivalence_test_0() { + let v = BrainfuckOpcodes::from_str("+++>><<++>--->+++<><><><><<<<<+++"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, true).to_string(); + assert_eq!(o, ">--->+++<<+++++<<<+++"); + } + + #[test] + #[ignore] + fn exhaustive_program_equivalence_test_0() { + let v = BrainfuckOpcodes::from_str("<><><>++<+[--++>>+<<-]"); + let o: String = optimise(v, true).to_string(); + assert_eq!(o, "++<+[>>+<<-]"); + } + + #[test] + #[ignore] + fn exhaustive_program_equivalence_test_1() { + let v = BrainfuckOpcodes::from_str( + "+++++++++>>+++>---->>>++++--<--++<>++<+<->]++--->+", + ); // [9] 0 (7) -4 0 0 2 // [(0)] 2 // -1 1 + let o: String = optimise(v, true).to_string(); + assert_eq!(o, "+++++++++>>+++++++>>>>++<<<----<[>++<]"); + } + + #[test] + #[ignore] + fn exhaustive_program_equivalence_test_2() { + let v = BrainfuckOpcodes::from_str(">><."); + let o: String = optimise(v, true).to_string(); + assert_eq!(o, ">."); + } + + #[test] + #[ignore] + fn exhaustive_subset_equivalence_test_1() { + let v = BrainfuckOpcodes::from_str("+++<+++>[-]+++"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, true).to_string(); + assert_eq!(o, "[-]+++<+++>"); + } + + #[test] + #[ignore] + fn exhaustive_subset_equivalence_test_2() { + let v = BrainfuckOpcodes::from_str("+++<+++>[-]+++[-]<[-]--+>-"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, true).to_string(); + assert_eq!(o, "[-]-<[-]->"); + } + + #[test] + #[ignore] + fn exhaustive_program_equivalence_test_3() { + let v = BrainfuckOpcodes::from_str( + "+++++[-]+++++++++>>+++>---->>>++++--<--++<>++<+<->]++--->+", + ); // [9] 0 (7) -4 0 0 2 // [(0)] 2 // -1 1 + let o: String = optimise(v, true).to_string(); + assert_eq!(o, "[-]+++++++++>>+++++++>---->>>++<<<<[[-]+>++<]"); + } + + #[test] + #[ignore] + fn exhaustive_two_dimensional_subset_equivalence_test_0() { + let v = BrainfuckOpcodes::from_str("+++^^vv++^---^+++v^v^v^v^vvvvv+++"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, true).to_string(); + assert_eq!(o, "^^+++v---v+++++vvv+++"); + } + + #[test] + #[ignore] + fn exhaustive_two_dimensional_program_equivalence_test_0() { + let v = BrainfuckOpcodes::from_str("v^v^v^++v+[--++^^+vv-]"); + let o: String = optimise(v, true).to_string(); + assert_eq!(o, "++v+[^^+vv-]"); + } + + #[test] + #[ignore] + fn exhaustive_two_dimensional_program_equivalence_test_1() { + let v = BrainfuckOpcodes::from_str( + "+++++++++^^+++^----^^^++++--v--++vvhellov++++[-v+^^++v+v-^]++---^+", + ); // [9] 0 (7) -4 0 0 2 // [(0)] 2 // -1 1 + let o: String = optimise(v, true).to_string(); + assert_eq!(o, "+++++++++^^+++++++^----^^^++vvvv[^++v]"); + } + + #[test] + #[ignore] + fn exhaustive_two_dimensional_program_equivalence_test_2() { + let v = BrainfuckOpcodes::from_str("^^v."); + let o: String = optimise(v, true).to_string(); + assert_eq!(o, "^."); + } + + #[test] + #[ignore] + fn exhaustive_two_dimensional_subset_equivalence_test_1() { + let v = BrainfuckOpcodes::from_str("+++v+++^[-]+++"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, true).to_string(); + assert_eq!(o, "[-]+++v+++^"); + } + + #[test] + #[ignore] + fn exhaustive_two_dimensional_subset_equivalence_test_2() { + let v = BrainfuckOpcodes::from_str("+++v+++^[-]+++[-]v[-]--+^-"); //(3) 0 0 [5] -3 3 + let o = optimise_subset(v, true).to_string(); + assert_eq!(o, "[-]-v[-]-^"); + } + + #[test] + #[ignore] + fn exhaustive_two_dimensional_program_equivalence_test_3() { + let v = BrainfuckOpcodes::from_str( + "+++++[-]+++++++++^^+++^----^^^++++--v--++vvhellov++++[[-]v+^^++v+v-^]++---^+", + ); // [9] 0 (7) -4 0 0 2 // [(0)] 2 // -1 1 + let o: String = optimise(v, true).to_string(); + assert_eq!(o, "[-]+++++++++^^^^^^++vvv----v+++++++[^++v[-]+]"); + } + + fn subset_edge_case_0() { + let v = BrainfuckOpcodes::from_str( + "-++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++", + ); + let o: String = optimise_subset(v, false).to_string(); + println!("{o}"); + assert_eq!(o.len(), 127); + } + + #[test] + fn subset_edge_case_1() { + let v = BrainfuckOpcodes::from_str( + "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++", + ); + let o: String = optimise_subset(v, false).to_string(); + println!("{o}"); + assert_eq!(o.len(), 128); + } + + #[test] + fn subset_edge_case_2() { + let v = BrainfuckOpcodes::from_str( + "+--------------------------------------------------------------------------------------------------------------------------------" + ); + let o: String = optimise_subset(v, false).to_string(); + println!("{o}"); + assert_eq!(o.len(), 127); + } + + #[test] + fn subset_edge_case_3() { + let v = BrainfuckOpcodes::from_str( + "--------------------------------------------------------------------------------------------------------------------------------" + ); + let o: String = optimise_subset(v, false).to_string(); + println!("{o}"); + assert_eq!(o.len(), 128); + } + + #[test] + fn subset_edge_case_3a() { + let v = BrainfuckOpcodes::from_str( + "- --------------------------------------------------------------------------------------------------------------------------------" + ); + let o: String = optimise_subset(v, false).to_string(); + println!("{o}"); + assert_eq!(o.len(), 127); + } + + #[test] + fn subset_edge_case_4() { + let v = BrainfuckOpcodes::from_str( + "[-]--------------------------------------------------------------------------------------------------------------------------------" + ); + let o: String = optimise_subset(v, false).to_string(); + println!("{o}"); + assert_eq!(o.len(), 131); + } } diff --git a/compiler/src/builder.rs b/compiler/src/builder.rs index 91108a9..bf35cc4 100644 --- a/compiler/src/builder.rs +++ b/compiler/src/builder.rs @@ -11,7 +11,7 @@ use std::{ }; use crate::{ - compiler::{Instruction, MemoryId}, + compiler::{CellLocation, Instruction, MemoryId}, constants_optimiser::calculate_optimal_addition, macros::macros::{r_assert, r_panic}, MastermindConfig, @@ -22,7 +22,7 @@ pub struct Builder<'a> { } type LoopDepth = usize; -pub type TapeCell = i32; +pub type TapeCell = (i32, i32); type TapeValue = u8; impl Builder<'_> { @@ -59,10 +59,14 @@ impl Builder<'_> { } match instruction { // the ids (indices really) given by the compiler are guaranteed to be unique (at the time of writing) - // however they will absolutely not be very efficient + // however they will absolutely not be very efficient if used directly as cell locations Instruction::Allocate(memory, location_specifier) => { - let cell = allocator.allocate(location_specifier, memory.len())?; - let old = alloc_map.insert( + let cell = allocator.allocate( + location_specifier, + memory.len(), + self.config.memory_allocation_method, + )?; + let None = alloc_map.insert( memory.id(), ( cell, @@ -70,9 +74,7 @@ impl Builder<'_> { current_loop_depth, vec![Some(0); memory.len()], ), - ); - - let None = old else { + ) else { r_panic!("Attempted to reallocate memory {memory:#?}"); }; } @@ -136,7 +138,7 @@ outside of loop it was allocated" mem_idx < *size, "Attempted to access memory outside of allocation" ); - let cell = *cell_base + mem_idx as i32; + let cell = (cell_base.0 + mem_idx as i32, cell_base.1); let known_value = &mut known_values[mem_idx]; let mut open = true; @@ -173,7 +175,7 @@ outside of loop it was allocated" mem_idx < *size, "Attempted to access memory outside of allocation" ); - let cell = *cell_base + mem_idx as i32; + let cell = (cell_base.0 + mem_idx as i32, cell_base.1); let known_value = &mut known_values[mem_idx]; let Some(stack_cell) = loop_stack.pop() else { @@ -204,7 +206,7 @@ outside of loop it was allocated" mem_idx < *size, "Attempted to access memory outside of allocation" ); - let cell = *cell_base + mem_idx as i32; + let cell = (cell_base.0 + mem_idx as i32, cell_base.1); let known_value = &mut known_values[mem_idx]; // TODO: fix bug, if only one multiplication then we can have a value already in the cell, but never otherwise @@ -252,7 +254,7 @@ outside of loop it was allocated" mem_idx < *size, "Attempted to access memory outside of allocation" ); - let cell = *cell_base + mem_idx as i32; + let cell = (cell_base.0 + mem_idx as i32, cell_base.1); let known_value = &mut known_values[mem_idx]; ops.move_to_cell(cell); @@ -273,7 +275,7 @@ outside of loop it was allocated" mem_idx < *size, "Attempted to access memory outside of allocation" ); - let cell = *cell_base + mem_idx as i32; + let cell = (cell_base.0 + mem_idx as i32, cell_base.1); let known_value = &mut known_values[mem_idx]; ops.move_to_cell(cell); @@ -321,34 +323,33 @@ outside of loop it was allocated" mem_idx < *size, "Attempted to access memory outside of allocation" ); - let cell = *cell_base + mem_idx as i32; + let cell = (cell_base.0 + mem_idx as i32, cell_base.1); ops.move_to_cell(cell); ops.push(Opcode::Output); } Instruction::InsertBrainfuckAtCell(operations, location_specifier) => { - // I don't think this is a good idea (moving to an unallocated cell to run brainfuck) - // ops.move_to_cell(location_specifier.unwrap_or({ - // // default position to run brainfuck in? - // // not sure what is best here, probably the last cell in the allocation tape - // let mut first_unallocated = None; - // for i in 0..alloc_tape.len() { - // match (first_unallocated, &alloc_tape[i]) { - // (None, false) => { - // first_unallocated = Some(i); - // } - // (Some(_), true) => { - // first_unallocated = None; - // } - // _ => (), - // } - // } - - // first_unallocated.unwrap_or(alloc_tape.len()) - // })); - if let Some(loc) = location_specifier { - ops.move_to_cell(loc); + // move to the correct cell, based on the location specifier + match location_specifier { + CellLocation::FixedCell(cell) => ops.move_to_cell(cell), + CellLocation::MemoryCell(cell_obj) => { + let Some((cell_base, size, _alloc_loop_depth, _known_values)) = + alloc_map.get(&cell_obj.memory_id) + else { + r_panic!("Attempted to use location of cell {cell_obj:#?} which could not be found"); + }; + let mem_idx = cell_obj.index.unwrap_or(0); + r_assert!( + mem_idx < *size, + "Attempted to access memory outside of allocation" + ); + let cell = (cell_base.0 + mem_idx as i32, cell_base.1); + ops.move_to_cell(cell); + } + CellLocation::Unspecified => (), } + + // paste the in-line BF operations ops.extend(operations); } } @@ -356,7 +357,7 @@ outside of loop it was allocated" // this is used in embedded brainfuck contexts to preserve head position if return_to_origin { - ops.move_to_cell(0); + ops.move_to_cell((0, 0)); } Ok(ops.opcodes) @@ -364,7 +365,7 @@ outside of loop it was allocated" } struct CellAllocator { - alloc_map: HashSet, + alloc_map: HashSet, } // allocator will not automatically allocate negative-index cells @@ -376,28 +377,161 @@ impl CellAllocator { } } - fn allocate(&mut self, location: Option, size: usize) -> Result { - // should the region start at the current tape head? - let mut region_start = location.unwrap_or(0); - - for i in region_start.. { - if self.alloc_map.contains(&i) { - // if a specifier was set, it is invalid so throw an error - // TODO: not sure if this should throw here or not - if let Some(l) = location { - r_panic!("Location specifier @{l} conflicts with another allocation"); - }; - // reset search to start at next cell - region_start = i + 1; - } else if i - region_start == (size as i32 - 1) { - break; + /// Checks if the memory size can be allocated to the right of a given location e.g. arrays + fn check_allocatable(&mut self, location: &TapeCell, size: usize) -> bool { + for k in 0..size { + if self + .alloc_map + .contains(&(location.0 + k as i32, location.1)) + { + return false; + } + } + return true; + } + + /// Will either check a specific location can be allocated at the chosen size or if no location is + /// provided it will find a memory location where this size can be allocated + /// Uses a variety of memory allocation methods based on settings + fn allocate( + &mut self, + location: Option, + size: usize, + method: u8, + ) -> Result { + let mut region_start = location.unwrap_or((0, 0)); + //Check specified memory allocation above to ensure that this works nicely with all algorithms + if let Some(l) = location { + if !self.check_allocatable(&l, size) { + r_panic!( + "Location specifier @{0},{1} conflicts with another allocation", + l.0, + l.1 + ); + } + } else { + // should the region start at the current tape head? + if method == 0 { + for i in region_start.0.. { + if self.alloc_map.contains(&(i, region_start.1)) { + region_start = (i + 1, region_start.1); + } else if i - region_start.0 == (size as i32 - 1) { + break; + } + } + } else if method == 1 { + //Zig Zag + let mut found = false; + let mut loops = 0; + let mut i; + let mut j; + while !found { + i = region_start.0 + loops; + j = region_start.1; + for _ in 0..=loops { + if self.check_allocatable(&(i, j), size) { + found = true; + region_start = (i, j); + break; + } + i = i - 1; + j = j + 1; + } + loops += 1; + } + } else if method == 2 { + //Spiral + let mut found = false; + let mut loops = 1; + let directions = ['N', 'E', 'S', 'W']; + let mut i = region_start.0; + let mut j = region_start.1; + while !found { + for dir in directions { + match dir { + 'N' => { + for _ in 0..loops { + j += 1; + if self.check_allocatable(&(i, j), size) { + found = true; + region_start = (i, j); + break; + } + } + } + 'E' => { + for _ in 0..loops { + i += 1; + if self.check_allocatable(&(i, j), size) { + found = true; + region_start = (i, j); + break; + } + } + } + 'S' => { + for _ in 0..loops { + j -= 1; + if self.check_allocatable(&(i, j), size) { + found = true; + region_start = (i, j); + break; + } + } + } + 'W' => { + for _ in 0..loops { + i -= 1; + if self.check_allocatable(&(i, j), size) { + found = true; + region_start = (i, j); + break; + } + } + } + _ => {} + } + if found { + break; + } + } + if found { + break; + } + i -= 1; + j -= 1; + loops += 2; + } + } else if method == 3 { + //Tiles + let mut found = false; + let mut loops = 0; + while !found { + for i in -loops..=loops { + for j in -loops..=loops { + if self + .check_allocatable(&(region_start.0 + i, region_start.1 + j), size) + { + found = true; + region_start = (region_start.0 + i, region_start.1 + j); + break; + } + } + if found { + break; + } + } + loops += 1; + } + } else { + panic!("Memory Allocation Method not implemented"); } } // make all cells in the specified region allocated - for i in region_start..(region_start + size as i32) { - if !self.alloc_map.contains(&i) { - self.alloc_map.insert(i); + for i in region_start.0..(region_start.0 + size as i32) { + if !self.alloc_map.contains(&(i, region_start.1)) { + self.alloc_map.insert((i, region_start.1)); } } @@ -415,31 +549,34 @@ impl CellAllocator { // alternate left then right, getting further and further out // there is surely a nice one liner rusty iterator way of doing it but somehow this is clearer until I learn that - let mut left_iter = (0..location).rev(); - let mut right_iter = (location + 1)..; + let mut left_iter = (0..location.0).rev(); + let mut right_iter = (location.0 + 1)..; loop { if let Some(i) = left_iter.next() { // unallocated cell, allocate it and return - if self.alloc_map.insert(i) { - return i; + if self.alloc_map.insert((i, location.1)) { + return (i, location.1); + } else { } } if let Some(i) = right_iter.next() { - if self.alloc_map.insert(i) { - return i; + if self.alloc_map.insert((i, location.1)) { + return (i, location.1); } } } } fn free(&mut self, cell: TapeCell, size: usize) -> Result<(), String> { - for i in cell..(cell + size as i32) { + for i in cell.0..(cell.0 + size as i32) { r_assert!( - self.alloc_map.contains(&i), - "Cannot free cell {i} as it is not allocated." + self.alloc_map.contains(&(i, cell.1)), + "Cannot free cell @{0},{1} as it is not allocated.", + i, + cell.1 ); - self.alloc_map.remove(&i); + self.alloc_map.remove(&(i, cell.1)); } Ok(()) @@ -457,11 +594,13 @@ pub enum Opcode { Output, Input, Clear, + Up, + Down, } pub struct BrainfuckCodeBuilder { opcodes: Vec, - pub head_pos: i32, + pub head_pos: TapeCell, } pub trait BrainfuckOpcodes { @@ -483,6 +622,8 @@ impl BrainfuckOpcodes for Vec { Opcode::Output => ".", Opcode::Input => ",", Opcode::Clear => "[-]", + Opcode::Up => "^", + Opcode::Down => "v", }) }); s @@ -506,6 +647,8 @@ impl BrainfuckOpcodes for Vec { ']' => ops.push(Opcode::CloseLoop), '.' => ops.push(Opcode::Output), ',' => ops.push(Opcode::Input), + '^' => ops.push(Opcode::Up), + 'v' => ops.push(Opcode::Down), _ => (), // could put a little special opcode in for other characters } i += 1; @@ -524,7 +667,7 @@ impl BrainfuckOpcodes for BrainfuckCodeBuilder { fn from_str(s: &str) -> Self { BrainfuckCodeBuilder { opcodes: BrainfuckOpcodes::from_str(s), - head_pos: 0, + head_pos: (0, 0), } } } @@ -533,7 +676,7 @@ impl BrainfuckCodeBuilder { pub fn new() -> BrainfuckCodeBuilder { BrainfuckCodeBuilder { opcodes: Vec::new(), - head_pos: 0, + head_pos: (0, 0), } } pub fn len(&self) -> usize { @@ -548,17 +691,33 @@ impl BrainfuckCodeBuilder { { self.opcodes.extend(ops); } - pub fn move_to_cell(&mut self, cell: i32) { - if self.head_pos < cell { - for _ in self.head_pos..cell { + pub fn move_to_cell(&mut self, cell: TapeCell) { + let x = cell.0; + let y = cell.1; + let x_pos = self.head_pos.0; + let y_pos = self.head_pos.1; + //Move x level + if x_pos < x { + for _ in x_pos..x { self.opcodes.push(Opcode::Right); } - } else if cell < self.head_pos { + } else if x < x_pos { // theoretically equivalent to cell..head_pos? - for _ in ((cell + 1)..=(self.head_pos)).rev() { + for _ in ((x + 1)..=x_pos).rev() { self.opcodes.push(Opcode::Left); } } + //Move y level + if y_pos < y { + for _ in y_pos..y { + self.opcodes.push(Opcode::Up); + } + } else if y < y_pos { + // theoretically equivalent to cell..head_pos? + for _ in ((y + 1)..=y_pos).rev() { + self.opcodes.push(Opcode::Down); + } + } self.head_pos = cell; } @@ -568,7 +727,8 @@ impl BrainfuckCodeBuilder { self.opcodes.push(Opcode::Add); } } else if imm < 0 { - for _ in 0..-imm { + // needs to be i32 because -(-128) = -128 in i8-land + for _ in 0..-(imm as i32) { self.opcodes.push(Opcode::Subtract); } } diff --git a/compiler/src/compiler.rs b/compiler/src/compiler.rs index 4c3b651..2ed69f3 100644 --- a/compiler/src/compiler.rs +++ b/compiler/src/compiler.rs @@ -5,7 +5,10 @@ use std::{collections::HashMap, iter::zip}; use crate::{ builder::{Builder, Opcode, TapeCell}, macros::macros::{r_assert, r_panic}, - parser::{Clause, Expression, ExtendedOpcode, VariableDefinition, VariableTarget}, + parser::{ + Clause, Expression, ExtendedOpcode, LocationSpecifier, Reference, VariableDefinition, + VariableTarget, VariableTargetReferenceChain, VariableTypeReference, + }, MastermindConfig, }; @@ -20,255 +23,170 @@ impl Compiler<'_> { &'a self, clauses: &[Clause], outer_scope: Option<&'a Scope>, - ) -> Result { + ) -> Result, String> { let mut scope = if let Some(outer) = outer_scope { outer.open_inner() } else { Scope::new() }; - // hoist functions to top - // also keep track of all variables that are allocated - let mut filtered_clauses: Vec = Vec::new(); - + // TODO: fix unnecessary clones, and reimplement this with iterators somehow + // hoist structs, then functions to top + let mut filtered_clauses_1: Vec = vec![]; + // first stage: structs (these need to be defined before functions, so they can be used as arguments) for clause in clauses { - if let Clause::DeclareVariable { - var, - location_specifier: _, - } - | Clause::DefineVariable { - var, - location_specifier: _, - value: _, - } = clause - { - // hoisting scope allocations to the top - scope.allocate_variable_memory(var.clone())?; + match clause { + Clause::DefineStruct { name, fields } => { + scope.register_struct_definition(name, fields.clone())?; + } + _ => filtered_clauses_1.push(clause.clone()), } - - if let Clause::DefineFunction { - name, - arguments, - block, - } = clause - { - scope.functions.insert( - name.clone(), - Function { - arguments: arguments.clone(), - block: block.clone(), - }, - ); - } else { - filtered_clauses.push(clause.clone()); + } + // second stage: functions + let mut filtered_clauses_2: Vec = vec![]; + for clause in filtered_clauses_1 { + match clause { + Clause::DefineFunction { + name, + arguments, + block, + } => { + scope.register_function_definition(&name, arguments.clone(), block.clone())?; + } + _ => { + filtered_clauses_2.push(clause); + } } } - for clause in filtered_clauses { + for clause in filtered_clauses_2 { match clause { - Clause::DeclareVariable { - var, - location_specifier, - } => { + Clause::DeclareVariable { var } => { // create an allocation in the scope - let memory = scope.get_memory(&var.clone().to_target())?; - // push instruction to allocate cell(s) (the number of cells is stored in the Memory object) - scope.push_instruction(Instruction::Allocate(memory, location_specifier)); + scope.allocate_variable(var)?; } - Clause::DefineVariable { - var, - location_specifier, - value, - } => { + Clause::DefineVariable { var, value } => { // same as above except we initialise the variable - let memory = scope.get_memory(&var.clone().to_target())?; - let memory_id = memory.id(); - scope.push_instruction(Instruction::Allocate( - memory.clone(), - location_specifier, - )); + let absolute_type = scope.allocate_variable(var.clone())?; - match (&var, &value, memory) { + match (absolute_type, &value) { ( - VariableDefinition::Single { name: _ }, - Expression::SumExpression { + ValueType::Cell, + Expression::NaturalNumber(_) + | Expression::SumExpression { sign: _, summands: _, } - | Expression::NaturalNumber(_) | Expression::VariableReference(_), - Memory::Cell { id: _ }, ) => { - _add_expr_to_cell( - &mut scope, - value, - Cell { - memory_id, - index: None, - }, - )?; + let cell = scope.get_cell(&VariableTarget::from_definition(&var))?; + _add_expr_to_cell(&mut scope, &value, cell)?; } - ( - VariableDefinition::Multi { name, len }, - Expression::ArrayLiteral(expressions), - Memory::Cells { - id: _, - len: _, - target_index: _, - }, - ) => { - // for each expression in the array, perform above operations + + // multi-cell arrays and (array literals or strings) + (ValueType::Array(_, _), Expression::ArrayLiteral(expressions)) => { + let cells = + scope.get_array_cells(&VariableTarget::from_definition(&var))?; r_assert!( - expressions.len() == *len, - "Variable \"{name}\" of length {len} cannot be initialised \ -to array expression of length {}", + expressions.len() == cells.len(), + "Variable \"{var}\" cannot be initialised to array of length {}", expressions.len() ); - for (i, expr) in zip(0..*len, expressions) { - _add_expr_to_cell( - &mut scope, - expr.clone(), - Cell { - memory_id, - index: Some(i), - }, - )?; + for (cell, expr) in zip(cells, expressions) { + _add_expr_to_cell(&mut scope, expr, cell)?; } } - ( - VariableDefinition::Multi { name, len }, - Expression::StringLiteral(s), - Memory::Cells { - id: _, - len: _, - target_index: _, - }, - ) => { - // for each byte of the string, add it to its respective cell + (ValueType::Array(_, _), Expression::StringLiteral(s)) => { + let cells = + scope.get_array_cells(&VariableTarget::from_definition(&var))?; r_assert!( - s.len() == *len, - "Variable \"{name}\" of length {len} cannot \ -be initialised to string of length {}", + s.len() == cells.len(), + "Variable \"{var}\" cannot be initialised to string of length {}", s.len() ); - for (i, c) in zip(0..*len, s.bytes()) { - scope.push_instruction(Instruction::AddToCell( - Cell { - memory_id, - index: Some(i), - }, - c, - )); + for (cell, chr) in zip(cells, s.bytes()) { + scope.push_instruction(Instruction::AddToCell(cell, chr)); } } + ( - VariableDefinition::Single { name: _ }, - _, - Memory::Cells { - id: _, - len: _, - target_index: _, - }, - ) - | ( - VariableDefinition::Multi { name: _, len: _ }, - _, - Memory::Cell { id: _ }, + ValueType::Array(_, _), + Expression::VariableReference(variable_target), ) => r_panic!( - "Something went wrong when initialising variable \"{var}\". \ -This error should never occur." + "Cannot assign array \"{var}\" from variable reference \ +\"{variable_target}\". Unimplemented." ), - _ => r_panic!( - "Cannot initialise variable \"{var}\" with expression {value:#?}" + ( + ValueType::Array(_, _), + Expression::NaturalNumber(_) + | Expression::SumExpression { + sign: _, + summands: _, + }, + ) => r_panic!("Cannot assign single value to array \"{var}\"."), + + ( + ValueType::DictStruct(_), + Expression::SumExpression { + sign: _, + summands: _, + } + | Expression::NaturalNumber(_) + | Expression::VariableReference(_) + | Expression::ArrayLiteral(_) + | Expression::StringLiteral(_), + ) => r_panic!( + "Cannot assign value to struct type \"{var}\", initialise it instead." ), - }; - } - Clause::SetVariable { var, value } => match (&var, &value) { - ( - VariableTarget::Single { name: _ }, - Expression::SumExpression { - sign: _, - summands: _, + + (ValueType::Cell, Expression::ArrayLiteral(_)) => { + r_panic!("Cannot assign array to single-cell variable \"{var}\".") } - | Expression::NaturalNumber(_) - | Expression::VariableReference(_), - ) => { - let mem = scope.get_memory(&var)?; - let cell = Cell { - memory_id: mem.id(), - index: None, - }; - scope.push_instruction(Instruction::ClearCell(cell.clone())); - _add_expr_to_cell(&mut scope, value, cell)?; - } - ( - VariableTarget::MultiCell { name: _, index }, - Expression::SumExpression { - sign: _, - summands: _, + (ValueType::Cell, Expression::StringLiteral(_)) => { + r_panic!("Cannot assign string to single-cell variable \"{var}\".") } - | Expression::NaturalNumber(_) - | Expression::VariableReference(_), - ) => { - let mem = scope.get_memory(&var)?; - let cell = Cell { - memory_id: mem.id(), - index: Some(*index), - }; + } + } + Clause::SetVariable { + var, + value, + self_referencing, + } => match (var.is_spread, self_referencing) { + (false, false) => { + let cell = scope.get_cell(&var)?; scope.push_instruction(Instruction::ClearCell(cell.clone())); - _add_expr_to_cell(&mut scope, value, cell)?; + _add_expr_to_cell(&mut scope, &value, cell)?; + } + (false, true) => { + let cell = scope.get_cell(&var)?; + _add_self_referencing_expr_to_cell(&mut scope, value, cell, true)?; + } + (true, _) => { + r_panic!("Unsupported operation, assigning to spread variable: {var}"); + // TODO: support spread assigns? + // let cells = scope.get_array_cells(&var)?; + // etc... } - ( - VariableTarget::MultiSpread { name: _ }, - Expression::SumExpression { - sign: _, - summands: _, - } - | Expression::NaturalNumber(_) - | Expression::VariableReference(_), - ) => r_panic!( - "Cannot set multi-byte variables using \ -spread syntax, use drain into {var} instead." - ), - (_, Expression::ArrayLiteral(_) | Expression::StringLiteral(_)) => r_panic!( - "Cannot set multi-byte variables after initialisation\ -, set individual bytes with [] subscript operator instead." - ), - // _ => r_panic!("Cannot set variable \"{var}\" to expression {value:#?}"), }, - Clause::AddToVariable { var, value } => match (&var, &value) { - ( - VariableTarget::Single { name: _ } - | VariableTarget::MultiCell { name: _, index: _ }, - Expression::SumExpression { - sign: _, - summands: _, - } - | Expression::NaturalNumber(_) - | Expression::VariableReference(_), - ) => { - let Some(cell) = scope.get_memory(&var)?.target_cell() else { - r_panic!("Unreachable error occurred when adding to {var}"); - }; - _add_expr_to_cell(&mut scope, value, cell)?; + Clause::AddToVariable { + var, + value, + self_referencing, + } => match (var.is_spread, self_referencing) { + (false, false) => { + let cell = scope.get_cell(&var)?; + _add_expr_to_cell(&mut scope, &value, cell)?; + } + (false, true) => { + let cell = scope.get_cell(&var)?; + _add_self_referencing_expr_to_cell(&mut scope, value, cell, false)?; + } + (true, _) => { + r_panic!("Unsupported operation, add-assigning to spread variable: {var}"); + // TODO: support spread assigns? + // let cells = scope.get_array_cells(&var)?; + // etc... } - ( - VariableTarget::MultiSpread { name: _ }, - Expression::SumExpression { - sign: _, - summands: _, - } - | Expression::NaturalNumber(_) - | Expression::VariableReference(_), - ) => r_panic!( - "Cannot add to multi-byte variables using \ -spread syntax, use drain into {var} instead." - ), - (_, Expression::ArrayLiteral(_) | Expression::StringLiteral(_)) => r_panic!( - "Cannot add to multi-byte variables after initialisation\ -, set individual bytes with [] subscript operator instead." - ), - // _ => r_panic!("Cannot add expression {value:#?} to variable \"{var}\""), }, Clause::AssertVariableValue { var, value } => { // unfortunately no array assertions due to a limitation with my data-structure/algorithm design @@ -288,150 +206,62 @@ spread syntax, use drain into {var} instead." } }; - let mem = scope.get_memory(&var)?; - match &var { - VariableTarget::Single { name: _ } - | VariableTarget::MultiCell { name: _, index: _ } => { - let cell = match mem { - Memory::Cell { id } => Cell { - memory_id: id, - index: None, - }, - Memory::Cells { - id, - len: _, - target_index: Some(idx), - } => Cell { - memory_id: id, - index: Some(idx), - }, - _ => r_panic!( - "Could not access {var} in assertion. This should not occur." - ), - }; + match var.is_spread { + false => { + let cell = scope.get_cell(&var)?; scope.push_instruction(Instruction::AssertCellValue(cell, imm)); } - VariableTarget::MultiSpread { name: _ } => match mem { - Memory::Cells { - id, - len, - target_index: None, - } => { - for i in 0..len { - let cell = Cell { - memory_id: id, - index: Some(i), - }; - scope.push_instruction(Instruction::AssertCellValue(cell, imm)); - } + true => { + let cells = scope.get_array_cells(&var)?; + for cell in cells { + scope.push_instruction(Instruction::AssertCellValue(cell, imm)); } - _ => r_panic!("Could not access spread variable {var} in assertion."), - }, + } } - // _ => r_panic!("Unsupported compile-time assertion: {var} = {value:#?}"), } - Clause::InputVariable { var } => { - let mem = scope.get_memory(&var)?; - match (&var, mem) { - (VariableTarget::Single { name: _ }, Memory::Cell { id: memory_id }) => { - scope.push_instruction(Instruction::InputToCell(Cell { - memory_id, - index: None, - })) - } - ( - VariableTarget::Single { name: _ } - | VariableTarget::MultiCell { name: _, index: _ }, - Memory::Cells { - id: memory_id, - len: _, - target_index: Some(index), - }, - ) => scope.push_instruction(Instruction::InputToCell(Cell { - memory_id, - index: Some(index), - })), - ( - VariableTarget::MultiSpread { name: _ }, - Memory::Cells { - id: memory_id, - len, - target_index: None, - }, - ) => { - // run the low level input , operator once for each byte in the variable - for i in 0..len { - scope.push_instruction(Instruction::InputToCell(Cell { - memory_id, - index: Some(i), - })); - } + Clause::InputVariable { var } => match var.is_spread { + false => { + let cell = scope.get_cell(&var)?; + scope.push_instruction(Instruction::InputToCell(cell)); + } + true => { + let cells = scope.get_array_cells(&var)?; + for cell in cells { + scope.push_instruction(Instruction::InputToCell(cell)); } - _ => r_panic!("Cannot input into variable \"{var}\""), } - } + }, Clause::OutputValue { value } => { match value { - Expression::VariableReference(var) => { - let mem = scope.get_memory(&var)?; - match (&var, mem) { - ( - VariableTarget::Single { name: _ }, - Memory::Cell { id: memory_id }, - ) => scope.push_instruction(Instruction::OutputCell(Cell { - memory_id, - index: None, - })), - ( - VariableTarget::Single { name: _ } - | VariableTarget::MultiCell { name: _, index: _ }, - Memory::Cells { - id: memory_id, - len: _, - // hack - target_index: Some(index), - }, - ) => scope.push_instruction(Instruction::OutputCell(Cell { - memory_id, - index: Some(index), - })), - ( - VariableTarget::MultiSpread { name: _ }, - Memory::Cells { - id: memory_id, - len, - // hack - target_index: None, - }, - ) => { - // run the low level output . operator once for each byte in the variable - for i in 0..len { - scope.push_instruction(Instruction::OutputCell(Cell { - memory_id, - index: Some(i), - })); - } + Expression::VariableReference(var) => match var.is_spread { + false => { + let cell = scope.get_cell(&var)?; + scope.push_instruction(Instruction::OutputCell(cell)); + } + true => { + let cells = scope.get_array_cells(&var)?; + for cell in cells { + scope.push_instruction(Instruction::OutputCell(cell)); } - _ => r_panic!("Cannot output variable \"{var}\""), } - } + }, Expression::SumExpression { sign: _, summands: _, } | Expression::NaturalNumber(_) => { // allocate a temporary cell and add the expression to it, output, then clear - let temp_mem_id = scope.create_memory_id(); + let temp_mem_id = scope.push_memory_id(); scope.push_instruction(Instruction::Allocate( Memory::Cell { id: temp_mem_id }, None, )); - let cell = Cell { + let cell = CellReference { memory_id: temp_mem_id, index: None, }; - _add_expr_to_cell(&mut scope, value, cell)?; + _add_expr_to_cell(&mut scope, &value, cell)?; scope.push_instruction(Instruction::OutputCell(cell)); scope.push_instruction(Instruction::ClearCell(cell)); @@ -439,18 +269,18 @@ spread syntax, use drain into {var} instead." } Expression::ArrayLiteral(expressions) => { // same as above, except reuse the temporary cell after each output - let temp_mem_id = scope.create_memory_id(); + let temp_mem_id = scope.push_memory_id(); scope.push_instruction(Instruction::Allocate( Memory::Cell { id: temp_mem_id }, None, )); - let cell = Cell { + let cell = CellReference { memory_id: temp_mem_id, index: None, }; for value in expressions { - _add_expr_to_cell(&mut scope, value, cell)?; + _add_expr_to_cell(&mut scope, &value, cell)?; scope.push_instruction(Instruction::OutputCell(cell)); scope.push_instruction(Instruction::ClearCell(cell)); } @@ -459,12 +289,12 @@ spread syntax, use drain into {var} instead." } Expression::StringLiteral(s) => { // same as above, allocate one temporary cell and reuse it for each character - let temp_mem_id = scope.create_memory_id(); + let temp_mem_id = scope.push_memory_id(); scope.push_instruction(Instruction::Allocate( Memory::Cell { id: temp_mem_id }, None, )); - let cell = Cell { + let cell = CellReference { memory_id: temp_mem_id, index: None, }; @@ -480,20 +310,7 @@ spread syntax, use drain into {var} instead." } } Clause::WhileLoop { var, block } => { - let mem = scope.get_memory(&var)?; - let cell = match var { - VariableTarget::Single { name: _ } => Cell { - memory_id: mem.id(), - index: None, - }, - VariableTarget::MultiCell { name: _, index } => Cell { - memory_id: mem.id(), - index: Some(index), - }, - VariableTarget::MultiSpread { name: _ } => { - r_panic!("Cannot open while loop on spread variable \"{var}\"") - } - }; + let cell = scope.get_cell(&var)?; // open loop on variable scope.push_instruction(Instruction::OpenLoop(cell)); @@ -514,219 +331,75 @@ spread syntax, use drain into {var} instead." block, is_draining, } => { - // TODO: refactor this drain/copy loop business - match is_draining { - true => { - // draining loops can drain from an expression or a variable - let (source_cell, free_source_cell) = match &source { - Expression::VariableReference(var) => { - let mem = scope.get_memory(var)?; - match (var, mem) { - ( - VariableTarget::Single { name: _ }, - Memory::Cell { id: memory_id }, - ) => ( - Cell { - memory_id, - index: None, - }, - false, - ), - ( - VariableTarget::Single { name: _ } - | VariableTarget::MultiCell { name: _, index: _ }, - Memory::Cells { - id: memory_id, - len: _, - target_index: Some(index), - }, - ) => ( - Cell { - memory_id, - index: Some(index), - }, - false, - ), - _ => r_panic!("Cannot drain from expression {source:#?}"), - } - } - _ => { - let id = scope.create_memory_id(); - scope.push_instruction(Instruction::Allocate( - Memory::Cell { id }, - None, - )); - let new_cell = Cell { - memory_id: id, - index: None, - }; - _add_expr_to_cell(&mut scope, source, new_cell)?; - (new_cell, true) - } + // TODO: refactor this, there is duplicate code with copying the source value cell + let (source_cell, free_source_cell) = match (is_draining, &source) { + // draining loops can drain from an expression or a variable + (true, Expression::VariableReference(var)) => (scope.get_cell(var)?, false), + (true, _) => { + // any other kind of expression, allocate memory for it automatically + let id = scope.push_memory_id(); + scope + .push_instruction(Instruction::Allocate(Memory::Cell { id }, None)); + let new_cell = CellReference { + memory_id: id, + index: None, }; - scope.push_instruction(Instruction::OpenLoop(source_cell)); + _add_expr_to_cell(&mut scope, &source, new_cell)?; + (new_cell, true) + } + (false, Expression::VariableReference(var)) => { + let cell = scope.get_cell(var)?; - // recurse - let loop_scope = self.compile(&block, Some(&scope))?; - scope - .instructions - .extend(loop_scope.finalise_instructions(true)); - - // copy into each target and decrement the source - for target in targets { - let mem = scope.get_memory(&target)?; - match mem { - Memory::Cell { id: memory_id } => { - scope.push_instruction(Instruction::AddToCell( - Cell { - memory_id, - index: None, - }, - 1, - )) - } - Memory::Cells { - id: memory_id, - len, - target_index, - } => match target_index { - None => { - // should only happen if the spread operator is used, ideally this should be obvious with the code, (TODO: refactor target index hack) - for i in 0..len { - scope.push_instruction(Instruction::AddToCell( - Cell { - memory_id, - index: Some(i), - }, - 1, - )); - } - } - Some(index) => { - scope.push_instruction(Instruction::AddToCell( - Cell { - memory_id, - index: Some(index), - }, - 1, - )) - } - }, - } - } + let new_mem_id = scope.push_memory_id(); + scope.push_instruction(Instruction::Allocate( + Memory::Cell { id: new_mem_id }, + None, + )); - scope.push_instruction(Instruction::AddToCell(source_cell, -1i8 as u8)); // 255 - scope.push_instruction(Instruction::CloseLoop(source_cell)); + let new_cell = CellReference { + memory_id: new_mem_id, + index: None, + }; - // free the source cell if it was a expression we just created - if free_source_cell { - scope.push_instruction(Instruction::Free(source_cell.memory_id)); - } + _copy_cell(&mut scope, cell, new_cell, 1); + + (new_cell, true) } - false => { - let source_cell = match &source { - Expression::VariableReference(var) => { - let var_mem = scope.get_memory(var)?; - let var_cell = match (var, var_mem) { - ( - VariableTarget::Single { name: _ }, - Memory::Cell { id: memory_id }, - ) => Cell { - memory_id, - index: None, - }, - ( - VariableTarget::Single { name: _ } - | VariableTarget::MultiCell { name: _, index: _ }, - Memory::Cells { - id: memory_id, - len: _, - target_index: Some(index), - }, - ) => Cell { - memory_id, - index: Some(index), - }, - _ => r_panic!("Cannot drain from expression {source:#?}"), - }; - - let new_mem_id = scope.create_memory_id(); - scope.push_instruction(Instruction::Allocate( - Memory::Cell { id: new_mem_id }, - None, - )); - - let new_cell = Cell { - memory_id: new_mem_id, - index: None, - }; - - _copy_cell(&mut scope, var_cell, new_cell, 1); - - new_cell - } - _ => r_panic!( - "Cannot copy from {source:#?}, use a drain loop instead" - ), - }; + (false, _) => { + r_panic!("Cannot copy from {source:#?}, use a drain loop instead") + } + }; + scope.push_instruction(Instruction::OpenLoop(source_cell)); - scope.push_instruction(Instruction::OpenLoop(source_cell)); + // recurse + let loop_scope = self.compile(&block, Some(&scope))?; + // TODO: refactor, make a function in scope trait to do this automatically + scope + .instructions + .extend(loop_scope.finalise_instructions(true)); - // recurse - let loop_scope = self.compile(&block, Some(&scope))?; - scope - .instructions - .extend(loop_scope.finalise_instructions(true)); - - // copy into each target and decrement the source - for target in targets { - let mem = scope.get_memory(&target)?; - match mem { - Memory::Cell { id: memory_id } => { - scope.push_instruction(Instruction::AddToCell( - Cell { - memory_id, - index: None, - }, - 1, - )) - } - Memory::Cells { - id: memory_id, - len, - target_index, - } => match target_index { - None => { - // should only happen if the spread operator is used, ideally this should be obvious with the code, (TODO: refactor target index hack) - for i in 0..len { - scope.push_instruction(Instruction::AddToCell( - Cell { - memory_id, - index: Some(i), - }, - 1, - )); - } - } - Some(index) => { - scope.push_instruction(Instruction::AddToCell( - Cell { - memory_id, - index: Some(index), - }, - 1, - )) - } - }, + // copy into each target and decrement the source + for target in targets { + match target.is_spread { + false => { + let cell = scope.get_cell(&target)?; + scope.push_instruction(Instruction::AddToCell(cell, 1)); + } + true => { + let cells = scope.get_array_cells(&target)?; + for cell in cells { + scope.push_instruction(Instruction::AddToCell(cell, 1)); } } + } + } - scope.push_instruction(Instruction::AddToCell(source_cell, -1i8 as u8)); // 255 - scope.push_instruction(Instruction::CloseLoop(source_cell)); + scope.push_instruction(Instruction::AddToCell(source_cell, -1i8 as u8)); // 255 + scope.push_instruction(Instruction::CloseLoop(source_cell)); - // free the temporary cell - scope.push_instruction(Instruction::Free(source_cell.memory_id)); - } + // free the source cell if it was a expression we just created + if free_source_cell { + scope.push_instruction(Instruction::Free(source_cell.memory_id)); } } Clause::IfElse { @@ -739,26 +412,26 @@ spread syntax, use drain into {var} instead." }; let mut new_scope = scope.open_inner(); - let condition_mem_id = new_scope.create_memory_id(); + let condition_mem_id = new_scope.push_memory_id(); new_scope.push_instruction(Instruction::Allocate( Memory::Cell { id: condition_mem_id, }, None, )); - let condition_cell = Cell { + let condition_cell = CellReference { memory_id: condition_mem_id, index: None, }; let else_condition_cell = match else_block { Some(_) => { - let else_mem_id = new_scope.create_memory_id(); + let else_mem_id = new_scope.push_memory_id(); new_scope.push_instruction(Instruction::Allocate( Memory::Cell { id: else_mem_id }, None, )); - let else_cell = Cell { + let else_cell = CellReference { memory_id: else_mem_id, index: None, }; @@ -769,7 +442,7 @@ spread syntax, use drain into {var} instead." }; // copy the condition expression to the temporary condition cell - _add_expr_to_cell(&mut new_scope, condition, condition_cell)?; + _add_expr_to_cell(&mut new_scope, &condition, condition_cell)?; new_scope.push_instruction(Instruction::OpenLoop(condition_cell)); // TODO: think about optimisations for clearing this variable, as the builder won't shorten it for safety as it doesn't know this loop is special @@ -843,13 +516,11 @@ spread syntax, use drain into {var} instead." .compile(&mm_clauses, Some(&functions_scope))? .finalise_instructions(false); // compile without cleaning up top level variables, this is the brainfuck programmer's responsibility - // TODO: figure out how to make the compiler return to the initial head position before building and re-adding? - // IMPORTANT!!!!!!!!!!!! + // it is also the brainfuck programmer's responsibility to return to the start position let builder = Builder { config: &self.config, }; let built_code = builder.build(instructions, true)?; - // IMPORTANT TODO: MAKE SURE IT RETURNS TO THE SAME POSITION expanded_bf.extend(built_code); } ExtendedOpcode::Add => expanded_bf.push(Opcode::Add), @@ -860,54 +531,39 @@ spread syntax, use drain into {var} instead." ExtendedOpcode::CloseLoop => expanded_bf.push(Opcode::CloseLoop), ExtendedOpcode::Output => expanded_bf.push(Opcode::Output), ExtendedOpcode::Input => expanded_bf.push(Opcode::Input), + ExtendedOpcode::Up => expanded_bf.push(Opcode::Up), + ExtendedOpcode::Down => expanded_bf.push(Opcode::Down), } } + + // handle the location specifier + let location = match location_specifier { + LocationSpecifier::None => CellLocation::Unspecified, + LocationSpecifier::Cell(cell) => CellLocation::FixedCell(cell), + LocationSpecifier::Variable(var) => { + CellLocation::MemoryCell(scope.get_target_cell_reference(&var)?) + } + }; + scope.push_instruction(Instruction::InsertBrainfuckAtCell( expanded_bf, - location_specifier, + location, )); // assert that we clobbered the variables // not sure whether this should go before or after the actual bf code for var in clobbered_variables { - let mem = scope.get_memory(&var)?; - // little bit of duplicate code from the copyloop clause here: - match mem { - Memory::Cell { id } => { - scope.push_instruction(Instruction::AssertCellValue( - Cell { - memory_id: id, - index: None, - }, - None, - )) + match var.is_spread { + false => { + let cell = scope.get_cell(&var)?; + scope.push_instruction(Instruction::AssertCellValue(cell, None)); } - Memory::Cells { - id, - len, - target_index, - } => match target_index { - None => { - // should only happen if the spread operator is used, ideally this should be obvious with the code, (TODO: refactor target index hack) - for i in 0..len { - scope.push_instruction(Instruction::AssertCellValue( - Cell { - memory_id: id, - index: Some(i), - }, - None, - )); - } - } - Some(index) => { - scope.push_instruction(Instruction::AssertCellValue( - Cell { - memory_id: id, - index: Some(index), - }, - None, - )) + true => { + let cells = scope.get_array_cells(&var)?; + for cell in cells { + scope + .push_instruction(Instruction::AssertCellValue(cell, None)); } - }, + } } } } @@ -916,62 +572,53 @@ spread syntax, use drain into {var} instead." arguments, } => { // create variable translations and recursively compile the inner variable block - let function_definition = scope.get_function(&function_name)?; - let mut new_scope = scope.open_inner(); - let zipped: Result, String> = - zip(function_definition.arguments.clone().into_iter(), arguments) - .map(|(arg_def, calling_arg)| { - Ok(match (arg_def, calling_arg) { - ( - VariableDefinition::Single { name: def_name }, - VariableTarget::Single { name: call_name }, - ) => ArgumentTranslation::SingleFromSingle(def_name, call_name), - ( - // this is a minor hack, the parser will parse a calling argument as a single even though it is really targeting a multi - VariableDefinition::Multi { - name: def_name, - len: _, - }, - VariableTarget::Single { name: call_name }, - ) => ArgumentTranslation::MultiFromMulti(def_name, call_name), - ( - VariableDefinition::Single { name: def_name }, - VariableTarget::MultiCell { - name: call_name, - index, - }, - ) => ArgumentTranslation::SingleFromMultiCell( - def_name, - (call_name, index), - ), - (def_var, call_var) => { - r_panic!( - "Cannot translate {call_var} as argument {def_var}" - ) - } - }) - }) - .collect(); - new_scope.variable_aliases.extend(zipped?); + let calling_argument_types = arguments + .iter() + .map(|a| scope.get_target_type(&a)) + .collect::, _>>()?; + + let function_definition = + scope.get_function(&function_name, &calling_argument_types)?; + + let mut argument_translation_scope = scope.open_inner(); + + // TODO: refactor this mess + // deal with argument memory mappings: + for ((calling_argument, calling_argument_type), (arg_name, expected_type)) in + zip( + zip(arguments, calling_argument_types), + function_definition.arguments.iter(), + ) { + // TODO: fix this duplicate call, get_target_type() internally gets the memory allocation details + // then these are gotten again in create_mapped_variable() + r_assert!(calling_argument_type == expected_type, "Expected argument of type \"{expected_type:#?}\" in function call \"{function_name}\", received argument of type \"{calling_argument_type:#?}\". This should not occur"); + // register an argument translation in the scope + argument_translation_scope + .create_mapped_variable(arg_name.clone(), &calling_argument)?; + } // recurse - let loop_scope = self.compile(&function_definition.block, Some(&new_scope))?; - new_scope + let function_scope = self.compile( + &function_definition.block, + Some(&argument_translation_scope), + )?; + argument_translation_scope .instructions - .extend(loop_scope.finalise_instructions(true)); + .extend(function_scope.finalise_instructions(true)); // extend the inner scope instructions onto the outer scope // maybe function call compiling should be its own function? scope .instructions - .extend(new_scope.finalise_instructions(false)); + .extend(argument_translation_scope.finalise_instructions(false)); } - Clause::DefineFunction { + Clause::DefineStruct { name: _, fields: _ } + | Clause::DefineFunction { name: _, arguments: _, block: _, - } => (), + } => unreachable!(), } } @@ -982,7 +629,11 @@ spread syntax, use drain into {var} instead." // not sure if this should be in the scope impl? // helper function for a common use-case // flatten an expression and add it to a specific cell (using copies and adds, etc) -fn _add_expr_to_cell(scope: &mut Scope, expr: Expression, cell: Cell) -> Result<(), String> { +fn _add_expr_to_cell( + scope: &mut Scope, + expr: &Expression, + cell: CellReference, +) -> Result<(), String> { let (imm, adds, subs) = expr.flatten()?; scope.push_instruction(Instruction::AddToCell(cell.clone(), imm)); @@ -998,31 +649,91 @@ fn _add_expr_to_cell(scope: &mut Scope, expr: Expression, cell: Cell) -> Result< } for (source, constant) in adds_set { - let Some(source_cell) = scope.get_memory(&source)?.target_cell() else { - r_panic!("Cannot sum variable \"{source}\" in expression"); - }; + let source_cell = scope.get_cell(&source)?; _copy_cell(scope, source_cell, cell.clone(), constant); } Ok(()) } -// another helper function to copy a cell from one to another leaving the original unaffected -fn _copy_cell(scope: &mut Scope, source_cell: Cell, target_cell: Cell, constant: i32) { +//This function allows you to add a self referencing expression to the cell +//Separate this to ensure that normal expression don't require the overhead of copying +fn _add_self_referencing_expr_to_cell( + scope: &mut Scope, + expr: Expression, + cell: CellReference, + pre_clear: bool, +) -> Result<(), String> { + //Create a new temp cell to store the current cell value + let temp_mem_id = scope.push_memory_id(); + scope.push_instruction(Instruction::Allocate( + Memory::Cell { id: temp_mem_id }, + None, + )); + let temp_cell = CellReference { + memory_id: temp_mem_id, + index: None, + }; + // TODO: make this more efficent by not requiring a clear cell after, + // i.e. simple move instead of copy by default for set operations (instead of +=) + _copy_cell(scope, cell, temp_cell, 1); + // Then if we are doing a += don't pre-clear otherwise Clear the current cell and run the same actions as _add_expr_to_cell + if pre_clear { + scope.push_instruction(Instruction::ClearCell(cell.clone())); + } + + let (imm, adds, subs) = expr.flatten()?; + + scope.push_instruction(Instruction::AddToCell(cell.clone(), imm)); + + let mut adds_set = HashMap::new(); + for var in adds { + let n = adds_set.remove(&var).unwrap_or(0); + adds_set.insert(var, n + 1); + } + for var in subs { + let n = adds_set.remove(&var).unwrap_or(0); + adds_set.insert(var, n - 1); + } + + for (source, constant) in adds_set { + let source_cell = scope.get_cell(&source)?; + //If we have an instance of the original cell being added simply use our temp cell value + // (crucial special sauce) + if source_cell.memory_id == cell.memory_id && source_cell.index == cell.index { + _copy_cell(scope, temp_cell, cell.clone(), constant); + } else { + _copy_cell(scope, source_cell, cell.clone(), constant); + } + } + //Cleanup + scope.push_instruction(Instruction::ClearCell(temp_cell)); + scope.push_instruction(Instruction::Free(temp_mem_id)); + + Ok(()) +} + +/// Helper function to copy a cell from one to another leaving the original unaffected +// TODO: make one for draining a cell +fn _copy_cell( + scope: &mut Scope, + source_cell: CellReference, + target_cell: CellReference, + constant: i32, +) { if constant == 0 { return; } // allocate a temporary cell - let temp_mem_id = scope.create_memory_id(); + let temp_mem_id = scope.push_memory_id(); scope.push_instruction(Instruction::Allocate( Memory::Cell { id: temp_mem_id }, None, )); - let temp_cell = Cell { + let temp_cell = CellReference { memory_id: temp_mem_id, index: None, }; - // copy source to target and temp scope.push_instruction(Instruction::OpenLoop(source_cell)); scope.push_instruction(Instruction::AddToCell(target_cell, constant as u8)); @@ -1040,16 +751,24 @@ fn _copy_cell(scope: &mut Scope, source_cell: Cell, target_cell: Cell, constant: // this is subject to change #[derive(Debug, Clone)] pub enum Instruction { - Allocate(Memory, Option), // most of the below comments are wrong, usize is a unique id of an allocated cell + Allocate(Memory, Option), Free(MemoryId), // the number indicates which cell in the allocation stack should be freed (cell 0, is the top of the stack, 1 is the second element, etc) - OpenLoop(Cell), // same with other numbers here, they indicate the cell in the allocation stack to use in the instruction - CloseLoop(Cell), // pass in the cell id, this originally wasn't there but may be useful later on - AddToCell(Cell, u8), - InputToCell(Cell), - ClearCell(Cell), // not sure if this should be here, seems common enough that it should be - AssertCellValue(Cell, Option), // allows the user to hand-tune optimisations further - OutputCell(Cell), - InsertBrainfuckAtCell(Vec, Option), + OpenLoop(CellReference), // same with other numbers here, they indicate the cell in the allocation stack to use in the instruction + CloseLoop(CellReference), // pass in the cell id, this originally wasn't there but may be useful later on + AddToCell(CellReference, u8), + InputToCell(CellReference), + ClearCell(CellReference), // not sure if this should be here, seems common enough that it should be + AssertCellValue(CellReference, Option), // allows the user to hand-tune optimisations further + OutputCell(CellReference), + InsertBrainfuckAtCell(Vec, CellLocation), +} + +#[derive(Debug, Clone)] +/// Either a fixed constant cell or a reference to some existing memory +pub enum CellLocation { + Unspecified, + FixedCell((i32, i32)), + MemoryCell(CellReference), } #[derive(Debug, Clone)] @@ -1057,19 +776,27 @@ pub enum Memory { Cell { id: MemoryId, }, - // this comment was originally in a different place which is why it might be a bit odd, highly relevant still - // a little hack was added which holds the targeted cell inside the memory, this is for when you pass in a multi-byte cell reference to a function Cells { id: MemoryId, len: usize, - target_index: Option, + }, + /// A memory cell that references a previously allocated cell in an outer scope, used for function arguments + MappedCell { + id: MemoryId, + index: Option, + }, + /// Memory mapped cells, referencing previously allocated cells in an outer scope + MappedCells { + id: MemoryId, + start_index: usize, + len: usize, }, // infinite cell something (TODO?) } pub type MemoryId = usize; #[derive(Debug, Clone, Copy)] -pub struct Cell { +pub struct CellReference { pub memory_id: MemoryId, pub index: Option, } @@ -1077,108 +804,201 @@ pub struct Cell { impl Memory { pub fn id(&self) -> MemoryId { match self { - Memory::Cell { id } => *id, - Memory::Cells { + Memory::Cell { id } + | Memory::Cells { id, len: _ } + | Memory::MappedCell { id, index: _ } + | Memory::MappedCells { id, + start_index: _, len: _, - target_index: _, } => *id, } } pub fn len(&self) -> usize { match self { - Memory::Cell { id: _ } => 1, - Memory::Cells { + Memory::Cell { id: _ } | Memory::MappedCell { id: _, index: _ } => 1, + Memory::Cells { id: _, len } + | Memory::MappedCells { id: _, + start_index: _, len, - target_index: _, } => *len, } } - - pub fn target_cell(&self) -> Option { - match self { - Memory::Cell { id } => Some(Cell { - memory_id: *id, - index: None, - }), - Memory::Cells { - id, - len: _, - target_index: Some(index), - } => Some(Cell { - memory_id: *id, - index: Some(*index), - }), - _ => None, - } - } } #[derive(Clone, Debug)] +/// Scope type represents a Mastermind code block, +/// any variables or functions defined within a {block} are owned by the scope and cleaned up before continuing pub struct Scope<'a> { + /// a reference to the parent scope, for accessing things defined outside of this scope outer_scope: Option<&'a Scope<'a>>, - // syntactic context instead of normal context - // used for embedded mm so that the inner mm can use outer functions but not variables - fn_only: bool, + /// fn_only: true if syntactic context instead of normal context. + /// Used for embedded mm so that the inner mm can use outer functions but not variables. + types_only: bool, - // number of memory allocations + /// Number of memory allocations in current scope allocations: usize, - // mappings for variable names to places on above stack - variable_memory: HashMap, - // used for function arguments, translates an outer scope variable to an inner one, assumed they are the same array length if multi-cell - // originally this was just string to string, but we need to be able to map a single-bit variable to a cell of an outer array variable - variable_aliases: Vec, - // functions accessible by any code within or in the current scope - functions: HashMap, + /// Mappings for variable names to memory allocation IDs in current scope + variable_memory: HashMap, + + /// Functions accessible by any code within or in the current scope + functions: Vec<(String, Vec<(String, ValueType)>, Vec)>, + /// Struct types definitions + structs: HashMap, - // experimental: This is where we keep track of the instructions generated by the compiler, then we return the scope to the calling function + /// Intermediate instructions generated by the compiler instructions: Vec, - // very experimental: this is used for optimisations to keep track of how variables are used - // variable_accesses: HashMap, // (reads, writes) } -#[derive(Clone, Debug)] -enum ArgumentTranslation { - SingleFromSingle(String, String), - SingleFromMultiCell(String, (String, usize)), - MultiFromMulti(String, String), +#[derive(Clone, Debug)] // probably shouldn't be cloning here but whatever +struct Function { + arguments: Vec<(String, ValueType)>, + block: Vec, } -impl ArgumentTranslation { - fn get_def_name(&self) -> &String { - let (ArgumentTranslation::SingleFromSingle(def_name, _) - | ArgumentTranslation::SingleFromMultiCell(def_name, _) - | ArgumentTranslation::MultiFromMulti(def_name, _)) = self; - def_name + +#[derive(Clone, Debug, PartialEq, Eq)] +/// an absolute definition of a type, as opposed to `VariableTypeReference` which is more of a reference +enum ValueType { + Cell, + Array(usize, Box), + DictStruct(Vec<(String, ValueType, Option)>), + // TupleStruct(Vec), +} + +#[derive(Clone, Debug)] +/// equivalent to ValueType::DictStruct enum variant, +/// Rust doesn't support enum variants as types yet so need this workaround for struct definitions in scope object +struct DictStructType(Vec<(String, ValueType, Option)>); +impl ValueType { + fn from_struct(struct_def: DictStructType) -> Self { + ValueType::DictStruct(struct_def.0) } - fn get_call_name(&self) -> &String { - match self { - ArgumentTranslation::SingleFromSingle(_, call_name) - | ArgumentTranslation::MultiFromMulti(_, call_name) => call_name, - ArgumentTranslation::SingleFromMultiCell(_, (call_var, _)) => call_var, + + // TODO: make size() and get_and_validate_subfield_cell_map() more efficient, + // currently these two recurse back and forth and are a bit of a monster combo + + /// return the type size in cells + fn size(&self) -> Result { + Ok(match self { + ValueType::Cell => 1, + ValueType::Array(len, value_type) => *len * value_type.size()?, + ValueType::DictStruct(fields) => Self::get_and_validate_subfield_cell_map(fields)?.1, + }) + } + + /// deterministically place all struct subfields on a non-negative cell, return the positions of each and the total length + /// return Err() if location specified subfields overlap + fn get_and_validate_subfield_cell_map( + fields: &Vec<(String, ValueType, Option)>, + ) -> Result<(HashMap<&String, (usize, &ValueType)>, usize), String> { + // (set of cells, max cell) + let mut cell_map = HashMap::new(); + + // map of field names and their starting cells + let mut subfield_map = HashMap::new(); + let mut max_cell = 0usize; + let mut unfixed_fields = vec![]; + // handle the cells with specified locations + for (field_name, field_type, field_location) in fields { + match field_location { + Some(location) => { + subfield_map.insert(field_name, (*location, field_type)); + for cell_index in *location..(*location + field_type.size()?) { + // this assumes the field locations have been validated + if let Some(other_name) = cell_map.insert(cell_index, field_name) { + r_panic!( + "Subfields \"{other_name}\" and \"{field_name}\" overlap in struct." + ); + }; + max_cell = max_cell.max(cell_index); + } + } + None => { + unfixed_fields.push((field_name, field_type)); + } + } } + + for (field_name, field_type) in unfixed_fields { + let field_size = field_type.size()?; + // repeatedly try to insert the fields into leftover memory locations + let mut start_index = 0usize; + for cur_index in 0.. { + if cell_map.contains_key(&cur_index) { + start_index = cur_index + 1; + } else if (cur_index - start_index + 1) >= field_size { + // found a run with the right amount of cells free + break; + } + } + subfield_map.insert(field_name, (start_index, field_type)); + for cell_index in start_index..(start_index + field_size) { + // inefficient but whatever, this insert is not necessary + cell_map.insert(cell_index, field_name); + max_cell = max_cell.max(cell_index); + } + } + + let size = max_cell + 1; + + Ok((subfield_map, size)) } -} -#[derive(Clone, Debug)] // probably shouldn't be cloning here but whatever -struct Function { - arguments: Vec, - block: Vec, + /// get a subfield's type as well as memory cell index + pub fn get_subfield( + &self, + subfield_chain: &VariableTargetReferenceChain, + ) -> Result<(&ValueType, usize), String> { + let mut cur_field = self; + let mut cur_index = 0; + for subfield_ref in subfield_chain.0.iter() { + match (cur_field, subfield_ref) { + (ValueType::Array(len, element_type), Reference::Index(index)) => { + r_assert!( + index < len, + "Index \"{subfield_ref}\" must be less than array length ({len})." + ); + cur_index += element_type.size()? * index; + cur_field = element_type; + } + (ValueType::DictStruct(fields), Reference::NamedField(subfield_name)) => { + let (subfield_map, _size) = Self::get_and_validate_subfield_cell_map(fields)?; + let Some((subfield_cell_offset, subfield_type)) = + subfield_map.get(subfield_name) + else { + r_panic!("Could not find subfield \"{subfield_ref}\" in struct type") + }; + cur_index += subfield_cell_offset; + cur_field = subfield_type; + } + + (ValueType::DictStruct(_), Reference::Index(_)) => { + r_panic!("Cannot read index subfield \"{subfield_ref}\" of struct type.") + } + (ValueType::Array(_, _), Reference::NamedField(_)) => { + r_panic!("Cannot read named subfield \"{subfield_ref}\" of array type.") + } + (ValueType::Cell, subfield_ref) => { + r_panic!("Attempted to get subfield \"{subfield_ref}\" of cell type.") + } + } + } + Ok((cur_field, cur_index)) + } } -// represents a position in a stack relative to the head/top impl Scope<'_> { pub fn new() -> Scope<'static> { Scope { outer_scope: None, - fn_only: false, + types_only: false, allocations: 0, variable_memory: HashMap::new(), - variable_aliases: Vec::new(), - functions: HashMap::new(), + functions: Vec::new(), + structs: HashMap::new(), instructions: Vec::new(), - // variable_accesses: HashMap::new(), } } @@ -1193,25 +1013,32 @@ impl Scope<'_> { // TODO: add some optimisations from the builder to here // create instructions to free cells - let mut clear_instructions = Vec::new(); - for (var_def, mem_id) in self.variable_memory.iter() { - match &var_def { - VariableDefinition::Single { name: _ } => { - clear_instructions.push(Instruction::ClearCell(Cell { - memory_id: *mem_id, + let mut clear_instructions = vec![]; + for (_var_name, (_var_type, memory)) in self.variable_memory.iter() { + match memory { + Memory::Cell { id } => { + clear_instructions.push(Instruction::ClearCell(CellReference { + memory_id: *id, index: None, - })) + })); + clear_instructions.push(Instruction::Free(*id)); } - VariableDefinition::Multi { name: _, len } => { + Memory::Cells { id, len } => { for i in 0..*len { - clear_instructions.push(Instruction::ClearCell(Cell { - memory_id: *mem_id, + clear_instructions.push(Instruction::ClearCell(CellReference { + memory_id: *id, index: Some(i), })) } + clear_instructions.push(Instruction::Free(*id)); } + Memory::MappedCell { id: _, index: _ } + | Memory::MappedCells { + id: _, + start_index: _, + len: _, + } => (), } - clear_instructions.push(Instruction::Free(*mem_id)); } for instr in clear_instructions { self.push_instruction(instr); @@ -1224,16 +1051,16 @@ impl Scope<'_> { self.instructions.push(instruction); } + /// Open a scope within the current one, any time there is a {} in Mastermind, this is called fn open_inner(&self) -> Scope { Scope { outer_scope: Some(self), - fn_only: false, + types_only: false, allocations: 0, variable_memory: HashMap::new(), - variable_aliases: Vec::new(), - functions: HashMap::new(), + functions: Vec::new(), + structs: HashMap::new(), instructions: Vec::new(), - // variable_accesses: HashMap::new(), } } @@ -1242,37 +1069,55 @@ impl Scope<'_> { fn open_inner_templates_only(&self) -> Scope { Scope { outer_scope: Some(self), - fn_only: true, + types_only: true, allocations: 0, variable_memory: HashMap::new(), - variable_aliases: Vec::new(), - functions: HashMap::new(), + functions: Vec::new(), + structs: HashMap::new(), instructions: Vec::new(), - // variable_accesses: HashMap::new(), } } - // not sure if this function should create the allocation instruction or not - fn allocate_variable_memory(&mut self, var: VariableDefinition) -> Result { - let id = self.create_memory_id(); - let result = Ok(match &var { - VariableDefinition::Single { name: _ } => Memory::Cell { id }, - VariableDefinition::Multi { name: _, len } => Memory::Cells { + /// Get the correct variable type and allocate the right amount of cells for it + fn allocate_variable(&mut self, var: VariableDefinition) -> Result<&ValueType, String> { + r_assert!( + !self.variable_memory.contains_key(&var.name), + "Cannot allocate variable {var} twice in the same scope" + ); + + // get absolute type + let full_type = self.create_absolute_type(&var.var_type)?; + // get absolute type size + let id = self.push_memory_id(); + let memory = match &full_type { + ValueType::Cell => Memory::Cell { id }, + _ => Memory::Cells { id, - len: *len, - target_index: None, + len: full_type.size()?, }, - }); + }; + // save variable in scope memory + let None = self + .variable_memory + .insert(var.name.clone(), (full_type, memory.clone())) + else { + r_panic!("Unreachable error occurred when allocating {var}"); + }; - let None = self.current_level_get_variable_definition(&var.name()) else { - r_panic!("Cannot allocate variable {var} twice in the same scope"); + // verify location specifier + let location = match var.location_specifier { + LocationSpecifier::None => None, + LocationSpecifier::Cell(cell) => Some(cell), + LocationSpecifier::Variable(_) => r_panic!( + "Cannot use variable as location specifier target when allocating variable: {var}" + ), }; - if let Some(var) = self.variable_memory.insert(var, id) { - r_panic!("Unreachable error occurred when allocating {var}"); - } + // allocate + self.push_instruction(Instruction::Allocate(memory.clone(), location)); - result + // return a reference to the created full type + Ok(&self.variable_memory.get(&var.name).unwrap().0) } // fn allocate_unnamed_cell(&mut self) -> Memory { @@ -1280,16 +1125,16 @@ impl Scope<'_> { // Memory::Cell { id: mem_id } // } - fn create_memory_id(&mut self) -> MemoryId { + fn push_memory_id(&mut self) -> MemoryId { let current_scope_relative = self.allocations; self.allocations += 1; current_scope_relative + self.allocation_offset() } - // recursively tallies the allocation stack size of the outer scope, does not include this scope + /// recursively tally the allocation stack size of the outer scope, does not include this scope fn allocation_offset(&self) -> usize { // little bit of a hack but works for now - if self.fn_only { + if self.types_only { return 0; } if let Some(outer_scope) = self.outer_scope { @@ -1299,113 +1144,485 @@ impl Scope<'_> { } } - fn get_function(&self, name: &str) -> Result<&Function, String> { + fn get_function( + &self, + calling_name: &str, + calling_arg_types: &Vec<&ValueType>, + ) -> Result { // this function is unaffected by the self.fn_only flag - if let Some(func) = self.functions.get(name) { - Ok(func) + Ok( + if let Some(func) = self.functions.iter().find(|(name, args, _)| { + if name != calling_name || args.len() != calling_arg_types.len() { + return false; + } + for ((_, arg_type), calling_arg_type) in zip(args, calling_arg_types) { + if *arg_type != **calling_arg_type { + return false; + } + } + true + }) { + // TODO: stop cloning! This function overload stuff is tacked on and needs refactoring + let (_, arguments, block) = func.clone(); + Function { arguments, block } + } else if let Some(outer_scope) = self.outer_scope { + outer_scope.get_function(calling_name, calling_arg_types)? + } else { + r_panic!("Could not find function \"{calling_name}\" with correct arguments in current scope"); + }, + ) + } + + /// Define a struct in this scope + fn register_struct_definition( + &mut self, + struct_name: &str, + fields: Vec, + ) -> Result<(), String> { + let mut absolute_fields = vec![]; + + for var_def in fields { + let absolute_type = self.create_absolute_type(&var_def.var_type)?; + let non_neg_location_specifier = match &var_def.location_specifier { + LocationSpecifier::None => None, + LocationSpecifier::Cell(l) => { + // assert the y coordinate is 0 + r_assert!(l.1 == 0, "Struct field location specifiers do not support 2D grid cells: {var_def}"); + r_assert!( + l.0 >= 0, + "Struct field location specifiers must be non-negative: {var_def}" + ); + Some(l.0 as usize) + } + LocationSpecifier::Variable(_) => r_panic!("Location specifiers in struct definitions must be relative, not variables: {var_def}"), + }; + absolute_fields.push((var_def.name, absolute_type, non_neg_location_specifier)); + } + + let None = self + .structs + .insert(struct_name.to_string(), DictStructType(absolute_fields)) + else { + r_panic!("Cannot define struct {struct_name} more than once in same scope."); + }; + + Ok(()) + } + + /// Define a function in this scope + fn register_function_definition( + &mut self, + new_function_name: &str, + new_arguments: Vec, + new_block: Vec, + ) -> Result<(), String> { + let absolute_arguments = new_arguments + .into_iter() + .map(|f| { + let LocationSpecifier::None = f.location_specifier else { + r_panic!("Cannot specify variable location in function argument \"{f}\"."); + }; + Ok((f.name, self.create_absolute_type(&f.var_type)?)) + }) + .collect::, _>>()?; + + // This is some fucked C-style loop break logic, basically GOTOs + // basically it only gets to the panic if the functions have identical signature (except argument names) + 'func_loop: for (name, args, _) in self.functions.iter() { + if name != new_function_name || args.len() != absolute_arguments.len() { + continue; + } + for ((_, new_arg_type), (_, arg_type)) in zip(&absolute_arguments, args) { + if *new_arg_type != *arg_type { + // early continue if any of the arguments are different type + continue 'func_loop; + } + } + r_panic!("Cannot define a function with the same signature more than once in the same scope: \"{new_function_name}\""); + } + + self.functions + .push((new_function_name.to_string(), absolute_arguments, new_block)); + + Ok(()) + } + + /// Recursively find the definition of a struct type by searching up the scope call stack + fn get_struct_definition(&self, struct_name: &str) -> Result<&DictStructType, String> { + Ok(if let Some(struct_def) = self.structs.get(struct_name) { + struct_def } else if let Some(outer_scope) = self.outer_scope { - // again not sure if Ok ? is a good pattern - Ok(outer_scope.get_function(name)?) + // recurse + outer_scope.get_struct_definition(struct_name)? } else { - r_panic!("Could not find function \"{name}\" in current scope"); - } + r_panic!("No definition found for struct \"{struct_name}\"."); + }) } - fn current_level_get_variable_definition(&self, var_name: &str) -> Option<&VariableDefinition> { - self.variable_memory - .keys() - .find(|var_def| var_def.name() == var_name) + /// Construct an absolute type from a type reference + fn create_absolute_type(&self, type_ref: &VariableTypeReference) -> Result { + Ok(match type_ref { + VariableTypeReference::Cell => ValueType::Cell, + VariableTypeReference::Struct(struct_type_name) => { + ValueType::from_struct(self.get_struct_definition(struct_type_name)?.clone()) + } + VariableTypeReference::Array(variable_type_reference, len) => ValueType::Array( + *len, + Box::new(self.create_absolute_type(variable_type_reference)?), + ), + }) } - // returns a memory allocation id, unfortunately we also have a little hack to add the length of the variable in here as well because we ended up needing it - fn get_memory(&self, var: &VariableTarget) -> Result { - if let Some(var_def) = self.current_level_get_variable_definition(var.name()) { - let Some(mem_id) = self.variable_memory.get(var_def) else { - r_panic!("Something went wrong when compiling. This error should never occur."); - }; - // base case, variable is defined in this scope level - Ok(match (var_def, var) { - (VariableDefinition::Single { name: _ }, VariableTarget::Single { name: _ }) => { - Memory::Cell { id: *mem_id } - } - ( - VariableDefinition::Multi { name: _, len }, - VariableTarget::MultiCell { name: _, index }, - ) => { - r_assert!( - *index < *len, - "Memory access attempt: \"{var}\" out of range for variable: \"{var_def}\"" - ); - Memory::Cells { - id: *mem_id, - len: *len, - target_index: Some(*index), - } + /// Return a cell reference for a variable target + fn get_cell(&self, target: &VariableTarget) -> Result { + // get the absolute type of the variable, as well as the memory allocations + let (full_type, memory) = self.get_base_variable_memory(&target.name)?; + // get the correct index within the memory and return + Ok(match (&target.subfields, full_type, memory) { + (None, ValueType::Cell, Memory::Cell { id }) => CellReference { + memory_id: *id, + index: None, + }, + (None, ValueType::Cell, Memory::MappedCell { id, index }) => CellReference { + memory_id: *id, + index: *index, + }, + ( + Some(subfield_chain), + ValueType::Array(_, _) | ValueType::DictStruct(_), + Memory::Cells { id, len } + | Memory::MappedCells { + id, + start_index: _, + len, + }, + ) => { + let (subfield_type, cell_index) = full_type.get_subfield(&subfield_chain)?; + let ValueType::Cell = subfield_type else { + r_panic!("Expected cell type in variable target: {target}"); + }; + r_assert!(cell_index < *len, "Cell reference out of bounds on variable target: {target}. This should not occur."); + CellReference { + memory_id: *id, + index: Some(match memory { + Memory::Cells { id: _, len: _ } => cell_index, + Memory::MappedCells { + id: _, + start_index, + len: _, + } => *start_index + cell_index, + _ => unreachable!(), + }), } - ( - VariableDefinition::Multi { name: _, len }, - VariableTarget::MultiSpread { name: _ }, - ) => Memory::Cells { - id: *mem_id, - len: *len, - target_index: None, + } + // valid states, user error + ( + Some(_), + ValueType::Cell, + Memory::Cell { id: _ } | Memory::MappedCell { id: _, index: _ }, + ) => r_panic!("Cannot get subfields of cell type: {target}"), + ( + None, + ValueType::Array(_, _) | ValueType::DictStruct(_), + Memory::Cells { id: _, len: _ } + | Memory::MappedCells { + id: _, + start_index: _, + len: _, }, - _ => { - r_panic!("Malformed variable reference {var} to {var_def}") + ) => r_panic!("Expected single cell reference in target: {target}"), + // invalid states, indicating an internal compiler issue (akin to 5xx error) + ( + _, + ValueType::Cell, + Memory::Cells { id: _, len: _ } + | Memory::MappedCells { + id: _, + start_index: _, + len: _, + }, + ) + | ( + _, + ValueType::Array(_, _) | ValueType::DictStruct(_), + Memory::Cell { id: _ } | Memory::MappedCell { id: _, index: _ }, + ) => r_panic!( + "Invalid memory for value type in target: {target}. This should not occur." + ), + }) + } + + /// Return a list of cell references for an array of cells (not an array of structs) + fn get_array_cells(&self, target: &VariableTarget) -> Result, String> { + let (full_type, memory) = self.get_base_variable_memory(&target.name)?; + Ok(match (&target.subfields, full_type, memory) { + ( + None, + ValueType::Array(arr_len, element_type), + Memory::Cells { id, len } + | Memory::MappedCells { + id, + start_index: _, + len, + }, + ) => { + let ValueType::Cell = **element_type else { + r_panic!("Cannot get array cells of struct array: {target}"); + }; + r_assert!( + *arr_len == *len, + "Array memory incorrect length {len} for array length {arr_len}." + ); + (match memory { + Memory::Cells { id: _, len } => 0..*len, + Memory::MappedCells { + id: _, + start_index, + len, + } => *start_index..(*start_index + *len), + _ => unreachable!(), + }) + .map(|i| CellReference { + memory_id: *id, + index: Some(i), + }) + .collect() + } + ( + Some(subfields), + ValueType::Array(_, _) | ValueType::DictStruct(_), + Memory::Cells { id, len: _ } + | Memory::MappedCells { + id, + start_index: _, + len: _, + }, + ) => { + let (subfield_type, offset_index) = full_type.get_subfield(subfields)?; + let ValueType::Array(arr_len, element_type) = subfield_type else { + r_panic!("Expected array type in subfield variable target \"{target}\"."); + }; + let ValueType::Cell = **element_type else { + r_panic!("Expected cell array in subfield variable target \"{target}\"."); + }; + + (match memory { + Memory::Cells { id: _, len: _ } => offset_index..(offset_index + *arr_len), + Memory::MappedCells { + id: _, + start_index, + len: _, + } => (*start_index + offset_index)..(*start_index + offset_index + *arr_len), + _ => unreachable!(), + }) + .map(|i| CellReference { + memory_id: *id, + index: Some(i), + }) + .collect() + } + ( + None, + ValueType::DictStruct(_), + Memory::Cells { id: _, len: _ } + | Memory::MappedCells { + id: _, + start_index: _, + len: _, + }, + ) + | ( + None, + ValueType::Cell, + Memory::Cell { id: _ } | Memory::MappedCell { id: _, index: _ }, + ) => { + r_panic!("Expected cell array type in variable target: {target}") + } + ( + Some(_), + ValueType::Cell, + Memory::Cell { id: _ } | Memory::MappedCell { id: _, index: _ }, + ) => { + r_panic!("Attempted to retrieve array subfield from cell type: {target}") + } + ( + _, + ValueType::Cell, + Memory::Cells { id: _, len: _ } + | Memory::MappedCells { + id: _, + start_index: _, + len: _, + }, + ) + | ( + _, + ValueType::Array(_, _) | ValueType::DictStruct(_), + Memory::Cell { id: _ } | Memory::MappedCell { id: _, index: _ }, + ) => r_panic!( + "Invalid memory for value type in target: {target}. This should not occur." + ), + }) + } + + /// Return the first memory cell of a target allocation, used for location specifiers + fn get_target_cell_reference(&self, target: &VariableTarget) -> Result { + let (full_type, memory) = self.get_base_variable_memory(&target.name)?; + Ok(match &target.subfields { + None => match memory { + Memory::Cell { id } => CellReference { + memory_id: *id, + index: None, + }, + Memory::MappedCell { id, index } => CellReference { + memory_id: *id, + index: *index, + }, + Memory::Cells { id, len: _ } => CellReference { + memory_id: *id, + index: Some(0), + }, + Memory::MappedCells { + id, + start_index, + len: _, + } => CellReference { + memory_id: *id, + index: Some(*start_index), + }, + }, + Some(subfield_chain) => { + let (_subfield_type, offset_index) = full_type.get_subfield(&subfield_chain)?; + match memory { + Memory::Cell { id: _ } |Memory::MappedCell { id: _, index: _ } => r_panic!("Attempted to get cell reference of subfield of single cell memory: {target}"), + Memory::Cells { id, len } | Memory::MappedCells { id, start_index: _, len } => { + r_assert!(offset_index < *len, "Subfield memory index out of allocation range. This should not occur. ({target})"); + let index = match memory { + Memory::Cells { id: _, len: _ } => offset_index, + Memory::MappedCells { id: _, start_index, len: _ } => *start_index + offset_index, + Memory::Cell { id: _ } | Memory::MappedCell { id: _, index: _ } => unreachable!() + }; + CellReference {memory_id: *id, index: Some(index)} + } } - }) - } else if self.fn_only { - r_panic!("Attempted to access variable memory outside of embedded Mastermind context."); - } else if let Some(outer_scope) = self.outer_scope { - // recursive case - if let Some(translation) = self - .variable_aliases - .iter() - .find(|translation| *translation.get_def_name() == *var.name()) - { - let alias_var = match (translation, var) { - ( - ArgumentTranslation::SingleFromSingle(_, call_name), - VariableTarget::Single { name: _ }, - // single variable let g;f(g);def f(h){++h;}c - ) => VariableTarget::Single { - name: call_name.clone(), + } + }) + } + + /// Return the absolute type and memory allocation for a variable name + fn get_base_variable_memory(&self, var_name: &str) -> Result<(&ValueType, &Memory), String> { + // TODO: add function argument translations and embedded bf/mmi scope function restrictions + match (self.outer_scope, self.variable_memory.get(var_name)) { + (_, Some((value_type, memory))) => Ok((value_type, memory)), + (Some(outer_scope), None) => outer_scope.get_base_variable_memory(var_name), + (None, None) => r_panic!("No variable found with name \"{var_name}\"."), + } + } + + /// Get the absolute type of a full variable target, not just a name like `get_base_variable_memory` + fn get_target_type(&self, target: &VariableTarget) -> Result<&ValueType, String> { + let (var_type, _memory) = self.get_base_variable_memory(&target.name)?; + Ok(match &target.subfields { + None => var_type, + Some(subfields) => { + let (subfield_type, _memory_index) = var_type.get_subfield(subfields)?; + subfield_type + } + }) + } + + /// Create memory mapping between a pre-existing variable and a new one, used for function arguments + fn create_mapped_variable( + &mut self, + mapped_var_name: String, + target: &VariableTarget, + ) -> Result<(), String> { + let (base_var_type, base_var_memory) = self.get_base_variable_memory(&target.name)?; + let (var_type, mapped_memory) = match &target.subfields { + None => ( + base_var_type, + match base_var_memory { + Memory::Cell { id } => Memory::MappedCell { + id: *id, + index: None, }, - ( - ArgumentTranslation::SingleFromMultiCell(_, (call_name, call_index)), - VariableTarget::Single { name: _ }, - // referenced byte passed as single let g[9];f(g[0]);def f(h){++h;} - ) => VariableTarget::MultiCell { - name: call_name.clone(), - index: *call_index, + Memory::Cells { id, len } => Memory::MappedCells { + id: *id, + start_index: 0, + len: *len, }, - ( - ArgumentTranslation::MultiFromMulti(_, call_name), - VariableTarget::MultiCell { name: _, index }, - // referenced byte from multi-byte variable let g[9];f(g);def f(h[9]){++h[0];} - ) => VariableTarget::MultiCell { - name: call_name.clone(), + Memory::MappedCell { id, index } => Memory::MappedCell { + id: *id, index: *index, }, - ( - ArgumentTranslation::MultiFromMulti(_, call_name), - VariableTarget::MultiSpread { name: _ }, - // spread from multi-byte variable let g[9];f(g);def f(h[9]){output *h;} - ) => VariableTarget::MultiSpread { - name: call_name.clone(), + Memory::MappedCells { + id, + start_index, + len, + } => Memory::MappedCells { + id: *id, + start_index: *start_index, + len: *len, }, - _ => r_panic!( - "Malformed argument/variable translation {translation:#?}, target: {var}. \ - I realise this error doesn't tell you much, this error should not occur anyway." - ), - }; - Ok(outer_scope.get_memory(&alias_var)?) - } else { - // again not sure if Ok + ? is a bad pattern - Ok(outer_scope.get_memory(var)?) + }, + ), + Some(subfields) => { + let (subfield_type, offset_index) = base_var_type.get_subfield(subfields)?; + // let subfield_size = subfield_type.size(); + ( + subfield_type, + match (subfield_type, base_var_memory) { + (ValueType::Cell, Memory::Cells { id, len: _ }) => { + // r_assert!((offset_index + subfield_size) <= *len, "Subfield \"{target}\" size and offset out of memory bounds. This should never occur."); + Memory::MappedCell { + id: *id, + index: Some(offset_index), + } + } + ( + ValueType::Cell, + Memory::MappedCells { + id, + start_index, + len: _, + }, + ) => Memory::MappedCell { + id: *id, + index: Some(*start_index + offset_index), + }, + ( + ValueType::Array(_, _) | ValueType::DictStruct(_), + Memory::Cells { id, len: _ }, + ) => Memory::MappedCells { + id: *id, + start_index: offset_index, + len: subfield_type.size()?, + }, + ( + ValueType::Array(_, _) | ValueType::DictStruct(_), + Memory::MappedCells { + id, + start_index, + len: _, + }, + ) => Memory::MappedCells { + id: *id, + start_index: *start_index + offset_index, + len: subfield_type.size()?, + }, + (_, Memory::Cell { id: _ } | Memory::MappedCell { id: _, index: _ }) => { + r_panic!( + "Attempted to map a subfield of a single cell in \ +mapping: {mapped_var_name} -> {target}" + ) + } + }, + ) } - } else { - r_panic!("No variable {var} found in current scope."); - } + }; + + self.variable_memory + .insert(mapped_var_name, (var_type.clone(), mapped_memory)); + Ok(()) } } diff --git a/compiler/src/constants_optimiser.rs b/compiler/src/constants_optimiser.rs index 8769784..3ecf5ea 100644 --- a/compiler/src/constants_optimiser.rs +++ b/compiler/src/constants_optimiser.rs @@ -1,3 +1,4 @@ +// TODO: make unit tests for this use crate::builder::{BrainfuckCodeBuilder, Opcode, TapeCell}; // basically, most ascii characters are large numbers, which are more efficient to calculate with multiplication than with a bunch of + or - @@ -13,7 +14,8 @@ pub fn calculate_optimal_addition( target_cell: TapeCell, temp_cell: TapeCell, ) -> BrainfuckCodeBuilder { - let abs_value = value.abs(); + // can't abs() i8 directly because there is no +128i8, so abs(-128i8) crashes + let abs_value = (value as i32).abs(); // STAGE 0: // for efficiency's sake, calculate the cost of just adding the constant to the cell diff --git a/compiler/src/lib.rs b/compiler/src/lib.rs index e950e5c..955aea5 100644 --- a/compiler/src/lib.rs +++ b/compiler/src/lib.rs @@ -13,7 +13,7 @@ mod parser; mod preprocessor; mod tokeniser; -use brainfuck::BVM; +use brainfuck::{BVMConfig, BVM}; use brainfuck_optimiser::optimise; use builder::{BrainfuckOpcodes, Builder}; use compiler::Compiler; @@ -59,7 +59,7 @@ pub fn wasm_compile( let bf_code = builder.build(instructions.finalise_instructions(false), false)?; Ok(match config.optimise_generated_code { - true => optimise(bf_code).to_string(), + true => optimise(bf_code, config.optimise_generated_all_permutations).to_string(), false => bf_code.to_string(), }) } @@ -67,12 +67,17 @@ pub fn wasm_compile( #[wasm_bindgen] pub async fn wasm_run_bf( code: String, + enable_2d_grid: bool, output_callback: &js_sys::Function, input_callback: &js_sys::Function, ) -> Result { set_panic_hook(); - let mut bf = BVM::new(code.chars().collect()); + let config = BVMConfig { + enable_debug_symbols: false, + enable_2d_grid: enable_2d_grid, + }; + let mut bf = BVM::new(config, code.chars().collect()); // hack, TODO: refactor let r = bf.run_async(output_callback, input_callback).await?; diff --git a/compiler/src/main.rs b/compiler/src/main.rs index 315200e..9b38a0f 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -15,7 +15,7 @@ mod tokeniser; // 1. Tokenise mod misc; mod tests; -use brainfuck::BVM; +use brainfuck::{BVMConfig, BVM}; use brainfuck_optimiser::optimise; use builder::{BrainfuckOpcodes, Builder}; use compiler::Compiler; @@ -110,7 +110,9 @@ fn main() -> Result<(), String> { let bf_program = builder.build(instructions, false)?; match config.optimise_generated_code { - true => optimise(bf_program).to_string(), + true => { + optimise(bf_program, config.optimise_generated_all_permutations).to_string() + } false => bf_program.to_string(), } } @@ -119,12 +121,16 @@ fn main() -> Result<(), String> { if args.run || !args.compile { // run brainfuck - let mut bvm = BVM::new(bf_program.chars().collect()); + let config = BVMConfig { + enable_debug_symbols: false, + enable_2d_grid: false, + }; + let mut bvm = BVM::new(config, bf_program.chars().collect()); if args.input.is_some() { - bvm.run(&mut Cursor::new(args.input.unwrap()), &mut stdout())?; + bvm.run(&mut Cursor::new(args.input.unwrap()), &mut stdout(), None)?; } else { - bvm.run(&mut stdin(), &mut stdout())?; + bvm.run(&mut stdin(), &mut stdout(), None)?; } } else { print!("{bf_program}"); diff --git a/compiler/src/misc.rs b/compiler/src/misc.rs index 752e0be..b61e3bf 100644 --- a/compiler/src/misc.rs +++ b/compiler/src/misc.rs @@ -2,6 +2,7 @@ pub struct MastermindConfig { // basic pure brainfuck optimisations pub optimise_generated_code: bool, + pub optimise_generated_all_permutations: bool, // track cell value and clear with constant addition if possible pub optimise_cell_clearing: bool, // track cell value and skip loops which can never be entered @@ -16,18 +17,29 @@ pub struct MastermindConfig { pub optimise_constants: bool, // TODO: recursively prune if statements/loops if they do nothing pub optimise_empty_blocks: bool, + // Memory Allocation Method + //'1D Mastermind' 0 + //'2D Mastermind - ZigZag' 1 + // '2D Mastermind - Spiral' 2 + // '2D Mastermind - Tiles' 2 + // '2D Mastermind - Nearest' 3 + pub memory_allocation_method: u8, + pub enable_2d_grid: bool, } impl MastermindConfig { pub fn new(optimise_bitmask: usize) -> MastermindConfig { MastermindConfig { optimise_generated_code: (optimise_bitmask & 0b00000001) > 0, + optimise_generated_all_permutations: (optimise_bitmask & 0b00001000) > 0, optimise_cell_clearing: (optimise_bitmask & 0b00000010) > 0, optimise_unreachable_loops: (optimise_bitmask & 0b00000100) > 0, optimise_variable_usage: false, optimise_memory_allocation: false, optimise_constants: false, optimise_empty_blocks: false, + memory_allocation_method: 0, + enable_2d_grid: false, } } } diff --git a/compiler/src/parser.rs b/compiler/src/parser.rs index a6c9e62..4b8b809 100644 --- a/compiler/src/parser.rs +++ b/compiler/src/parser.rs @@ -1,10 +1,9 @@ -use std::{fmt::Display, mem::discriminant, num::Wrapping}; - use crate::{ builder::TapeCell, macros::macros::{r_assert, r_panic}, tokeniser::Token, }; +use std::{fmt::Display, mem::discriminant, num::Wrapping}; // recursive function to create a tree representation of the program pub fn parse(tokens: &[Token]) -> Result, String> { @@ -18,13 +17,17 @@ pub fn parse(tokens: &[Token]) -> Result, String> { &clause.get(1).unwrap_or(&Token::None), &clause.get(2).unwrap_or(&Token::None), ) { - (Token::Let, _, _) => { + (Token::Cell, _, _) + | (Token::Struct, Token::Name(_), Token::Name(_) | Token::OpenSquareBracket) => { clauses.push(parse_let_clause(clause)?); } + (Token::Struct, Token::Name(_), Token::OpenBrace) => { + clauses.push(parse_struct_clause(clause)?); + } (Token::Plus, Token::Plus, _) | (Token::Minus, Token::Minus, _) => { clauses.push(parse_increment_clause(clause)?); } - (Token::Name(_), Token::EqualsSign, _) => { + (Token::Name(_), Token::EqualsSign | Token::Dot | Token::OpenSquareBracket, _) => { clauses.extend(parse_set_clause(clause)?); } (Token::Drain, _, _) => { @@ -42,10 +45,10 @@ pub fn parse(tokens: &[Token]) -> Result, String> { (Token::Input, _, _) => { clauses.push(parse_input_clause(clause)?); } - (Token::Name(_), Token::OpenAngledBracket, _) => { + (Token::Name(_), Token::OpenParenthesis, _) => { clauses.push(parse_function_call_clause(clause)?); } - (Token::Def, _, _) => { + (Token::Fn, _, _) => { clauses.push(parse_function_definition_clause(clause)?); } (Token::Name(_), Token::Plus | Token::Minus, Token::EqualsSign) => { @@ -54,20 +57,6 @@ pub fn parse(tokens: &[Token]) -> Result, String> { (Token::If, _, _) => { clauses.push(parse_if_else_clause(clause)?); } - (Token::Name(_), Token::OpenSquareBracket, _) - | (Token::Asterisk, Token::Name(_), _) => { - let (_, len) = parse_var_target(clause)?; - let remaining = &clause[len..]; - match (&remaining[0], &remaining[1]) { - (Token::EqualsSign, _) => { - clauses.extend(parse_set_clause(clause)?); - } - (Token::Plus | Token::Minus, Token::EqualsSign) => { - clauses.extend(parse_add_clause(clause)?); - } - _ => r_panic!("Invalid clause: {clause:#?}"), - } - } (Token::OpenBrace, _, _) => { let braced_tokens = get_braced_tokens(clause, BRACES)?; let inner_clauses = parse(braced_tokens)?; @@ -90,8 +79,8 @@ pub fn parse(tokens: &[Token]) -> Result, String> { | Token::ClosingSquareBracket | Token::OpenParenthesis | Token::ClosingParenthesis - | Token::OpenAngledBracket - | Token::ClosingAngledBracket + | Token::LessThan + | Token::MoreThan | Token::Comma | Token::Plus | Token::Minus @@ -108,7 +97,9 @@ pub fn parse(tokens: &[Token]) -> Result, String> { | Token::Equals | Token::Unknown | Token::Dot - | Token::At, + | Token::At + | Token::Struct + | Token::UpToken, _, _, ) => r_panic!("Invalid clause: {clause:#?}"), @@ -120,17 +111,12 @@ pub fn parse(tokens: &[Token]) -> Result, String> { } fn parse_let_clause(clause: &[Token]) -> Result { - // let foo = 9; - // let arr[2] = ??; - // let g; - // let why[9]; - let mut i = 1usize; + // cell x = 0; + // struct DummyStruct y + let mut i = 0usize; // this kind of logic could probably be done with iterators, (TODO for future refactors) - let (var, len) = parse_var_definition(&clause[i..])?; - i += len; - - let (location_specifier, len) = parse_location_specifier(&clause[i..])?; + let (var, len) = parse_var_definition(&clause[i..], true)?; i += len; if let Token::EqualsSign = &clause[i] { @@ -139,21 +125,63 @@ fn parse_let_clause(clause: &[Token]) -> Result { let expr = Expression::parse(remaining)?; // equivalent to set clause stuff // except we need to convert a variable definition to a variable target - Ok(Clause::DefineVariable { - var, - location_specifier, - value: expr, - }) + Ok(Clause::DefineVariable { var, value: expr }) } else if i < (clause.len() - 1) { r_panic!("Invalid token in let clause: {clause:#?}"); } else { - Ok(Clause::DeclareVariable { - var, - location_specifier, - }) + Ok(Clause::DeclareVariable { var }) } } +/// Parse tokens representing a struct definition into a clause +fn parse_struct_clause(clause: &[Token]) -> Result { + let mut i = 0usize; + let Token::Struct = &clause[i] else { + r_panic!("Expected struct keyword in struct clause. This should never occur. {clause:#?}"); + }; + i += 1; + + let Token::Name(struct_name) = &clause[i] else { + r_panic!("Expected identifier in struct clause. This should never occur. {clause:#?}"); + }; + i += 1; + + let Token::OpenBrace = &clause[i] else { + r_panic!("Expected open brace in struct clause: {clause:#?}"); + }; + let braced_tokens = get_braced_tokens(&clause[i..], BRACES)?; + + let mut fields = vec![]; + + let mut j = 0usize; + loop { + let (field, len) = parse_var_definition(&braced_tokens[j..], true)?; + j += len; + fields.push(field); + r_assert!( + j <= braced_tokens.len(), + "Struct definition field exceeded braces. This should never occur. {clause:#?}" + ); + let Token::Semicolon = &braced_tokens[j] else { + r_panic!("Expected semicolon in struct definition field: {clause:#?}"); + }; + j += 1; + if j == braced_tokens.len() { + break; + } + } + r_assert!( + j == braced_tokens.len(), + "Struct definitions exceeded braces. This should never occur. {clause:#?}" + ); + // i += j + 2; + + Ok(Clause::DefineStruct { + name: struct_name.clone(), + fields, + }) +} + fn parse_add_clause(clause: &[Token]) -> Result, String> { let mut clauses: Vec = Vec::new(); let mut i = 0usize; @@ -176,8 +204,14 @@ fn parse_add_clause(clause: &[Token]) -> Result, String> { summands: vec![raw_expr], }, }; + //Check if this add clause self references + let self_referencing = expr.check_self_referencing(&var); - clauses.push(Clause::AddToVariable { var, value: expr }); + clauses.push(Clause::AddToVariable { + var, + value: expr, + self_referencing: self_referencing, + }); Ok(clauses) } @@ -185,15 +219,17 @@ fn parse_add_clause(clause: &[Token]) -> Result, String> { // currently just syntax sugar, should make it actually do post/pre increments fn parse_increment_clause(clause: &[Token]) -> Result { let (var, _) = parse_var_target(&clause[2..])?; - + //An increment clause can never be self referencing since it just VAR++ Ok(match (&clause[0], &clause[1]) { (Token::Plus, Token::Plus) => Clause::AddToVariable { var, value: Expression::NaturalNumber(1), + self_referencing: false, }, (Token::Minus, Token::Minus) => Clause::AddToVariable { var, value: Expression::NaturalNumber((-1i8 as u8) as usize), + self_referencing: false, }, _ => { r_panic!("Invalid pattern in increment clause: {clause:#?}"); @@ -203,21 +239,50 @@ fn parse_increment_clause(clause: &[Token]) -> Result { } fn parse_set_clause(clause: &[Token]) -> Result, String> { - // TODO: what do we do about arrays and strings? + // TODO: what do we do about arrays and strings and structs? let mut clauses: Vec = Vec::new(); let mut i = 0usize; let (var, len) = parse_var_target(&clause[i..])?; i += len; // definitely could use iterators instead (TODO for refactor) - let Token::EqualsSign = &clause[i] else { - r_panic!("Expected equals sign in set clause: {clause:#?}"); - }; - i += 1; + match &clause[i] { + Token::EqualsSign => { + i += 1; + let expr = Expression::parse(&clause[i..(clause.len() - 1)])?; + let self_referencing = expr.check_self_referencing(&var); + clauses.push(Clause::SetVariable { + var, + value: expr, + self_referencing, + }); + } + Token::Plus | Token::Minus => { + let is_add = if let Token::Plus = &clause[i] { + true + } else { + false + }; + i += 1; + let Token::EqualsSign = &clause[i] else { + r_panic!("Expected equals sign in add-assign operator: {clause:#?}"); + }; + i += 1; - let expr = Expression::parse(&clause[i..(clause.len() - 1)])?; + let mut expr = Expression::parse(&clause[i..(clause.len() - 1)])?; + if !is_add { + expr = expr.flipped_sign()?; + } - clauses.push(Clause::SetVariable { var, value: expr }); + let self_referencing = expr.check_self_referencing(&var); + clauses.push(Clause::AddToVariable { + var, + value: expr, + self_referencing, + }); + } + _ => r_panic!("Expected assignment operator in set clause: {clause:#?}"), + } Ok(clauses) } @@ -417,31 +482,79 @@ fn parse_assert_clause(clause: &[Token]) -> Result { } // parse any memory location specifiers -// let g @4 = 68; -fn parse_location_specifier(tokens: &[Token]) -> Result<(Option, usize), String> { +// let g @4,2 = 68; +// or +// let p @3 = 68; +fn parse_location_specifier(tokens: &[Token]) -> Result<(LocationSpecifier, usize), String> { + if tokens.len() == 0 { + return Ok((LocationSpecifier::None, 0)); + } if let Token::At = &tokens[0] { let mut i = 1; - let positive = if let Token::Minus = &tokens[i] { - i += 1; - false - } else { - true - }; - let Token::Digits(raw) = &tokens[i] else { - r_panic!("Expected constant number in memory location specifier: {tokens:#?}"); - }; - i += 1; + match &tokens[i] { + Token::Digits(_) | Token::Minus => { + let x_offset = { + let mut positive = true; + if let Token::Minus = &tokens[i] { + i += 1; + positive = false; + } + let Token::Digits(raw) = &tokens[i] else { + r_panic!( + "Expected number after \"-\" in memory location specifier: {tokens:#?}" + ); + }; + i += 1; + + // TODO: error handling + let mut offset: i32 = raw.parse().unwrap(); + if !positive { + offset = -offset; + } + offset + }; + + let y_offset = { + if let Token::Comma = &tokens[i] { + i += 1; + let mut positive = true; + if let Token::Minus = &tokens[i] { + i += 1; + positive = false; + } + let Token::Digits(raw) = &tokens[i] else { + r_panic!( + "Expected number after \"-\" in memory location specifier: {tokens:#?}" + ); + }; + i += 1; + + // TODO: error handling + let mut offset: i32 = raw.parse().unwrap(); + if !positive { + offset = -offset; + } + offset + } else { + 0 + } + }; - // TODO: error handling - let mut offset: i32 = raw.parse().unwrap(); - if !positive { - offset = -offset; + return Ok((LocationSpecifier::Cell((x_offset, y_offset)), i)); + } + Token::Name(_) => { + // variable location specifier + let (var, len) = parse_var_target(&tokens[i..])?; + i += len; + + return Ok((LocationSpecifier::Variable(var), i)); + } + _ => r_panic!("Expected constant or variable in location specifier: {tokens:#?}"), } - Ok((Some(offset), i)) - } else { - Ok((None, 0)) } + + Ok((LocationSpecifier::None, 0)) } fn parse_brainfuck_clause(clause: &[Token]) -> Result { @@ -486,12 +599,22 @@ fn parse_brainfuck_clause(clause: &[Token]) -> Result { match &bf_tokens[j] { Token::Plus => ops.push(ExtendedOpcode::Add), Token::Minus => ops.push(ExtendedOpcode::Subtract), - Token::ClosingAngledBracket => ops.push(ExtendedOpcode::Right), - Token::OpenAngledBracket => ops.push(ExtendedOpcode::Left), + Token::MoreThan => ops.push(ExtendedOpcode::Right), + Token::LessThan => ops.push(ExtendedOpcode::Left), + Token::UpToken => ops.push(ExtendedOpcode::Up), Token::OpenSquareBracket => ops.push(ExtendedOpcode::OpenLoop), Token::ClosingSquareBracket => ops.push(ExtendedOpcode::CloseLoop), Token::Dot => ops.push(ExtendedOpcode::Output), Token::Comma => ops.push(ExtendedOpcode::Input), + Token::Name(s) => { + for c in s.chars() { + if c == 'v' { + ops.push(ExtendedOpcode::Down); + } else { + panic!("Invalid Inline Brainfuck Characters in {s}"); + } + } + } Token::OpenBrace => { // embedded mastermind let block_tokens = get_braced_tokens(&bf_tokens[j..], BRACES)?; @@ -520,18 +643,18 @@ fn parse_function_definition_clause(clause: &[Token]) -> Result }; let mut args = Vec::new(); i += 1; - let Token::OpenAngledBracket = &clause[i] else { + let Token::OpenParenthesis = &clause[i] else { r_panic!("Expected argument list in function definition clause: {clause:#?}"); }; - let arg_tokens = get_braced_tokens(&clause[i..], ANGLED_BRACKETS)?; + let arg_tokens = get_braced_tokens(&clause[i..], PARENTHESES)?; let mut j = 0usize; // parse function argument names while j < arg_tokens.len() { - // this used to be in the while condition but moved it here to check for the case of no arguments - let Token::Name(_) = &arg_tokens[j] else { + // break if no more arguments + let (Token::Cell | Token::Struct) = &arg_tokens[j] else { break; }; - let (var, len) = parse_var_definition(&arg_tokens[j..])?; + let (var, len) = parse_var_definition(&arg_tokens[j..], false)?; j += len; args.push(var); @@ -571,10 +694,10 @@ fn parse_function_call_clause(clause: &[Token]) -> Result { let mut args = Vec::new(); i += 1; - let Token::OpenAngledBracket = &clause[i] else { + let Token::OpenParenthesis = &clause[i] else { r_panic!("Expected argument list in function call clause: {clause:#?}"); }; - let arg_tokens = get_braced_tokens(&clause[i..], ANGLED_BRACKETS)?; + let arg_tokens = get_braced_tokens(&clause[i..], PARENTHESES)?; let mut j = 0usize; while j < arg_tokens.len() { @@ -610,90 +733,136 @@ fn parse_function_call_clause(clause: &[Token]) -> Result { fn parse_var_target(tokens: &[Token]) -> Result<(VariableTarget, usize), String> { let mut i = 0usize; - let spread = if let Token::Asterisk = &tokens[i] { - // spread syntax + let is_spread = if let Token::Asterisk = &tokens[i] { i += 1; true } else { false }; + let Token::Name(var_name) = &tokens[i] else { - r_panic!("Expected identifier in variable identifier: {tokens:#?}"); + r_panic!("Expected identifier in variable target identifier: {tokens:#?}"); }; i += 1; - if let Some(Token::OpenSquareBracket) = &tokens.get(i) { - if spread { - r_panic!( - "Cannot use spread operator and subscript on the same variable target: {tokens:#?}" - ); + let mut ref_chain = vec![]; + while i < tokens.len() { + match &tokens[i] { + Token::OpenSquareBracket => { + let (index, tokens_used) = parse_subscript(&tokens[i..])?; + i += tokens_used; + ref_chain.push(Reference::Index(index)); + } + Token::Dot => { + i += 1; + let Token::Name(subfield_name) = &tokens[i] else { + r_panic!("Expected subfield name in variable target identifier: {tokens:#?}"); + }; + i += 1; + + ref_chain.push(Reference::NamedField(subfield_name.clone())); + } + _ => { + break; + } } - let subscript = get_braced_tokens(&tokens[i..], SQUARE_BRACKETS)?; - let Expression::NaturalNumber(index) = Expression::parse(subscript)? else { - r_panic!( - "Expected a constant array index specifier in variable identifier: {tokens:#?}" - ); - }; - i += 2 + subscript.len(); + } - Ok(( - VariableTarget::MultiCell { - name: var_name.clone(), - index, - }, - i, - )) - } else if spread { - Ok(( - VariableTarget::MultiSpread { - name: var_name.clone(), - }, - i, - )) - } else { - Ok(( - VariableTarget::Single { - name: var_name.clone(), + Ok(( + VariableTarget { + name: var_name.clone(), + subfields: if ref_chain.len() > 0 { + Some(VariableTargetReferenceChain(ref_chain)) + } else { + None }, - i, - )) - } - // also return the length of tokens read + is_spread, + }, + i, + )) } -fn parse_var_definition(tokens: &[Token]) -> Result<(VariableDefinition, usize), String> { +/// convert tokens of a variable definition into data representation, e.g. `cell x`, `struct G g`, `cell[5] x_arr`, `struct H[100] hs` +fn parse_var_definition( + tokens: &[Token], + allow_location: bool, +) -> Result<(VariableDefinition, usize), String> { let mut i = 0usize; + let mut var_type = match &tokens[i] { + Token::Cell => { + i += 1; + + VariableTypeReference::Cell + } + Token::Struct => { + i += 1; + + let Token::Name(struct_name) = &tokens[i] else { + r_panic!("Expected struct type name in variable definition: {tokens:#?}"); + }; + i += 1; + + VariableTypeReference::Struct(struct_name.clone()) + } + _ => { + r_panic!("Unexpected token in variable definition, this should not occur: {tokens:#?}") + } + }; + + // parse array specifiers + while let Token::OpenSquareBracket = &tokens[i] { + let (len, j) = parse_array_length(&tokens[i..])?; + i += j; + + var_type = VariableTypeReference::Array(Box::new(var_type), len); + } + let Token::Name(var_name) = &tokens[i] else { r_panic!("Expected identifier in variable definition: {tokens:#?}"); }; i += 1; - Ok(( - if let Some(Token::OpenSquareBracket) = &tokens.get(i) { - let subscript = get_braced_tokens(&tokens[i..], SQUARE_BRACKETS)?; - let Expression::NaturalNumber(len) = Expression::parse(subscript)? else { - r_panic!("Expected a constant array length specifier in variable definition: {tokens:#?}"); - }; - r_assert!( - len > 0, - "Multi-byte variable cannot be zero-length: {tokens:#?}" - ); - i += 2 + subscript.len(); + let (location_specifier, len) = parse_location_specifier(&tokens[i..])?; - VariableDefinition::Multi { - name: var_name.clone(), - len, - } - } else { - VariableDefinition::Single { - name: var_name.clone(), - } + r_assert!( + location_specifier.is_none() || allow_location, + "Unexpected location specifier in variable definition: {tokens:#?}" + ); + i += len; + + Ok(( + VariableDefinition { + var_type, + name: var_name.clone(), + location_specifier, }, - // also return the length of tokens read i, )) } +/// parse the subscript of an array variable, e.g. [4] [6] +/// must be compile-time constant +/// returns (array length, tokens used) +/// assumes the first token is an open square bracket +fn parse_subscript(tokens: &[Token]) -> Result<(usize, usize), String> { + let mut i = 0usize; + let subscript = get_braced_tokens(&tokens[i..], SQUARE_BRACKETS)?; + let Expression::NaturalNumber(len) = Expression::parse(subscript)? else { + r_panic!("Expected a compile-time constant in subscript: {tokens:#?}"); + }; + + i += 2 + subscript.len(); + + Ok((len, i)) +} + +/// parse_array_subscript but with a length check +fn parse_array_length(tokens: &[Token]) -> Result<(usize, usize), String> { + let (len, i) = parse_subscript(tokens)?; + r_assert!(len > 0, "Array variable cannot be zero-length: {tokens:#?}"); + Ok((len, i)) +} + // get a clause, typically a line, bounded by ; fn get_clause_tokens(tokens: &[Token]) -> Result, String> { if tokens.len() < 2 { @@ -732,21 +901,21 @@ fn get_clause_tokens(tokens: &[Token]) -> Result, String> { const SQUARE_BRACKETS: (Token, Token) = (Token::OpenSquareBracket, Token::ClosingSquareBracket); const BRACES: (Token, Token) = (Token::OpenBrace, Token::ClosingBrace); const PARENTHESES: (Token, Token) = (Token::OpenParenthesis, Token::ClosingParenthesis); -const ANGLED_BRACKETS: (Token, Token) = (Token::OpenAngledBracket, Token::ClosingAngledBracket); +const ANGLED_BRACKETS: (Token, Token) = (Token::LessThan, Token::MoreThan); // this should be a generic function but rust doesn't support enum variants as type arguments yet // find tokens bounded by matching brackets // TODO: make an impl for &[Token] and put all these functions in it fn get_braced_tokens(tokens: &[Token], braces: (Token, Token)) -> Result<&[Token], String> { - let _braces = (discriminant(&braces.0), discriminant(&braces.1)); + let (open_brace, closing_brace) = (discriminant(&braces.0), discriminant(&braces.1)); // find corresponding bracket, the depth check is unnecessary but whatever let len = { let mut i = 1usize; let mut depth = 1; while i < tokens.len() && depth > 0 { let g = discriminant(&tokens[i]); - if g == _braces.0 { + if g == open_brace { depth += 1; - } else if g == _braces.1 { + } else if g == closing_brace { depth -= 1; } i += 1; @@ -755,7 +924,8 @@ fn get_braced_tokens(tokens: &[Token], braces: (Token, Token)) -> Result<&[Token }; if len >= 2 { - if _braces.0 == discriminant(&tokens[0]) && _braces.1 == discriminant(&tokens[len - 1]) { + if open_brace == discriminant(&tokens[0]) && closing_brace == discriminant(&tokens[len - 1]) + { return Ok(&tokens[1..(len - 1)]); } } @@ -930,11 +1100,33 @@ impl Expression { } } + /// flip the sign of an expression, equivalent to `x => -(x)` + pub fn flipped_sign(self) -> Result { + Ok(match self { + Expression::SumExpression { sign, summands } => Expression::SumExpression { + sign: sign.flipped(), + summands, + }, + Expression::NaturalNumber(_) | Expression::VariableReference(_) => { + Expression::SumExpression { + sign: Sign::Negative, + summands: vec![self], + } + } + Expression::ArrayLiteral(_) | Expression::StringLiteral(_) => { + r_panic!( + "Attempted to invert sign of array or string literal, \ + do not use += or -= on arrays or strings." + ); + } + }) + } + // not sure if this is the compiler's concern or if it should be the parser // (constant to add, variables to add, variables to subtract) // currently multiplication is not supported so order of operations and flattening is very trivial // If we add multiplication in future it will likely be constant multiplication only, so no variable on variable multiplication - pub fn flatten(self) -> Result<(u8, Vec, Vec), String> { + pub fn flatten(&self) -> Result<(u8, Vec, Vec), String> { let expr = self; let mut imm_sum = Wrapping(0u8); let mut additions = Vec::new(); @@ -969,10 +1161,10 @@ impl Expression { }; } Expression::NaturalNumber(number) => { - imm_sum += Wrapping(number as u8); + imm_sum += Wrapping(*number as u8); } Expression::VariableReference(var) => { - additions.push(var); + additions.push(var.clone()); } Expression::ArrayLiteral(_) | Expression::StringLiteral(_) => { r_panic!("Attempt to flatten an array-like expression: {expr:#?}"); @@ -981,6 +1173,23 @@ impl Expression { Ok((imm_sum.0, additions, subtractions)) } + + //Recursively Check If This Is Self Referencing + pub fn check_self_referencing(&self, parent: &VariableTarget) -> bool { + // TODO: make sure nested values work correctly + match self { + Expression::SumExpression { + sign: _sign, + summands, + } => summands + .iter() + .any(|summand| summand.check_self_referencing(parent)), + Expression::VariableReference(var) => *var == *parent, + Expression::ArrayLiteral(_) + | Expression::StringLiteral(_) + | Expression::NaturalNumber(_) => false, + } + } } // TODO: add multiplication @@ -1002,25 +1211,37 @@ pub enum Sign { Positive, Negative, } +impl Sign { + fn flipped(self) -> Sign { + match self { + Sign::Positive => Sign::Negative, + Sign::Negative => Sign::Positive, + } + } +} #[derive(Debug, Clone)] pub enum Clause { DeclareVariable { var: VariableDefinition, - location_specifier: Option, }, DefineVariable { var: VariableDefinition, - location_specifier: Option, value: Expression, }, + DefineStruct { + name: String, + fields: Vec, + }, AddToVariable { var: VariableTarget, value: Expression, + self_referencing: bool, }, SetVariable { var: VariableTarget, value: Expression, + self_referencing: bool, }, AssertVariableValue { var: VariableTarget, @@ -1060,7 +1281,7 @@ pub enum Clause { }, Block(Vec), InlineBrainfuck { - location_specifier: Option, + location_specifier: LocationSpecifier, clobbered_variables: Vec, // TODO: make this support embedded mastermind operations: Vec, @@ -1079,70 +1300,134 @@ pub enum ExtendedOpcode { Output, Input, Block(Vec), + Up, + Down, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +/// the type of a variable according to the user, not validated yet as the parser does not keep track of types +// maybe it should keep track of types? +pub enum VariableTypeReference { + Cell, + Struct(String), + Array(Box, usize), +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum LocationSpecifier { + None, + Cell(TapeCell), + Variable(VariableTarget), +} +impl LocationSpecifier { + fn is_none(&self) -> bool { + match self { + LocationSpecifier::None => true, + _ => false, + } + } } -// TODO: refactor to this instead: #[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum VariableDefinition { - Single { name: String }, - Multi { name: String, len: usize }, +pub struct VariableDefinition { + pub name: String, + pub var_type: VariableTypeReference, + pub location_specifier: LocationSpecifier, // Infinite {name: String, pattern: ???}, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum VariableTarget { - Single { name: String }, - MultiCell { name: String, index: usize }, - MultiSpread { name: String }, +pub enum Reference { + NamedField(String), + Index(usize), } -impl VariableDefinition { - pub fn name(&self) -> &String { - match self { - VariableDefinition::Single { name } => name, - VariableDefinition::Multi { name, len: _ } => name, +/// Represents a list of subfield references after the `.` or `[x]` operators, e.g. `obj.h[6]` would have `['h', '[6]']` +// a bit verbose, not quite sure about this +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct VariableTargetReferenceChain(pub Vec); +/// Represents a target variable in an expression, this has no type informatino +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct VariableTarget { + pub name: String, + pub subfields: Option, + pub is_spread: bool, +} +impl VariableTarget { + /// converts a definition to a target for use with definition clauses (as opposed to declarations) + pub fn from_definition(var_def: &VariableDefinition) -> Self { + VariableTarget { + name: var_def.name.clone(), + subfields: None, + is_spread: false, } } - pub fn len(&self) -> Option { - match self { - VariableDefinition::Single { name: _ } => None, - VariableDefinition::Multi { name: _, len } => Some(*len), +} + +impl Display for VariableTypeReference { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + VariableTypeReference::Cell => f.write_str(&format!("cell")), + VariableTypeReference::Struct(struct_name) => { + f.write_str(&format!("struct {struct_name}")) + } + VariableTypeReference::Array(element_type, len) => { + f.write_str(&format!("{element_type}[{len}]")) + } } } - // get this variable definition as a variable target, strips length information and defaults to a spread reference - pub fn to_target(self) -> VariableTarget { - match self { - VariableDefinition::Single { name } => VariableTarget::Single { name }, - VariableDefinition::Multi { name, len: _ } => VariableTarget::MultiSpread { name }, +} + +impl Display for VariableDefinition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{} {}", self.var_type, self.name))?; + match &self.location_specifier { + LocationSpecifier::Cell(_) | LocationSpecifier::Variable(_) => { + f.write_str(&format!(" {}", self.location_specifier))? + } + LocationSpecifier::None => (), } + + Ok(()) } } -impl VariableTarget { - pub fn name(&self) -> &String { +impl Display for LocationSpecifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("@")?; match self { - VariableTarget::Single { name } => name, - VariableTarget::MultiCell { name, index: _ } => name, - VariableTarget::MultiSpread { name } => name, + LocationSpecifier::Cell(cell) => f.write_str(&format!("{:?}", cell))?, + LocationSpecifier::Variable(var) => f.write_str(&format!("{}", var))?, + LocationSpecifier::None => (), } + + Ok(()) } } -impl Display for VariableDefinition { +impl Display for Reference { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - VariableDefinition::Single { name } => f.write_str(name), - VariableDefinition::Multi { name, len } => f.write_str(&format!("{name}[{len}]")), + Reference::NamedField(subfield_name) => f.write_str(&format!(".{subfield_name}"))?, + Reference::Index(index) => f.write_str(&format!("[{index}]"))?, } + + Ok(()) } } impl Display for VariableTarget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VariableTarget::Single { name } => f.write_str(name), - VariableTarget::MultiCell { name, index } => f.write_str(&format!("{name}[{index}]")), - VariableTarget::MultiSpread { name } => f.write_str(&format!("*{name}")), + if self.is_spread { + f.write_str("*")?; } + f.write_str(&self.name)?; + if let Some(subfield_refs) = &self.subfields { + for ref_step in subfield_refs.0.iter() { + f.write_str(&format!("{ref_step}"))?; + } + } + + Ok(()) } } diff --git a/compiler/src/tests.rs b/compiler/src/tests.rs index 8d0faaa..c82b8b8 100644 --- a/compiler/src/tests.rs +++ b/compiler/src/tests.rs @@ -4,7 +4,7 @@ #[cfg(test)] pub mod tests { use crate::{ - brainfuck::tests::run_code, + brainfuck::{tests::run_code, BVMConfig}, builder::{BrainfuckOpcodes, Builder, Opcode}, compiler::Compiler, parser::parse, @@ -14,24 +14,81 @@ pub mod tests { // TODO: run test suite with different optimisations turned on const OPT_NONE: MastermindConfig = MastermindConfig { optimise_generated_code: false, + optimise_generated_all_permutations: false, optimise_cell_clearing: false, optimise_variable_usage: false, optimise_memory_allocation: false, optimise_unreachable_loops: false, optimise_constants: false, optimise_empty_blocks: false, + memory_allocation_method: 0, + enable_2d_grid: false, }; const OPT_ALL: MastermindConfig = MastermindConfig { optimise_generated_code: true, + optimise_generated_all_permutations: false, optimise_cell_clearing: true, optimise_variable_usage: true, optimise_memory_allocation: true, optimise_unreachable_loops: true, optimise_constants: true, optimise_empty_blocks: true, + memory_allocation_method: 0, + enable_2d_grid: false, }; + const OPT_NONE_TILES: MastermindConfig = MastermindConfig { + optimise_generated_code: false, + optimise_generated_all_permutations: false, + optimise_cell_clearing: false, + optimise_variable_usage: false, + optimise_memory_allocation: false, + optimise_unreachable_loops: false, + optimise_constants: false, + optimise_empty_blocks: false, + memory_allocation_method: 3, + enable_2d_grid: false, + }; + + const OPT_NONE_SPIRAL: MastermindConfig = MastermindConfig { + optimise_generated_code: false, + optimise_generated_all_permutations: false, + optimise_cell_clearing: false, + optimise_variable_usage: false, + optimise_memory_allocation: false, + optimise_unreachable_loops: false, + optimise_constants: false, + optimise_empty_blocks: false, + memory_allocation_method: 2, + enable_2d_grid: false, + }; + + const OPT_NONE_ZIG_ZAG: MastermindConfig = MastermindConfig { + optimise_generated_code: false, + optimise_generated_all_permutations: false, + optimise_cell_clearing: false, + optimise_variable_usage: false, + optimise_memory_allocation: false, + optimise_unreachable_loops: false, + optimise_constants: false, + optimise_empty_blocks: false, + memory_allocation_method: 1, + enable_2d_grid: false, + }; + + const BVM_CONFIG_1D: BVMConfig = BVMConfig { + enable_debug_symbols: false, + enable_2d_grid: false, + }; + + const BVM_CONFIG_2D: BVMConfig = BVMConfig { + enable_debug_symbols: false, + enable_2d_grid: true, + }; + + const TESTING_BVM_MAX_STEPS: usize = 100_000_000; + fn compile_and_run(program: String, input: String) -> Result { // println!("{program}"); // compile mastermind @@ -47,7 +104,12 @@ pub mod tests { let bfs = bf_program.to_string(); // println!("{}", bfs); // run generated brainfuck with input - Ok(run_code(bfs, input)) + Ok(run_code( + BVM_CONFIG_1D, + bfs, + input, + Some(TESTING_BVM_MAX_STEPS), + )) } fn compile_program( @@ -102,7 +164,7 @@ pub mod tests { let input = String::from(""); let desired_output = String::from(""); - let output = run_code(code, input); + let output = run_code(BVM_CONFIG_1D, code, input, None); println!("{output}"); assert_eq!(desired_output, output) } @@ -111,19 +173,19 @@ pub mod tests { fn hello_1() { let program = String::from( " -let h = 8; -let e = 5; -let l = 12; -let o = 15; +cell h = 8; +cell e = 5; +cell l = 12; +cell o = 15; // comment! -let a_char = 96; +cell a_char = 96; drain a_char into h e l o; output h; output e; output l; output l; output o; -let ten = 10; +cell ten = 10; output ten; ", ); @@ -155,7 +217,7 @@ output 10; r#"; output 'h' ;;; // comment -let EEL[5] = "ello\n"; +cell[5] EEL = "ello\n"; output EEL[0]; output EEL[1]; output EEL[2]; @@ -177,8 +239,8 @@ output 70; fn hello_4() { let program = String::from( r#" -let str[4] = [5, 12, 12, 15]; -let a = 'a' - 1; +cell[4] str = [5, 12, 12, 15]; +cell a = 'a' - 1; drain a into *str; output 'H'; output *str; @@ -227,12 +289,12 @@ output '@' + 256 + 1 + false + true + 'e' - '@'; fn expressions_2() { let program = String::from( r#"; -let p = 9 - (true + true -(-7)); +cell p = 9 - (true + true -(-7)); if not p { output "Hi friend!\n"; } -let q = 8 + p - (4 + p); +cell q = 8 + p - (4 + p); q -= 4; if q { output "path a"; @@ -258,7 +320,7 @@ if 56 - 7 { output 'B'; } -let not_a = 'a' + (-1) - (0 - 1); +cell not_a = 'a' + (-1) - (0 - 1); if not not_a - 'a' { output 'C'; } else { @@ -284,8 +346,8 @@ if not_a - 'a' { fn expressions_4() { let program = String::from( r#"; -let x = 5; -let A = 'A'; +cell x = 5; +cell A = 'A'; drain 0 + x + 1 into A { output '6'; @@ -302,18 +364,229 @@ output A; assert_eq!(desired_output, output) } + #[test] + fn assignments_1() { + let program = String::from( + r#"; +cell x = 5; +output '0' + x; +x += 1; +output '0' + x; + "#, + ); + let input = String::from(""); + let desired_output = String::from("56"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_2() { + let program = String::from( + r#"; +cell x = 5; +output '0' + x; +x = x + 1; +output '0' + x; + "#, + ); + let input = String::from(""); + let desired_output = String::from("56"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + #[test] + fn assignments_3() { + let program = String::from( + r#"; +cell x = 5; +output '0' + x; +x += 1 + x; +output '0' + x; + "#, + ); + let input = String::from(""); + let desired_output = String::from("5;"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_4() { + let program = String::from( + r#"; +cell x = 2; +output '0' + x; +x = x + x + x; +output '0' + x; + "#, + ); + let input = String::from(""); + let desired_output = String::from("26"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_5() { + let program = String::from( + r#"; +cell x = 2; +x = (2 + 3) - ((x + 4) + 1) + 4 - (12) + (3 + 10); +output '0' + x; + "#, + ); + let input = String::from(""); + let desired_output = String::from("3"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_6() { + let program = String::from( + r#"; +cell[2] x = [4, 5]; +x[0] = x[0] + 4; +x[1] = x[1] - 3; + +x[0] += '0'; +x[1] += '0'; +output *x; + "#, + ); + let input = String::from(""); + let desired_output = String::from("82"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_7() { + let program = String::from( + r#"; +cell[2] x = [1, 2]; +x[0] = x[1] + 5; // 7 +x[1] = x[0] + x[1]; // 9 + +x[0] += '0'; +x[1] += '0'; +output *x; + "#, + ); + let input = String::from(""); + let desired_output = String::from("79"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_8() { + let program = String::from( + r#"; +cell x = 128; +output x - 2; + "#, + ); + let input = String::from(""); + let desired_output = String::from("~"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_8a() { + let program = String::from( + r#"; +cell x = 127; +cell y = 64; +x += y + y; +output x + 'f' + 1; + "#, + ); + let input = String::from(""); + let desired_output = String::from("f"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_8b() { + let program = String::from( + r#"; +cell x = 128; +cell y = 64; +x += y + y; +output x + 'f'; + "#, + ); + let input = String::from(""); + let desired_output = String::from("f"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn assignments_9() -> Result<(), String> { + let program = String::from( + r#"; +cell x = 128; +x += 128; +output x + 'f'; + "#, + ); + let input = String::from(""); + let desired_output = String::from("f"); + let code = compile_program(program, Some(&OPT_ALL))?; + assert_eq!( + desired_output, + run_code(BVM_CONFIG_1D, code.to_string(), input, None) + ); + Ok(()) + } + + #[test] + fn assignments_9a() -> Result<(), String> { + let program = String::from( + r#"; +cell x = 126; +x += 2; +x += 128; +output x + 'f'; + "#, + ); + let input = String::from(""); + let desired_output = String::from("f"); + let code = compile_program(program, Some(&OPT_ALL))?; + assert_eq!( + desired_output, + run_code(BVM_CONFIG_1D, code.to_string(), input, None) + ); + Ok(()) + } + #[test] fn loops_1() { let program = String::from( " -let n = '0'; -let a = 10; -let b = 1; +cell n = '0'; +cell a = 10; +cell b = 1; drain a { output n; ++n; output 'A'; - let c = b; + cell c = b; drain c { output 'B'; }; @@ -331,8 +604,8 @@ drain a { fn loops_2() { let program = String::from( " -let a = 4; -let b[6] = [65, 65, 65, 65, 65, 1]; +cell a = 4; +cell[6] b = [65, 65, 65, 65, 65, 1]; copy a into b[0] b[1] b[4] b[5] { copy b[5] into b[2]; @@ -344,7 +617,7 @@ copy a into b[0] b[1] b[4] b[5] { output 10; }a+='a';output a; -let g = 5; +cell g = 5; drain g into a {output a;} ", ); @@ -370,10 +643,10 @@ output 'h'; fn ifs_1() { let program = String::from( " -let x = 7; -let y = 9; +cell x = 7; +cell y = 9; -let z = x - y; +cell z = x - y; if z { output 'A'; } else { @@ -412,10 +685,10 @@ output 10; fn ifs_2() { let program = String::from( " -let x = 7; -let y = 9; +cell x = 7; +cell y = 9; -let z = x - y; +cell z = x - y; if z { output 'A'; } else { @@ -456,9 +729,9 @@ output 10; fn ifs_3() { let program = String::from( " -let a = 5; +cell a = 5; if a { - let b = a + '0'; + cell b = a + '0'; output b; } output 10; @@ -475,16 +748,16 @@ output 10; fn loops_and_ifs_1() { let program = String::from( " -let n = '0'; -let a = 6; -let b; +cell n = '0'; +cell a = 6; +cell b; drain a { output n;++n; ;;;;;; output 'A'; - let c; - let nt_eq = a - b; + cell c; + cell nt_eq = a - b; if nt_eq { c = 2; @@ -510,27 +783,27 @@ drain a { fn functions_1() { let program = String::from( " -let global_var = '0'; +cell global_var = '0'; -def func_0 { - let n = grape + 1; +fn func_0(cell grape) { + cell n = grape + 1; output n; n = 0; };; -def func_1 { - let n = grape + 2; +fn func_1(cell grape) { + cell n = grape + 2; output n; n = 0; } output global_var; -func_0; +func_0(global_var); output global_var; global_var += 1;;; output global_var; -;;func_1; +;;func_1(global_var); output global_var; output 10; @@ -547,26 +820,26 @@ output 10; fn functions_2() -> Result<(), String> { let program = String::from( " -let global_var = '0'; +cell global_var = '0'; -def func_0 { - let n = grape + 1; +fn func_0(cell grape) { + cell n = grape + 1; output n; - def func_1 { + fn func_1(cell grape) { grape += 1; output grape; grape += 1; }; - func_1; + func_1(n); output n; grape += 1; }; output global_var; -func_0; +func_0(global_var); output global_var; output 10; @@ -576,7 +849,7 @@ output 10; let desired_output = String::from("01231\n"); let code = compile_program(program, Some(&OPT_NONE))?.to_string(); println!("{}", code); - let output = run_code(code, input); + let output = run_code(BVM_CONFIG_1D, code, input, None); println!("{output}"); assert_eq!(desired_output, output); @@ -587,48 +860,48 @@ output 10; fn functions_3() { let program = String::from( " -let global_var = '0'; +cell global_var = '0'; -let global_vars[2] = ['0', 64]; +cell[2] global_vars = ['0', 64]; -def func_0 { - let n = grape + 1; +fn func_0(cell grape) { + cell n = grape + 1; output n; - def func_1 { + fn func_1(cell grape) { grape += 1; output grape; grape += 1; - let frog[4]; - let zero = '0'; + cell[4] frog; + cell zero = '0'; drain zero into *frog; frog[1] += 2; zero = grape + 3; - func_2; + func_2(frog, zero); output zero; }; - func_1; + func_1(n); output n; grape += 1; }; output global_var; -func_0; +func_0(global_var); output global_var; output 10; output global_vars[1]; -func_0; +func_0(global_vars[0]); output global_vars[0]; output 10; -def func_2 { +fn func_2(cell[4] think, cell green) { think[2] += 7; think[3] += 2; @@ -640,7 +913,7 @@ def func_2 { output green; // this originally worked but I realised I don't actually need this // technically green is not declared in this scope because functions are more like templates but I still think removing this functionality is justified - // let green = '$'; + // cell green = '$'; // output green; // green = 0; }; @@ -654,227 +927,1651 @@ def func_2 { } #[test] - fn functions_4() { + fn functions_3a() { let program = String::from( r#" -def hello<> { - output "hello"; -} +cell[4] a = "AACD"; +add_one(a[1]); +output *a; -hello<>; -output 10; - "#, +fn add_one(cell cel) { + ++cel; +} +"#, ); let input = String::from(""); - let desired_output = String::from("hello\n"); + let desired_output = String::from("ABCD"); let output = compile_and_run(program, input).expect(""); println!("{output}"); assert_eq!(desired_output, output) } #[test] - fn input_1() { + fn functions_3b() { let program = String::from( - " -let b; -input b; -++b; -output b; -", + r#" +struct A {cell[3] arr;}; +struct A a; +a.arr[0] = '0'; +a.arr[1] = '0'; +a.arr[2] = '0'; + +add_one_to_three(a.arr); +output *a.arr; + +fn add_one_to_three(cell[3] t) { + t[0] += 1; + t[1] += 1; + t[2] += 1; +} +"#, ); - let input = String::from("A"); - let desired_output = String::from("B"); + let input = String::from(""); + let desired_output = String::from("111"); let output = compile_and_run(program, input).expect(""); println!("{output}"); assert_eq!(desired_output, output) } #[test] - fn input_2() { + fn functions_3c() { let program = String::from( r#" -let b[3]; -input b[0]; -input b[1]; -input b[2]; -output b[0]; -output b[1]; -output b[2]; -b[0]+=3; -b[1]+=2; -output '\n'; -b[2]+=1; -output b[2]; -output b[1]; -output b[0]; +struct A {cell b; cell c;}; +struct A a; +a.b = '0'; +a.c = '0'; + +add_one(a.b); +add_one(a.c); +add_one(a.c); +output a.b; +output a.c; + +fn add_one(cell t) { + ++t; +} "#, ); - let input = String::from("ABC"); - let desired_output = String::from("ABC\nDDD"); + let input = String::from(""); + let desired_output = String::from("12"); let output = compile_and_run(program, input).expect(""); println!("{output}"); assert_eq!(desired_output, output) } #[test] - fn memory_1() { + fn functions_3d() { let program = String::from( r#" -let b[3] = "Foo"; - -def inc { - g += 1; - if h {h += 1;} else {h = 'Z';} -} +struct A {cell b; cell c;}; +struct A a; +a.b = '0'; +a.c = '0'; -output *b; -inc; -output *b; +add_one(a.b); +add_one(a.c); +add_one(a.c); +output a.b; +output a.c; output 10; -let c = -1; -inc; -output c; +add_one(a); +output a.b; +output a.c; + +fn add_one(cell t) { + ++t; +} + +fn add_one(struct A t) { + ++t.b; + ++t.c; +} "#, ); let input = String::from(""); - let desired_output = String::from("FooFpp\nZ"); + let desired_output = String::from("12\n23"); let output = compile_and_run(program, input).expect(""); println!("{output}"); assert_eq!(desired_output, output) } #[test] - fn memory_2() { + fn functions_3e() { let program = String::from( r#" -let b[3] = [1, 2, 3]; +struct A {cell b; cell c;}; +struct A a; +a.b = '0'; +a.c = '0'; -def drain_h { - drain h { - output 'h'; - } -} +add_one(a.b); +add_one(a.c); +add_one(a.c); +output a.b; +output a.c; -drain_h; -drain_h; -output ' '; -drain_h; -output ' '; +output 10; -def drain_into { - drain a into *b; +add_one(a, a.b); +output a.b; +output a.c; + +fn add_one(cell t) { + ++t; } -let u = 'a' - 1; -let v[5] = [8, 5, 12, 12, 15]; -drain_into; -output *v; +fn add_one(struct A t, cell a) { + ++t.b; + ++t.c; + ++a; +} "#, ); let input = String::from(""); - let desired_output = String::from("hhh hh hello"); + let desired_output = String::from("12\n33"); let output = compile_and_run(program, input).expect(""); println!("{output}"); assert_eq!(desired_output, output) } #[test] - fn blocks_1() { + #[should_panic] + fn functions_3f() { let program = String::from( r#" -{{{{{{{ - let g = 0 + 5 + (-(-5)); - output "Freidns"; - { - output g; - } -}}}}}}} +struct A {cell b; cell c;}; +struct A a; +a.b = '0'; +a.c = '0'; + +add_one(a.b); +add_one(a.c); +add_one(a.c); +output a.b; +output a.c; + +output 10; + +add_one(a, a.b); +output a.b; +output a.c; + +fn add_one(cell t) { + ++t; +} + +fn add_one(struct A t, cell a) { + ++t.b; + ++t.c; + ++a; +} + +fn add_one(struct A tfoaishjdf, cell aaewofjas) { + output "hello"; +} "#, ); let input = String::from(""); - let desired_output = String::from("Freidns\n"); + let desired_output = String::from("12\n33"); let output = compile_and_run(program, input).expect(""); println!("{output}"); assert_eq!(desired_output, output) } #[test] - fn blocks_2() { + fn functions_4() { let program = String::from( r#" -let f = 'f'; -output f; -{ - let f = 'F'; - output f; +fn hello() { + output "hello"; } -output f; - "#, + +hello(); +output 10; + "#, + ); + let input = String::from(""); + let desired_output = String::from("hello\n"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn function_overloads_1() { + let program = String::from( + r#" +fn hello(cell h) { + output "hello: "; + output h; +} +fn hello() { + output "hello"; +} + +hello(); +output 10; +cell g = 'g'; +hello(g); +output 10; + "#, + ); + let input = String::from(""); + let desired_output = String::from("hello\nhello: g\n"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn function_overloads_1a() { + let program = String::from( + r#" +fn hello() { + output "hello"; +} +fn hello(cell h) { + hello(); + output ": "; + output h; +} + +hello(); +output 10; +cell g = 'g'; +hello(g); +output 10; + "#, + ); + let input = String::from(""); + let desired_output = String::from("hello\nhello: g\n"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn input_1() { + let program = String::from( + " +cell b; +input b; +++b; +output b; +", + ); + let input = String::from("A"); + let desired_output = String::from("B"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn input_2() { + let program = String::from( + r#" +cell[3] b; +input b[0]; +input b[1]; +input b[2]; +output b[0]; +output b[1]; +output b[2]; +b[0]+=3; +b[1]+=2; +output '\n'; +b[2]+=1; +output b[2]; +output b[1]; +output b[0]; +"#, + ); + let input = String::from("ABC"); + let desired_output = String::from("ABC\nDDD"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn memory_1() { + let program = String::from( + r#" +cell[3] b = "Foo"; + +fn inc(cell h, cell g) { + g += 1; + if h {h += 1;} else {h = 'Z';} +} + +output *b; +inc(b[1], b[2]); +output *b; + +output 10; + +cell c = -1; +inc(c, c); +output c; +"#, + ); + let input = String::from(""); + let desired_output = String::from("FooFpp\nZ"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn memory_2() { + let program = String::from( + r#" +cell[3] b = [1, 2, 3]; + +fn drain_h(cell h) { + drain h { + output 'h'; + } +} + +drain_h(b[2]); +drain_h(b[2]); +output ' '; +drain_h(b[1]); +output ' '; + +fn drain_into(cell a, cell[5] b) { + drain a into *b; +} + +cell u = 'a' - 1; +cell[5] v = [8, 5, 12, 12, 15]; +drain_into(u, v); +output *v; +"#, + ); + let input = String::from(""); + let desired_output = String::from("hhh hh hello"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn blocks_1() { + let program = String::from( + r#" +{{{{{{{ + cell g = 0 + 5 + (-(-5)); + output "Freidns"; + { + output g; + } +}}}}}}} +"#, + ); + let input = String::from(""); + let desired_output = String::from("Freidns\n"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn blocks_2() { + let program = String::from( + r#" +cell f = 'f'; +output f; +{ + cell f = 'F'; + output f; +} +output f; + "#, ); let input = String::from(""); let desired_output = String::from("fFf"); let output = compile_and_run(program, input).expect(""); println!("{output}"); - assert_eq!(desired_output, output) + assert_eq!(desired_output, output) + } + + #[test] + fn dimensional_arrays_1() { + let program = String::from( + r#" +cell[4][3] g; +g[0][0] = 5 + '0'; +g[0][1] = 4 + '0'; +g[0][2] = 3 + '0'; + +g[1][0] = 1 + '0'; +g[1][1] = 2 + '0'; +g[1][2] = 3 + '0'; + +g[0][3] = 1 + '0'; +g[1][3] = 2 + '0'; +g[2][3] = 3 + '0'; + +g[2][0] = 0 + '0'; +g[2][1] = 0 + '0'; +g[2][2] = 0 + '0'; + +output g[0][0]; +output g[0][1]; +output g[0][2]; +output g[0][3]; +output g[1][0]; +output g[1][1]; +output g[1][2]; +output g[1][3]; +output g[2][0]; +output g[2][1]; +output g[2][2]; +output g[2][3]; +"#, + ); + let input = String::from(""); + let desired_output = String::from("543112320003"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_1() { + let program = String::from( + r#"; +struct AA { + cell green; + cell yellow; +} + +struct AA a; +output '0' + a.green; +output '0' + a.yellow; + +a.green = 6; +a.yellow = 4; +output '0' + a.green; +output '0' + a.yellow; + "#, + ); + let input = String::from(""); + let desired_output = String::from("0064"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_2() { + let program = String::from( + r#"; +struct AA { + cell green; + cell yellow; +} + +// struct AA a {green = 3, yellow = 4}; +struct AA a; a.green = 3; a.yellow = 4; +output '0' + a.green; +output '0' + a.yellow; + +a.green = 5; +a.yellow = 2; +output '0' + a.green; +output '0' + a.yellow; + "#, + ); + let input = String::from(""); + let desired_output = String::from("3452"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_3() { + let program = String::from( + r#"; +struct AA { + cell green; + cell yellow; +} + +struct AA a; + +fn input_AA(struct AA bbb) { + input bbb.green; + input bbb.yellow; +} + +input_AA(a); + +output a.yellow; +output a.green; + "#, + ); + let input = String::from("gh"); + let desired_output = String::from("hg"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_3a() { + let program = String::from( + r#"; +struct AA a; + +fn input_AA(struct AA bbb) { + input bbb.green; + input bbb.yellow; + + struct AA { + cell[10] g; + } +} + +input_AA(a); + +output a.yellow; +output a.green; + +struct AA { + cell green; + cell yellow; +} + "#, + ); + let input = String::from("gh"); + let desired_output = String::from("hg"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_3b() { + let program = String::from( + r#"; +struct AA a; + +fn input_AA(struct AA bbb) { + input bbb.green; + input bbb.yellow; + + struct AA alt_a; + input *alt_a.l; + + output alt_a.l[4]; + + struct AA { + cell[10] l; + } +} + +input_AA(a); + +output a.yellow; +output a.green; + +struct AA { + cell green; + cell yellow; +} + "#, + ); + let input = String::from("ghpalindrome"); + let desired_output = String::from("nhg"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_4a() { + let program = String::from( + r#"; +struct AA a; +input a.green; +input a.yellow; +input a.reds[3]; +input a.reds[0]; +input a.reds[1]; +input a.reds[2]; + +struct AA { + cell green; + cell yellow; + cell[4] reds; +} + +output a.green; +output a.yellow; +output a.reds[0]; +output a.reds[1]; +output a.reds[2]; +output a.reds[3]; +output '\n'; + "#, + ); + let input = String::from("hellow"); + let desired_output = String::from("helowl\n"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_4b() { + let program = String::from( + r#"; +struct AA a; +input a.green; +input a.yellow; +input *a.reds; + +struct AA { + cell green; + cell yellow; + cell[4] reds; +} + +output *a.reds; +output a.yellow; +output a.green; +output '\n'; + "#, + ); + let input = String::from("gy0123"); + let desired_output = String::from("0123yg\n"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_4c() { + let program = String::from( + r#"; +struct AA a; +input a.green; +input a.yellow; +// input *a.reds; +input *a.sub.blues; +input a.sub.t; + +struct BB { + cell[2] blues; + cell t; +} + +struct AA { + cell green; + cell yellow; + // cell[4] reds; + struct BB sub; +} + +output a.sub.t; +output *a.sub.blues; +// output *a.reds; +output a.yellow; +output a.green; +output '\n'; + "#, + ); + let input = String::from("gy-+t"); + let desired_output = String::from("t-+yg\n"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + #[should_panic] + fn structs_4d() { + let program = String::from( + r#"; +struct AA a; +input *a.reds; + +struct AA { + cell[4] reds; + cell green; +} + +output a.reds[4]; +output '\n'; + "#, + ); + let input = String::from("0123a"); + let desired_output = String::from("a\n"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_5() { + let program = String::from( + r#"; +struct AA { + cell green; +} + +struct AA[2] as; +as[0].green = 5; +as[1].green = 3; + +output '0' + as[0].green; +output '0' + as[1].green; + "#, + ); + let input = String::from(""); + let desired_output = String::from("53"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_5a() { + let program = String::from( + r#" +struct AAA[2] as; +as[0].green = 5; +as[1].green = 3; + +output '0' + as[0].green; +output '0' + as[1].green; + +struct AAA { + cell green; +} +"#, + ); + let input = String::from(""); + let desired_output = String::from("53"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_6() { + let program = String::from( + r#"; +struct AA { + cell green; +} +struct BB { + cell green; +} + +struct AA[2] as; +// struct BB b { +// green = 6 +// }; +struct BB b; +b.green = 6; + +fn input_AAs(struct AA[2] aaas) { + input aaas[0].green; + input aaas[1].green; + output "HI\n"; +} +input_AAs(as); + +output '0' + b.green; +output as[0].green; +output as[1].green; + "#, + ); + let input = String::from("tr"); + let desired_output = String::from("HI\n6tr"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_7() { + let program = String::from( + r#"; +struct BB { + cell green; +} +struct AA { + cell green; + struct BB[3] bbb; +} + +struct AA[2] as; + +fn input_AAs(struct AA[2] aaas) { + fn input_BB(struct BB b) { + input b.green; + } + input_BB(aaas[0].bbb[0]); + input_BB(aaas[0].bbb[1]); + input_BB(aaas[0].bbb[2]); + input_BB(aaas[1].bbb[0]); + input_BB(aaas[1].bbb[1]); + input_BB(aaas[1].bbb[2]); + + input aaas[0].green; + input aaas[1].green; + output "HI\n"; +} +input_AAs(as); + +output as[0].green; +output as[0].bbb[0].green; +output as[0].bbb[1].green; +output as[0].bbb[2].green; +output as[1].green; +output as[1].bbb[0].green; +output as[1].bbb[1].green; +output as[1].bbb[2].green; + "#, + ); + let input = String::from("abcdefgh"); + let desired_output = String::from("HI\ngabchdef"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_7a() { + let program = String::from( + r#"; +struct BB { + cell green @2; +} +struct AA { + cell green @10; + struct BB[3] bbb @1; +} + +struct AA[2] as @-9; + +fn input_AAs(struct AA[2] aaas) { + fn input_BB(struct BB b) { + input b.green; + } + input_BB(aaas[0].bbb[0]); + input_BB(aaas[0].bbb[1]); + input_BB(aaas[0].bbb[2]); + input_BB(aaas[1].bbb[0]); + input_BB(aaas[1].bbb[1]); + input_BB(aaas[1].bbb[2]); + + input aaas[0].green; + input aaas[1].green; + output "HI\n"; +} +input_AAs(as); + +output as[0].green; +output as[0].bbb[0].green; +output as[0].bbb[1].green; +output as[0].bbb[2].green; +output as[1].green; +output as[1].bbb[0].green; +output as[1].bbb[1].green; +output as[1].bbb[2].green; + "#, + ); + let input = String::from("abcdefgh"); + let desired_output = String::from("HI\ngabchdef"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_bf_1() { + let program = String::from( + r#"; +struct Frame { + cell marker @3; + cell value @0; + cell[2] temp_cells @1; +} +struct Vector { + struct Frame[10] frames @1; + cell marker @0; +} + +struct Vector vec1 @2; +vec1.marker = true; + +vec1.frames[0].marker = true; +vec1.frames[0].value = 'j'; +vec1.frames[1].marker = true; +vec1.frames[1].value = 'k'; +vec1.frames[2].value = 'l'; + +bf @2 { + [>.>>>] +} + "#, + ); + let input = String::from(""); + let desired_output = String::from("jkl"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + // TODO: fix the r_panic macro that makes this error have unescaped quotes in it (weird) + // #[should_panic(expected = r#"Subfields "marker" and "temp_cells" overlap in struct."#)] + #[should_panic] + fn structs_bf_1a() { + let program = String::from( + r#"; +struct Frame { + cell marker @2; + cell value @0; + cell[2] temp_cells @1; +} + +struct Frame f; + "#, + ); + let input = String::from(""); + let desired_output = String::from(""); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + // TODO: fix the r_panic macro that makes this error have unescaped quotes in it (weird) + // #[should_panic(expected = r#"Subfields "marker" and "temp_cells" overlap in struct."#)] + #[should_panic] + fn structs_bf_1b() { + let program = String::from( + r#"; +struct Frame { + cell marker @-2; + cell value @0; + cell[2] temp_cells @1; +} + +struct Frame f; + "#, + ); + let input = String::from(""); + let desired_output = String::from(""); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + #[should_panic] + fn structs_bf_1c() { + let program = String::from( + r#"; +struct G { + cell a @1; + cell b @1; +} + +struct G g; +g.a = 'a'; +g.b = 'b'; + +output g.a; +output g.b; + "#, + ); + let input = String::from(""); + let desired_output = String::from("ab"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn structs_bf_2() { + let program = String::from( + r#"; +struct Green { + // no @0 cell + cell blue @1; +} +struct Green g @4; +g.blue = '5'; + +output g.blue; +bf @4 { + >.< +} + "#, + ); + let input = String::from(""); + let desired_output = String::from("55"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_0() { + let program = String::from( + r#"; +output '0' + sizeof(cell); + "#, + ); + let input = String::from(""); + let desired_output = String::from("1"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_0a() { + let program = String::from( + r#"; +output '0' + sizeof(cell[5]); + "#, + ); + let input = String::from(""); + let desired_output = String::from("5"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_0b() { + let program = String::from( + r#"; +cell a; +cell b[4]; +output '0' + sizeof(a); +output '0' + sizeof(b); +output '0' + sizeof(b[2]); + "#, + ); + let input = String::from(""); + let desired_output = String::from("141"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_1() { + let program = String::from( + r#"; +struct Green { + cell blue; +} +let s = sizeof(struct Green); +output '0' + s; + "#, + ); + let input = String::from(""); + let desired_output = String::from("1"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_1a() { + let program = String::from( + r#"; +struct Green { + cell blue; +} +let s = sizeof(struct Green[3]); +output '0' + s; + "#, + ); + let input = String::from(""); + let desired_output = String::from("3"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_1b() { + let program = String::from( + r#"; +struct Green { + cell blue; +} +let s = sizeof(struct Green[3][2]); +output '0' + s; + "#, + ); + let input = String::from(""); + let desired_output = String::from("6"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_2() { + let program = String::from( + r#"; +struct Green { + cell blue; + cell red; +} +struct Green g; +output '0' + sizeof(g); + "#, + ); + let input = String::from(""); + let desired_output = String::from("2"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_3() { + let program = String::from( + r#"; +struct Green { + cell blue; + cell[5] red; + cell yellow; +} +struct Green[2] g; +output '0' + sizeof(g) - 13; + +output '0' + sizeof(g[0].blue); +output '0' + sizeof(g[0].red); + "#, + ); + let input = String::from(""); + let desired_output = String::from("115"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_4() { + let program = String::from( + r#"; +struct Green { + cell blue @2; +} +struct Green[3] g; +output '0' + sizeof(struct Green); +output '0' + sizeof(g); +output '0' + sizeof(g[2].blue) + "#, + ); + let input = String::from(""); + let desired_output = String::from("391"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[ignore] + #[test] + fn sizeof_5() { + let program = String::from( + r#"; +struct Blue { + cell[2] blues; +} +struct Red { + cell a; + struct Blue blues; +} +struct Green { + cell blue @2; + struct Red red; +} +output '0' + sizeof(struct Blue); +output '0' + sizeof(struct Red); +struct Green[3] g; +output '0' + sizeof(struct Green); +output '0' + sizeof(g) - 17; +output '0' + sizeof(g[2].blue) + "#, + ); + let input = String::from(""); + let desired_output = String::from("23612"); + let output = compile_and_run(program, input).expect(""); + println!("{output}"); + assert_eq!(desired_output, output) + } + + #[test] + fn memory_specifiers_1() -> Result<(), String> { + let program = String::from( + r#" +cell foo @3 = 2; +{ + cell n = 12; + while n { + n -= 1; + foo += 10; + } +} +output foo; +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from(""); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(code, ">>>++<<<++++++++++++[->>>++++++++++<<<][-]>>>."); + assert_eq!(output, "z"); + Ok(()) + } + + #[test] + fn memory_specifiers_2() -> Result<(), String> { + let program = String::from( + r#" +cell a @5 = 4; +cell foo @0 = 2; +cell b = 10; +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + assert!(code.starts_with(">>>>>++++<<<<<++>++++++++++")); + Ok(()) + } + + #[test] + fn memory_specifiers_3() -> Result<(), String> { + let program = String::from( + r#" +cell a @1 = 1; +cell foo @0 = 2; +cell b = 3; +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + assert!(code.starts_with(">+<++>>+++")); + Ok(()) + } + + #[test] + fn memory_specifiers_4() -> Result<(), String> { + let program = String::from( + r#" +cell a @1,2 = 1; +cell foo @0 = 2; +cell b = 3; +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + assert!(code.starts_with(">^^++++")); + Ok(()) + } + + #[test] + fn memory_specifiers_5() -> Result<(), String> { + let program = String::from( + r#" +cell[4][3] g @1,2; +g[0][0] = 1; +g[1][1] = 2; +g[2][2] = 3; +cell foo @0 = 2; +cell b = 3; +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + assert!(code.starts_with(">^^[-]+>>>>>[-]++>>>>>[-]+++<<<<<<<<<<+++")); + Ok(()) + } + + #[test] + fn memory_specifiers_6() { + let program = String::from( + r#" +cell a @1 = 1; +cell foo @1 = 2; +cell b = 3; +"#, + ); + let code = compile_program(program, None); + assert!(code.is_err()); + assert!(code + .unwrap_err() + .to_string() + .contains("Location specifier @1,0 conflicts with another allocation")); + } + + #[test] + fn memory_specifiers_7() { + let program = String::from( + r#" +cell a @1,3 = 1; +cell foo @1,3 = 2; +cell b = 3; +"#, + ); + let code = compile_program(program, None); + assert!(code.is_err()); + assert!(code + .unwrap_err() + .to_string() + .contains("Location specifier @1,3 conflicts with another allocation")); + } + + #[test] + fn memory_specifiers_8() { + let program = String::from( + r#" +cell a @2 = 1; +cell foo @2,0 = 2; +cell b = 3; +"#, + ); + let code = compile_program(program, None); + assert!(code.is_err()); + assert!(code + .unwrap_err() + .to_string() + .contains("Location specifier @2,0 conflicts with another allocation")); + } + + #[test] + fn memory_specifiers_9() { + let program = String::from( + r#" +cell a @2,4 = 1; +cell[4] b @0,4; +"#, + ); + let code = compile_program(program, None); + assert!(code.is_err()); + assert!(code + .unwrap_err() + .to_string() + .contains("Location specifier @0,4 conflicts with another allocation")); + } + + #[test] + fn variable_location_specifiers_1() -> Result<(), String> { + let program = String::from( + r#" +cell a = 'h'; +bf @a {.} +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from("wxy"); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(output, "h"); + Ok(()) + } + + #[test] + fn variable_location_specifiers_1a() -> Result<(), String> { + let program = String::from( + r#" +cell[100] _; +cell a = 'h'; +cell[4] b; +bf @a {.} +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from(""); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(output, "h"); + Ok(()) + } + + #[test] + fn variable_location_specifiers_2() -> Result<(), String> { + let program = String::from( + r#" +struct Test {cell[3] a @0; cell b;} +struct Test t; +input *t.a; +bf @t.a { +[+.>] +} +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from("wxy"); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(code, ",>,>,<<[+.>]"); + assert_eq!(output, "xyz"); + Ok(()) + } + + #[test] + fn variable_location_specifiers_2a() -> Result<(), String> { + let program = String::from( + r#" +struct Test {cell[3] a @0; cell b;} +struct Test t; +input *t.a; +bf @t { +[+.>] +} +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from("wxy"); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(code, ",>,>,<<[+.>]"); + assert_eq!(output, "xyz"); + Ok(()) } #[test] - fn memory_specifiers_1() -> Result<(), String> { + fn variable_location_specifiers_3() -> Result<(), String> { let program = String::from( r#" -let foo @3 = 2; -{ - let n = 12; - while n { - n -= 1; - foo += 10; +cell[5] f @6 = "abcde"; +bf @f[2] clobbers *f {.+++.} +output 10; +output *f; +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from(""); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(output, "cf\nabfde"); + Ok(()) + } + + #[test] + fn variable_location_specifiers_3a() -> Result<(), String> { + let program = String::from( + r#" +cell[4] f @8 = "xyz "; +bf @f {[.>]} +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from(""); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(output, "xyz "); + Ok(()) } + + #[test] + fn variable_location_specifiers_4() -> Result<(), String> { + let program = String::from( + r#" +fn func(cell g) { + bf @g {+.-} } -output foo; + +cell a = '5'; +func(a); "#, ); let code = compile_program(program, None)?.to_string(); println!("{code}"); let input = String::from(""); - let output = run_code(code.clone(), input); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); println!("{output}"); - assert_eq!(code, ">>>++<<<++++++++++++[->>>++++++++++<<<][-]>>>."); - assert_eq!(output, "z"); + assert_eq!(output, "6"); Ok(()) } #[test] - fn memory_specifiers_2() -> Result<(), String> { + fn variable_location_specifiers_4a() -> Result<(), String> { let program = String::from( r#" -let a @5 = 4; -let foo @0 = 2; -let b = 10; +fn func(cell g) { + bf @g {+.-} +} + +cell[3] a = "456"; +func(a[1]); "#, ); let code = compile_program(program, None)?.to_string(); println!("{code}"); - assert!(code.starts_with(">>>>>++++<<<<<++>++++++++++")); + let input = String::from(""); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(output, "6"); Ok(()) } #[test] - fn memory_specifiers_3() -> Result<(), String> { + fn variable_location_specifiers_4b() -> Result<(), String> { let program = String::from( r#" -let a @1 = 1; -let foo @0 = 2; -let b = 3; +fn func(cell g) { + bf @g {+.-} +} + +struct H {cell[3] r;} +struct H a; +a.r[0] = '4'; +a.r[1] = '5'; +a.r[2] = '6'; +func(a.r[1]); "#, ); let code = compile_program(program, None)?.to_string(); println!("{code}"); - assert!(code.starts_with(">+<++>>+++")); + let input = String::from(""); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(output, "6"); + Ok(()) + } + + #[test] + fn variable_location_specifiers_4c() -> Result<(), String> { + let program = String::from( + r#" +fn func(struct H h) { + bf @h {+.-} +} + +struct H {cell[3] r @0;} +struct H a; +a.r[0] = '4'; +a.r[1] = '5'; +a.r[2] = '6'; +func(a); +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from(""); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(output, "5"); + Ok(()) + } + + #[test] + fn variable_location_specifiers_4d() -> Result<(), String> { + let program = String::from( + r#" +fn func(cell[2] g) { + bf @g {+.-} +} + +struct J {cell[2] j;} +struct H {cell[20] a; struct J jj @1;} +struct H a; +a.jj.j[0] = '3'; +a.jj.j[1] = '4'; +func(a.jj.j); +"#, + ); + let code = compile_program(program, None)?.to_string(); + println!("{code}"); + + let input = String::from(""); + let output = run_code(BVM_CONFIG_1D, code.clone(), input, None); + println!("{output}"); + assert_eq!(output, "4"); Ok(()) } @@ -882,7 +2579,7 @@ let b = 3; fn assertions_1() -> Result<(), String> { let program = String::from( r#" -let a @0 = 5; +cell a @0 = 5; output a; assert a equals 2; a = 0; @@ -900,7 +2597,7 @@ output a; fn assertions_2() -> Result<(), String> { let program = String::from( r#" -let a @0 = 2; +cell a @0 = 2; output a; assert a unknown; a = 0; @@ -932,7 +2629,7 @@ bf { ",.[-]+[-->-[>>+>-----<<]<--<---]>-.>>>+.>>..+++[.>]<<<<.+++.------.<<-.>>>>+." ); - let output = run_code(code, String::from("~")); + let output = run_code(BVM_CONFIG_1D, code, String::from("~"), None); assert_eq!(output, "~Hello, World!"); Ok(()) } @@ -941,8 +2638,8 @@ bf { fn inline_brainfuck_2() -> Result<(), String> { let program = String::from( r#" -// let a @0; -// let b @1; +// cell a @0; +// cell b @1; bf @3 { ,.[-] +[-->-[>>+>-----<<]<--<---]>-.>>>+.>>..+++[.>]<<<<.+++.------.<<-.>>>>+. @@ -956,7 +2653,7 @@ bf @3 { ">>>,.[-]+[-->-[>>+>-----<<]<--<---]>-.>>>+.>>..+++[.>]<<<<.+++.------.<<-.>>>>+." )); - let output = run_code(code, String::from("~")); + let output = run_code(BVM_CONFIG_1D, code, String::from("~"), None); assert_eq!(output, "~Hello, World!"); Ok(()) } @@ -965,7 +2662,7 @@ bf @3 { fn inline_brainfuck_3() -> Result<(), String> { let program = String::from( r#" -let str[3] @0; +cell[3] str @0; bf @0 clobbers *str { ,>,>, @@ -989,7 +2686,7 @@ assert *str equals 0; assert!(code.starts_with(",>,>,<<[+>]<<<[.[-]>]<<<")); - let output = run_code(code, String::from("HEY")); + let output = run_code(BVM_CONFIG_1D, code, String::from("HEY"), None); assert_eq!(output, "IFZ"); Ok(()) } @@ -1004,7 +2701,7 @@ bf { ,----------[ ++++++++++ { - let chr @0; + cell chr @0; assert chr unknown; output chr; chr += 1; @@ -1019,7 +2716,7 @@ bf { let code = compile_program(program, None)?.to_string(); println!("{code}"); - let output = run_code(code, String::from("line of input\n")); + let output = run_code(BVM_CONFIG_1D, code, String::from("line of input\n"), None); assert_eq!(output, "lmijnoef !opfg !ijnopquvtu"); Ok(()) } @@ -1029,7 +2726,7 @@ bf { let program = String::from( r#" // external function within the same file, could be tricky to implement -def quote { +fn quote(cell n) { // H 'H' output 39; output n; @@ -1042,10 +2739,9 @@ bf { ,----------[ ++++++++++ { - // TODO: make sure top level variables aren't cleared automatically - let chr @0; + cell chr @0; assert chr unknown; - quote; + quote(chr); output 10; // this time it may be tricky because the compiler needs to return to the start cell } @@ -1058,7 +2754,7 @@ bf { let code = compile_program(program, None)?.to_string(); println!("{code}"); - let output = run_code(code, String::from("hello\n")); + let output = run_code(BVM_CONFIG_1D, code, String::from("hello\n"), None); assert_eq!(output, "'h'\n'e'\n'l'\n'l'\n'o'\n"); Ok(()) } @@ -1067,7 +2763,7 @@ bf { fn inline_brainfuck_6() -> Result<(), String> { let program = String::from( r#" -let b = 4; +cell b = 4; bf { ++-- @@ -1091,7 +2787,7 @@ bf { bf { ,>,>, << - {{{{{{let g @5 = 1;}}}}}} + {{{{{{cell g @5 = 1;}}}}}} } "#, ); @@ -1101,7 +2797,47 @@ bf { assert_eq!(code, ",>,>,<<>>>>>+[-]<<<<<"); Ok(()) } + #[test] + fn inline_2d_brainfuck() -> Result<(), String> { + let program = String::from( + r#" + bf {,.[-]+[--^-[^^+^-----vv]v--v---]^-.^^^+.^^..+++[.^]vvvv.+++.------.vv-.^^^^+.} + "#, + ); + let code = compile_program(program, None)?.to_string(); + + assert_eq!( + code, + ",.[-]+[--^-[^^+^-----vv]v--v---]^-.^^^+.^^..+++[.^]vvvv.+++.------.vv-.^^^^+." + ); + + let output = run_code(BVM_CONFIG_2D, code, String::from("~"), None); + assert_eq!(output, "~Hello, World!"); + Ok(()) + } + #[test] + #[should_panic(expected = "Invalid Inline Brainfuck Characters in vvstvv")] + fn invalid_inline_2d_brainfuck() { + let program = String::from( + r#" + bf {,.[-]+[--^-[^^+^-----vv]v--v---]^-.^^^+.^^..+++[.^]vvstvv.+++.------.vv-.^^^^+.} + "#, + ); + let _result = compile_program(program, None); + } + #[test] + #[should_panic(expected = "2D Brainfuck currently disabled")] + fn inline_2d_brainfuck_disabled() { + run_code( + BVM_CONFIG_1D, + String::from( + ",.[-]+[--^-[^^+^-----vv]v--v---]^-.^^^+.^^..+++[.^]vvvv.+++.------.vv-.^^^^+.", + ), + String::from("~"), + None, + ); + } #[test] fn constant_optimisations_1() -> Result<(), String> { let program = String::from( @@ -1114,7 +2850,10 @@ output 'h'; let code = compile_program(program, Some(&OPT_ALL))?; println!("{}", code.clone().to_string()); - assert_eq!(desired_output, run_code(code.to_string(), input)); + assert_eq!( + desired_output, + run_code(BVM_CONFIG_1D, code.to_string(), input, None) + ); Ok(()) } @@ -1123,9 +2862,9 @@ output 'h'; fn constant_optimisations_2() -> Result<(), String> { let program = String::from( r#" -let arr[15] @1; -let a = 'G'; -let b = a + 45; +cell[15] arr @1; +cell a = 'G'; +cell b = a + 45; output b; b -= 43; output b; @@ -1137,8 +2876,324 @@ output a + 3; let code = compile_program(program, Some(&OPT_ALL))?.to_string(); println!("{}", code); - assert_eq!(desired_output, run_code(code, input)); + assert_eq!(desired_output, run_code(BVM_CONFIG_1D, code, input, None)); + + Ok(()) + } + #[test] + #[should_panic(expected = "Memory Allocation Method not implemented")] + fn unimplemented_memory_allocation() { + let program = String::from( + r#" + cell[15] arr @1; + cell a = 'G'; + "#, + ); + let cfg = MastermindConfig { + optimise_generated_code: false, + optimise_generated_all_permutations: false, + optimise_cell_clearing: false, + optimise_variable_usage: false, + optimise_memory_allocation: false, + optimise_unreachable_loops: false, + optimise_constants: false, + optimise_empty_blocks: false, + memory_allocation_method: 128, + enable_2d_grid: false, + }; + let _code = compile_program(program, Some(&cfg)); + } + #[test] + fn tiles_memory_allocation_1() -> Result<(), String> { + let program = String::from( + r#" +cell a = 1; +cell b = 1; +cell c = 1; +cell d = 1; +cell e = 1; +cell f = 1; +cell h = 1; +cell i = 1; +cell j = 1; + "#, + ); + let desired_output = String::from("+vv+^^+>vv+^+^+"); + + let code = compile_program(program, Some(&OPT_NONE_TILES))?.to_string(); + assert_eq!(desired_output, code); + + Ok(()) + } + #[test] + fn tiles_memory_allocation_2() -> Result<(), String> { + let program = String::from( + r#" +cell a = '1'; +cell b = '2'; +cell c = '3'; +cell d = '4'; +cell e = '5'; +cell f = '6'; +cell g = '7'; +cell h = '8'; +cell i = '9'; +output a; +output b; +output c; +output d; +output e; +output f; +output g; +output h; +output i; + "#, + ); + let input = String::from(""); + let desired_output = String::from("123456789"); + + let code = compile_program(program, Some(&OPT_NONE_TILES))?.to_string(); + println!("{}", code); + assert_eq!(desired_output, run_code(BVM_CONFIG_2D, code, input, None)); + + Ok(()) + } + + #[test] + fn tiles_memory_allocation_3() { + let program = String::from( + r#" +cell a @2,4 = 1; +cell[4] b @0,4; +"#, + ); + let code = compile_program(program, Some(&OPT_NONE_TILES)); + assert!(code.is_err()); + assert!(code + .unwrap_err() + .to_string() + .contains("Location specifier @0,4 conflicts with another allocation")); + } + + #[test] + fn tiles_memory_allocation_4() -> Result<(), String> { + let program = String::from( + r#" +cell a @2 = 1; +cell[4] b; +a = '5'; +b[0] = '1'; +b[1] = '2'; +b[2] = '3'; +b[3] = '4'; +output b[0]; +output b[1]; +output b[2]; +output b[3]; +output a; +"#, + ); + let code = compile_program(program, Some(&OPT_NONE_TILES))?.to_string(); + println!("{}", code); + let input = String::from(""); + let desired_output = String::from("12345"); + assert_eq!(desired_output, run_code(BVM_CONFIG_2D, code, input, None)); + Ok(()) + } + + #[test] + fn zig_zag_memory_allocation_1() -> Result<(), String> { + let program = String::from( + r#" +cell a = 1; +cell b = 1; +cell c = 1; +cell d = 1; +cell e = 1; +cell f = 1; +cell h = 1; +cell i = 1; +cell j = 1; + "#, + ); + let desired_output = String::from("+>+<^+>>v+<^+<^+>>>vv+<^+<^+"); + + let code = compile_program(program, Some(&OPT_NONE_ZIG_ZAG))?.to_string(); + assert_eq!(desired_output, code); + + Ok(()) + } + #[test] + fn zig_zag_memory_allocation_2() -> Result<(), String> { + let program = String::from( + r#" +cell a = '1'; +cell b = '2'; +cell c = '3'; +cell d = '4'; +cell e = '5'; +cell f = '6'; +cell g = '7'; +cell h = '8'; +cell i = '9'; +output a; +output b; +output c; +output d; +output e; +output f; +output g; +output h; +output i; + "#, + ); + let input = String::from(""); + let desired_output = String::from("123456789"); + + let code = compile_program(program, Some(&OPT_NONE_ZIG_ZAG))?.to_string(); + println!("{}", code); + assert_eq!(desired_output, run_code(BVM_CONFIG_2D, code, input, None)); + + Ok(()) + } + + #[test] + fn zig_zag_memory_allocation_3() { + let program = String::from( + r#" +cell a @2,4 = 1; +cell[4] b @0,4; +"#, + ); + let code = compile_program(program, Some(&OPT_NONE_ZIG_ZAG)); + assert!(code.is_err()); + assert!(code + .unwrap_err() + .to_string() + .contains("Location specifier @0,4 conflicts with another allocation")); + } + + #[test] + fn zig_zag_memory_allocation_4() -> Result<(), String> { + let program = String::from( + r#" +cell a @2 = 1; +cell[4] b; +a = '5'; +b[0] = '1'; +b[1] = '2'; +b[2] = '3'; +b[3] = '4'; +output b[0]; +output b[1]; +output b[2]; +output b[3]; +output a; +"#, + ); + let code = compile_program(program, Some(&OPT_NONE_ZIG_ZAG))?.to_string(); + println!("{}", code); + let input = String::from(""); + let desired_output = String::from("12345"); + assert_eq!(desired_output, run_code(BVM_CONFIG_2D, code, input, None)); + Ok(()) + } + + #[test] + fn spiral_memory_allocation_1() -> Result<(), String> { + let program = String::from( + r#" +cell a = 1; +cell b = 1; +cell c = 1; +cell d = 1; +cell e = 1; +cell f = 1; +cell h = 1; +cell i = 1; +cell j = 1; + "#, + ); + let desired_output = String::from("^+>+v+<+<+^+^+>+>+"); + + let code = compile_program(program, Some(&OPT_NONE_SPIRAL))?.to_string(); + assert_eq!(desired_output, code); + + Ok(()) + } + #[test] + fn spiral_memory_allocation_2() -> Result<(), String> { + let program = String::from( + r#" +cell a = '1'; +cell b = '2'; +cell c = '3'; +cell d = '4'; +cell e = '5'; +cell f = '6'; +cell g = '7'; +cell h = '8'; +cell i = '9'; +output a; +output b; +output c; +output d; +output e; +output f; +output g; +output h; +output i; + "#, + ); + let input = String::from(""); + let desired_output = String::from("123456789"); + + let code = compile_program(program, Some(&OPT_NONE_SPIRAL))?.to_string(); + println!("{}", code); + assert_eq!(desired_output, run_code(BVM_CONFIG_2D, code, input, None)); + + Ok(()) + } + + #[test] + fn spiral_memory_allocation_3() { + let program = String::from( + r#" +cell a @2,4 = 1; +cell[4] b @0,4; +"#, + ); + let code = compile_program(program, Some(&OPT_NONE_SPIRAL)); + assert!(code.is_err()); + assert!(code + .unwrap_err() + .to_string() + .contains("Location specifier @0,4 conflicts with another allocation")); + } + #[test] + fn spiral_memory_allocation_4() -> Result<(), String> { + let program = String::from( + r#" +cell a @2 = 1; +cell[4] b; +a = '5'; +b[0] = '1'; +b[1] = '2'; +b[2] = '3'; +b[3] = '4'; +output b[0]; +output b[1]; +output b[2]; +output b[3]; +output a; +"#, + ); + let code = compile_program(program, Some(&OPT_NONE_SPIRAL))?.to_string(); + println!("{}", code); + let input = String::from(""); + let desired_output = String::from("12345"); + assert_eq!(desired_output, run_code(BVM_CONFIG_2D, code, input, None)); Ok(()) } } diff --git a/compiler/src/tokeniser.rs b/compiler/src/tokeniser.rs index 8356156..9bc72ce 100644 --- a/compiler/src/tokeniser.rs +++ b/compiler/src/tokeniser.rs @@ -17,7 +17,9 @@ pub fn tokenise(source: &String) -> Result, String> { ("output", Token::Output), ("input", Token::Input), // ("#debug", Token::Debug), - ("let", Token::Let), + // ("let", Token::Let), + ("cell", Token::Cell), + ("struct", Token::Struct), ("=", Token::EqualsSign), ("while", Token::While), ("drain", Token::Drain), @@ -36,7 +38,8 @@ pub fn tokenise(source: &String) -> Result, String> { // ("free", Token::Free), // ("push", Token::Push), // ("deal", Token::Deal), - ("def", Token::Def), + // ("def", Token::Def), + ("fn", Token::Fn), // ("int", Token::Int), // ("add", Token::Add), // ("sub", Token::Sub), @@ -50,8 +53,9 @@ pub fn tokenise(source: &String) -> Result, String> { ("]", Token::ClosingSquareBracket), ("(", Token::OpenParenthesis), (")", Token::ClosingParenthesis), - ("<", Token::OpenAngledBracket), - (">", Token::ClosingAngledBracket), + ("<", Token::LessThan), + (">", Token::MoreThan), + ("^", Token::UpToken), ("true", Token::True), ("false", Token::False), (",", Token::Comma), @@ -171,8 +175,11 @@ pub enum Token { None, Output, Input, - Def, - Let, + // Def, + Fn, + // Let, + Cell, + Struct, // Assert, // Free, While, @@ -187,8 +194,8 @@ pub enum Token { ClosingSquareBracket, OpenParenthesis, ClosingParenthesis, - OpenAngledBracket, - ClosingAngledBracket, + LessThan, + MoreThan, Comma, Dot, Asterisk, @@ -215,4 +222,5 @@ pub enum Token { Plus, EqualsSign, Semicolon, + UpToken, } diff --git a/docs/brainfuck.md b/docs/brainfuck.md new file mode 100644 index 0000000..cb2fbce --- /dev/null +++ b/docs/brainfuck.md @@ -0,0 +1,34 @@ +### Brainfuck + +Brainfuck is an esoteric programming language, originally designed as a theoretical example of a Turing complete language with an extremely minimal compiler. The name is due to its difficulty, it is significantly more difficult to create complex programs than in any popular modern language. + +### Specification + +When a Brainfuck program is run, it operates on a array/tape of cells, performing operations on the tape. Each cell contains an integer, initialised to 0 by default. The program operates on one cell at a time based on the position of a "tape head". Brainfuck supports the following operations: + +- `+`: increments the value of the current cell +- `-`: decrement the value of the current cell +- `>`: move the tape head one cell to the right +- `<`: move the tape head one cell to the left +- `.`: output the current cell as a byte to stdout +- `,`: input a byte from stdin, overwriting the current cell +- `[`: jump to the corresponding `]` if the current cell is 0 +- `]`: jump to the corresponding `[` if the current cell is not 0 + +A Brainfuck program consists of a list of these commands, which are executed sequentially. The program terminates if the final operation in the list is executed. + +### Interpreter Implementation Details + +The Mastermind IDE and compiler library contains an implementation of a Brainfuck interpreter. This implementation is intended to match the behaviour of the most popular Brainfuck implementations: + +#### 8-bit Wrapping Cells + +In this implementation, each cell is an 8-bit integer that wraps if an increment or decrement operation overflows or underflows. + +E.g. given the current tape cell value is `255`, after an increment (`+`), the cell value is now `0`. + +Similarly: `0`, after a decrement (`-`) becomes `255` + +#### Infinite Bidirectional Tape + +In this implementation, the tape extends infinitely in both directions. diff --git a/docs/conditionals.md b/docs/conditionals.md new file mode 100644 index 0000000..664d54b --- /dev/null +++ b/docs/conditionals.md @@ -0,0 +1,27 @@ +### Conditionals + +Mastermind supports basic `if`/`else` statements. An `if` statement takes in a single cell expression, if the expression is evaluated to be truthy, then the `if` block is executed, otherwise the optional `else` block is executed. This behaviour can be inverted using the `not` keyword. + +``` +if 13 { + output "13"; +} + +if not true { + // unreachable +} + +cell var = 4; +if var { + output "true"; +} else { + output "false"; +} + +// typical equivalence use-case: +if not var - 10 { + // == +} else { + // != +} +``` diff --git a/docs/functions.md b/docs/functions.md new file mode 100644 index 0000000..a1378e6 --- /dev/null +++ b/docs/functions.md @@ -0,0 +1,99 @@ +### Functions + +Mastermind supports a minimal functions system: Functions can be defined with a name and a fixed number of typed arguments. + +``` +fn newline() { output '\n'; } + +fn print_zeros(cell num) { + copy num { + output '0'; + } + newline(); +} + +// expressions as arguments are currently not supported, +// i.e. print_zeros(9) +cell g = 9; +print_zeros(g); +``` + +Functions are in-lined at compile-time, and all arguments are passed by reference. Values can be returned by editing the arguments, or editing variables in an outer scope, although the latter makes a function less portable. + +``` +fn is_zero(cell in, cell out) { + out = true; + if in { + out = false; + } +} + +cell value = 'h'; +cell falsy; +is_zero(value, falsy); +``` + +Example showing a function reading a variable from an outer scope: + +``` +fn print_global_g(cell count) { + copy count { + output g; + output ' '; + } +} + +cell g = 'g'; +cell count = 11; +print_global_g(count); +// g g g g g g g g g g g + +{ + // inner scope with a new 'g' allocation + cell g = 'G'; + count = 4; + print_global_g(count); + // G G G G +} + +// same call again, now the inner 'G' has been freed +print_global_g(count); +// g g g g +``` + +#### Structs and Overloads + +Example of supported behaviour: + +``` +fn func1() { + output '1'; +} +fn func1(cell a) { + output '2'; +} +fn func1(cell a, cell b) { + output '3'; +} +struct X { cell a; } +fn func1(struct X x) { + output '4'; +} +struct Y { cell a; } +fn func1(struct Y y) { + output '5'; +} +fn func1(cell a, struct X x, struct Y y) { + output '6'; +} +cell n; +struct X x; +struct Y y; +func1(); +func1(n); +func1(n, n); +func1(x); +func1(y); +func1(n, x, y); +// 123456 +``` diff --git a/docs/inlinebrainfuck.md b/docs/inlinebrainfuck.md new file mode 100644 index 0000000..b2cea17 --- /dev/null +++ b/docs/inlinebrainfuck.md @@ -0,0 +1,155 @@ +### In-line Brainfuck + +In-line Brainfuck allows the programmer to define custom behaviour as if writing raw Brainfuck, much in the same way as C has in-line assembly syntax. + +``` +// This is its most basic form: +// find the next cell that equals -1 +bf { + +[->+]- +} + +// This is its more advanced form: +// input a line of lowercase letters and output the uppercase version +// this is an intentionally inefficient example +bf @3 clobbers var *spread_var etc { + ,----------[++++++++++>,----------] + <[<]> + [ + { + cell g @0; + assert g unknown; + output g + ('A' - 'a'); + // embedded Mastermind! + } + > + ] + // now clear and return + <[[-]<]> +} +``` + +It is the programmer's responsibility to clear used cells and return back to the cell in which they started the in-line Brainfuck context. If the programmer does not do this, any mastermind code after the in-line Brainfuck command will likely break. + +#### Memory location specifiers + +For hand-tuning optimisations and in-line Brainfuck that reads from Mastermind variables, you can specify the location on the Brainfuck tape: + +``` +cell var @3 = 4; +// compiled: >>>++++ + +bf @4 { + <><><> +} +// compiled: >>>><><><> +``` + +Alternatively if using the 2D grid you can use a comma seperated list with a second value: + +``` + +bf @4,3 { + <><><> +} +// compiled: >>>>^^^<><><> +``` + +#### Clobbering and Assertions + +Mastermind will try to predict the value of cells at compile-time, so it can prevent unnecessary cell clean-ups and unreachable code (with optimisations turned on). If your in-line Brainfuck affects existing Mastermind variables, you should tell the compiler using the `clobbers` keyword, the syntax is similar to the `drain into` list: + +``` +bf clobbers var *spread_var other_var etc {} +``` + +The compiler will now assume nothing about the values of those variables afterwards. + +If instead you want to tell the compiler specifically that a variable has become a certain value, you can use `assert`: + +``` +assert var equals 3; +// most common use cases: +assert var equals 0; +assert var unknown; +``` + +Asserting a variable as `unknown` is equivalent to clobbering. + +#### Embedded Mastermind + +You can embed high-level Mastermind code within a Brainfuck context, this allows you to control precisely what the generated Brainfuck code is doing, whilst also taking advantage of the syntax features of Mastermind. + +``` +cell sum @0; + +bf @0 { + >> + // read input (until eof) to the tape, nullifying any spaces or newlines + // (this is probably not a good practical example, ideas are appreciated) + ,[ + { + cell c @0; + assert c unknown; // needed otherwise the compiler assumes c = 0 + + if not (c - '\n') { + c = 0; + } + if not (c - ' ') { + c = 0; + } + } + >, + ] +} +``` + +The compiler cannot guarantee the global head position at compile time within an in-line Brainfuck context. Therefore memory location specifiers are relative to the current embedded Mastermind context, not the entire program. + +Also, top-level variables are not cleared by default in Mastermind contexts, this allows you to "leave" variables in cells for your Brainfuck to use. If you want variables in your embedded Mastermind to be automatically cleared, you can open a scope at the top level: + +``` +bf { + ++----++[][][<><><>] // the program doesn't matter for this example + { + // variables here will not be cleared + cell g @2; + assert g unknown; + { + // variables here will be cleared + let b = 32; + } + } + {{ + // self-cleaning Mastermind code here + }} +} +``` + +#### Craziness + +You can put in-line Brainfuck inside your embedded Mastermind. + +``` +bf { + ++++[ + { + cell i @0; + assert i unknown; + cell j @1 = i + 1; + + bf @1 { + [.+] + { + // even more layers are possible + bf { + { + output "h" + } + } + } + } + } + -] +} +``` diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 0000000..29aeb35 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,19 @@ +Mastermind is a programming language designed to compile to the well-known esoteric language "Brainfuck". + +Brainfuck is essentially a modern interpretation of the classical Turing machine. It consists of a tape of 8-bit values, with simple increment/decrement, move left/right, input/output, and looping operations. The full language only uses 8 control characters: `+-><.,[]`. + +Imagine if C was designed for computer architectures that run Brainfuck, that is what Mastermind is intended to be. + +## Contents + +This documentation currently includes the following articles: + +- Brainfuck +- Variables +- Conditionals +- Loops +- Functions +- Inline Brainfuck +- Standard Library +- 2D Mastermind +- Optimisations diff --git a/docs/loops.md b/docs/loops.md new file mode 100644 index 0000000..adbf9c9 --- /dev/null +++ b/docs/loops.md @@ -0,0 +1,106 @@ +Looping in Mastermind has 3 main forms. These are the: + +- While Loop +- Drain Loop +- Copy Loop + +all 3 looping styles are essentially variations of a while loop + +## While Loop + +The simplest is the `while` loop, which only supports cell references, currently not expressions: + +``` +while var { + //do stuff + var -= 1; + //etc +} +``` + +## Drain Loop + +The `drain` loop is a form of syntax sugar for a self decrementing while loop. This form of loop is extremely common in Brainfuck +so it has been shortened with this syntax + +``` +drain var { + // do stuff +} +``` + +shorthand for the following: + +``` +while var { + // do stuff + var -= 1; +} +``` + +This destructively loops as many times as the value in the cell being referenced, this can be used with expressions: + +drain 10 {} + +drain var - 6 {} + +Drain additionally supports the ability to add a variable `into` multiple other variables + +``` +drain var into other_var other_var_2 *spread_array etc; +``` + +Equivalent to: + +``` +drain var { + other_var += 1; + other_var_2 += 1; + spread_array[0] += 1; + spread_array[1] += 1; + spread_array[2] += 1; + // ... +} + +// example of typical "for loop": +cell i; +drain 10 into i { + output '0' + i; // inefficient for the example +} +// "0123456789" +// equivalent to the following: +cell i = 0; +cell N = 10; +while N { + output '0' + i; + i += 1; + N -= 1; +} +``` + +## Copy Loop + +The `copy` loop is similar to a `drain` loop however it is designed to preserve the initial state of the loop variable. +A copy loop is shorthand designed to replace the usage of a temporary variable in a drain loop. + +``` +copy var { + // do stuff +} +``` + +Equivalent to: + +``` +cell temp = var; +while temp { + // do stuff + temp -= 1; +} +``` + +You can also `copy into` multiple other variables, similar to the `drain` loop: + +``` +copy var into other_var other_var_2 *spread_array etc; +``` diff --git a/docs/optimisations.md b/docs/optimisations.md new file mode 100644 index 0000000..716060c --- /dev/null +++ b/docs/optimisations.md @@ -0,0 +1,72 @@ +### Optimisations + +The optimisations in the Mastermind compiler are aimed at reducing the compiled Brainfuck code length, not necessarily execution speed. This is due to the original goal of the project: Code Golf in Brainfuck. + +#### Cell Clearing + +This optimises the clearing of cells by tracking their values at compile-time. For instance, if a cell can be proven at compile-time to have the value `2`, it is more efficient to clear with `--`, than the typical Brainfuck clear: `[-]`. + +#### Constants + +When large values are added in Brainfuck, the naive approach is to use the increment `-` operator for as many times as needed. The constants optimiser will use multiplication to shorten the code needed to add/subtract large values. Example: the value `45` can be achieved by either `+++++++++++++++++++++++++++++++++++++++++++++` or the shorter: `+++++[<+++++++++>-]>`. + +#### Empty Blocks + +This detects if a code block is empty, and does not compile the clause associated. This is helpful for `if` statements and `copy` loops especially, as those can imply extra overhead for copying cells. + +#### Generated Code + +This is a final pass optimisation that operates directly on Brainfuck code, optimising subsets of programs which can be shortened while still guaranteeing equivalent behaviour. Example: + +``` +--->>><<<++ +``` + +Is equivalent to: + +``` +- +``` + +It is difficult to analyse the behaviour of a Brainfuck program at compile time, so this optimiser is limited to subsets of a program's operations between I/O operations and loops (with exception). Example: + +``` +cell h = 4; +cell j = 3; + +h += 10; + +drain 10 { + j = 5; + h += 4; + j += 1; +} +``` + +Compiles to: + +``` +++++>+++<++++++++++>>++++++++++[<+<++++>[-]+++++>-] +``` + +After optimisation: + +``` +++++++++++++++>+++>++++++++++[-<[-]+++++<++++>>] +``` + +For the 2D compiler extensions, this system can use an exhaustive search to determine the least movement between cells. This could become slow depending on the project, so it can be configured to use a greedy approach. This is done via the _Generated Code Permutations_ setting in the web IDE. + +#### Unreachable Loops + +If a cell is known to have a value of `0` at compile time, and that cell is used to open a Brainfuck loop, then that entire loop is omitted. This is implemented at a low level, so it is agnostic of the syntactic structure that it is optimising, i.e `if`, `while`, `drain`. + +### Unimplemented Optimisations + +#### Memory Allocations + +The goal of this is to optimise placing variables in tape memory to minimise movement between them. + +#### Variable Usage + +The goal of this is to automatically change the order of variable allocations/frees to ensure tape memory is allocated for the smallest amount of execution steps possible. This would allow allocation to be more efficient, as cells can be allocated which would otherwise be taken by variables that are not in use. diff --git a/docs/standardlib.md b/docs/standardlib.md new file mode 100644 index 0000000..5792084 --- /dev/null +++ b/docs/standardlib.md @@ -0,0 +1,77 @@ +### Mastermind Standard Library + +Currently the Mastermind standard library is very limited, and is effectively a set of example programs included in the web IDE and source repository. + +#### Including files + +You can include/import other files using preprocessor directives. The Mastermind preprocessor is intended to mirror the C preprocessor, however it currently only supports the `#include` directive. + +The following is a basic example: + +``` +// file1.mmi +struct H { + cell a; +} +fn print(struct H h) { + output h.a; +} +``` + +``` +// main file being compiled +#include "file1.mmi" + +struct H h; +h.a = 64; +print(h); +// @ +``` + +#### Standard Library Examples + +The most mature files in the included examples are the following: + +- `bitops`: bitshifting operations for cell types +- `i8`: signed type for 8-bit integers and supporting functions +- `u8`: common supporting functions for cell types +- `u16`: a 16-bit unsigned integer type and supporting functions +- `ifp16`: a signed 16-bit fixed-point number type and supporting functions + +NOTE: due to current lack of header-guard support, importing multiple of these will likely cause a compiler error, until this is implemented, the best way to work around this is to only include `ifp16` as that includes the others. + +Example usage: + +``` +#include + +// read a 16 bit number from stdin, add 55, then print + +struct u16 n; +read(n); + +cell ff = 55; +add(n, ff); +print(n); +output ' '; +debug(n); // print the binary representation +// example input: 16000 +// output: 16055 0011111010110111 +``` + +Example fixed-point usage: + +``` +#include + +struct ifp16 n; +_99p99609375(n); // constant 99.99609375 +struct ifp16 m; +__1p5(m); // constant -1.5 + +divide(n, m); +print(n); +output ' '; +debug(n); +// -66.66 10111101.01010110 +``` diff --git a/docs/twodimensional.md b/docs/twodimensional.md new file mode 100644 index 0000000..bf75f72 --- /dev/null +++ b/docs/twodimensional.md @@ -0,0 +1,57 @@ +### Two-Dimensional Brainfuck + +Two-dimensional Brainfuck is an extension which provides an additional dimension to the memory tape. + +To support this, two new operations have been added to this extended version of the language: + +- `^`: move up one cell on the grid +- `v`: move down one cell on the grid + +#### Using 2D Brainfuck in Mastermind + +This behaviour must be enabled in the included Brainfuck interpreter. In the web IDE this is done via the settings modal. + +When this setting is enabled in isolation, the compiler will still generate typical 1D Brainfuck code. To make the compiler use multiple dimensions you must either: + +- Use a 2D-specific memory allocation algorithm +- Use a 2D location specifier on a variable +- Use in-line Brainfuck with 2D instructions + +### Memory Allocation Algorithms + +There are currently four allocation strategies implemented (including the original 1D). + +#### 1D Mastermind + +_1D Mastermind_ allocates the closest free cells to the right of the origin. + +#### 2D Mastermind - Zig Zag + +_2D Mastermind - Zig Zag_ treats the memory as a grid and fills in values from x 0 and some y value diagonally until it reaches y 0 and the same x value as the starting y. The table below shows the order that this is populated + +| 7 | | | | +| --- | --- | --- | --- | +| 4 | 8 | | | +| 2 | 5 | 9 | | +| 1 | 3 | 6 | 10 | + +#### 2D Mastermind - Spiral + +_2D Mastermind - Spiral_ starts from 0,0 and move in a Spiral such that each subsequent memory +value is only 1 step away from the last. This means that it will start by filling a 2x2 grid then from the bottom corner of +that grid it will iterate around that 2x2 filling a 4x4 area + +| 10 | 11 | 12 | +| --- | --- | --- | +| 9 | 2 | 3 | +| 8 | 1 | 4 | +| 7 | 6 | 5 | + +#### 2D Mastermind - Tiles + +_2D Mastermind - Tiles_ allocates a tile of memory and check all cells in that area before expanding to check new cells. This algorithm starts at 0,0 with a 1x1 area then will move down to -1, -1 and check a new 3x3 area it will check each area column by column from the bottom row up so (-1, -1), (0, -1), (1, -1), (-1, 0)... + +| 4 | 6 | 9 | +| --- | --- | --- | +| 3 | 1 | 8 | +| 2 | 5 | 7 | diff --git a/docs/variables.md b/docs/variables.md new file mode 100644 index 0000000..5ca056d --- /dev/null +++ b/docs/variables.md @@ -0,0 +1,148 @@ +### Variables + +#### Cells + +The base data type in Mastermind is the `cell`, this corresponds to a a single 8-bit cell on the Brainfuck tape. + +``` +cell var = 56; +cell c = 'g'; +cell bool = true; // true/false equivalent to 1/0 +``` + +#### Input/Output + +The `input` and `output` keywords in Mastermind correspond to the `,` and `.` operators in Brainfuck. `input` simply inputs the next byte from stdin, and `output` outputs a byte to stdout. + +``` +// stdin: 00abc +cell g; +drain 5 { + // do this 5 times + input g; + g += 1; + output g; +} +// stdout: 11bcd +``` + +The simplest way to display text is to output valid ASCII characters, however if your Brainfuck implementation supports unicode, that is also possible by outputting multiple bytes. + +``` +output 240; +output 159; +output 164; +output 145; +output 10; +// displays 🤑 (emoji with green cash for tongue) +``` + +#### Cell Arrays + +Variables can also be defined as contiguous arrays of cells. + +``` +// multi-cell: +cell[4] array_example = [1, 2, 3, 4]; +cell[5] string_example = "hello"; +cell[2] foo; +foo[0] = 204; +``` + +#### Structs + +Structure types can be defined with named fields, then instantiated as variables. + +``` +struct struct_name { + cell x; + cell y; + cell[5] zzzzz; +} + +struct struct_name s; +s.x = 4; +s.y = 123; +s.zzzzz[0] += 3; +s.zzzzz[4] = 180; + +// nested struct: +struct Nested { + struct struct_name n; +} +``` + +### Structs and Arrays + +Any type can be repeated into an array/contiguous allocation. This includes cells, structs, arrays of cells, and arrays of structs. + +``` +cell[4][6] mult_arr; // a 6-length array of cell[4] arrays +cell[4][6][2] mult_arr; // 2 * (6-length arrays of cell[4] arrays) + +struct T { + cell a; + cell[4][2] b; +} + +struct T[10] ten_T_structs; +ten_T_structs[4].b[1][3] = 45; + +struct S { + struct T[2][4] matrix_of_T_structs; + cell other; +} + +struct S[3] three_S_structs; +three_S_structs[1].matrix_of_T_structs[3][0] = '5'; +``` + +#### Note: Array indices must be compile-time constant integers + +This is a limitation of Brainfuck, getting around this problem requires more runtime code is worth including for the sake of optimisations. You can implement equivalent behaviour using in-line Brainfuck, structs, and functions. + +### Location specifiers + +The exact memory cells occupied by a variable can be specified: + +``` +cell a @4 = 1; // value 1 at tape position 4 +``` + +#### Struct subfields + +The byte-order and positioning of a struct's subfields can be specified: + +``` +struct T { + cell a @1; + cell b[2] @3; +} +// struct T's layout: +// (-, a, -, b[0], b[1]) +// '-' denotes an untracked padding cell +``` + +#### Variable + +When using in-line Brainfuck (see other document), the Brainfuck scope's starting position can be specified with variables: + +``` +cell d; +bf @d { + // brainfuck code here +} + +struct G { + cell h; + cell i; + cell j; +} +struct G g; + +bf @g { + // starts on the first cell of g's allocation +} +// equivalent to: +bf @g.h {} +``` diff --git a/firebase.json b/firebase.json index 1fa0a36..2516965 100644 --- a/firebase.json +++ b/firebase.json @@ -1,13 +1,27 @@ { - "hosting": { - "target": "mastermind", - "public": "dist", - "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - } + "$schema": "https://raw.githubusercontent.com/firebase/firebase-tools/master/schema/firebase-config.json", + "hosting": [ + { + "target": "mastermind-prod", + "public": "dist", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + }, + { + "target": "mastermind-staging", + "public": "dist", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } + ] } diff --git a/package.json b/package.json index fbf909e..c620392 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,11 @@ "@codemirror/lang-cpp": "^6.0.2", "@lezer/highlight": "^1.2.0", "@solid-primitives/storage": "^2.1.1", + "@soorria/solid-dropzone": "^1.0.1", "@thisbeyond/solid-dnd": "^0.7.5", "@uiw/codemirror-theme-tokyo-night": "^4.21.21", "codemirror": "^6.0.1", + "jszip": "^3.10.1", "remark-gfm": "3.0.1", "solid-icons": "^1.1.0", "solid-js": "^1.8.5", diff --git a/programs/examples/basic_calculator.mmi b/programs/examples/basic_calculator.mmi new file mode 100644 index 0000000..f7280d7 --- /dev/null +++ b/programs/examples/basic_calculator.mmi @@ -0,0 +1,39 @@ +// Takes a three characters as input input: +// +// single digit number - operator (either +/-) - single digit number +// +// ...and performs the provided operation on the two single digit numbers, +// then outputs the result. + +#include + +cell n1; +input n1; + +cell operator; +input operator; + +cell n2; +input n2; + +cell result; + +// Checking if character is equal to the operator we need +if not operator - '+' { + result = n1 - '0' + n2 - '0'; +} + +if not operator - '-' { + result = (n1 - '0') - (n2 - '0'); +} + +// Nicely format the output +output n1; +output " "; +output operator; +output " "; +output n2; +output " "; +output '='; +output " "; +print(result); diff --git a/programs/examples/brainfuck.mmi b/programs/examples/brainfuck.mmi new file mode 100644 index 0000000..3996d70 --- /dev/null +++ b/programs/examples/brainfuck.mmi @@ -0,0 +1,178 @@ +// Accepts a brainfuck program as input, then runs it + +// frames: +// @0: bf_instr_marker +// @1: bf_instr +// @2: tape_boundary +// @3: tape_head +// @4: tape_value +// @5: tape_value_copy +// @6: +// @7: +// @8: +// @9: +// @10: loop_count +// @11: loop_reversed +// @12: +// @13: +// @14: +// @15: + +cell initial_tape_boundary @2 = 1; +cell initial_tape_head @3 = 1; + +bf @16 { + >,[< +>>>>>>>>>>>>>>>> >,]<- <<<<<<<<<<<<<<<<[<<<<<<<<<<<<<<<<]>>>>>>>>>>>>>>>> + -- // start pc set as -1 + + [ + // start by finding the current instruction, marked as -1 in the bf_instr_marker + [<<<<<<<<<<<<<<<<]+[->>>>>>>>>>>>>>>>+]- + { + cell bf_instr_marker @0; assert bf_instr_marker equals -1; + cell bf_instr @1; assert bf_instr unknown; + cell tape_boundary @2; assert tape_boundary unknown; + cell tape_head @3; assert tape_head unknown; + cell tape_value @4; assert tape_value unknown; + + cell loop_count @10; assert loop_count unknown; + cell loop_reversed @11; assert loop_reversed unknown; + + cell right_bf_marker @16; assert right_bf_marker unknown; + cell right_bf_instr @17; assert right_bf_instr unknown; + + { + // tape value copy should be cleared automatically so we open a new scope + cell tape_value_copy @5; + // copy current virtual cell value to current frame + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + >[->+>+<<]>>[-<<+>>]< + // now move copied cell value to current frame + [- + <<<-[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // current frame + >>>>>+ + <<<-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + >> + ] + // now move back to current frame + <<<-[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // current frame + } + assert tape_value_copy unknown; + + if loop_count { + // if there is a loop being skipped, we ignore other instructions except loops + if not bf_instr - '[' { + loop_count += 1; + } + if not bf_instr - ']' { + loop_count -= 1; + } + } else { + // cell editing + if not bf_instr - '+' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + > + <<-[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // back to the marker + } + } + if not bf_instr - '-' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ + >-[+>>>>>>>>>>>>>>>>-]+ + > - <<-[+<<<<<<<<<<<<<<<<-]+ + <<+[->>>>>>>>>>>>>>>>+]- + } + } + + // I/O + if not bf_instr - '.' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ + >-[+>>>>>>>>>>>>>>>>-]+ + > . <<-[+<<<<<<<<<<<<<<<<-]+ + <<+[->>>>>>>>>>>>>>>>+]- + } + } + if not bf_instr - ',' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ + >-[+>>>>>>>>>>>>>>>>-]+ + > , <<-[+<<<<<<<<<<<<<<<<-]+ + <<+[->>>>>>>>>>>>>>>>+]- + } + } + + // moves + if not bf_instr - '>' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + ->>>>>>>>>>>>>>>>+ // new tape head + <-[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // back to current frame + } + } + if not bf_instr - '<' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + -<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>> // move tape head left and come back + // move boundary if needed: + <[-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>] // old or no boundary + -[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // back to current frame + } + } + + // loops + if not bf_instr - '[' { + // if virtual tape cell is zero, skip to corresponding loop + if not tape_value_copy { + loop_count += 1; + } + } + if not bf_instr - ']' { + // if virtual tape cell is non-zero, jump back to corresponding loop + if tape_value_copy { + loop_count -= 1; + loop_reversed = true; + } + } + } + + if not loop_count { + loop_reversed = false; + } + + // move any data required for the next iteration + bf_instr_marker = 1; + if loop_reversed { + // move loop count to left frame + bf @10 {[-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>]} + // move loop reversed value + bf @11 {-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>} + // set next pc + bf @0 {<<<<<<<<<<<<<<<<-->>>>>>>>>>>>>>>>} + } else { + // move the loop count to right frame + bf @10 {[->>>> >>>> >>>> >>>>+<<<< <<<< <<<< <<<<]} + // set next pc + if right_bf_instr { + // continue with the program + right_bf_marker = -1; + } else { + // set the current marker to 0, which will exit the main loop + bf_instr_marker = 0; + } + } + } + } + ] +} diff --git a/programs/examples/christmas_trees.mmi b/programs/examples/christmas_trees.mmi new file mode 100644 index 0000000..c6dfdd3 --- /dev/null +++ b/programs/examples/christmas_trees.mmi @@ -0,0 +1,30 @@ +cell start = 3; +cell end = 10; + +cell loops = end - start; + +drain loops into start { + cell num_prints = 1; + cell spaces = start; + // Print The Trees + copy start { + copy spaces { + output " "; + } + copy num_prints{ + output "*"; + } + output "\n"; + num_prints += 2; + spaces -= 1; + } + // Offset the stem + copy start{ + output " "; + } + // Print the stem + output "*"; + output "\n"; + // Newline to separate trees + output "\n"; +} \ No newline at end of file diff --git a/src/assets/divisors_example.mmi b/programs/examples/divisors_example.mmi similarity index 68% rename from src/assets/divisors_example.mmi rename to programs/examples/divisors_example.mmi index 7e316a8..7d127b8 100644 --- a/src/assets/divisors_example.mmi +++ b/programs/examples/divisors_example.mmi @@ -1,43 +1,43 @@ -#include "print.mmi" +#include -let N = 100; -let number = 1; +cell N = 100; +cell number = 1; // a draining loop, loops n times, clearing the variable drain N into number { output "The factors of "; - print; + print(number); output " are 1"; if number - 1 { - let num_copy = number - 2; - let divisor = 2; + cell num_copy = number - 2; + cell divisor = 2; drain num_copy into divisor { // currently functions are more like macros // there are no arguments-by-values or return values - let result; - divisible; + cell result; + divisible(result, number, divisor); if result { output ", "; - print; + print(divisor); } } output " and "; - print; + print(number); } output '\n'; } -def divisible { +fn divisible(cell result, cell dividend, cell divisor) { // result = 0 if dividend % divisor = 0 - let b = dividend; + cell b = dividend; - let iterating = true; + cell iterating = true; while iterating { // a copying loop, loops n times without modifying the variable diff --git a/programs/examples/hello_world.mmi b/programs/examples/hello_world.mmi new file mode 100644 index 0000000..9c49296 --- /dev/null +++ b/programs/examples/hello_world.mmi @@ -0,0 +1,16 @@ +#include + +// Print a constant string +output "Hello world!"; + +// Use \n for newline character. +// Single characters are represented with single quotes +// Strings are represented with double quotes +output '\n'; + +// Printing a number variable +// Requires 'print' module +cell number = 123; +output "The number is: "; +print(number); +output '\n'; diff --git a/programs/examples/ifp16calculator.mmi b/programs/examples/ifp16calculator.mmi new file mode 100644 index 0000000..259c102 --- /dev/null +++ b/programs/examples/ifp16calculator.mmi @@ -0,0 +1,52 @@ +//THIS CALCULATOR REQUIRES AN INPUT IN A SPECIFIC FORMAT +//Each float seperated by a newline with commands between E.G +// 11.11 +// + +// 11.11 +// + +// 12.10 +// = + +// This will execute operations in the order given not BODMAS/PEMDAS + +#include +struct ifp16 dig1; +struct ifp16 dig2; +cell symbol; +cell blank; +_0(dig1); read(dig1); +_0(dig2); +input symbol; +input blank; +cell next_input = 1; +while next_input { + read(dig2); + //Calculator stuff in here + print(dig1); + // output " ("; debug(dig1); output ")"; + output " "; + output symbol; + output " "; + print(dig2); + // output " ("; debug(dig2); output ")"; + output " = "; + if not symbol - '+'{ + add(dig1,dig2); + } + if not symbol - '-'{ + sub(dig1,dig2); + } + if not symbol - '/'{ + divide(dig1,dig2); + } + if not symbol - '*'{ + mult(dig1,dig2); + } + print(dig1); + // output " ("; debug(dig1); output ")"; + input next_input; + input blank; + symbol = next_input; + next_input -= 61; + output "\n"; +} \ No newline at end of file diff --git a/programs/examples/prime_1_to_100.mmi b/programs/examples/prime_1_to_100.mmi new file mode 100644 index 0000000..113ebb2 --- /dev/null +++ b/programs/examples/prime_1_to_100.mmi @@ -0,0 +1,45 @@ +#include + +cell N = 99; +cell number = 2; + +// a draining loop, loops n times, clearing the variable +drain N into number { + cell result1; + is_prime(result1, number); + if not result1 { + print(number); + output "\n"; + } +} + +fn is_prime(cell result1, cell num) { + result1 = false; + cell divisor = num - 1; + drain divisor{ + if divisor - 1 { + divisible(result1, num, divisor); + } + } +} + +fn divisible(cell result1, cell dividend, cell divisor) { + // result2 = 0 if dividend % divisor = 0 + cell b = dividend; + + cell iterating = true; + + while iterating { + // a copying loop, loops n times without modifying the variable + copy divisor { + b -= 1; + if not b { + iterating = false; + } + } + } + // if there is no remainder then we are divisible + if not b { + result1 = true; + } +} diff --git a/programs/other/brainfuck.2d.bf b/programs/other/brainfuck.2d.bf new file mode 100644 index 0000000..f8d26ae --- /dev/null +++ b/programs/other/brainfuck.2d.bf @@ -0,0 +1,340 @@ + + +input a brainfuck program till null character +-v> +, +[>,] +^-<+[-<+]- + +need to translate to small numbers for easier coding later +> ++[- + v------ ------ ------ ------ ------ ------ ------^ +>+]-<+[-<+]- + + + +plus 1 1 +minus 3 2 +left 18 4 +right 20 3 + +openb 49 5 +closeb 51 6 + +dot 4 7 +comma 2 8 + +other 0 + + +for each character in the bf file convert the char to a digit as above +>+[-v +copy n up two spaces +[-^+^+vv]^[-v+^]^ + +if n == 1: +- +>+<[>-< +else n != 1 + + if n == 2: + - + >+<[>-< + else n != 2 + + if n == 3: + - + >+<[>-< + else n != 3 + + if n == 4: + - + >+<[>-< + else n != 4 + + if n == 18: + ------- ------- + >+<[>-< + else n != 18 + + if n == 20: + -- + >+<[>-< + else n != 20 + + if n == 49: + ----- ----- ----- ----- --- --- --- + >+<[>-< + else n != 49 + + if n == 51: + -- + >+<[>-< + else n != 51 + + vv + [-] + ^^ + + [-]]>[[-]<[-] + if n == 51 (closeb) + vv + [-]++++++ + v++^ + ^^ + >]< + + [-]]>[[-]<[-] + if n == 49 (openb) + vv + [-]+++++ + v+^ + ^^ + >]< + + [-]]>[[-]<[-] + if n == 20 (right) + vv + [-]+++ + ^^ + >]< + + [-]]>[[-]<[-] + if n == 18 (left) + vv + [-]++++ + ^^ + >]< + + [-]]>[[-]<[-] + if n == 4 (dot) + vv + [-]+++++++ + ^^ + >]< + + [-]]>[[-]<[-] + if n == 3 (minus) + vv + [-]++ + ^^ + >]< + + [-]]>[[-]<[-] + if n == 2 (comma) + vv + [-]++++++++ + ^^ + >]< + +[-]]>[[-]<[-] +if n == 1 (plus) + vv + [-]+ + ^^ +>]+]- + + +add boundaries +v-v-v-v-^^^^<+[-<+]<->v-v-v<-v->^^^^> + +tape head +vvv+^^^ +pc +<+> + +main brainfuck loop: + ++[- +<->+ + + copy the current command to a clean space + v[-^^+^+vvv]^^[-vv+^^]^ + + if else chain: + >+<-[>[-]< + else not plus + >+<-[>[-]< + else not minus + >+<-[>[-]< + else not right + >+<-[>[-]< + else not left + >+<-[>[-]< + else not openb + >+<-[>[-]< + else not closeb + >+<-[>[-]< + else not dot + >+<-[>[-]<[-] + else not comma + non brainfuck character + do nothing + ]>[-< + if comma + vvvvv+[-<+]->-[+>-]+ + + v,^ + + +[-<+]-<^^^+[->+]->-[+>-]+^^ + >]< + ]>[-< + if dot + vvvvv+[-<+]->-[+>-]+ + + v.^ + + +[-<+]-<^^^+[->+]->-[+>-]+^^ + >]< + ]>[-< + if closeb + find the current cell + vvvvv+[-<+]->-[+>-]+ + + v[-v+v+^^]v[-^+v]v + + [[-] + if tape cell is not 0 go through the loops and find the right one to skip back to + starts 2 below the tape + + ^^^+[-<+]-^^^+[->+]- + set the loop counter to 1 + <+ + + [>>-[+>-]<+vv + go through each loop marker until the counter is 0 + copy to above for if else + [-^^^+^+vvvv]^^^[-vvv+^^^]^ + + ->+<[>-< + else not loop start + ->+<[>-<[-] + else neither loop start or end + pass + ]>[-< + if loop end take 1 from counter + find counter + vv+[-<+]-< + + + navigate back + >>-[+>-]+^^ + >]< + ]>[-< + if loop start add 1 to counter + vv+[-<+]-< + - + >>-[+>-]+^^ + >]< + vv+[-<+]-<] + + clear loop counter just in case + [-] + + return back to 2 below the tape + >vvv+[-<+]->-[+>-]+vvv + ] + + ^^^+[-<+]-<^^^+[->+]->-[+>-]+^^ + >]< + ]>[-< + if openb + find the current cell + vvvvv+[-<+]->-[+>-]+ + + v[-v+v+^^]v[-^+v]v + + >+<[>-<[-]]>[-< + if tape cell is 0 go through the loops and find the right one to skip to + starts 2 below the tape + + ^^^+[-<+]-^^^+[->+]- + set the loop counter to 1 + <+ + + [>>-[+>-]>+vv + go through each loop marker until the counter is 0 + copy to above for if else + [-^^^+^+vvvv]^^^[-vvv+^^^]^ + + ->+<[>-< + else not loop start + ->+<[>-<[-] + else neither loop start or end + pass + ]>[-< + if loop end take 1 from counter + find counter + vv+[-<+]-< + - + navigate back + >>-[+>-]+^^ + >]< + ]>[-< + if loop start add 1 to counter + vv+[-<+]-< + + + >>-[+>-]+^^ + >]< + vv+[-<+]-<] + + clear loop counter just in case + [-] + + return back to 2 below the tape + >vvv+[-<+]->-[+>-]+vvv + >]< + + ^^^+[-<+]-<^^^+[->+]->-[+>-]+^^ + >]< + ]>[-< + if left + vvvvv+[-<+]->-[+>-]+ + + check for and extend boundary if needed + <<-v-^>+v+^ + [<+v+^>-v-^] + +>- + + +[-<+]-<^^^+[->+]->-[+>-]+^^ + >]< + ]>[-< + if right + vvvvv+[-<+]->-[+>-]+ + + >>-v-^<+v+^ + [>+v+^<-v-^] + +<- + + +[-<+]-<^^^+[->+]->-[+>-]+^^ + >]< + ]>[-< + if minus + vvvvv+[-<+]->-[+>-]+ + + v-^ + + +[-<+]-<^^^+[->+]->-[+>-]+^^ + >]< + ]>[- + check right for a tape head + -[+>-]+ + increment + v+^ + find the pc again + +[-<+]- + <^^^ + +[->+]- + >-[+>-]+ + + ^^>]< + + vv +> ++]- \ No newline at end of file diff --git a/programs/other/brainfuck.min.2d.bf b/programs/other/brainfuck.min.2d.bf new file mode 100644 index 0000000..58e18f9 --- /dev/null +++ b/programs/other/brainfuck.min.2d.bf @@ -0,0 +1 @@ +-v>,[>,]^-<+[-<+]->+[-v------------------------------------------^>+]-<+[-<+]->+[-v[-^+^+vv]^[-v+^]^->+<[>-<->+<[>-<->+<[>-<->+<[>-<-------------->+<[>-<-->+<[>-<----------------------------->+<[>-<-->+<[>-[[-]<[-]vv[-]++++++v++^^^>]<[-]]>[[-]<[-]vv[-]+++++v+^^^>]<[-]]>[[-]<[-]vv[-]+++^^>]<[-]]>[[-]<[-]vv[-]++++^^>]<[-]]>[[-]<[-]vv[-]+++++++^^>]<[-]]>[[-]<[-]vv[-]++^^>]<[-]]>[[-]<[-]vv[-]++++++++^^>]<[-]]>[[-]<[-]vv[-]+^^>]+]-v-v-v-v-^^^^<+[-<+]<->v-v-v<-v->^^^^>vvv+^^^<+>+[-<->+v[-^^+^+vvv]^^[-vv+^^]^>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<>+<-[>[-]<[-]]>[--[+>-]+v,^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v.^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v[-v+v+^^]v[-^+v]v[[-]^^^+[-<+]-^^^+[->+]-<+[>>-[+>-]<+vv[-^^^+^+vvvv]^^^[-vvv+^^^]^->+<[>-<->+<[>-<[-]]>[->-[+>-]+^^>]<]>[->-[+>-]+^^>]vvv+[-<+]->-[+>-]+vvv]^^^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v[-v+v+^^]v[-^+v]v>+<[>-<[-]]>[-<^^^+[-<+]-^^^+[->+]-<+[>>-[+>-]>+vv[-^^^+^+vvvv]^^^[-vvv+^^^]^->+<[>-<->+<[>-<[-]]>[->-[+>-]+^^>]<]>[->-[+>-]+^^>]vvv+[-<+]->-[+>-]+vvv>]<^^^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+<<-v-^>+v+^[<+v+^>-v-^]+>-+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+>>-v-^<+v+^[>+v+^<-v-^]+<-+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v-^+[-<+]-<^^^+[->+]->-[+>-]+^^>]<]>[--[+>-]+v+^+[-<+]-<^^^+[->+]->-[+>-]+^^>]+]- \ No newline at end of file diff --git a/programs/other/brainfuck.mmi b/programs/other/brainfuck.mmi new file mode 100644 index 0000000..3996d70 --- /dev/null +++ b/programs/other/brainfuck.mmi @@ -0,0 +1,178 @@ +// Accepts a brainfuck program as input, then runs it + +// frames: +// @0: bf_instr_marker +// @1: bf_instr +// @2: tape_boundary +// @3: tape_head +// @4: tape_value +// @5: tape_value_copy +// @6: +// @7: +// @8: +// @9: +// @10: loop_count +// @11: loop_reversed +// @12: +// @13: +// @14: +// @15: + +cell initial_tape_boundary @2 = 1; +cell initial_tape_head @3 = 1; + +bf @16 { + >,[< +>>>>>>>>>>>>>>>> >,]<- <<<<<<<<<<<<<<<<[<<<<<<<<<<<<<<<<]>>>>>>>>>>>>>>>> + -- // start pc set as -1 + + [ + // start by finding the current instruction, marked as -1 in the bf_instr_marker + [<<<<<<<<<<<<<<<<]+[->>>>>>>>>>>>>>>>+]- + { + cell bf_instr_marker @0; assert bf_instr_marker equals -1; + cell bf_instr @1; assert bf_instr unknown; + cell tape_boundary @2; assert tape_boundary unknown; + cell tape_head @3; assert tape_head unknown; + cell tape_value @4; assert tape_value unknown; + + cell loop_count @10; assert loop_count unknown; + cell loop_reversed @11; assert loop_reversed unknown; + + cell right_bf_marker @16; assert right_bf_marker unknown; + cell right_bf_instr @17; assert right_bf_instr unknown; + + { + // tape value copy should be cleared automatically so we open a new scope + cell tape_value_copy @5; + // copy current virtual cell value to current frame + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + >[->+>+<<]>>[-<<+>>]< + // now move copied cell value to current frame + [- + <<<-[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // current frame + >>>>>+ + <<<-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + >> + ] + // now move back to current frame + <<<-[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // current frame + } + assert tape_value_copy unknown; + + if loop_count { + // if there is a loop being skipped, we ignore other instructions except loops + if not bf_instr - '[' { + loop_count += 1; + } + if not bf_instr - ']' { + loop_count -= 1; + } + } else { + // cell editing + if not bf_instr - '+' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + > + <<-[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // back to the marker + } + } + if not bf_instr - '-' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ + >-[+>>>>>>>>>>>>>>>>-]+ + > - <<-[+<<<<<<<<<<<<<<<<-]+ + <<+[->>>>>>>>>>>>>>>>+]- + } + } + + // I/O + if not bf_instr - '.' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ + >-[+>>>>>>>>>>>>>>>>-]+ + > . <<-[+<<<<<<<<<<<<<<<<-]+ + <<+[->>>>>>>>>>>>>>>>+]- + } + } + if not bf_instr - ',' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ + >-[+>>>>>>>>>>>>>>>>-]+ + > , <<-[+<<<<<<<<<<<<<<<<-]+ + <<+[->>>>>>>>>>>>>>>>+]- + } + } + + // moves + if not bf_instr - '>' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + ->>>>>>>>>>>>>>>>+ // new tape head + <-[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // back to current frame + } + } + if not bf_instr - '<' { + bf @0 { + >>-[+<<<<<<<<<<<<<<<<-]+ // boundary + >-[+>>>>>>>>>>>>>>>>-]+ // tape head + -<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>> // move tape head left and come back + // move boundary if needed: + <[-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>] // old or no boundary + -[+<<<<<<<<<<<<<<<<-]+ // boundary + <<+[->>>>>>>>>>>>>>>>+]- // back to current frame + } + } + + // loops + if not bf_instr - '[' { + // if virtual tape cell is zero, skip to corresponding loop + if not tape_value_copy { + loop_count += 1; + } + } + if not bf_instr - ']' { + // if virtual tape cell is non-zero, jump back to corresponding loop + if tape_value_copy { + loop_count -= 1; + loop_reversed = true; + } + } + } + + if not loop_count { + loop_reversed = false; + } + + // move any data required for the next iteration + bf_instr_marker = 1; + if loop_reversed { + // move loop count to left frame + bf @10 {[-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>]} + // move loop reversed value + bf @11 {-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>} + // set next pc + bf @0 {<<<<<<<<<<<<<<<<-->>>>>>>>>>>>>>>>} + } else { + // move the loop count to right frame + bf @10 {[->>>> >>>> >>>> >>>>+<<<< <<<< <<<< <<<<]} + // set next pc + if right_bf_instr { + // continue with the program + right_bf_marker = -1; + } else { + // set the current marker to 0, which will exit the main loop + bf_instr_marker = 0; + } + } + } + } + ] +} diff --git a/programs/other/day_of_the_week.mmi b/programs/other/day_of_the_week.mmi new file mode 100644 index 0000000..59ffc05 --- /dev/null +++ b/programs/other/day_of_the_week.mmi @@ -0,0 +1,215 @@ +// 1 = Sunday +// 7 = Saturday +cell count; +cell left; +cell right; +cell iterating = 1; +//Keep looping until we get 2 null characters in a row +while iterating { + cell day = 7; + // Since days are cyclical we can just loop from 1 to 7 and overflow back to 1 + fn iterate(day) { + if not day - 7 { + day = 1; + } else { + day += 1; + } + } + //Iterate days 1 by 1 + fn drain_days(cell count, cell day) { + drain count { + iterate(day); + } + } + //Since we know how many days in each month we can skip any multiple of 7 and only do the modulo iteration + fn drain_months(cell count, cell day) { + cell iterates; + // Loop from current month to jan and add that many days skip leap year logic since that is year dependent + drain count { + iterates = 0; + if not count - 12 { + iterates += 3; + } + if not count - 11 { + iterates += 2; + } + if not count - 10 { + iterates += 3; + } + if not count - 9 { + iterates += 2; + } + if not count - 8 { + iterates += 3; + } + if not count - 7 { + iterates += 3; + } + if not count - 6 { + iterates += 2; + } + if not count - 5 { + iterates += 3; + } + if not count - 4 { + iterates += 2; + } + if not count - 3 { + iterates += 3; + } + if not count - 1 { + iterates += 3; + } + drain iterates { + iterate(day); + } + } + } + + // This function is the most complex part of the code it is required to handle the leap years + fn drain_years(cell left, cell right, cell day, cell m2) { + // Since we know we only run from 1583 to 9999 we can setup our leap year logic from that date + // We then loop through each year and add 1 day since 365 % 7 = 1 and then any extra leap years + cell hun_years = 4; + cell years = 83; + cell leap = 3; + left -= 15; + if left{ + right += 100; + left -= 1; + } + right -= 83; + left += 1; + drain left { + if left - 1 { + right += 100; + } + drain right { + iterate(day); + //Leap year logic add a day every year that is a multiple of 4 unless it is also a multiple of 100 + // but not a multiple of 400 + if not leap - 4 { + if not years - 100 { + if not hun_years - 4 { + iterate(day); + hun_years = 0; + } + hun_years += 1; + years = 0; + } else { + iterate(day); + } + leap = 0; + } + leap += 1; + years += 1; + } + } + //This logic is here to handle if the current year is a leap year since the above doesn't add the current year + if m2 - 1 { + if m2 - 2 { + if not leap - 4 { + if not years - 100 { + if not hun_years - 4 { + iterate(day); + } + } else { + iterate(day); + } + } + } + } + } + //Accept our input + cell y1; + cell y2; + cell y3; + cell y4; + cell m1; + cell m2; + cell d1; + cell d2; + cell waiting = 1; + //Check if to see if we get a null character and if so skip it and if the next is also null break the loop + while waiting { + input y1; + if y1 { + waiting = 0; + } else { + waiting += 1; + if not waiting - 3 { + iterating = 0; + waiting = 0; + } + } + } + if iterating { + // input y1; + input y2; + input y3; + input y4; + input m1; + input m1; + input m2; + input d1; + input d1; + input d2; + // Convert ASCII Nums into real Nums 0-255 + y1 -= '0'; + y2 -= '0'; + y3 -= '0'; + y4 -= '0'; + m1 -= '0'; + m2 -= '0'; + d1 -= '0'; + d2 -= '0'; + // iterate Day Of The Week For Each Day d1 = 10s and d2 = 1s + count = d2; + drain d1 { + count += 10; + } + day -= 1; + drain_days(count, day); + // iterate Day Of The Week For Each Month m1 = 10s and m2 = 1s + count = m2; + drain m1 { + count += 10; + } + m2 = count; + count -= 1; + drain_months(count, day); + // Now iterate through years since 4 places is to big we split left and right so eg 1999 = 19 99 + left = y2; + right = y4; + drain y1 { + left += 10; + } + drain y3 { + right += 10; + } + drain_years(left, right, day, m2); + //Then output based on the day value + if not day - 7 { + output "Saturday"; + } + if not day - 6 { + output "Friday"; + } + if not day - 5 { + output "Thursday"; + } + if not day - 4 { + output "Wednesday"; + } + if not day - 3 { + output "Tuesday"; + } + if not day - 2 { + output "Monday"; + } + if not day - 1 { + output "Sunday"; + } + output "\n"; + } +} diff --git a/compiler/programs/divisors.mmi b/programs/other/divisors.mmi similarity index 100% rename from compiler/programs/divisors.mmi rename to programs/other/divisors.mmi diff --git a/compiler/programs/divisors_optimised.mmi b/programs/other/divisors_optimised.mmi similarity index 100% rename from compiler/programs/divisors_optimised.mmi rename to programs/other/divisors_optimised.mmi diff --git a/programs/other/input.mmi b/programs/other/input.mmi new file mode 100644 index 0000000..8bfeaff --- /dev/null +++ b/programs/other/input.mmi @@ -0,0 +1,61 @@ + +def is_num { + bool = 0; + if not number - '0' { + bool = 1; + } + if not number - '1' { + bool = 1; + } + if not number - '2' { + bool = 1; + } + if not number - '3' { + bool = 1; + } + if not number - '4' { + bool = 1; + } + if not number - '5' { + bool = 1; + } + if not number - '6' { + bool = 1; + } + if not number - '7' { + bool = 1; + } + if not number - '8' { + bool = 1; + } + if not number - '9' { + bool = 1; + } +} + + +//Accepts input as a number with a space afterwards +def num_input { + num = 0; + let cur_input; + //If we have a space skip + input cur_input; + let iterating; + is_num; + while iterating{ + cur_input -= '0'; + let loops = 10; + let mult = num; + num = 0; + drain loops { + copy mult { + num += 1; + } + } + drain cur_input { + num += 1; + } + input cur_input; + is_num; + } +} \ No newline at end of file diff --git a/compiler/programs/print.mmi b/programs/other/print.mmi similarity index 100% rename from compiler/programs/print.mmi rename to programs/other/print.mmi diff --git a/compiler/programs/test.mmi b/programs/other/test.mmi similarity index 100% rename from compiler/programs/test.mmi rename to programs/other/test.mmi diff --git a/programs/std/bitops b/programs/std/bitops new file mode 100644 index 0000000..a5d56d0 --- /dev/null +++ b/programs/std/bitops @@ -0,0 +1,116 @@ +/// shift left and wrap +fn wshift_l(cell n) { + cell val = n; + drain val into n { + // wrapping annihilates the bit, here we correct it + // (efficient if not) + n += 1; + if n { n -= 1; } + } +} +fn wshift_l(cell n, cell count) { + copy count { + wshift_l(n); + } +} + +/// shift left and truncate +fn tshift_l(cell n) { + // no need to detect overflows because overflowing annihilates the bit + n += n; +} +fn tshift_l(cell n, cell count) { + copy count { + tshift_l(n); + } +} + +/// shift right and wrap +fn wshift_r(cell n) { + // repeatedly divide by two, adding 128 if there's a remainder + cell i = 0; + cell carry = false; + while n { + n -= 1; + if not n { + carry = true; + } else { + i += 1; + n -= 1; + } + } + n = i; + if carry { + n += 128; + } + // this is fairly inefficient + // maybe could cheat and shift (8 - count) times the other way? +} +fn wshift_r(cell n, cell count) { + copy count { + wshift_r(n); + } +} + +/// shift right and truncate +fn tshift_r(cell n) { + cell i = 0; + while n { + n -= 1; + if n { + i += 1; + n -= 1; + } + } + drain i into n; +} +fn tshift_r(cell n, cell count) { + copy count { + tshift_r(n); + } +} + +/// set the most-signifant bit to zero +fn zero_final_bit(cell n) { + tshift_l(n); + // more efficient right shift now that we know it is even (least-sig = 0) + cell i = 0; + while n { + n -= 2; + i += 1; + } + drain i into n; +} + + +// debug/test code: +// #include "print.mmi" +// cell a = 5; +// cell bitshift_count = 1; +// drain 10 { +// print(a); +// output '\n'; +// wshift_l(a, bitshift_count); +// } +// print(a); +// output "\n\n"; +// drain 10 { +// print(a); +// output '\n'; +// tshift_l(a, bitshift_count); +// } + +// output "\n\n"; +// a = 104; +// drain 8 { +// print(a); +// output '\n'; +// wshift_r(a, bitshift_count); +// } +// print(a); +// output "\n\n"; +// drain 8 { +// print(a); +// output '\n'; +// tshift_r(a, bitshift_count); +// } diff --git a/programs/std/i8 b/programs/std/i8 new file mode 100644 index 0000000..ec55a83 --- /dev/null +++ b/programs/std/i8 @@ -0,0 +1,87 @@ +#include +#include + +struct i8 { + cell n; +} +// 0b11111111 = -1 +// 0b00000000 = 0 +// 0b00000001 = 1 +// 0b00000010 = 2 +// 0b01111111 = 127 +// 0b10000000 = -128 +// 0b10000001 = -127 +// 0b10000010 = -126 + +// cell MAX_i8 = 127; +// cell MIN_i8 = -128; + +fn is_neg(struct i8 self, cell _sign_neg) { + copy self.n into _sign_neg; + cell i = 7; + tshift_r(_sign_neg, i); +} + +fn to_u8(struct i8 self, cell _abs, cell _sign_neg) { + copy self.n into _abs _sign_neg; + + { + // shift so that _sign_neg is the most significant bit + cell i = 7; + tshift_r(_sign_neg, i); + } + + if _sign_neg { + _abs = -_abs; + } +} + +/// add-assign operator +fn add(struct i8 self, struct i8 other) { + self.n += other.n; +} + +/// subtract-assign operator +fn sub(struct i8 self, struct i8 other) { + self.n -= other.n; +} + +/// increment operator +fn inc(struct i8 self) { + self.n += 1; +} + +/// decrement operator +fn dec(struct i8 self) { + self.n -= 1; +} + +/// print function for signed 8bit integers +fn print(struct i8 self) { + cell abs; + cell neg; + to_u8(self, abs, neg); + if neg { + output '-'; + } + print(abs); +} + +// debug/test code: +// #include "print.mmi" +// struct i8 a; +// a.n = 0; +// drain 2 { +// drain 128 into a.n { +// cell abs; +// cell neg; +// to_u8(n, abs, neg); +// if not neg { +// output '+'; +// } else { +// output '-'; +// } +// print(abs); +// output '\n'; +// } +// } diff --git a/programs/std/ifp16 b/programs/std/ifp16 new file mode 100644 index 0000000..5d52a19 --- /dev/null +++ b/programs/std/ifp16 @@ -0,0 +1,569 @@ +// #include +// #include +#include + +struct ifp16 { + struct i8 int; + cell frac; +} + +// 00000000.00000000 +// 01111111.11111111 +// 12.156 + +// struct ifp16 { +// cell sign_neg; +// cell int; +// cell frac; +// } +// (+/-)(int + frac * 2^-8) + +fn _0(struct ifp16 n) {n.int.n = 0; n.frac = 0;} +fn _1(struct ifp16 n) {n.int.n = 1; n.frac = 0;} +fn _2(struct ifp16 n) {n.int.n = 2; n.frac = 0;} +fn _4(struct ifp16 n) {n.int.n = 4; n.frac = 0;} +fn _8(struct ifp16 n) {n.int.n = 8; n.frac = 0;} +fn _16(struct ifp16 n) {n.int.n = 16; n.frac = 0;} +fn _32(struct ifp16 n) {n.int.n = 32; n.frac = 0;} +fn _64(struct ifp16 n) {n.int.n = 64; n.frac = 0;} +fn _127(struct ifp16 n) {n.int.n = 127; n.frac = 0;} +fn _127p99609375(struct ifp16 n) {n.int.n = 127; n.frac = 255;} +fn __128(struct ifp16 n) {n.int.n = -128; n.frac = 0;} +fn _0p99609375(struct ifp16 n) {n.int.n = 0; n.frac = 255;} +fn _0p5(struct ifp16 n) {n.int.n = 0; n.frac = 128;} +fn _0p25(struct ifp16 n) {n.int.n = 0; n.frac = 64;} +fn _0p125(struct ifp16 n) {n.int.n = 0; n.frac = 32;} +fn _0p0625(struct ifp16 n) {n.int.n = 0; n.frac = 16;} +fn _0p03125(struct ifp16 n) {n.int.n = 0; n.frac = 8;} +fn _0p015625(struct ifp16 n) {n.int.n = 0; n.frac = 4;} +fn _0p0078125(struct ifp16 n) {n.int.n = 0; n.frac = 2;} +fn _0p00390625(struct ifp16 n) {n.int.n = 0; n.frac = 1;} +fn __0p00390625(struct ifp16 n) {n.int.n = -1; n.frac = 255;} +fn __0p0078125(struct ifp16 n) {n.int.n = -1; n.frac = 254;} +fn __0p015625(struct ifp16 n) {n.int.n = -1; n.frac = 252;} +fn __0p03125(struct ifp16 n) {n.int.n = -1; n.frac = 248;} +fn __0p0625(struct ifp16 n) {n.int.n = -1; n.frac = 240;} +fn __0p125(struct ifp16 n) {n.int.n = -1; n.frac = 224;} +fn __0p25(struct ifp16 n) {n.int.n = -1; n.frac = 192;} +fn __0p5(struct ifp16 n) {n.int.n = -1; n.frac = 128;} +fn __0p99609375(struct ifp16 n) {n.int.n = -1; n.frac = 1;} +fn _1p5(struct ifp16 n) {n.int.n = 1; n.frac = 128;} +fn _2p5(struct ifp16 n) {n.int.n = 2; n.frac = 128;} +fn _7p75(struct ifp16 n) {n.int.n = 7; n.frac = 192;} +fn __1p5(struct ifp16 n) {n.int.n = -2; n.frac = 128;} +fn __2p5(struct ifp16 n) {n.int.n = -3; n.frac = 128;} +fn __6p25(struct ifp16 n) {n.int.n = -7; n.frac = 192;} +fn _99p99609375(struct ifp16 n) {n.int.n = 99; n.frac = 255;} +fn __99p99609375(struct ifp16 n) {n.int.n = -100; n.frac = 1;} +fn _127p12890625(struct ifp16 n) {n.int.n = 127; n.frac = 33;} +fn __127p12890625(struct ifp16 n) {n.int.n = -128; n.frac = 223;} + +fn MIN(struct ifp16 n) {__128(n);} +fn MAX(struct ifp16 n) {_127p99609375(n);} +fn EPSILON(struct ifp16 n) {_0p00390625(n);} +fn _EPSILON(struct ifp16 n) {__0p00390625(n);} + +/// check if the number is non-zero +fn ne_zero(struct ifp16 self, cell _non_zero) { + _non_zero = false; + if self.int.n { + _non_zero = true; + } + if self.frac { + _non_zero = true; + } +} + +/// add-assign two ifp16 numbers +fn add(struct ifp16 self, struct ifp16 other) { + add(self.int, other.int); + copy other.frac { + self.frac += 1; + inc(self.int); + if self.frac { + dec(self.int); + } + } +} + +/// subtract-assign two ifp16 numbers +fn sub(struct ifp16 self, struct ifp16 other) { + sub(self.int, other.int); + copy other.frac { + self.frac -= 1; + dec(self.int); + if self.frac { + inc(self.int); + } + } +} + +/// multiply-assign an ifp16 and an u8 (cell) +fn mult(struct ifp16 self, cell other) { + ///Setup our starting variables result + cell int_result = 0; + cell frac_result = 0; + cell sub_frac_result = 0; + cell int1; + cell neg1; + to_u8(self.int, int1, neg1); + cell int2 = other; + ///Firstly int_result = int_abs1 * int_abs2 + copy int1 { + copy int2 { + int_result += 1; + } + } + /// Next frac_result = int_abs2 * frac1 + copy self.frac { + copy int2 { + frac_result += 1; + if not frac_result { + int_result += 1; + } + } + } + self.int.n = int_result; + self.frac = frac_result; + if neg1 { + self.int.n = -self.int.n; + } +} + +/// multiply-assign an ifp16 and an i8 +fn mult(struct ifp16 self, struct i8 other) { + ///Setup our starting variables result + cell int_result = 0; + cell frac_result = 0; + cell sub_frac_result = 0; + cell int1; + cell int2; + cell neg1; + cell neg2; + to_u8(self.int, int1, neg1); + to_u8(other, int2, neg2); + neg1 = neg1 - neg2; + ///Firstly int_result = int_abs1 * int_abs2 + copy int1 { + copy int2 { + int_result += 1; + } + } + /// Next frac_result = int_abs1 * frac_2 + int_abs2 * frac1 + copy self.frac { + copy int2 { + frac_result += 1; + if not frac_result { + int_result += 1; + } + } + } + self.int.n = int_result; + self.frac = frac_result; + if neg1 { + self.int.n = -self.int.n; + } +} + +/// multiply-assign two ifp16 numbers +fn mult(struct ifp16 self, struct ifp16 other) { + ///Setup our starting variables result + cell int_result = 0; + cell frac_result = 0; + cell sub_frac_result = 0; + cell int1; + cell int2; + cell neg1; + cell neg2; + to_u8(self.int, int1, neg1); + to_u8(other.int, int2, neg2); + neg1 = neg1 - neg2; + ///Firstly int_result = int_abs1 * int_abs2 + copy int1 { + copy int2 { + int_result += 1; + } + } + /// Next frac_result = int_abs1 * frac_2 + int_abs2 * frac1 + copy self.frac { + copy int2 { + frac_result += 1; + if not frac_result { + int_result += 1; + } + } + } + copy other.frac { + copy int1 { + frac_result += 1; + if not frac_result { + int_result += 1; + } + } + } + /// Finally we do sub_frac_result = frac_1 * frac_2 + copy self.frac { + copy other.frac { + sub_frac_result += 1; + if not sub_frac_result { + frac_result += 1; + if not frac_result { + int_result += 1; + } + } + } + } + self.int.n = int_result; + self.frac = frac_result; + if neg1 { + self.int.n = -self.int.n; + } +} + +/// divide-assign two ifp16 numbers +/// self / other +fn divide(struct ifp16 self, struct ifp16 other) { +// let x = (u16)self +// let y = (u16)other +// let result = 0 +// let b = 8 +// while b > 0: +// let d = x // y +// result += d * (b ^ 2) +// if d == 0: +// x *= 2 +// else: +// x = x % y +// b -= 1 + + struct u16 x; + struct u16 y; + + cell output_neg; + { + x.n0 = self.frac; + cell self_neg; + to_u8(self.int, x.n1, self_neg); + if self_neg { + x.n1 -= 1; + x.n0 = -self.frac; + } + + y.n0 = other.frac; + cell other_neg; + to_u8(other.int, y.n1, other_neg); + if other_neg { + if other.frac { + y.n1 -= 1; + } + y.n0 = -other.frac; + } + + // efficient xor + output_neg = self_neg - other_neg; + } + + + struct u16 result; + struct u16 b; _256(b); + + cell b_ = true; + while b_ { + struct u16 d; + cell dividing = true; + while dividing { + // make a copy of y, then run a drain loop effectively + struct u16 yy; set(yy, y); + cell yy_ = true; + while yy_ { + cell x_; ne_zero(x, x_); + if not x_ { + // x is 0 + dividing = false; + } + dec(x); + + + dec(yy); + ne_zero(yy, yy_); + } + if not dividing { + cell x_; ne_zero(x, x_); + if x_ { + // there is a remainder + add(x, y); + } + } else { + inc(d); + } + } + + cell d_; ne_zero(d, d_); + while d_ { + add(result, b); + + dec(d); + ne_zero(d, d_); + } + + div_2(b); + struct u16 xx; set(xx, x); + add(x, xx); + ne_zero(b, b_); + } + + // cast the u16 back to self ifp16 + if not output_neg { + self.int.n = result.n1; + self.frac = result.n0; + } else { + self.int.n = -result.n1; + if result.n0 { + self.int.n -= 1; + } + self.frac = -result.n0; + } +} + +/// read an fp16 from stdin, wraps integer digits, truncates fractional part +fn read(struct ifp16 self) { + cell sign_neg = false; + cell digit; + input digit; + if not digit - '-' { + sign_neg = true; + input digit; + } + // input the abs integer amount + cell abs_int; + { + cell iterating = true; + while iterating { + if not digit - '.' { + iterating = false; + } + if not digit - '\n' { + iterating = false; + } + if iterating { + // shift digits (times by 10) and add the new digit + mult_10(abs_int); + abs_int += digit - '0'; + + input digit; + } + } + } + + // read the decimal part if needed + if not digit - '.' { + // input a number up to 2 digits, count the leading zeros + // we input directly into the most-signifant byte of a u16 + // so it is automatically x256 + struct u16 n; + { + cell frac_digit_count; + { + cell iterating = 2; // limit 2 digits for fp16 + input digit; + digit -= '\n'; + while iterating { + if not digit { + iterating = 0; + } else { + digit += '\n'; + digit -= '0'; + + // another digit + frac_digit_count += 1; + + mult_10(n.n1); + n.n1 += digit; + + iterating -= 1; + + input digit; + digit -= '\n'; + } + } + // read the rest of the line + while digit { + input digit; + digit -= '\n'; + } + } + + // we have multiplied our integer by 256 + // divide by 10^(frac_digit_count) + drain frac_digit_count { + div_10(n); + } + } + self.frac = n.n0; + } + + if sign_neg { + self.int.n = -abs_int; + if self.frac { + self.int.n -= 1; + self.frac = -self.frac; + } + } else { + self.int.n = abs_int; + } +} + +/// format and print to 2 decimal places +fn print(struct ifp16 self) { + cell int_abs; + cell neg; + to_u8(self.int, int_abs, neg); + + // convert from integer to decimal digits * 2^-8 + // (frac * 25) >> 6 + // (frac * 10 * 10) >> 8 + struct u16 w; + w.n0 = self.frac; + if neg { + // minus sign + output '-'; + w.n0 = -self.frac; + if self.frac { + // correct for sign stuff + int_abs -= 1; + } + } + // x100 + { + cell c = 100; + mult_n_d(w, c); + } + // { + // // round? + // cell c = 7; + // tshift_r(n.n0, 7); + // drain n.n0 into n.n1; + // } + + // integer part + print(int_abs); + // decimal point + output '.'; + + // cannibilised print function: + + { + cell[3] ds; + fn shift_stack() { + ds[2] = 0; + drain ds[1] into ds[2]; + drain ds[0] into ds[1]; + } + fn unshift_stack() { + ds[0] = 0; + drain ds[1] into ds[0]; + drain ds[2] into ds[1]; + } + + cell n = w.n1; + // essentially repeatedly divide by 10 + drain 3 { + shift_stack(); + cell dividing = true; + cell m; // quotient + ds[0] = 10; // remainder, add 1 for magic later on + while dividing { + drain 10 { + if n { + n -= 1; + } else { + ds[0] -= 1; + dividing = false; + } + } + + if dividing { + m += 1; + } + } + // copy new quotient to value so we start again + n = 0; drain m into n; + } + + drain '0' into ds[1] ds[2]; + output ds[1]; + output ds[2]; + } +} + +/// print a debug representation of an ifp16 +fn debug(struct ifp16 self) { + // 11111111.11111111 + debug(self.int.n); + output '.'; + debug(self.frac); +} + +// debug/test code: +// struct ifp16 h; +// _0(h); read(h); print(h); output " ("; debug(h); output ")\n\n"; +// _0(h); read(h); print(h); output " ("; debug(h); output ")\n\n"; +// _0(h); read(h); print(h); output " ("; debug(h); output ")\n\n"; +// _0(h); read(h); print(h); output " ("; debug(h); output ")\n\n"; +// _0(h); read(h); print(h); output " ("; debug(h); output ")\n\n"; +// _0(h); read(h); print(h); output " ("; debug(h); output ")\n\n"; + +// struct ifp16 t; _99p99609375(t); print(t); output " ("; debug(t); output ")\n"; +// struct ifp16 u; _16(u); print(u); output " ("; debug(u); output ")\n"; +// mult(t, u); +// print(t); + + + +// struct ifp16 a; +// struct ifp16 e; EPSILON(e); +// struct ifp16 _e; _EPSILON(_e); +// output "e = "; print(e); output " ("; debug(e); output ")\n"; +// _0p99609375(a); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// __128(a); print(a); output " ("; debug(a); output ")\n"; +// _64(a); print(a); output " ("; debug(a); output ")\n"; +// _32(a); print(a); output " ("; debug(a); output ")\n"; +// _16(a); print(a); output " ("; debug(a); output ")\n"; +// _8(a); print(a); output " ("; debug(a); output ")\n"; +// _4(a); print(a); output " ("; debug(a); output ")\n"; +// _2(a); print(a); output " ("; debug(a); output ")\n"; +// _1(a); print(a); output " ("; debug(a); output ")\n"; +// _0p5(a); print(a); output " ("; debug(a); output ")\n"; +// _0p25(a); print(a); output " ("; debug(a); output ")\n"; +// _0p125(a); print(a); output " ("; debug(a); output ")\n"; +// _0p0625(a); print(a); output " ("; debug(a); output ")\n"; +// _0p03125(a); print(a); output " ("; debug(a); output ")\n"; +// _0p015625(a); print(a); output " ("; debug(a); output ")\n"; +// _0p0078125(a); print(a); output " ("; debug(a); output ")\n"; +// _0p00390625(a); print(a); output " ("; debug(a); output ")\n"; +// _1p5(a); print(a); output " ("; debug(a); output ")\n"; +// __0p5(a); print(a); output " ("; debug(a); output ")\n"; +// __0p25(a); print(a); output " ("; debug(a); output ")\n"; +// __6p25(a); print(a); output " ("; debug(a); output ")\n"; +// _99p99609375(a); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// __99p99609375(a); print(a); output " ("; debug(a); output ")\n"; +// subtract(a, _e); print(a); output " ("; debug(a); output ")\n"; +// subtract(a, _e); print(a); output " ("; debug(a); output ")\n"; +// add(a, _e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; +// _127p12890625(a); print(a); output " ("; debug(a); output ")\n"; +// __127p12890625(a); print(a); output " ("; debug(a); output ")\n"; +// MAX(a); print(a); output " ("; debug(a); output ")\n"; +// add(a, e); print(a); output " ("; debug(a); output ")\n"; + diff --git a/programs/std/u16 b/programs/std/u16 new file mode 100644 index 0000000..06fa443 --- /dev/null +++ b/programs/std/u16 @@ -0,0 +1,395 @@ +// #include +#include + +struct u16 { + // big-endian? + cell n1; + cell n0; +} + +fn _0(struct u16 n) {n.n1 = 0; n.n0 = 0;} +fn _1(struct u16 n) {n.n1 = 0; n.n0 = 1;} +fn _10(struct u16 n) {n.n1 = 0; n.n0 = 10;} +fn _100(struct u16 n) {n.n1 = 0; n.n0 = 100;} +fn _200(struct u16 n) {n.n1 = 0; n.n0 = 200;} +fn _300(struct u16 n) {n.n1 = 1; n.n0 = 44;} +fn _1000(struct u16 n) {n.n1 = 3; n.n0 = 232;} +fn _2(struct u16 n) {n.n1 = 0; n.n0 = 2;} +fn _4(struct u16 n) {n.n1 = 0; n.n0 = 4;} +fn _8(struct u16 n) {n.n1 = 0; n.n0 = 8;} +fn _16(struct u16 n) {n.n1 = 0; n.n0 = 16;} +fn _32(struct u16 n) {n.n1 = 0; n.n0 = 32;} +fn _64(struct u16 n) {n.n1 = 0; n.n0 = 64;} +fn _128(struct u16 n) {n.n1 = 0; n.n0 = 128;} +fn _256(struct u16 n) {n.n1 = 1; n.n0 = 0;} +fn _512(struct u16 n) {n.n1 = 2; n.n0 = 0;} +fn _1024(struct u16 n) {n.n1 = 4; n.n0 = 0;} +fn _2048(struct u16 n) {n.n1 = 8; n.n0 = 0;} +fn _4096(struct u16 n) {n.n1 = 16; n.n0 = 0;} +fn _8192(struct u16 n) {n.n1 = 32; n.n0 = 0;} +fn _16384(struct u16 n) {n.n1 = 64; n.n0 = 0;} +fn _10000(struct u16 n) {n.n1 = 39; n.n0 = 16;} +fn _20000(struct u16 n) {n.n1 = 78; n.n0 = 32;} +fn _30000(struct u16 n) {n.n1 = 117; n.n0 = 48;} +fn _40000(struct u16 n) {n.n1 = 156; n.n0 = 64;} +fn _50000(struct u16 n) {n.n1 = 195; n.n0 = 80;} +fn _60000(struct u16 n) {n.n1 = 234; n.n0 = 96;} +fn _65280(struct u16 n) {n.n1 = 255; n.n0 = 0;} +fn _65535(struct u16 n) {n.n1 = 255; n.n0 = 255;} + +fn MAX(struct u16 n) {_65535(n);} + +/// set one u16 to be a copy of another +fn set(struct u16 dest, struct u16 src) { + dest.n0 = src.n0; + dest.n1 = src.n1; +} +/// set overload for u8 +fn set(struct u16 dest, cell src) { + dest.n0 = src; + dest.n1 = 0; +} +/// set destructively +fn set_d(struct u16 dest, struct u16 src) { + dest.n0 = 0; + drain src.n0 into dest.n0; + dest.n1 = 0; + drain src.n1 into dest.n1; +} + +/// increment a u16 by 1 +fn inc(struct u16 self) { + self.n0 += 1; + self.n1 += 1; + if self.n0 { + self.n1 -= 1; + } + // more efficient version of the following: + // if not self.n0 { + // self.n1 += 1; + // } +} + +/// decrement a u16 by 1 +fn dec(struct u16 self) { + self.n1 -= 1; + if self.n0 { + self.n1 += 1; + } + self.n0 -= 1; +} + +/// add a cell's value as a u8 in place to the current u16 +fn add(struct u16 self, cell other) { + copy other { + inc(self); + } +} +/// overload for an i8 (signed 8-bit) +fn add(struct u16 self, struct i8 other) { + cell abs; + cell neg; + to_u8(other, abs, neg); + + if not neg { + drain abs { + inc(self); + } + } else { + drain abs { + dec(self); + } + } + assert abs equals 0; +} +/// overload for another u16 +fn add(struct u16 self, struct u16 other) { + // most significant first + self.n1 += other.n1; + copy other.n0 { + self.n0 += 1; + self.n1 += 1; + if self.n0 { + self.n1 -= 1; + } + } +} + +/// subtract a cell as a u8 from the current u16 +fn sub(struct u16 self, cell other) { + copy other { + dec(self); + } +} +// subtract overload for another u16 +fn sub(struct u16 self, struct u16 other) { + self.n1 -= other.n1; + copy other.n0 { + self.n1 -= 1; + if self.n0 { + self.n1 += 1; + } + self.n0 -= 1; + } +} + +/// check if number is non-zero +fn ne_zero(struct u16 self, cell _output) { + _output = false; + // return n0 || n1 + if self.n0 { + _output = true; + } + if self.n1 { + _output = true; + } +} + +/// shift the numbers base-10 digits left, by multiplying by 10 +fn mult_10(struct u16 self) { + // naively multiply by 10 + // it is unclear if there is a better way with bitshifts + struct u16 n; + set(n, self); + drain 9 { + add(self, n); + } +} + +/// divide by 10 +fn div_10(struct u16 self) { + struct u16 quotient; + cell dividing = true; + while dividing { + drain 10 { + cell o; ne_zero(self, o); + if o { + dec(self); + } else { + // remainder += 1; + dividing = false; + } + } + if dividing { + inc(quotient); + } + } + set_d(self, quotient); +} + +/// lazy implementation of a divide by 2 +// TODO: make more efficient +fn div_2(struct u16 self) { + struct u16 quotient; + cell dividing = true; + while dividing { + drain 2 { + cell o; ne_zero(self, o); + if o { + dec(self); + } else { + // remainder += 1; + dividing = false; + } + } + if dividing { + inc(quotient); + } + } + set_d(self, quotient); +} + +/// multiply by 25, drains n +fn mult_n_d(struct u16 self, cell n) { + struct u16 c; + set(c, self); + n -= 1; + drain n { + add(self, c); + } +} + +/// print the binary +fn debug(struct u16 self) { + debug(self.n1); + debug(self.n0); +} + +/// print the u16 as a base-10 natural number [0, 65535] +fn print(struct u16 self) { + struct DS { + // markers either side of the digit stack + // cell m0 @0; + cell[5] stack; + // cell m1 @6; + } + struct DS ds; + fn shift_stack() { + ds.stack[4] = 0; + drain ds.stack[3] into ds.stack[4]; + drain ds.stack[2] into ds.stack[3]; + drain ds.stack[1] into ds.stack[2]; + drain ds.stack[0] into ds.stack[1]; + } + fn unshift_stack() { + ds.stack[0] = 0; + drain ds.stack[1] into ds.stack[0]; + drain ds.stack[2] into ds.stack[1]; + drain ds.stack[3] into ds.stack[2]; + drain ds.stack[4] into ds.stack[3]; + } + fn debug_stack() { + output ds.stack[0] + '0'; + output ds.stack[1] + '0'; + output ds.stack[2] + '0'; + output ds.stack[3] + '0'; + output ds.stack[4] + '0'; + } + + struct u16 n; + set(n, self); + // essentially repeatedly divide by 10 + drain 5 { + shift_stack(); + cell dividing = true; + struct u16 m;// MAX(m); // quotient set to -1 = 65535 + ds.stack[0] = 10; // remainder, add 1 for magic later on + while dividing { + drain 10 { + cell o; ne_zero(n, o); + if o { + dec(n); + } else { + ds.stack[0] -= 1; + dividing = false; + } + } + + if dividing { + inc(m); + } + } + // copy new quotient to value so we start again + set_d(n, m); + } + +// debug_stack(); output 10; + { + cell not_leading = false; + drain 4 { + if ds.stack[0] { + not_leading = true; + } + if not_leading { + ds.stack[0] += '0'; + output ds.stack[0]; + } + unshift_stack(); + } + } + ds.stack[0] += '0'; + output ds.stack[0]; +} + +/// read a u16 number from stdin, assumes n = 0 and valid newline-terminated input +fn read(struct u16 n) { + cell digit; + input digit; + digit -= '\n'; + while digit { + digit += '\n' - '0'; + // shift digits (times by 10) and add the new digit + mult_10(n); + add(n, digit); + + input digit; + digit -= '\n'; + } +} + +// debug/test code: +// struct u16 s; _20000(s); +// output "s = "; print(s); output " ("; debug(s); output ")\n"; +// div_10(s); output "s/10 = "; print(s); output " ("; debug(s); output ")\n"; +// div_10(s); output "s/10 = "; print(s); output " ("; debug(s); output ")\n"; +// div_10(s); output "s/10 = "; print(s); output " ("; debug(s); output ")\n"; +// div_10(s); output "s/10 = "; print(s); output " ("; debug(s); output ")\n"; +// div_10(s); output "s/10 = "; print(s); output " ("; debug(s); output ")\n"; + +// #include +// struct u16 j; +// struct u16 k; _256(k); +// read(j); +// print(j); output '\n'; + +// struct u16 a; +// output "a = "; print(a); output "\n"; +// MAX(a); +// output "(a = "; print(a); output ")\n"; +// cell c = 5; +// output "(c = 5)\n"; +// add(a, c); +// output "(a += c)\n"; +// output "a = "; print(a); output "\n"; +// struct u16 b; +// _100(a); +// output "(a = "; print(a); output ")\n"; +// _1000(b); +// output "(b = "; print(b); output ")\n"; +// add(a, b); +// output "(a += b)\n"; +// output "a = "; print(a); output '\n'; +// _16384(b); +// output "(b = "; print(b); output ")\n"; +// add(a, b); +// output "(a += b)\n"; +// output "a = "; print(a); output '\n'; +// _50000(b); +// output "(b = "; print(b); output ")\n"; +// add(a, b); +// output "(a += b)\n"; +// output "a = "; print(a); output '\n'; + +// struct u16 m; m.n0 = 1; +// struct u16 n; n.n0 = 0; +// print(m); output 10; +// sub(m, n); output "-"; print(n); output ": "; print(m); output "\n"; +// _1(n); +// sub(m, n); output "-"; print(n); output ": "; print(m); output "\n"; +// sub(m, n); output "-"; print(n); output ": "; print(m); output "\n"; +// add(m, n); output "+"; print(n); output ": "; print(m); output "\n"; +// _0(n); n.n1 = 1; +// sub(m, n); output "-"; print(n); output ": "; print(m); output "\n"; + +// output '\n'; +// struct u16 x; _10(x); +// cell y = 4; +// print(x); output " - "; print(y); output " = "; +// sub(x, y); +// print(x); output "\n"; +// y = 45; +// print(x); output " - "; print(y); output " = "; +// sub(x, y); +// print(x); output "\n"; +// print(x); output " + "; print(y); output " = "; +// add(x, y); +// print(x); output "\n"; +// _50000(x); +// y = 239; +// print(x); output " - "; print(y); output " = "; +// sub(x, y); +// print(x); output "\n"; +// struct u16 z; _40000(z); +// print(x); output " - "; print(z); output " = "; +// sub(x, z); +// print(x); output "\n"; +// _10000(z); +// print(x); output " - "; print(z); output " = "; +// sub(x, z); +// print(x); output "\n"; +// z.n0 = 0; z.n1 = 0; +// y = 239; +// sub(z, y); +// print(x); output " - "; print(z); output " = "; +// sub(x, z); +// print(x); output "\n"; diff --git a/programs/std/u8 b/programs/std/u8 new file mode 100644 index 0000000..20e7a68 --- /dev/null +++ b/programs/std/u8 @@ -0,0 +1,135 @@ +// 11111111 [0-255] + +// common cell operations + +/// function wrapper for += +fn add(cell self, cell other) { + self += other; +} + +/// function wrapper for -= +fn sub(cell self, cell other) { + self -= other; +} + +/// shift the numbers base-10 digits left, by multiplying by 10 +fn mult_10(cell self) { + cell n = self; + drain 9 { + self += n; + } +} + +/// read a u8 from stdin, assumes n = 0 and valid newline-terminated input +fn read(cell self) { + cell digit; + input digit; + digit -= '\n'; + while digit { + digit += '\n' - '0'; + // shift digits (times by 10) and add the new digit + mult_10(self); + self += digit; + + input digit; + digit -= '\n'; + } +} + +fn print(cell self) { + cell[3] ds; + fn shift_stack() { + ds[2] = 0; + drain ds[1] into ds[2]; + drain ds[0] into ds[1]; + } + fn unshift_stack() { + ds[0] = 0; + drain ds[1] into ds[0]; + drain ds[2] into ds[1]; + } + + cell n = self; + // essentially repeatedly divide by 10 + drain 3 { + shift_stack(); + cell dividing = true; + cell m; // quotient + ds[0] = 10; // remainder, add 1 for magic later on + while dividing { + drain 10 { + if n { + n -= 1; + } else { + ds[0] -= 1; + dividing = false; + } + } + + if dividing { + m += 1; + } + } + // copy new quotient to value so we start again + n = 0; drain m into n; + } + + { + cell not_leading = false; + drain 2 { + if ds[0] { + not_leading = true; + } + if not_leading { + ds[0] += '0'; + output ds[0]; + } + unshift_stack(); + } + } + ds[0] += '0'; + output ds[0]; +} + +/// shift left and truncate, return the truncated bit +fn get_most_sig_d(cell self, cell _bit) { + _bit = 1; + { + cell n = self; + drain 128 { + if not n { + _bit = 0; + } + n -= 1; + } + } + self += self; +} + +/// print the binary digits +fn debug(cell self) { + cell n = self; + drain 8 { + cell bit; + get_most_sig_d(n, bit); + bit += '0'; + output bit; + } +} + +// debug/test code +// cell a; read(a); +// cell b; read(b); +// print(a); output "\n"; +// print(b); output "\n"; +// add(a, b); +// print(a); output "\n"; +// add(a, b); +// print(a); output "\n"; +// cell c = 0; +// drain 2 { +// drain 128 { +// print(c); output " = 0b"; debug(c); output "\n"; +// c += 1; +// } +// } diff --git a/programs/stubs/f16 b/programs/stubs/f16 new file mode 100644 index 0000000..84f74ee --- /dev/null +++ b/programs/stubs/f16 @@ -0,0 +1,49 @@ +STUB CODE +(plans changed, originally were doing floats, this is as far as we got) + +struct f16 { + cell mantissa; + struct i8 exponent; + cell sign_neg; +} + +fn ZERO(struct f16 n) { + n.mantissa = 0; + n.exponent = 0; // does the exponent matter for 0.0? + n.sign_neg = false; +} + +fn ONE(struct f16 n) { + n.mantissa = 1; + n.exponent = ; + n.sign_neg = false; +} + +fn TWO(struct f16 n) { + n.mantissa = 2; + n.exponent = ; + n.sign_neg = false; +} + +fn FOUR(struct f16 n) { + n.mantissa = 1; + n.exponent = ; + n.sign_neg = false; +} + +fn cpy(struct f16 src, struct f16 _dest) { + _dest.mantissa = src.mantissa; + _dest.exponent = src.exponent; + _dest.sign = src.sign; +} + +fn add(struct f16 a, struct f16 b, struct f16 _c) { + // create copies + struct float16 a_; cpy(a, a_); + struct float16 b_; cpy(b, b_); + + // shift the +} + + + diff --git a/programs/stubs/fp32 b/programs/stubs/fp32 new file mode 100644 index 0000000..fbc2414 --- /dev/null +++ b/programs/stubs/fp32 @@ -0,0 +1,52 @@ +#include + +/// fixed-point 32bit unsigned number +// struct ufp32 { +// struct u16 most_sig; +// struct u16 least_sig; +// // most_sig.least_sig +// } +// 00000000 00000000.00000000 00000000 +// most_sig least_sig + +// max number that could be represented by this: +// = (2^32 - 1) * 2^-16 = 65535 + 65535 * 2^-16 +// = 65535 * (1 + 2^-16) +// = 65535.9999847 + +// struct ifp32 { +// struct i16 most_sig; +// struct u16 least_sig; +// } + +struct ifp32 { + cell sign_neg; + struct u16 most_sig; + struct u16 least_sig; +} + +/// add a u8 cell to the most significant (2^8) bits of this ufp32 +fn add_3(struct ifp32 self, cell other) { + +} + +/// add a u8 cell to the ones (2^0) digits of this ufp32 +fn add_2(struct ifp32 self, cell other) { + +} +fn add(struct ifp32 self, cell other) {add_2(self, other);} + +/// add a u8 cell to the 2^-8 digits (most-sig below the decimal point) +fn add_1(struct ifp32 self, cell other) { + +} + +/// add a u8 cell to the 2^-16 digits (most-sig below the decimal point) +fn add_0(struct ifp32 self, cell other) { + +} + +/// overload for adding two fixed-point numbers +fn add(struct ifp32 self, struct ifp32 other) { + +} diff --git a/runningMastermind.md b/runningMastermind.md new file mode 100644 index 0000000..0c24a89 --- /dev/null +++ b/runningMastermind.md @@ -0,0 +1,36 @@ +# Running Mastermind + +### 1. Install Rust +Install rust through the following website - https://www.rust-lang.org/tools/install + +This will also install Cargo which is needed to build the project + +### 2. Install Yarn +If not currently installed please install Yarn if unsure follow this guide - https://classic.yarnpkg.com/lang/en/docs/install + +### 3. Install wasm-pack +Install using Yarn, Cargo or the following guide https://rustwasm.github.io/wasm-pack/installer/ + +### 4. Run Yarn Install +Install the Javascript dendencies by running +```bash + yarn install +``` + +### 5. Build the grammar +Build the grammar using the following yarn command +```bash + yarn build:grammar +``` + +### 6. Build Web Assembly Pack +Build Web Assembly Pack using the following yarn command +```bash + yarn build:wasm +``` + +### 7. Run Dev Mode +Run Dev mode using the following command +```bash + yarn dev +``` \ No newline at end of file diff --git a/src/App.css b/src/App.css index f09e685..c02d1ad 100644 --- a/src/App.css +++ b/src/App.css @@ -21,6 +21,17 @@ overflow: hidden; } +.sidebar { + flex: 1; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; +} + + .code-panel { position: relative; background-color: var(--BG-2); @@ -96,4 +107,8 @@ .text-button:hover:not(.text-button-disabled) { background-color: rgb(0, 0, 0, 0.25); -} \ No newline at end of file +} + +.dropzone-root { + position: relative; +} diff --git a/src/App.tsx b/src/App.tsx index 0d3209e..533790e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,30 +17,56 @@ import { EditorState } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { v4 as uuidv4 } from "uuid"; -import divisorsExample from "./assets/divisors_example.mmi?raw"; -import printExample from "./assets/print.mmi?raw"; +// Example programs and standard library: (TODO: organise this better, this should not be in the main App.tsx) +import divisorsExample from "../programs/examples/divisors_example.mmi?raw"; +import primeExample from "../programs/examples/prime_1_to_100.mmi?raw"; +import christmasTreeExample from "../programs/examples/christmas_trees.mmi?raw"; +import brainfuckExample from "../programs/examples/brainfuck.mmi?raw"; +import helloWorldExample from "../programs/examples/hello_world.mmi?raw"; +import basicCalculatorExample from "../programs/examples/basic_calculator.mmi?raw"; +import ifp16CalculatorExample from "../programs/examples/ifp16calculator.mmi?raw"; + +import std_bitops from "../programs/std/bitops?raw"; +import std_i8 from "../programs/std/i8?raw"; +import std_u8 from "../programs/std/u8?raw"; +import std_u16 from "../programs/std/u16?raw"; +import std_ifp16 from "../programs/std/ifp16?raw"; import "./App.css"; import Divider from "./components/Divider"; import EditorPanel from "./panels/EditorPanel"; import InputPanel from "./panels/InputPanel"; +import BrainfuckPanel from "./panels/BrainfuckPanel.tsx"; +import SideBar from "./panels/SideBar.tsx"; import OutputPanel from "./panels/OutputPanel"; -import SettingsPanel, { MastermindConfig } from "./panels/SettingsPanel"; +import CompilerPanel from "./panels/CompilerPanel.tsx"; import { defaultExtensions } from "./misc"; import { makePersisted } from "@solid-primitives/storage"; import { createStore } from "solid-js/store"; +import { + DEFAULT_MASTERMIND_CONFIG, + MastermindConfig, +} from "./components/Settings"; const AppContext = createContext(); // update this when you want the user to see new syntax -const MIGRATION_VERSION = 3; +const MIGRATION_VERSION = 11; const App: Component = () => { const [version, setVersion] = makePersisted(createSignal(), { name: "mastermind_version", }); const [helpOpen, setHelpOpen] = createSignal(false); + const [settingsOpen, setSettingsOpen] = createSignal(false); + const [fileBrowserOpen, setFileBrowserOpen] = createSignal(false); + const [fileUploaderOpen, setFileUploaderOpen] = createSignal(false); + const [docsOpen, setDocsOpen] = createSignal(false); + const [config, setConfig] = makePersisted( + createSignal(DEFAULT_MASTERMIND_CONFIG), + { name: "mastermind_config" } + ); createEffect( on([version], () => { const v = version(); @@ -51,8 +77,12 @@ const App: Component = () => { ); } loadExampleFiles(); + setConfig((oldConfig) => ({ + ...DEFAULT_MASTERMIND_CONFIG, + ...oldConfig, + })); setVersion(MIGRATION_VERSION); - setHelpOpen(true); + setDocsOpen(true); } }) ); @@ -108,43 +138,117 @@ const App: Component = () => { ); const loadExampleFiles = () => { - const newId = uuidv4(); - setFileStates((prev) => [ - ...[ - { - id: newId, - label: "divisors_example.mmi", - rawText: divisorsExample, - }, - { id: uuidv4(), label: "print.mmi", rawText: printExample }, - ].map((rawState) => ({ - // This could probably be common function, duplicate code of above deserialization and file creation functions (TODO: refactor) - id: rawState.id, - label: rawState.label, - editorState: EditorState.create({ - doc: rawState.rawText, - extensions: [ - ...defaultExtensions, - EditorView.updateListener.of((e) => { - // this basically saves the editor every time it updates, this may be inefficient - saveFileState(rawState.id, e.state); - }), - ], - }), - })), - ...prev, - ]); - setEntryFile(newId); + const defaultFileId = uuidv4(); + const newFiles = [ + { + id: uuidv4(), + label: "hello_world.mmi", + rawText: helloWorldExample, + }, + { + id: uuidv4(), + label: "basic_calculator.mmi", + rawText: basicCalculatorExample, + }, + { + id: defaultFileId, + label: "divisors_example.mmi", + rawText: divisorsExample, + }, + { id: uuidv4(), label: "prime_1_to_100.mmi", rawText: primeExample }, + { + id: uuidv4(), + label: "christmas_trees.mmi", + rawText: christmasTreeExample, + }, + { + id: uuidv4(), + label: "brainfuck.mmi", + rawText: brainfuckExample, + }, + { + id: uuidv4(), + label: "bitops", + rawText: std_bitops, + }, + { + id: uuidv4(), + label: "i8", + rawText: std_i8, + }, + { + id: uuidv4(), + label: "u8", + rawText: std_u8, + }, + { + id: uuidv4(), + label: "u16", + rawText: std_u16, + }, + { + id: uuidv4(), + label: "ifp16", + rawText: std_ifp16, + }, + { + id: uuidv4(), + label: "Fixed Point Calculator", + rawText: ifp16CalculatorExample, + }, + ].map((rawState) => ({ + // This could probably be common function, duplicate code of above deserialization and file creation functions (TODO: refactor) + id: rawState.id, + label: rawState.label, + editorState: EditorState.create({ + doc: rawState.rawText, + extensions: [ + ...defaultExtensions, + EditorView.updateListener.of((e) => { + // this basically saves the editor every time it updates, this may be inefficient + saveFileState(rawState.id, e.state); + }), + ], + }), + })); + + setFileStates((prev) => { + const fileStates = [...prev]; + const filteredNewFiles = newFiles.filter( + (newFile) => + !fileStates.find( + (prevFile) => + newFile.label === prevFile.label && + newFile.editorState.doc.toString() === + prevFile.editorState.doc.toString() + ) + ); + return [ + ...filteredNewFiles, + ...prev.map((oldFile) => ({ + ...oldFile, + label: filteredNewFiles.find( + (newFile) => newFile.label == oldFile.label + ) + ? `old_${oldFile.label}` + : oldFile.label, + })), + ]; + }); + setEntryFile(defaultFileId); }; - const createFile = () => { + const createFile = async (file?: File) => { const newId = uuidv4(); + let rawText: string | undefined; + if (file) rawText = await file.text(); setFileStates((prev) => [ ...prev, { id: newId, - label: `untitled_${prev.length}`, + label: file?.name ?? `untitled_${prev.length}`, editorState: EditorState.create({ + doc: rawText, extensions: [ ...defaultExtensions, EditorView.updateListener.of((e) => { @@ -209,7 +313,7 @@ const App: Component = () => { setInput((prev) => ({ text: prev.text, amountRead: null })); }; - const compile = (entryFileId: string, optimisations: MastermindConfig) => { + const compile = (entryFileId: string, config: MastermindConfig) => { return new Promise((resolve, reject) => { let entryFileName: string | undefined; const fileMap = Object.fromEntries( @@ -233,7 +337,11 @@ const App: Component = () => { removeCallback(); setBusy(false); if (e.data.success) { - setOutput({ type: "BF", content: e.data.message }); + setBrainfuck({ text: e.data.message, amountRead: null }); + setOutput({ + type: "OUTPUT", + content: "Successfully Compiled Program", + }); setStatus("IDLE"); resolve(e.data.message); } else { @@ -255,7 +363,7 @@ const App: Component = () => { arguments: { fileMap, entryFileName, - optimisations, + config, }, }); @@ -264,7 +372,7 @@ const App: Component = () => { }); }; - const run = (code: string) => { + const run = (code: string, enable_2d_grid: boolean) => { return new Promise((resolve, reject) => { const transaction = uuidv4(); const callback = (e: { @@ -338,14 +446,14 @@ const App: Component = () => { compilerWorker.postMessage({ command: "RUN", transaction, - arguments: { code }, + arguments: { code, enable_2d_grid }, }); }); }; const [output, setOutput] = makePersisted( createSignal<{ - type: "BF" | "ERROR" | "OUTPUT" | "LIVE_OUTPUT"; + type: "ERROR" | "OUTPUT" | "LIVE_OUTPUT"; content: string; }>(), { name: "mastermind_output" } @@ -358,6 +466,13 @@ const App: Component = () => { }), { name: "mastermind_input" } ); + const [brainfuck, setBrainfuck] = makePersisted( + createSignal<{ text: string; amountRead: number | null }>({ + text: "Brainfuck goes here...", + amountRead: null, + }), + { name: "bvm_input" } + ); // to fix a bug for when the program starts and it saved the amount read in the state: onMount(() => setInput((prev) => ({ ...prev, amountRead: null }))); const popNextInputCharacter = (): string | undefined => { @@ -425,18 +540,36 @@ const App: Component = () => { setHelpOpen, enableBlockingInput, setEnableBlockingInput, + settingsOpen, + setSettingsOpen, + fileBrowserOpen, + setFileBrowserOpen, + fileUploaderOpen, + setFileUploaderOpen, + docsOpen, + setDocsOpen, + config, + setConfig, + brainfuck, + setBrainfuck, }} >
- + + + - +
+ +
+ +
); @@ -452,37 +585,37 @@ interface AppContextProps { fileStates: FileState[]; entryFile: Accessor; setEntryFile: Setter; - createFile: () => string; + createFile: (file?: File) => Promise; deleteFile: (id: string) => void; saveFileState: (id: string, state: EditorState) => void; setFileLabel: (id: string, label: string) => void; setOutput: Setter< | { - type: "BF" | "ERROR" | "OUTPUT" | "LIVE_OUTPUT"; + type: "ERROR" | "OUTPUT" | "LIVE_OUTPUT"; content: string; } | undefined >; output: Accessor< | { - type: "BF" | "ERROR" | "OUTPUT" | "LIVE_OUTPUT"; + type: "ERROR" | "OUTPUT" | "LIVE_OUTPUT"; content: string; } | undefined >; input: Accessor<{ text: string; amountRead: number | null }>; setInput: Setter<{ text: string; amountRead: number | null }>; + //Not Needed yet but we might want to do stopping and starting + brainfuck: Accessor<{ text: string; amountRead: number | null }>; + setBrainfuck: Setter<{ text: string; amountRead: number | null }>; enableBlockingInput: Accessor; setEnableBlockingInput: Setter; reorderFiles: (from: string, to: string | null) => void; - compile: ( - entryFileId: string, - optimisations: MastermindConfig - ) => Promise; - run: (code: string) => Promise; + compile: (entryFileId: string, settings: MastermindConfig) => Promise; + run: (code: string, enable_2d_grid: boolean) => Promise; busy: Accessor; status: Accessor<"COMPILING" | "RUNNING" | "INPUT_BLOCKED" | "IDLE">; @@ -491,6 +624,21 @@ interface AppContextProps { helpOpen: Accessor; setHelpOpen: Setter; + + settingsOpen: Accessor; + setSettingsOpen: Setter; + + fileBrowserOpen: Accessor; + setFileBrowserOpen: Setter; + + fileUploaderOpen: Accessor; + setFileUploaderOpen: Setter; + + docsOpen: Accessor; + setDocsOpen: Setter; + + setConfig: Setter; + config: Accessor; } interface FileState { diff --git a/src/assets/print.mmi b/src/assets/print.mmi deleted file mode 100644 index 1a473b7..0000000 --- a/src/assets/print.mmi +++ /dev/null @@ -1,61 +0,0 @@ -def print { - let digits[3] = [-1, -1, num]; - - // scoping "iterating" to the loop only for memory allocation efficiency - { - // find hundreds - let iterating = true; - while iterating { - digits[0] += 1; - drain (100) { - if not digits[2] { - iterating = false; - } - digits[2] -= 1; - } - if not iterating { - digits[2] += 100; - } - } - } - - { - // find tens - let iterating = true; - while iterating { - digits[1] += 1; - drain 10 { - if not digits[2] { - iterating = false; - } - digits[2] -= 1; - } - if not iterating { - digits[2] += 10; - } - } - } - - // ones digits are the remainder - - // this is a semi-efficient way of defining chars[3] = "000" - let chars[3]; - // equivalent to [>+>+>+<<<-] - drain '0' into *chars; - - // uncomment this and you will see what I mean - // output *chars; - - if digits[0] { - drain digits[0] into chars[0]; - output chars[0]; - drain digits[1] into chars[1]; - output chars[1]; - } - if digits[1] { - drain digits[1] into chars[1]; - output chars[1]; - } - drain digits[2] into chars[2]; - output chars[2]; -} diff --git a/src/components/Docs.tsx b/src/components/Docs.tsx new file mode 100644 index 0000000..e4f2f52 --- /dev/null +++ b/src/components/Docs.tsx @@ -0,0 +1,104 @@ +import { Portal } from "solid-js/web"; +import { SolidMarkdown } from "solid-markdown"; +import remarkGfm from "remark-gfm"; +import { IoClose } from "solid-icons/io"; +import { Component, createEffect, createSignal, JSX, Show } from "solid-js"; +import { useAppContext } from "../App"; + +import intro from "../../docs/intro.md?raw"; +import brainfuck from "../../docs/brainfuck.md?raw"; +import variables from "../../docs/variables.md?raw"; +import conditionals from "../../docs/conditionals.md?raw"; +import loops from "../../docs/loops.md?raw"; +import functions from "../../docs/functions.md?raw"; +import inlinebrainfuck from "../../docs/inlinebrainfuck.md?raw"; +import standardlib from "../../docs/standardlib.md?raw"; +import twodimensional from "../../docs/twodimensional.md?raw"; +import optimisations from "../../docs/optimisations.md?raw"; + +import { FaSolidArrowLeftLong, FaSolidArrowRightLong } from "solid-icons/fa"; +const DocsModal: Component<{ style?: JSX.CSSProperties }> = () => { + const app = useAppContext()!; + const docs = { + Introduction: intro, + Brainfuck: brainfuck, + Variables: variables, + Conditionals: conditionals, + Loops: loops, + Functions: functions, + "Inline Brainfuck": inlinebrainfuck, + "Standard Library": standardlib, + "2D Mastermind": twodimensional, + Optimisations: optimisations, + }; + const titles = Object.keys(docs); + const [selected, setSelected] = createSignal(titles[0]); + const [docsContent, setDocsContent] = createSignal( + docs[selected() as keyof typeof docs] ?? "" + ); + createEffect(() => { + setDocsContent(docs[selected() as keyof typeof docs] ?? ""); + }); + + function nextDoc() { + setSelected(titles[(titles.indexOf(selected() ?? "") + 1) % titles.length]); + } + function prevDoc() { + setSelected( + titles[ + (titles.indexOf(selected() ?? "") - 1 + titles.length) % titles.length + ] + ); + } + + return ( + + {/* The weirdest solid js feature, puts the component into the top level html body */} + +
app.setDocsOpen(false)} + > +
e.stopPropagation()}> + + + +
+ + {docsContent()} + +
+ app.setDocsOpen(false)} + /> +
+
+
+
+ ); +}; + +export default DocsModal; diff --git a/src/components/FileBrowser.tsx b/src/components/FileBrowser.tsx new file mode 100644 index 0000000..ce533f8 --- /dev/null +++ b/src/components/FileBrowser.tsx @@ -0,0 +1,97 @@ +import { + Accessor, + createEffect, + createSignal, + For, + on, + Setter, + Show, +} from "solid-js"; +import { useAppContext } from "../App"; +import { Portal } from "solid-js/web"; +// import JSZip from "jszip"; +// import downloadBlob from "../utils/downloadBlob"; + +type FileBrowserProps = { + editingFile: Accessor; + setEditingFile: Setter; +}; + +export default function FileBrowserModal({ + editingFile, + setEditingFile, +}: FileBrowserProps) { + const app = useAppContext(); + + const [current, setCurrent] = createSignal(); + + if (!app) { + return <>; + } + + createEffect( + on([editingFile], () => { + if (!editingFile()) return; + setCurrent(editingFile()); + }) + ); + + const switchToFile = (fileId: string) => { + const firstId = app.fileStates[0]?.id; + if (!firstId) return; + + // Grabbing the ID of the first file in the file state list + // and moving the selected file to the first slot, then opening it + app.reorderFiles(fileId, firstId); + setEditingFile(fileId); + }; + + return ( + + +
app.setFileBrowserOpen(false)} + > +
+

FILES - {app.fileStates.length}

+
+ + {(state) => ( + + )} + +
+
+
+
+
+ ); +} + +function FileTile({ + file, + switchToFile, + current, +}: { + file: any; + switchToFile: (fileId: string) => void; + current: boolean; +}) { + const test = () => { + switchToFile(file.id as string); + }; + + return ( +
test()} + > + {file.label} +
+ ); +} diff --git a/src/components/FileUploader.tsx b/src/components/FileUploader.tsx new file mode 100644 index 0000000..b109cff --- /dev/null +++ b/src/components/FileUploader.tsx @@ -0,0 +1,63 @@ +import { Setter, Show } from "solid-js"; +import { useAppContext } from "../App"; +import { Portal } from "solid-js/web"; +import { AiOutlineUpload } from "solid-icons/ai"; +import { createDropzone } from "@soorria/solid-dropzone"; + +type FileUploaderProps = { + // editingFile: Accessor; + setEditingFile: Setter; +}; + +export default function FileUploaderModal({ + setEditingFile, +}: FileUploaderProps) { + const app = useAppContext(); + if (!app) return; + + const switchToFile = (fileId: string) => { + const firstId = app.fileStates[0]?.id; + if (!firstId) return; + + // Grabbing the ID of the first file in the file state list + // and moving the selected file to the first slot, then opening it + app.reorderFiles(fileId, firstId); + setEditingFile(fileId); + }; + + const onDrop = (acceptedFiles: File[]) => { + acceptedFiles.forEach(async (file: File) => { + const newId = await app.createFile(file); + switchToFile(newId); + app.setFileUploaderOpen(false); + }); + }; + + const dropzone = createDropzone({ onDrop }); + + return ( + + +
app.setFileUploaderOpen(false)} + > +
+
{ + e.stopPropagation(); + }} + > +
+ + + Click or drop files here to upload + +
+
+
+
+
+
+ ); +} diff --git a/src/components/Help.tsx b/src/components/Help.tsx new file mode 100644 index 0000000..e11361d --- /dev/null +++ b/src/components/Help.tsx @@ -0,0 +1,40 @@ +import {Portal} from "solid-js/web"; +import {SolidMarkdown} from "solid-markdown"; +import remarkGfm from "remark-gfm"; +import {IoClose} from "solid-icons/io"; +import {Component, JSX, Show} from "solid-js"; +import { useAppContext } from "../App"; +import readmeContent from "../../README.md?raw"; + +const HelpModal: Component<{ style?: JSX.CSSProperties }> = () => { + const app = useAppContext()!; + return ( + {/* The weirdest solid js feature, puts the component into the top level html body */} + +
app.setHelpOpen(false)} + > +
e.stopPropagation()}> +
+ + {readmeContent} + +
+ app.setHelpOpen(false)} + /> +
+
+
+
)} + +export default HelpModal; \ No newline at end of file diff --git a/src/components/PanelHeader.tsx b/src/components/PanelHeader.tsx new file mode 100644 index 0000000..c49c46c --- /dev/null +++ b/src/components/PanelHeader.tsx @@ -0,0 +1,29 @@ +import "./components.css"; +import {FiCopy} from "solid-icons/fi"; +import {createMemo} from "solid-js"; + +function PanelHeader({title, getContent} : {title: string, getContent: () => string}) { + const contentLength = createMemo(() => getContent().length); + + const copyToClipboard = () => { + const textToCopy = getContent(); + if (!textToCopy) return; + + window.navigator.clipboard + .writeText(textToCopy) + .then(() => window.alert("Output copied to clipboard!")); + }; + + + return
+

{title || "Unnamed Panel"}

+
+ +

({contentLength()} bytes)

+
+
; +} + +export default PanelHeader; diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx new file mode 100644 index 0000000..ae82d59 --- /dev/null +++ b/src/components/Settings.tsx @@ -0,0 +1,191 @@ +import { Portal } from "solid-js/web"; +import { IoClose } from "solid-icons/io"; +import { Component, JSX, Show, For } from "solid-js"; +import { useAppContext } from "../App"; + +// TODO: FIX THIS SO WE DON'T HAVE 2 PERSISTED VALUES ONLY ONE +const SettingsModal: Component<{ style?: JSX.CSSProperties }> = () => { + const MemoryAllocationOptions: string[] = [ + "1D Mastermind", + "2D Mastermind - Zig Zag", + "2D Mastermind - Spiral", + "2D Mastermind - Tiles", + //NOT IMPLEMENTED + // "2D Mastermind - Nearest", + ]; + + const tickboxKeys: (keyof OptimisationSettings)[] = [ + "optimise_cell_clearing", + "optimise_constants", + "optimise_empty_blocks", + "optimise_generated_code", + "optimise_generated_all_permutations", + "optimise_memory_allocation", + "optimise_unreachable_loops", + "optimise_variable_usage", + ]; + + const app = useAppContext()!; + return ( + + {/* The weirdest solid js feature, puts the component into the top level html body */} + +
app.setSettingsOpen(false)} + > +
e.stopPropagation()}> +

SETTINGS

+
+ Optimisations: + + app.setConfig((prev) => { + const b = tickboxKeys.some((key) => !prev[key]); + return { + ...prev, + ...Object.fromEntries( + tickboxKeys.map((key) => [key, b]) + ) + } as MastermindConfig; + }) + } + > + (toggle all) + + +
{ + const target = e.target as HTMLInputElement; + app.setConfig((prev) => ({ + ...prev, + [target.name]: !!target.checked, + })); + }} + > + tickboxKeys.includes(key as keyof OptimisationSettings) + )}> + {([key, enabled]: [string, boolean]) => ( + + )} + +
+ +
+ 2D GENERATION: +
+
+ + + +
+
+ app.setSettingsOpen(false)} + /> +
+
+
+
+ ); +}; + +export default SettingsModal; + +interface OptimisationSettings { + optimise_cell_clearing: boolean; + optimise_constants: boolean; + optimise_empty_blocks: boolean; + optimise_generated_code: boolean; + optimise_generated_all_permutations: boolean; + optimise_memory_allocation: boolean; + optimise_unreachable_loops: boolean; + optimise_variable_usage: boolean; +} + +interface TwoDimensionalSettings { + enable_2d_grid: boolean; + memory_allocation_method: number; +} + +export interface MastermindConfig + extends OptimisationSettings, + TwoDimensionalSettings {} + +const optimisationLabels: Record = { + optimise_cell_clearing: "cell clearing", + optimise_constants: "constants", + optimise_empty_blocks: "empty blocks", + optimise_generated_code: "generated code", + optimise_generated_all_permutations: "generated code permutations (May slow larger projects)", + optimise_memory_allocation: "memory allocations", + optimise_unreachable_loops: "unreachable loops", + optimise_variable_usage: "variable usage", +}; + +export const DEFAULT_MASTERMIND_CONFIG = { + optimise_cell_clearing: false, + optimise_constants: false, + optimise_empty_blocks: false, + optimise_generated_code: false, + optimise_generated_all_permutations: false, + optimise_memory_allocation: false, + optimise_unreachable_loops: false, + optimise_variable_usage: false, + memory_allocation_method: 0, + enable_2d_grid: false, + }; diff --git a/src/components/Tab.tsx b/src/components/Tab.tsx index fd31377..fc608d5 100644 --- a/src/components/Tab.tsx +++ b/src/components/Tab.tsx @@ -1,6 +1,10 @@ import { Component, Show, createSignal } from "solid-js"; -import { AiOutlineDelete, AiOutlineEdit } from "solid-icons/ai"; +import { + AiOutlineDelete, + AiOutlineDownload, + AiOutlineEdit, +} from "solid-icons/ai"; import { createDraggable, createDroppable, @@ -9,6 +13,7 @@ import { import { useAppContext } from "../App"; import "../panels/editor.css"; +import downloadBlob from "../utils/downloadBlob"; const Tab: Component<{ fileId: string; @@ -37,6 +42,14 @@ const Tab: Component<{ }); }; + const fileDownload = () => { + const fileState = app.fileStates.find((f) => f.id == props.fileId); + const fileData = fileState?.editorState.doc.toString(); + if (!fileData) return new Blob([]); + const blobFile = new Blob([fileData], { type: "text/plain" }); + downloadBlob(blobFile, props.fileLabel); + }; + return (
{ @@ -79,6 +92,13 @@ const Tab: Component<{ /> + 1 && props.fileActive}> + fileDownload()} + /> + 1}> * { + flex: 0 0 200px; + text-align: center; + min-width: 200px; + max-width: 200px; + min-height: 30px; + max-height: 100px; + overflow: auto; +} + +.file-tile { + padding: 5px; + gap: 5px; + display: flex; + align-items: center; + border: var(--default-border); + background-color: var(--BG-2); + user-select: none; + cursor: pointer; +} + +.current-file { + background-color: var(--BG-3); +} + +.file-tile:hover { + background-color: var(--BG-1); +} + +.file-tile-utility { + min-width: 50px; + display: flex; + justify-content: center; +} + +.file-upload-drop-zone { + margin-top: 10px; + border: var(--default-border); + width: 100%; + min-width: 320px; + min-height: 100px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + user-select: none; + cursor: pointer; +} diff --git a/src/index.css b/src/index.css index ebe385b..428eadc 100644 --- a/src/index.css +++ b/src/index.css @@ -5,6 +5,7 @@ --BG-2: #212637; --BG-3: #0f0f0f; --BG-4: #171717; + --BG-5: #3d3d3d; /* --BG-5: # */ --BORDER-0: #284357; diff --git a/src/lexer/mastermind.grammar b/src/lexer/mastermind.grammar index 3eb6dd4..e3f8b5f 100644 --- a/src/lexer/mastermind.grammar +++ b/src/lexer/mastermind.grammar @@ -43,6 +43,8 @@ BfMinus { "-" } BfRight { ">" } BfLeft { "<" } + BfUp { "^" } + BfDown { "v" } BfOpenLoop { "[" } BfCloseLoop { "]" } BfOutput { "." } @@ -53,21 +55,39 @@ kw { @specialize } -commaSep { - "" | content (Comma content?)* +commaSepList { + "" | content (Comma content)* } Boolean { kw<"true" | "false"> } -VariableDefinition { - Name SquareBrackets? +Struct { kw<"struct"> } + +VariableType { + ( + Cell { kw<"cell"> } | + (Struct Name) + ) SquareBrackets? } + +VariableDefinition { VariableType Name } + VariableTarget { - Name SquareBrackets? | - Asterisk Name + Asterisk? + Name + VariableSubfieldTarget { + (Dot { "." } Name) | + SquareBrackets + }* } -LocationSpecifier { At Number } +LocationSpecifier { + At + ( + (Number | (Number Comma Number)) | + VariableTarget + ) +} Constant { Number | Boolean | Character @@ -77,7 +97,7 @@ Expression { VariableTarget | Constant | String | - Array { SquareBrackets> } | + Array { SquareBrackets> } | Parentheses | (Expression AddOp !summation Expression) } @@ -89,6 +109,8 @@ EBrainfuck { BfMinus | BfRight | BfLeft | + BfUp | + BfDown | BfOpenLoop | BfCloseLoop | BfOutput | @@ -114,11 +136,11 @@ SquareBrackets { "]" } -AngledBrackets { - "<" - contents - ">" -} +// AngledBrackets { +// "<" +// contents +// ">" +// } Braces { "{" @@ -129,16 +151,22 @@ Braces { Clause { Empty { "" Semicolon} | - DefClause { - Def { kw<"def"> } Name AngledBrackets> Block + StructClause { + Struct Name Braces<( + VariableDefinition LocationSpecifier? Semicolon + )*> + } | + + FnClause { + Fn { kw<"fn"> } Name Parentheses> Block } | CallClause { - Name AngledBrackets> Semicolon + Name Parentheses> Semicolon } | LetClause { - Let { kw<"let"> } VariableDefinition LocationSpecifier? (EqualOp Expression)? Semicolon + VariableDefinition LocationSpecifier? (EqualOp Expression)? Semicolon } | SetClause { @@ -173,7 +201,9 @@ Clause { } | AssertClause { Assert { kw<"assert"> } VariableTarget ((Equals { kw<"equals"> } Constant) | Unknown { kw<"unknown"> }) Semicolon - } + } | + + Braces } PreprocessorDirective { diff --git a/src/misc.ts b/src/misc.ts index aa2cd5c..41d85af 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -69,56 +69,71 @@ export const defaultExtensions = [ // TODO: work on the indent support function mastermindLanguageSupport() { - return new LanguageSupport(LRLanguage.define({ - parser: parser.configure({ - props: [styleTags({ - "DefClause/Def": tags.function(tags.definitionKeyword), - "DefClause/Name": tags.function(tags.definition(tags.variableName)), - "CallClause/Name": tags.function(tags.variableName), - "LetClause/Let": tags.definitionKeyword, - "LocationSpecifier/At": tags.annotation, - "VariableDefinition/Name": tags.variableName, + return new LanguageSupport( + LRLanguage.define({ + parser: parser.configure({ + props: [ + styleTags({ + "FnClause/Fn": tags.function(tags.definitionKeyword), + "FnClause/Name": tags.function(tags.definition(tags.variableName)), + "CallClause/Name": tags.function(tags.variableName), + "LocationSpecifier/At": tags.annotation, + "VariableType/Cell VariableType/Struct": tags.definitionKeyword, + "VariableType/Name": tags.className, + "VariableDefinition/Name": tags.variableName, - "OutputClause/Output": tags.controlKeyword, - "InputClause/Input": tags.controlKeyword, - "DrainCopyClause/DrainCopy DrainCopyClause/Into": tags.controlKeyword, - "WhileClause/While": tags.controlKeyword, - "IfElseClause/If IfElseClause/Not IfElseClause/Else": tags.controlKeyword, + "StructClause/Struct": tags.definitionKeyword, + "StructClause/Name": tags.className, - Comment: tags.lineComment, - Include: tags.moduleKeyword, - IncludePath: tags.string, + "OutputClause/Output": tags.controlKeyword, + "InputClause/Input": tags.controlKeyword, + "DrainCopyClause/DrainCopy DrainCopyClause/Into": + tags.controlKeyword, + "WhileClause/While": tags.controlKeyword, + "IfElseClause/If IfElseClause/Not IfElseClause/Else": + tags.controlKeyword, - Boolean: tags.bool, - Number: tags.integer, - Character: tags.character, - String: tags.string, - "VariableTarget/Name": tags.variableName, + Comment: tags.lineComment, + Include: tags.moduleKeyword, + IncludePath: tags.string, - SquareBrackets: tags.squareBracket, - Parentheses: tags.paren, - Braces: tags.brace, + Boolean: tags.bool, + Number: tags.integer, + Character: tags.character, + String: tags.string, + "VariableTarget/Name": tags.variableName, + "VariableTarget/VariableSubfieldTarget/Name": tags.propertyName, + "VariableTarget/VariableSubfieldTarget/Dot": tags.separator, + "VariableTarget/VariableSubfieldTarget/SquareBrackets": + tags.squareBracket, - EqualOp: tags.updateOperator, - AddEqualOp: tags.updateOperator, - AddOp: tags.arithmeticOperator, - IncDecOp: tags.updateOperator, - "Semicolon Comma": tags.separator, - "Asterisk": tags.derefOperator, + SquareBrackets: tags.squareBracket, + Parentheses: tags.paren, + Braces: tags.brace, - "BrainfuckClause/Bf": tags.controlKeyword, - "BrainfuckClause/Clobbers": tags.controlKeyword, - "AssertClause/Assert AssertClause/Equals AssertClause/Unknown": tags.controlKeyword, + EqualOp: tags.updateOperator, + AddEqualOp: tags.updateOperator, + AddOp: tags.arithmeticOperator, + IncDecOp: tags.updateOperator, + "Semicolon Comma": tags.separator, + Asterisk: tags.derefOperator, - "EBrainfuck/BfPlus EBrainfuck/BfMinus": tags.arithmeticOperator, - "EBrainfuck/BfRight EBrainfuck/BfLeft": tags.angleBracket, - "EBrainfuck/BfOpenLoop EBrainfuck/BfCloseLoop": tags.squareBracket, - "EBrainfuck/BfOutput EBrainfuck/BfInput": tags.controlKeyword, - // "EBrainfuck/BfComment": tags.comment, - })] - }), - languageData: { - commentTokens: { line: "//" }, - } - })); -} + "BrainfuckClause/Bf": tags.controlKeyword, + "BrainfuckClause/Clobbers": tags.controlKeyword, + "AssertClause/Assert AssertClause/Equals AssertClause/Unknown": + tags.controlKeyword, + + "EBrainfuck/BfPlus EBrainfuck/BfMinus": tags.arithmeticOperator, + "EBrainfuck/BfRight EBrainfuck/BfLeft": tags.angleBracket, + "EBrainfuck/BfOpenLoop EBrainfuck/BfCloseLoop": tags.squareBracket, + "EBrainfuck/BfOutput EBrainfuck/BfInput": tags.controlKeyword, + // "EBrainfuck/BfComment": tags.comment, + }), + ], + }), + languageData: { + commentTokens: { line: "//" }, + }, + }) + ); +} diff --git a/src/panels/BrainfuckPanel.tsx b/src/panels/BrainfuckPanel.tsx new file mode 100644 index 0000000..1b17437 --- /dev/null +++ b/src/panels/BrainfuckPanel.tsx @@ -0,0 +1,87 @@ +import { EditorState, EditorSelection } from "@codemirror/state"; +import { EditorView, layer, RectangleMarker, keymap } from "@codemirror/view"; +import { + drawSelection, +} from "@codemirror/view"; +import { + defaultKeymap, +} from "@codemirror/commands"; + + +import { Component, JSX, createEffect, on } from "solid-js"; + +import './input.css'; +import { useAppContext } from "../App"; +import PanelHeader from "../components/PanelHeader.tsx"; +import Divider from "../components/Divider.tsx"; +// import { defaultExtensions } from "../misc"; + +const BrainfuckPanel: Component<{ style?: JSX.CSSProperties }> = (props) => { + const app = useAppContext()!; + // when the compiler is idle, allow the user to edit freely + // when the compiler is running code, the user can only append + const getBrainfuckText = () => app.brainfuck().text || ""; + + let editorView: EditorView | undefined; + + createEffect(on([() => !!editorView, app.output], () => { + if (!editorView) return; + // update the editorView when the input changes so that the layers re-render + editorView.dispatch({ + changes: { + from: 0, + to: editorView.state.doc.length, + insert: app.brainfuck().text || '', + }, + }); + })); + + return
+ + +
{ + editorView = new EditorView({ + parent: e, + state: EditorState.create({ + doc: app.brainfuck().text, + extensions: [ + EditorView.lineWrapping, + drawSelection(), + keymap.of(defaultKeymap), + EditorView.updateListener.of((update) => { + const {amountRead} = app.brainfuck(); + if (!update.docChanged) return; + + if (amountRead && update.changes.touchesRange(0, amountRead - 1)) { + // revert the change if the update affects the readonly portion of the input + editorView?.setState(update.startState); + } else { + const newText = update.state.doc.toString(); + // Update both the app state and local state + app.setBrainfuck((prev) => ({...prev, text: newText})); + } + }), + layer({ + above: true, class: "input-marker-layer", markers(view) { + const {text, amountRead} = app.brainfuck(); + + const markers = []; + if (amountRead) markers.push(...RectangleMarker.forRange(view, 'input-readonly-marker', EditorSelection.single(0, amountRead).main)); + + if (amountRead !== null) markers.push(...RectangleMarker.forRange(view, (amountRead >= text.length ? 'input-cursor-waiting-marker' : 'input-cursor-marker'), EditorSelection.single(amountRead).main)); + + return markers; + }, update(_update, _layer) { + // always update if something changes or if the above createEffect does an empty dispatch + // this triggers the above markers() function + return true; + }, + }) + ] + }) + }); + }}/> +
; +}; + +export default BrainfuckPanel; diff --git a/src/panels/CompilerPanel.tsx b/src/panels/CompilerPanel.tsx new file mode 100644 index 0000000..9c4341d --- /dev/null +++ b/src/panels/CompilerPanel.tsx @@ -0,0 +1,147 @@ +import { Component, For, createEffect, JSX } from "solid-js"; +import { useAppContext } from "../App"; +import { AiOutlineStop } from "solid-icons/ai"; +import { FaSolidPlay } from "solid-icons/fa"; +// import { FiSave } from "solid-icons/fi"; +// import downloadBlob from "../utils/downloadBlob"; +// import JSZip from "jszip"; + +import "./settings.css"; + +const CompilerPanel: Component<{ style?: JSX.CSSProperties }> = (props) => { + const app = useAppContext()!; + + createEffect(() => { + const fileStates = app.fileStates; + const entryFile = app.entryFile(); + if (app.fileStates.length && !entryFile) { + app.setEntryFile(fileStates[0]?.id); + } + }); + + const onRun = async () => { + // TODO: error handling here? is it needed? + const code = app.brainfuck(); + if (!code.text) return; + app.setOutput({ type: "OUTPUT", content: "" }); + await app.run(code.text, app.config().enable_2d_grid); + }; + + const onCompile = async () => { + const entryFileId = app.entryFile(); + if (!entryFileId) return; + + await app.compile(entryFileId, app.config()); + }; + + createEffect(() => { + console.log(app.fileStates); + }); + + return ( +
+
+
+ {/* entry file selection */} + +
+
+
+
+ Compile File +
+
+
+
+ {app.busy() ? ( +
{ + app.restartWorker(); + }} + title="kill brainfuck process" + > +
+ +
+ Stop Code +
+ ) : ( +
+
+ +
+ Run Code +
+ )} +
+
+ {/* misc options and markers */} +
app.setEnableBlockingInput((prev) => !prev)} + > + Blocking Input [ + {app.enableBlockingInput() ? ( +
enabled
+ ) : ( +
disabled
+ )} + ] +
+ {/* await zipAndSave()}*/} + {/*>*/} + {/* */} + {/* Zip All & Save*/} + {/*
*/} +
+
+
+ ); +}; + +export default CompilerPanel; diff --git a/src/panels/EditorPanel.tsx b/src/panels/EditorPanel.tsx index 2a53080..4000920 100644 --- a/src/panels/EditorPanel.tsx +++ b/src/panels/EditorPanel.tsx @@ -3,22 +3,55 @@ import { Component, createSignal, For, createEffect, on } from "solid-js"; import "./editor.css"; import { EditorView } from "@codemirror/view"; -import { AiOutlinePlus } from "solid-icons/ai"; +import { + AiOutlineFolder, + AiOutlinePlusCircle, + AiOutlineSave, + AiOutlineUpload, +} from "solid-icons/ai"; import { DragDropProvider, DragDropSensors, - createDroppable, - useDragDropContext, + // createDroppable, + // useDragDropContext, } from "@thisbeyond/solid-dnd"; import { useAppContext } from "../App"; import Tab from "../components/Tab"; +// import { IconTypes } from "solid-icons"; +import FileBrowserModal from "../components/FileBrowser"; +import JSZip from "jszip"; +import downloadBlob from "../utils/downloadBlob"; +import FileUploaderModal from "../components/FileUploader"; const EditorPanel: Component = () => { const app = useAppContext()!; const [editingFile, setEditingFile] = createSignal(); + const switchToFile = (fileId: string) => { + const firstId = app.fileStates[0]?.id; + if (!firstId) return; + + // Grabbing the ID of the first file in the file state list + // and moving the selected file to the first slot, then opening it + app.reorderFiles(fileId, firstId); + setEditingFile(fileId); + }; + + const zipAndSave = async () => { + const zip = new JSZip(); + app.fileStates.forEach((fileState) => { + const blob = new Blob([fileState.editorState.doc.toString()], { + type: "text/plain", + }); + zip.file(fileState.label, blob); + }); + await zip.generateAsync({ type: "blob" }).then((x) => { + downloadBlob(x); + }); + }; + createEffect( on([() => app.fileStates, editingFile], () => { // default behaviours for when files are deleted @@ -65,37 +98,72 @@ const EditorPanel: Component = () => { return (
-
- {/* tab rearranging logic for filestates in global file array */} - - droppable && - app.reorderFiles( - draggable.id as string, - droppable.id === TAB_END_ID ? null : (droppable.id as string) - ) - } - > - - - {(fileState) => ( - setEditingFile(fileState.id)} - /> - )} - - { - const newId = app.createFile(); - setEditingFile(newId); - // setEditingLabel(newFile.id); - }} - /> - - +
+
+ {/* tab rearranging logic for filestates in global file array */} + + droppable && + app.reorderFiles( + draggable.id as string, + droppable.id === TAB_END_ID ? null : (droppable.id as string) + ) + } + > + + + {(fileState) => ( + setEditingFile(fileState.id)} + /> + )} + + + +
+
+ { + const newId = await app.createFile(); + setEditingFile(newId); + switchToFile(newId); + }} + /> + { + app.setFileUploaderOpen(true); + }} + /> + +
+ { + app.setFileBrowserOpen(true); + }} + /> + { + zipAndSave(); + }} + /> + + + +
@@ -105,22 +173,27 @@ const EditorPanel: Component = () => { export default EditorPanel; const TAB_END_ID = "end"; -const TabFiller: Component<{ onAdd: () => void }> = (props) => { - // for dragging a file to the end of the list - // had to make this its own component because of dragDrop context issues - const droppableRef = createDroppable(TAB_END_ID); - const [isUnderDrag, setIsUnderDrag] = createSignal(false); - const [, { onDragOver, onDragEnd }] = useDragDropContext()!; - - onDragOver(({ droppable }) => setIsUnderDrag(droppable?.id === TAB_END_ID)); - onDragEnd(() => setIsUnderDrag(false)); - - return ( -
- -
- ); -}; +// const TabFiller: Component<{ +// onClick: () => void; +// iconComponent?: IconTypes; +// }> = (props) => { +// // for dragging a file to the end of the list +// // had to make this its own component because of dragDrop context issues +// const droppableRef = createDroppable(TAB_END_ID); +// const [isUnderDrag, setIsUnderDrag] = createSignal(false); +// const [, { onDragOver, onDragEnd }] = useDragDropContext()!; +// +// onDragOver(({ droppable }) => setIsUnderDrag(droppable?.id === TAB_END_ID)); +// onDragEnd(() => setIsUnderDrag(false)); +// +// const IconComponent = props.iconComponent ?? AiOutlinePlus; +// +// return ( +//
+// +//
+// ); +// }; diff --git a/src/panels/InputPanel.tsx b/src/panels/InputPanel.tsx index 1411ca3..fcd439c 100644 --- a/src/panels/InputPanel.tsx +++ b/src/panels/InputPanel.tsx @@ -12,12 +12,15 @@ import { Component, JSX, createEffect, on } from "solid-js"; import './input.css'; import { useAppContext } from "../App"; +import PanelHeader from "../components/PanelHeader.tsx"; +import Divider from "../components/Divider.tsx"; // import { defaultExtensions } from "../misc"; const InputPanel: Component<{ style?: JSX.CSSProperties }> = (props) => { const app = useAppContext()!; // when the compiler is idle, allow the user to edit freely // when the compiler is running code, the user can only append + const getInputText = () => app.input().text || ""; let editorView: EditorView | undefined; @@ -28,45 +31,52 @@ const InputPanel: Component<{ style?: JSX.CSSProperties }> = (props) => { editorView.dispatch(); })); - return
{ - editorView = new EditorView({ - parent: e, - state: EditorState.create({ - doc: app.input().text, - extensions: [ - drawSelection(), - keymap.of(defaultKeymap), - EditorView.updateListener.of((update) => { - const { amountRead } = app.input(); - if (!update.docChanged) return; + return
+ + +
{ + editorView = new EditorView({ + parent: e, + state: EditorState.create({ + doc: app.input().text, + extensions: [ + EditorView.lineWrapping, + drawSelection(), + keymap.of(defaultKeymap), + EditorView.updateListener.of((update) => { + const {amountRead} = app.input(); + if (!update.docChanged) return; - if (amountRead && update.changes.touchesRange(0, amountRead - 1)) { - // revert the change if the update affects the readonly portion of the input - editorView?.setState(update.startState); - } else { - app.setInput((prev) => ({ ...prev, text: update.state.doc.toString() })); - } - }), - layer({ - above: true, class: "input-marker-layer", markers(view) { - const { text, amountRead } = app.input(); + if (amountRead && update.changes.touchesRange(0, amountRead - 1)) { + // revert the change if the update affects the readonly portion of the input + editorView?.setState(update.startState); + } else { + const newText = update.state.doc.toString(); + // Update both the app state and local state + app.setInput((prev) => ({...prev, text: newText})); + } + }), + layer({ + above: true, class: "input-marker-layer", markers(view) { + const {text, amountRead} = app.input(); - const markers = []; - if (amountRead) markers.push(...RectangleMarker.forRange(view, 'input-readonly-marker', EditorSelection.single(0, amountRead).main)); + const markers = []; + if (amountRead) markers.push(...RectangleMarker.forRange(view, 'input-readonly-marker', EditorSelection.single(0, amountRead).main)); - if (amountRead !== null) markers.push(...RectangleMarker.forRange(view, (amountRead >= text.length ? 'input-cursor-waiting-marker' : 'input-cursor-marker'), EditorSelection.single(amountRead).main)); + if (amountRead !== null) markers.push(...RectangleMarker.forRange(view, (amountRead >= text.length ? 'input-cursor-waiting-marker' : 'input-cursor-marker'), EditorSelection.single(amountRead).main)); - return markers; - }, update(_update, _layer) { - // always update if something changes or if the above createEffect does an empty dispatch - // this triggers the above markers() function - return true; - }, - }) - ] - }) - }); - }} />; + return markers; + }, update(_update, _layer) { + // always update if something changes or if the above createEffect does an empty dispatch + // this triggers the above markers() function + return true; + }, + }) + ] + }) + }); + }}/> +
; }; export default InputPanel; diff --git a/src/panels/OutputPanel.tsx b/src/panels/OutputPanel.tsx index e44dde3..fed9e42 100644 --- a/src/panels/OutputPanel.tsx +++ b/src/panels/OutputPanel.tsx @@ -1,20 +1,39 @@ -import { Component, createEffect, on, JSX } from "solid-js"; +import {Component, createEffect, on, JSX, createSignal} from "solid-js"; import { EditorView, drawSelection } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; import { useAppContext } from "../App"; +import PanelHeader from "../components/PanelHeader.tsx"; +import Divider from "../components/Divider.tsx"; const OutputPanel: Component<{ style?: JSX.CSSProperties }> = (props) => { const app = useAppContext()!; // this component could handle logic for line by line output and auto scrolling // that is why this component even exists let editorView: EditorView | undefined; + let [errorState, setErrorState] = createSignal(false); + + const styles = () => { + if (errorState()){ + return { + ...props.style, + "color": "var(--NEGATIVE)" + } + } + return {...props.style} + }; + const getOutputText = () => app.output()?.content || ""; createEffect( on([() => !!editorView, app.output], () => { const output = app?.output(); if (!editorView || !output) return; + if (output.type == "ERROR") { + setErrorState(true); + } else { + setErrorState(false); + } editorView.dispatch({ changes: { from: 0, @@ -25,18 +44,23 @@ const OutputPanel: Component<{ style?: JSX.CSSProperties }> = (props) => { }) ); - return
{ - editorView = new EditorView({ - parent: e, - state: EditorState.create({ - extensions: [ - drawSelection(), - EditorView.lineWrapping, - EditorView.editable.of(false), - ], - }), - }); - }} />; -}; + return
+ + +
{ + editorView = new EditorView({ + parent: e, + state: EditorState.create({ + extensions: [ + drawSelection(), + EditorView.lineWrapping, + EditorView.editable.of(false), + ], + }), + }); + }}/> +
+ ; + }; -export default OutputPanel; + export default OutputPanel; diff --git a/src/panels/SettingsPanel.tsx b/src/panels/SettingsPanel.tsx deleted file mode 100644 index a15d7bb..0000000 --- a/src/panels/SettingsPanel.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { - Component, - For, - createEffect, - createSignal, - JSX, - Show, -} from "solid-js"; -import Divider from "../components/Divider"; -import { useAppContext } from "../App"; -import { makePersisted } from "@solid-primitives/storage"; -import { AiFillGithub, AiOutlineStop } from "solid-icons/ai"; -import { FiCopy } from "solid-icons/fi"; -import { IoHelpCircle } from "solid-icons/io"; - -import "./settings.css"; -import { Portal } from "solid-js/web"; -import { SolidMarkdown } from "solid-markdown"; -import readmeContent from "../../README.md?raw"; -import { IoClose } from "solid-icons/io"; -import remarkGfm from "remark-gfm"; -const SettingsPanel: Component<{ style?: JSX.CSSProperties }> = (props) => { - const app = useAppContext()!; - - const [enabledOptimisations, setEnabledOptimisations] = makePersisted( - createSignal({ - optimise_cell_clearing: false, - optimise_constants: false, - optimise_empty_blocks: false, - optimise_generated_code: false, - optimise_memory_allocation: false, - optimise_unreachable_loops: false, - optimise_variable_usage: false, - }), - { name: "mastermind_compiler_optimisations" } - ); - - createEffect(() => { - const fileStates = app.fileStates; - const entryFile = app.entryFile(); - if (app.fileStates.length && !entryFile) { - app.setEntryFile(fileStates[0]?.id); - } - }); - - const onRun = async () => { - // TODO: error handling here? is it needed? - const output = app.output(); - if (output?.type !== "BF") return; - - await app.run(output.content); - }; - - const onCompile = async () => { - const entryFileId = app.entryFile(); - if (!entryFileId) return; - - await app.compile(entryFileId, enabledOptimisations()); - }; - - createEffect(() => { - console.log(app.fileStates); - }); - - return ( -
-
-
- {/* entry file selection */} - - {/* button with 3 options (compile, run, or both) */} -
-
-
-
- compile program -
- -
- run code -
-
- -
{ - await onCompile(); - // technically this second await is pointless - await onRun(); - } - : undefined - } - > - compile & run -
-
- {/* status overlay on the button */} - {app.status() !== "IDLE" && ( -
-
- { - { - ["COMPILING"]: "compiling program", - ["RUNNING"]: "running code", - ["INPUT_BLOCKED"]: "waiting for input", - ["IDLE"]: null, - }[app.status()] - } - -
app.restartWorker()} - title="kill brainfuck process" - class="stop-button" - > - -
-
-
-
- )} -
- {/* misc options and markers */} -
{ - const output = app.output(); - if (!output) return; - window.navigator.clipboard - .writeText(output.content) - .then(() => window.alert("Output copied to clipboard!")); - }} - > - - { - { - ["BF"]: "compiled code", - ["ERROR"]: "error output", - ["OUTPUT"]: "code output", - ["LIVE_OUTPUT"]: "live output", - }[app.output()?.type ?? "OUTPUT"] - } - - {/* TODO: convert this to be more correct, or something? */} - {app.output() && ` (${app.output()?.content.length} bytes)`} -
-
app.setEnableBlockingInput((prev) => !prev)} - > - blocking input [ - {app.enableBlockingInput() ? ( -
enabled
- ) : ( -
disabled
- )} - ] -
-
- -
- - Optimisations: - - setEnabledOptimisations((prev) => { - const entries = Object.entries(prev); - const b = entries.some(([, v]) => !v); - return Object.fromEntries( - entries.map(([k]) => [k, b]) - ) as unknown as MastermindConfig; - // trust me on this one typescript - }) - } - > - (toggle all) - - -
{ - const target = e.target as HTMLInputElement; - setEnabledOptimisations((prev) => ({ - ...prev, - [target.name]: !!target.checked, - })); - }} - > - - {([key, enabled]: [string, boolean]) => ( - - )} - -
-
-
- {/* */} - {/* social media links, currently only github */} - -
- ); -}; - -export default SettingsPanel; - -export interface MastermindConfig { - optimise_cell_clearing: boolean; - optimise_constants: boolean; - optimise_empty_blocks: boolean; - optimise_generated_code: boolean; - optimise_memory_allocation: boolean; - optimise_unreachable_loops: boolean; - optimise_variable_usage: boolean; -} - -const configLabels: Record = { - optimise_cell_clearing: "cell clearing", - optimise_constants: "constants", - optimise_empty_blocks: "empty blocks", - optimise_generated_code: "generated code", - optimise_memory_allocation: "memory allocations", - optimise_unreachable_loops: "unreachable loops", - optimise_variable_usage: "variable usage", -}; diff --git a/src/panels/SideBar.tsx b/src/panels/SideBar.tsx new file mode 100644 index 0000000..ccbc621 --- /dev/null +++ b/src/panels/SideBar.tsx @@ -0,0 +1,50 @@ +import {Component, JSX} from "solid-js"; +import {useAppContext} from "../App.tsx"; +import {AiFillGithub, AiFillQuestionCircle, AiFillSetting} from "solid-icons/ai"; +import { FaSolidBookOpen } from 'solid-icons/fa' +import HelpModal from "../components/Help.tsx"; +import DocsModal from "../components/Docs.tsx"; +import "./settings.css"; +import SettingsModal from "../components/Settings.tsx"; + +const SideBar: Component<{ style?: JSX.CSSProperties }> = (props) => { + const app = useAppContext()!; + + return ( + + ); +} + +export default SideBar; \ No newline at end of file diff --git a/src/panels/editor.css b/src/panels/editor.css index fd1e3cd..a4eb0e0 100644 --- a/src/panels/editor.css +++ b/src/panels/editor.css @@ -3,7 +3,7 @@ height: 3rem; /* added these to fix a weird bug with heights changing */ min-height: 3rem; - max-height: 3rem;; + max-height: 3rem; display: flex; flex-direction: row; justify-content: flex-start; @@ -17,6 +17,7 @@ /* draggability things */ /* z-index: 1; */ + border-right: var(--default-border); } .tab-bar::-webkit-scrollbar { @@ -36,7 +37,7 @@ .tab { background-color: var(--BG-1); - border-right: var(--default-border); + border-left: var(--default-border); border-bottom: var(--default-border); display: flex; @@ -73,7 +74,7 @@ border: none; outline: none; background-color: unset; - + cursor: inherit; } @@ -84,3 +85,25 @@ .file-label-text { font-size: 1rem; } + +.tab-container { + margin: 0; + padding: 0; + width: 100%; + display: flex; + justify-content: space-between; +} + +.tab-controller { + display: flex; + flex-direction: row; + gap: 5px; + padding: 5px; +} + +.tab-controller-divider { + border: var(--default-border); + border-width: 0.5px; + height: 100%; + border-radius: 2px; +} diff --git a/src/panels/settings.css b/src/panels/settings.css index f100a9b..9f0e6db 100644 --- a/src/panels/settings.css +++ b/src/panels/settings.css @@ -78,11 +78,37 @@ a:active.socials-icon { border: 1px solid transparent; } -.stop-button:hover { - background-color: var(--DANGER-TRANSPARENT); - border: 1px solid var(--DANGER); +.start-button { + display: flex; + justify-content: center; + align-items: center; + color: var(--POSITIVE); + font-size: 1.25rem; + padding: 0.25rem; + border-radius: 0.25rem; + cursor: pointer; + border: 1px solid transparent; } +.run-button { + display: flex; + flex-direction: row; + align-items: center; + max-height: 1.5rem; + min-height: 1.5rem; + min-width: 8rem; + font-size: 0.9rem; +} + +.top-label { + +} + +/*.stop-button:hover {*/ +/* background-color: var(--DANGER-TRANSPARENT);*/ +/* border: 1px solid var(--DANGER);*/ +/*}*/ + .readme-modal-container { position: absolute; top: 0px; @@ -107,3 +133,67 @@ a:active.socials-icon { background-color: var(--BG-0); border: 1px solid var(--BORDER-0); } +.docs-modal { + position: relative; + max-width: 800px; + min-width: 800px; + + padding: 2rem; + background-color: var(--BG-0); + border: 1px solid var(--BORDER-0); +} + +.file-browser-modal { + position: relative; + max-width: 900px; + + padding: 0rem 2rem 1rem 2rem; + background-color: var(--BG-0); + border: 1px solid var(--BORDER-0); + display: flex; + align-items: center; + flex-direction: column; +} + +.settings-modal { + position: relative; + max-width: 800px; + + padding: 2rem; + background-color: var(--BG-0); + border: 1px solid var(--BORDER-0); +} + +.header-select { + font-size: 2rem; + font-weight: bold; + border: none; + background: transparent; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + padding: 0; + margin: 0; + color: inherit; + cursor: pointer; + text-align: center; +} + +.header-select:focus { + outline: none; +} + +.header-select-wrapper { + position: relative; + display: inline-block; +} + +.header-select-wrapper::after { + content: "▼"; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + pointer-events: none; +} + diff --git a/src/utils/downloadBlob.ts b/src/utils/downloadBlob.ts new file mode 100644 index 0000000..f7ab2a7 --- /dev/null +++ b/src/utils/downloadBlob.ts @@ -0,0 +1,16 @@ +export default function downloadBlob( + blob: Blob, + fileName: string = "Mastermind" +) { + // This is really awkward to programatically download files, but its the only way to do it cleanly + const objectUrl = URL.createObjectURL(blob); + const a: HTMLAnchorElement = document.createElement("a") as HTMLAnchorElement; + + a.href = objectUrl; + a.download = fileName; + document.body.appendChild(a); + a.click(); + + document.body.removeChild(a); + URL.revokeObjectURL(objectUrl); +} diff --git a/src/worker.ts b/src/worker.ts index 91b9d90..40fb8ae 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,5 +1,5 @@ import initWasm, { wasm_compile, wasm_run_bf } from "../compiler/pkg"; -import { MastermindConfig } from "./panels/SettingsPanel"; +import {MastermindConfig} from "./components/Settings"; import { v4 as uuidv4 } from "uuid"; @@ -19,14 +19,15 @@ self.addEventListener("message", ({ data }: MessageEvent< arguments: { fileMap: Record, entryFileName: string, - optimisations: MastermindConfig + config: MastermindConfig } } | { transaction: string, command: "RUN", arguments: { - code: string + code: string, + enable_2d_grid: boolean } }) >) => { @@ -51,7 +52,7 @@ self.addEventListener("message", ({ data }: MessageEvent< const compiledCode = wasm_compile( data.arguments.fileMap, data.arguments.entryFileName, - data.arguments.optimisations + data.arguments.config ); postMessage({ @@ -70,8 +71,7 @@ self.addEventListener("message", ({ data }: MessageEvent< break; case "RUN": try { - - _run(data.arguments.code, data.transaction).then(codeOutput => { + _run(data.arguments.code, data.arguments.enable_2d_grid, data.transaction).then(codeOutput => { console.log(codeOutput); postMessage({ transaction: data.transaction, @@ -98,8 +98,8 @@ self.addEventListener("message", ({ data }: MessageEvent< } }); -function _run(code: string, runTransaction: string) { - const result = wasm_run_bf(code, +function _run(code: string, enable_2d_grid: boolean, runTransaction: string) { + const result = wasm_run_bf(code, enable_2d_grid, function (byte: number) { // output a byte from the BVM diff --git a/yarn.lock b/yarn.lock index ab714db..e075102 100644 --- a/yarn.lock +++ b/yarn.lock @@ -635,6 +635,13 @@ resolved "https://registry.yarnpkg.com/@solid-primitives/utils/-/utils-6.2.1.tgz#320fb2180743031622b40fc43b0e63bf55686cfb" integrity sha512-TsecNzxiO5bLfzqb4OOuzfUmdOROcssuGqgh5rXMMaasoFZ3GoveUgdY1wcf17frMJM7kCNGNuK34EjErneZkg== +"@soorria/solid-dropzone@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@soorria/solid-dropzone/-/solid-dropzone-1.0.1.tgz#f8f66df8968a7e965fea6ddbed92650631341b3f" + integrity sha512-xQ6qkn3IK3IgAz7yyx+AgfmnCnRHkBTqQGjTcS64FDN7YnnvpZB//0rZjJKl5xwG9bGcJ3oyJAHnv2NK/KNSZA== + dependencies: + file-selector "^0.6.0" + "@thisbeyond/solid-dnd@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@thisbeyond/solid-dnd/-/solid-dnd-0.7.5.tgz#c7585511bc83b5b973315f956296431124b6562c" @@ -880,6 +887,11 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + crelt@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" @@ -977,6 +989,13 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +file-selector@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" + integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== + dependencies: + tslib "^2.4.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1024,6 +1043,11 @@ html-entities@2.3.3: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-from-esm@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/import-from-esm/-/import-from-esm-1.3.3.tgz#eea1c4ad86a54bf425b3b71fca56d50215ccc6b7" @@ -1045,7 +1069,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1087,6 +1111,11 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1102,11 +1131,28 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + kleur@^4.0.3: version "4.1.5" resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" @@ -1585,6 +1631,11 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -1609,11 +1660,29 @@ postcss@^8.4.32: picocolors "^1.0.0" source-map-js "^1.0.2" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + property-information@^6.3.0: version "6.4.1" resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.4.1.tgz#de8b79a7415fd2107dfbe65758bb2cc9dfcf60ac" integrity sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w== +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + regenerator-runtime@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" @@ -1697,6 +1766,11 @@ sade@^1.7.3: dependencies: mri "^1.1.0" +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -1707,6 +1781,11 @@ seroval@^0.15.1: resolved "https://registry.yarnpkg.com/seroval/-/seroval-0.15.1.tgz#598654725a3a680fd0d685e235ab7299a6096aff" integrity sha512-OPVtf0qmeC7RW+ScVX+7aOS+xoIM7pWcZ0jOWg2aTZigCydgRB04adfteBRbecZnnrO1WuGQ+C3tLeBBzX2zSQ== +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + solid-icons@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/solid-icons/-/solid-icons-1.1.0.tgz#6e914bce4f9a34a654230b68c68a7d04359ff2b6" @@ -1769,6 +1848,13 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -1817,6 +1903,11 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== +tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + typescript@^5.2.2: version "5.3.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" @@ -1886,6 +1977,11 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"