scratch/brainfuck/src/gbf/internal/vm.gleam (view raw)
| 1 | import gbf/internal/ascii |
| 2 | import gleam/dict.{type Dict} |
| 3 | import gleam/list |
| 4 | import gleam/result |
| 5 | |
| 6 | pub const tape_size = 30_000 |
| 7 | |
| 8 | pub const cell_size = 255 |
| 9 | |
| 10 | pub type Error { |
| 11 | PointerRanOffTape |
| 12 | IntegerOverflow |
| 13 | IntegerUnderflow |
| 14 | EmptyInput |
| 15 | InvalidChar(Int) |
| 16 | } |
| 17 | |
| 18 | /// The machine model we are going to use for this interpreter is very simple: |
| 19 | /// - Our memory consists of 30,000 cells (1000 rows * 30 columns). |
| 20 | /// - There's a data pointer which points to a specific cell and is initialized at |
| 21 | /// the leftmost cell, an error will be reported if the pointer runs off the |
| 22 | /// tape at either end. |
| 23 | /// pointer = 0 |
| 24 | /// - A data cell is 8 bits, and an error will be reported if the program tries |
| 25 | /// to perform under- or overflow, i.e. decrement 0 or increment 255. |
| 26 | /// - Two streams of bytes for input and output using the ASCII character |
| 27 | /// encoding. |
| 28 | /// |
| 29 | pub type VirtualMachine { |
| 30 | VirtualMachine(pointer: Index, cells: Cells, output: String, input: List(Int)) |
| 31 | } |
| 32 | |
| 33 | pub type Cells = |
| 34 | Dict(Int, Int) |
| 35 | |
| 36 | pub type Index = |
| 37 | Int |
| 38 | |
| 39 | pub fn new(input: List(Int)) -> VirtualMachine { |
| 40 | VirtualMachine(input:, pointer: 0, cells: dict.new(), output: "") |
| 41 | } |
| 42 | |
| 43 | /// Returns the accumulated output string from the virtual machine. |
| 44 | /// |
| 45 | pub fn output(vm: VirtualMachine) -> String { |
| 46 | vm.output |
| 47 | } |
| 48 | |
| 49 | /// Gets the value of the cell at the specified pointer. |
| 50 | /// Returns an error if the pointer is out of bounds. |
| 51 | /// |
| 52 | /// ```gleam |
| 53 | /// get_cell(vm, 0) // Ok(0) |
| 54 | /// get_cell(vm, -1) // Error(PointerRanOffTape) |
| 55 | /// ``` |
| 56 | pub fn get_cell(vm: VirtualMachine, pointer: Index) -> Result(Index, Error) { |
| 57 | use pointer <- result.try(validate_tape_size(pointer)) |
| 58 | |
| 59 | case dict.get(vm.cells, pointer) { |
| 60 | Ok(value) -> Ok(value) |
| 61 | Error(_) -> Ok(0) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | /// Sets the value of the cell at the specified pointer. |
| 66 | /// |
| 67 | /// Returns an updated virtual machine if successful, or an error if: |
| 68 | /// - The pointer is out of bounds (< 0 or > 30,000) |
| 69 | /// - The value is out of bounds (< 0 or > 255) |
| 70 | /// |
| 71 | /// ```gleam |
| 72 | /// set_cell(vm, 0, 65) // Ok(...) |
| 73 | /// set_cell(vm, 0, 256) // Error(IntegerOverflow) |
| 74 | /// ``` |
| 75 | pub fn set_cell( |
| 76 | vm: VirtualMachine, |
| 77 | pointer: Index, |
| 78 | value: Int, |
| 79 | ) -> Result(VirtualMachine, Error) { |
| 80 | use pointer <- result.try(validate_tape_size(pointer)) |
| 81 | use value <- result.try(validate_cell_size(value)) |
| 82 | let cells = dict.insert(vm.cells, pointer, value) |
| 83 | |
| 84 | VirtualMachine(..vm, cells:) |
| 85 | |> Ok |
| 86 | } |
| 87 | |
| 88 | /// Moves the data pointer to the specified position. |
| 89 | /// Returns error if pointer is out of bounds. |
| 90 | /// |
| 91 | /// ```gleam |
| 92 | /// set_pointer(vm, 100) // Ok(...) |
| 93 | /// set_pointer(vm, -1) // Error(PointerRanOffTape) |
| 94 | /// ``` |
| 95 | pub fn set_pointer( |
| 96 | vm: VirtualMachine, |
| 97 | pointer: Index, |
| 98 | ) -> Result(VirtualMachine, Error) { |
| 99 | use pointer <- result.try(validate_tape_size(pointer)) |
| 100 | |
| 101 | VirtualMachine(..vm, pointer:) |
| 102 | |> Ok |
| 103 | } |
| 104 | |
| 105 | /// Reads a byte from the input stream and stores it in the current cell. |
| 106 | /// |
| 107 | /// Consumes the first byte from the input list and writes it to the cell |
| 108 | /// at the current pointer position. |
| 109 | /// Returns an error if the input is empty. |
| 110 | /// |
| 111 | /// ```gleam |
| 112 | /// input_byte(vm) // Ok(...) |
| 113 | /// input_byte(empty_vm) // Error(EmptyInput) |
| 114 | /// ``` |
| 115 | pub fn input_byte(vm: VirtualMachine) -> Result(VirtualMachine, Error) { |
| 116 | case vm.input { |
| 117 | [] -> Error(EmptyInput) |
| 118 | [first, ..] -> { |
| 119 | use vm <- result.try(set_cell(vm, vm.pointer, first)) |
| 120 | |
| 121 | VirtualMachine(..vm, input: list.drop(vm.input, 1)) |
| 122 | |> Ok |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | /// Reads the value from the current cell and appends it to the output as a character. |
| 128 | /// |
| 129 | /// Converts the cell value to an ASCII character and adds it to the output string. |
| 130 | /// Returns an error if the cell value is not a valid ASCII code point. |
| 131 | /// |
| 132 | pub fn output_byte(vm: VirtualMachine) -> Result(VirtualMachine, Error) { |
| 133 | use cell_value <- result.try(get_cell(vm, vm.pointer)) |
| 134 | |
| 135 | case ascii.from_code(cell_value) { |
| 136 | "" -> Error(InvalidChar(cell_value)) |
| 137 | c -> |
| 138 | VirtualMachine(..vm, output: vm.output <> c) |
| 139 | |> Ok |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | fn validate_tape_size(pointer: Index) { |
| 144 | case pointer { |
| 145 | p if p > tape_size -> Error(PointerRanOffTape) |
| 146 | p if p < 0 -> Error(PointerRanOffTape) |
| 147 | _ -> Ok(pointer) |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | fn validate_cell_size(value: Int) { |
| 152 | case value { |
| 153 | v if v > cell_size -> Error(IntegerOverflow) |
| 154 | v if v < 0 -> Error(IntegerUnderflow) |
| 155 | _ -> Ok(value) |
| 156 | } |
| 157 | } |