├── .gitignore ├── img └── screenshot.png ├── .gitmodules ├── README.md ├── LICENSE └── src └── main.zig /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | zig-cache/ 3 | zig-out/ 4 | -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rvcas/crisp/HEAD/img/screenshot.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/zig-terminal"] 2 | path = deps/zig-terminal 3 | url = git@github.com:truemedian/zig-terminal.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crisp 2 | 3 | A very minimal lispy calculator implemented in [zig](https://ziglang.org) 4 | 5 | ![Running Crisp](https://github.com/rvcas/crisp/raw/main/img/screenshot.png) 6 | 7 | ## Operations 8 | 9 | - `+` add a sequence of numbers 10 | - `(+ 1 2 3)` 11 | - `-` subtract a sequence of numbers 12 | - `(- 3 2)` 13 | - `*` multiply a sequence of numbers 14 | - `(* 3 5 2)` 15 | - `/` divide a sequence of numbers 16 | - `(/ 12 4)` 17 | - decimals are not supported. meaning: `(/ 2 4) => 0` 18 | - everything is floored 19 | - divide by zero results in an error 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lucas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const terminal = @import("zig-terminal"); 3 | 4 | const ArrayList = std.ArrayList; 5 | 6 | const Op = enum { 7 | add, 8 | subtract, 9 | multiply, 10 | divide, 11 | }; 12 | 13 | const Token = union(enum) { 14 | paren: u8, 15 | op: Op, 16 | number: []u8, 17 | }; 18 | 19 | fn tokenize(src: []u8, allocator: *std.mem.Allocator) ![]Token { 20 | var tokens = ArrayList(Token).init(allocator); 21 | errdefer tokens.deinit(); 22 | 23 | var iter: usize = 0; 24 | 25 | while (iter < src.len) { 26 | const ch = src[iter]; 27 | 28 | iter += 1; 29 | 30 | switch (ch) { 31 | '(', ')' => try tokens.append(.{ .paren = ch }), 32 | '+' => try tokens.append(.{ .op = .add }), 33 | '-' => try tokens.append(.{ .op = .subtract }), 34 | '*' => try tokens.append(.{ .op = .multiply }), 35 | '/' => try tokens.append(.{ .op = .divide }), 36 | '0'...'9' => { 37 | var value = ArrayList(u8).init(allocator); 38 | errdefer value.deinit(); 39 | 40 | try value.append(ch); 41 | 42 | while (iter < src.len) { 43 | const next = src[iter]; 44 | 45 | switch (next) { 46 | '0'...'9' => { 47 | iter += 1; 48 | 49 | try value.append(next); 50 | }, 51 | else => break, 52 | } 53 | } 54 | 55 | try tokens.append(.{ .number = value.items }); 56 | }, 57 | ' ' => {}, 58 | else => return error.InvalidCharacter, 59 | } 60 | } 61 | 62 | return tokens.items; 63 | } 64 | 65 | const ComputeError = error{ 66 | BadInput, 67 | InvalidNumber, 68 | NotImplemented, 69 | ExpectedOp, 70 | OutOfMemory, 71 | BadDivisionParams, 72 | DivisionByZero, 73 | BadSubtractionParams, 74 | }; 75 | 76 | fn compute(tokens: []Token, iter: *usize, allocator: *std.mem.Allocator) ComputeError!isize { 77 | if (iter.* >= tokens.len) { 78 | return error.BadInput; 79 | } 80 | 81 | const token = tokens[iter.*]; 82 | 83 | iter.* += 1; 84 | 85 | switch (token) { 86 | .number => |num| { 87 | const n = std.fmt.parseInt(isize, num, 10) catch return error.InvalidNumber; 88 | 89 | return n; 90 | }, 91 | .paren => |paren| { 92 | if (paren == '(') { 93 | const next = tokens[iter.*]; 94 | 95 | iter.* += 1; 96 | 97 | switch (next) { 98 | .op => |op| switch (op) { 99 | .add => { 100 | var sum: isize = 0; 101 | 102 | while (iter.* < tokens.len) { 103 | const second_next = tokens[iter.*]; 104 | 105 | switch (second_next) { 106 | .paren => |subparen| { 107 | if (subparen == ')') { 108 | break; 109 | } 110 | 111 | sum += try compute(tokens, iter, allocator); 112 | }, 113 | else => { 114 | sum += try compute(tokens, iter, allocator); 115 | }, 116 | } 117 | } 118 | 119 | iter.* += 1; 120 | 121 | return sum; 122 | }, 123 | .multiply => { 124 | var product: isize = 1; 125 | 126 | while (iter.* < tokens.len) { 127 | const second_next = tokens[iter.*]; 128 | 129 | switch (second_next) { 130 | .paren => |subparen| { 131 | if (subparen == ')') { 132 | break; 133 | } 134 | 135 | product *= try compute(tokens, iter, allocator); 136 | }, 137 | else => { 138 | product *= try compute(tokens, iter, allocator); 139 | }, 140 | } 141 | } 142 | 143 | iter.* += 1; 144 | 145 | return product; 146 | }, 147 | .divide => { 148 | var params = ArrayList(isize).init(allocator); 149 | errdefer params.deinit(); 150 | 151 | while (iter.* < tokens.len) { 152 | const second_next = tokens[iter.*]; 153 | 154 | switch (second_next) { 155 | .paren => |subparen| { 156 | if (subparen == ')') { 157 | break; 158 | } 159 | 160 | const result = try compute(tokens, iter, allocator); 161 | 162 | try params.append(result); 163 | }, 164 | else => { 165 | const result = try compute(tokens, iter, allocator); 166 | 167 | try params.append(result); 168 | }, 169 | } 170 | } 171 | 172 | iter.* += 1; 173 | 174 | const items = params.items; 175 | 176 | if (items.len < 2) { 177 | return error.BadDivisionParams; 178 | } 179 | 180 | var result = items[0]; 181 | 182 | for (items[1..]) |item| { 183 | if (item == 0) return error.DivisionByZero; 184 | 185 | result = @divFloor(result, item); 186 | } 187 | 188 | return result; 189 | }, 190 | .subtract => { 191 | var sub: isize = 0; 192 | 193 | while (iter.* < tokens.len) { 194 | const second_next = tokens[iter.*]; 195 | 196 | switch (second_next) { 197 | .paren => |subparen| { 198 | if (subparen == ')') { 199 | break; 200 | } 201 | 202 | sub -= try compute(tokens, iter, allocator); 203 | }, 204 | else => { 205 | sub -= try compute(tokens, iter, allocator); 206 | }, 207 | } 208 | } 209 | 210 | iter.* += 1; 211 | 212 | return sub; 213 | }, 214 | }, 215 | else => return error.ExpectedOp, 216 | } 217 | } 218 | 219 | return error.BadInput; 220 | }, 221 | else => return error.BadInput, 222 | } 223 | } 224 | 225 | fn eval(expr: []u8) !isize { 226 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 227 | defer arena.deinit(); 228 | 229 | var tokens = try tokenize(expr, &arena.allocator); 230 | 231 | var iter: usize = 0; 232 | 233 | const result = try compute(tokens, &iter, &arena.allocator); 234 | 235 | return result; 236 | } 237 | 238 | pub fn main() anyerror!void { 239 | var term = terminal.Terminal.init(); 240 | 241 | try term.printWithAttributes(.{ 242 | .blue, 243 | "Crisp Version 0.0.1 🚀\n", 244 | "Type :q to Exit\n\n", 245 | .reset, 246 | }); 247 | 248 | while (true) { 249 | try term.printWithAttributes(.{ 250 | terminal.TextAttributes{ 251 | .foreground = .magenta, 252 | .bold = true, 253 | }, 254 | "crisp> ", 255 | terminal.TextAttributes{ 256 | .foreground = .cyan, 257 | .bold = true, 258 | }, 259 | }); 260 | 261 | const chars = try term.reader().readUntilDelimiterAlloc( 262 | std.heap.page_allocator, 263 | '\n', 264 | std.math.maxInt(usize), 265 | ); 266 | defer std.heap.page_allocator.free(chars); 267 | 268 | try term.resetAttributes(); 269 | 270 | if (std.mem.eql(u8, chars, ":q")) { 271 | try term.printWithAttributes(.{ 272 | terminal.TextAttributes{ 273 | .foreground = .green, 274 | .bold = true, 275 | }, 276 | "\nGoodbye 👋\n", 277 | .reset, 278 | }); 279 | 280 | std.process.exit(0); 281 | } 282 | 283 | if (eval(chars)) |result| { 284 | try term.printWithAttributes(.{ 285 | .{ .foreground = .green, .bold = true }, 286 | terminal.format("\n-> {}\n\n", .{result}), 287 | .reset, 288 | }); 289 | } else |err| switch (err) { 290 | error.OutOfMemory => try term.printWithAttributes(.{ 291 | terminal.TextAttributes{ 292 | .foreground = .red, 293 | .bold = true, 294 | }, 295 | "Error: out of memory \n\n", 296 | .reset, 297 | }), 298 | error.InvalidCharacter => try term.printWithAttributes(.{ 299 | terminal.TextAttributes{ 300 | .foreground = .red, 301 | .bold = true, 302 | }, 303 | "Error: invalid character \n\n", 304 | .reset, 305 | }), 306 | error.DivisionByZero => try term.printWithAttributes(.{ 307 | terminal.TextAttributes{ 308 | .foreground = .red, 309 | .bold = true, 310 | }, 311 | "Error: canot divide by zero \n\n", 312 | .reset, 313 | }), 314 | else => try term.printWithAttributes(.{ 315 | terminal.TextAttributes{ 316 | .foreground = .red, 317 | .bold = true, 318 | }, 319 | "Error: error calculating result \n\n", 320 | .reset, 321 | }), 322 | } 323 | } 324 | } 325 | --------------------------------------------------------------------------------