├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── examples ├── shebang.fy ├── sierp.fy └── test.fy ├── fy.png └── src ├── args.zig ├── asm.zig └── main.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | *.out 4 | *.o 5 | *.s 6 | yy 7 | *.txt -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "(gdb) Launch", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/zig-out/bin/fy", 9 | "args": [], 10 | "stopAtEntry": false, 11 | "cwd": "${workspaceFolder}", 12 | "environment": [], 13 | "externalConsole": false, 14 | "MIMode": "lldb", 15 | "preLaunchTask": "build", 16 | "setupCommands": [ 17 | { 18 | "description": "Enable pretty-printing for gdb", 19 | "text": "-enable-pretty-printing", 20 | "ignoreFailures": true 21 | } 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "zig build", 8 | "problemMatcher": [], 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marcin Gasperowicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fy 2 | 3 | ![ZigZig Top guys enjoying fy](./fy.png) 4 | 5 | Short for _funky yak_, _flying yacht_, or _funny yodeling_ depending on your mood. Also _fuck yeah_. 6 | 7 | `fy` is a tiny concatenative programming language JIT compiled to aarch64 machine code. 8 | 9 | `fy` is a toy, of the kind where the batteries constantly leak and only that weird guy in suspenders plays with it. 10 | 11 | Join [#fy on concatenative Discord](https://discord.com/channels/1150472957093744721/1166896397254131804). 12 | 13 | ## Building 14 | 15 | `fy` is written in Zig and targets aarch64 exclusively. You'll need a Zig compiler and a 64-bit ARM machine such as AppleSilicon or a Raspberry Pi. 16 | 17 | Build with: 18 | 19 | ```sh 20 | zig build 21 | ``` 22 | 23 | Run with: 24 | 25 | ```sh 26 | ./zig-out/bin/fy 27 | ``` 28 | 29 | Check `--help` for the latest news on available flags and arguments. 30 | 31 | ## Examples 32 | 33 | Examples can be found in `examples/`. 34 | 35 | ## Features 36 | 37 | [There is no plan.](https://github.com/SerenityOS/serenity/blob/master/Documentation/FAQ.md#will-serenityos-support-thing) 38 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | const zigline = b.dependency("zigline", .{}); 14 | 15 | // Standard optimization options allow the person running `zig build` to select 16 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 17 | // set a preferred release mode, allowing the user to decide how to optimize. 18 | const optimize = b.standardOptimizeOption(.{ 19 | .preferred_optimize_mode = .ReleaseSmall, 20 | }); 21 | 22 | const exe = b.addExecutable(.{ 23 | .name = "fy", 24 | // In this case the main source file is merely a path, however, in more 25 | // complicated build scripts, this could be a generated file. 26 | .root_source_file = .{ .path = "src/main.zig" }, 27 | .target = target, 28 | .optimize = optimize, 29 | }); 30 | exe.root_module.addImport("zigline", zigline.module("zigline")); 31 | 32 | // This declares intent for the executable to be installed into the 33 | // standard location when the user invokes the "install" step (the default 34 | // step when running `zig build`). 35 | b.installArtifact(exe); 36 | 37 | // This *creates* a Run step in the build graph, to be executed when another 38 | // step is evaluated that depends on it. The next line below will establish 39 | // such a dependency. 40 | const run_cmd = b.addRunArtifact(exe); 41 | 42 | // By making the run step depend on the install step, it will be run from the 43 | // installation directory rather than directly from within the cache directory. 44 | // This is not necessary, however, if the application depends on other installed 45 | // files, this ensures they will be present and in the expected location. 46 | run_cmd.step.dependOn(b.getInstallStep()); 47 | 48 | // This allows the user to pass arguments to the application in the build 49 | // command itself, like this: `zig build run -- arg1 arg2 etc` 50 | if (b.args) |args| { 51 | run_cmd.addArgs(args); 52 | } 53 | 54 | // This creates a build step. It will be visible in the `zig build --help` menu, 55 | // and can be selected like this: `zig build run` 56 | // This will evaluate the `run` step rather than the default, which is "install". 57 | const run_step = b.step("run", "Run the app"); 58 | run_step.dependOn(&run_cmd.step); 59 | 60 | // Creates a step for unit testing. This only builds the test executable 61 | // but does not run it. 62 | const unit_tests = b.addTest(.{ 63 | .root_source_file = .{ .path = "src/main.zig" }, 64 | .target = target, 65 | .optimize = optimize, 66 | }); 67 | 68 | const run_unit_tests = b.addRunArtifact(unit_tests); 69 | 70 | // Similar to creating the run step earlier, this exposes a `test` step to 71 | // the `zig build --help` menu, providing a way for the user to request 72 | // running the unit tests. 73 | const test_step = b.step("test", "Run unit tests"); 74 | test_step.dependOn(&run_unit_tests.step); 75 | } 76 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "fy", // 3 | .version = "0.0.1", 4 | .paths = .{ "src/", "build.zig", "build.zig.zon" }, 5 | .dependencies = .{ // 6 | .zigline = .{ 7 | .url = "https://github.com/alimpfard/zigline/archive/e18781dbc559e9a7aa3d6fa327335815803306dc.tar.gz", 8 | .hash = "12208f21bd7d85021b7d8d191e8c0114871cde63cc7d1056a8b4d7805fe8fc1b85a2", 9 | }, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /examples/shebang.fy: -------------------------------------------------------------------------------- 1 | #!./zig-out/bin/fy 2 | '! 'd 'l 'r 'o 'W ' 'o 'l 'l 'e 'H 3 | .c .c .c .c .c .c .c .c .c .c .c .c .nl -------------------------------------------------------------------------------- /examples/sierp.fy: -------------------------------------------------------------------------------- 1 | :N 16; 2 | N [ dup (y) 3 | [' .c] dotimes (print padding) 4 | N [ dup (x) 5 | over2 drop 1- (y' = y - 1) 6 | swap N !- (x' = N - x) 7 | & (x' & y') 8 | \' \'* ifte .c ' .c ] (print * or space) 9 | dotimes .nl] 10 | dotimes 11 | -------------------------------------------------------------------------------- /examples/test.fy: -------------------------------------------------------------------------------- 1 | 2 | 1 3 | 1 2 4 | 1 2 + 5 | 1 2 - 6 | 2 2 * 7 | 12 3 / 8 | 1 2 = 9 | 1 1 = 10 | 1 2 != 11 | 1 1 != 12 | 1 2 > 13 | 2 1 > 14 | 1 2 < 15 | 2 1 < 16 | 1 2 >= 17 | 2 1 >= 18 | 2 2 >= 19 | 1 2 <= 20 | 2 1 <= 21 | 2 2 <= 22 | 2 dup 23 | 2 3 swap 24 | 2 3 over 25 | 2 3 nip 26 | 2 3 tuck 27 | 2 3 drop -------------------------------------------------------------------------------- /fy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nooga/fy/c7858518afac0d570ec4756d12ca0979ed66917a/fy.png -------------------------------------------------------------------------------- /src/args.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Args = struct { 4 | repl: bool = false, 5 | eval: ?[]const u8 = null, 6 | help: bool = false, 7 | version: bool = false, 8 | image: bool = false, 9 | files: u32, 10 | other_args: std.ArrayList([]const u8) = undefined, // Initialize this later 11 | 12 | // Clean up resources 13 | pub fn deinit(self: *Args) void { 14 | self.other_args.deinit(); 15 | } 16 | }; 17 | 18 | pub fn parseArgs(allocator: std.mem.Allocator, args_it: *std.process.ArgIterator) !Args { 19 | var result = Args{ 20 | .other_args = std.ArrayList([]const u8).init(allocator), 21 | .files = 0, 22 | .help = false, 23 | .repl = false, 24 | .version = false, 25 | .image = false, 26 | .eval = null, 27 | }; 28 | 29 | while (args_it.next()) |arg| { 30 | if (std.mem.eql(u8, arg, "--repl") or std.mem.eql(u8, arg, "-r")) { 31 | result.repl = true; 32 | } else if (std.mem.eql(u8, arg, "--eval") or std.mem.eql(u8, arg, "-e")) { 33 | if (result.eval) |_| { 34 | std.debug.print("Error: '--eval' flag was already set\n", .{}); 35 | return result; 36 | } 37 | if (args_it.next()) |evalExp| { 38 | result.eval = evalExp; 39 | } else { 40 | std.debug.print("Error: '--eval' flag requires an argument\n", .{}); 41 | } 42 | } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { 43 | result.help = true; 44 | } else if (std.mem.eql(u8, arg, "--version") or std.mem.eql(u8, arg, "-v")) { 45 | result.version = true; 46 | } else if (std.mem.eql(u8, arg, "--image") or std.mem.eql(u8, arg, "-i")) { 47 | result.image = true; 48 | } else if (arg[0] == '-') { 49 | std.debug.print("Error: Unknown flag: '{s}'\n", .{arg}); 50 | result.help = true; 51 | return result; 52 | } else { 53 | result.files += 1; 54 | try result.other_args.append(arg); 55 | } 56 | } 57 | 58 | return result; 59 | } 60 | -------------------------------------------------------------------------------- /src/asm.zig: -------------------------------------------------------------------------------- 1 | pub const @"str x0, [sp, #-16]!" = 0xf81f0fe0; 2 | pub const @"str x1, [sp, #-16]!" = 0xf81f0fe1; 3 | pub const @"ldr x0, [sp], #16" = 0xf84107e0; 4 | pub const @"ldr x1, [sp], #16" = 0xf84107e1; 5 | pub const @"stp x29, x30, [sp, #0x10]!" = 0xa9bf7bfd; 6 | pub const @"ldp x29, x30, [sp], #0x10" = 0xa8c17bfd; 7 | 8 | pub const @"stp x0, x1, [x21, #-16]!" = 0xa9bf06a0; 9 | pub const @"ldp x0, x1, [x21], #16" = 0xa8c106a0; 10 | pub const @"stp x1, x0, [x21, #-16]!" = 0xa9bf02a1; 11 | pub const @"ldp x1, x0, [x21], #16" = 0xa8c102a1; 12 | pub const @"stp x2, x3, [x21, #-16]!" = 0xa9bf0ea2; 13 | pub const @"ldp x2, x3, [x21], #16" = 0xa8c10ea2; 14 | 15 | pub const @"mov x0, x21" = 0xaa1503e0; 16 | pub const @"mov x1, x22" = 0xaa1603e1; 17 | 18 | pub const @"str x0, [x21, #-8]!" = 0xf81f8ea0; 19 | pub const @"str x1, [x21, #-8]!" = 0xf81f8ea1; 20 | 21 | pub const @"ldr x0, [x21], #8" = 0xf84086a0; 22 | pub const @"ldr x1, [x21], #8" = 0xf84086a1; 23 | 24 | pub const @"mov x29, sp" = 0x910003fd; 25 | pub const @"mov sp, x29" = 0x910003bf; 26 | 27 | pub const @"mov x0, #0" = 0xd2800000; 28 | pub const @"mov x0, #1" = 0xd2800020; 29 | 30 | pub const @"add x0, x0, x1" = 0x8b010000; 31 | pub const @"sub x0, x1, x0" = 0xcb010000; 32 | pub const @"mul x0, x0, x1" = 0x9b017c00; 33 | pub const @"sdiv x0, x1, x0" = 0x9ac10c00; 34 | pub const @"and x0, x0, x1" = 0x8a010000; 35 | 36 | pub const @"add x0, x0, #1" = 0x91000400; 37 | pub const @"sub x0, x0, #1" = 0xd1000400; 38 | 39 | pub const @"sub x0, x22, x21" = 0xcb1502c0; 40 | pub const @"asr x0, x0, #3" = 0x9343fc00; 41 | 42 | pub const @"cbz x0, 0" = 0xb4000000; 43 | 44 | pub const @"cmp x0, x1" = 0xeb01001f; 45 | 46 | pub const @"cmp x2, #0" = 0xf100005f; 47 | pub const @"csel x0, x0, x1, ne" = 0x9a811000; 48 | 49 | pub const @"b 0" = 0x14000000; 50 | pub const @"b 2" = @"b 0" + 2; 51 | 52 | pub const @"beq #2" = 0x54000060; 53 | pub const @"bne #2" = 0x54000061; 54 | pub const @"bgt #2" = 0x5400006c; 55 | pub const @"blt #2" = 0x5400006b; 56 | pub const @"bge #2" = 0x5400006a; 57 | pub const @"ble #2" = 0x5400006d; 58 | 59 | pub const @"blr x0" = 0xd63f0000; 60 | 61 | pub const ret = 0xd65f03c0; 62 | 63 | // pseudo instructions 64 | 65 | // call slot is used to indicate where to put the function call address and is replaced with the actual address 66 | pub const CALLSLOT = 0xffffffff; 67 | 68 | pub const @".push x0" = @"str x0, [x21, #-8]!"; //@"str x0, [sp, #-16]!"; 69 | pub const @".push x1" = @"str x1, [x21, #-8]!"; //@"str x1, [sp, #-16]!"; 70 | pub const @".pop x0" = @"ldr x0, [x21], #8"; //@"ldr x0, [sp], #16"; 71 | pub const @".pop x1" = @"ldr x1, [x21], #8"; //@"ldr x1, [sp], #16"; 72 | 73 | pub const @".push x0, x1" = @"stp x1, x0, [x21, #-16]!"; 74 | pub const @".pop x0, x1" = @"ldp x0, x1, [x21], #16"; 75 | pub const @".push x1, x0" = @"stp x0, x1, [x21, #-16]!"; 76 | pub const @".pop x1, x0" = @"ldp x1, x0, [x21], #16"; 77 | pub const @".push x2, x3" = @"stp x2, x3, [x21, #-16]!"; 78 | pub const @".pop x2, x3" = @"ldp x2, x3, [x21], #16"; 79 | 80 | // register used to store call address: x20 81 | pub const REGCALL = 20; 82 | 83 | // helpers 84 | pub fn @"blr Xn"(n: u5) u32 { 85 | return @"blr x0" | @as(u32, @intCast(n)) << 5; 86 | } 87 | 88 | pub fn @"cbz Xn, offset"(n: u5, offset: u19) u32 { 89 | return @"cbz x0, 0" | @as(u32, @intCast(n)) | @as(u32, @intCast(offset)) << 5; 90 | } 91 | 92 | pub fn @"b offset"(offset: i26) u32 { 93 | //@compileLog("b", offset, @"b 0" | (@as(u32, @bitCast(@as(i32, offset)))) & 0x3ffffff); 94 | return @"b 0" | (@as(u32, @bitCast(@as(i32, offset))) & 0x3ffffff); 95 | } 96 | 97 | pub fn @".pop Xn"(n: usize) u32 { 98 | return @".pop x0" + @as(u32, @intCast(n)); 99 | } 100 | 101 | pub fn @"lsr Xn, Xm, #s"(n: u5, m: u5, s: u6) u32 { 102 | return 0x9ac12800 | @as(u32, @intCast(n)) | @as(u32, @intCast(m)) << 5 | @as(u32, @intCast(s)) << 10; 103 | } 104 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Editor = @import("zigline").Editor; 3 | const Asm = @import("asm.zig"); 4 | const Args = @import("args.zig"); 5 | 6 | extern fn __clear_cache(start: usize, end: usize) callconv(.C) void; 7 | 8 | fn debugSlice(mem: []u8, len: usize) void { 9 | // hexdump the image in 4 byte chunks 10 | var i: usize = 0; 11 | 12 | // print header with offsets 13 | std.debug.print(" ", .{}); 14 | while (i < 64) { 15 | std.debug.print("{x:0>2} ", .{i}); 16 | i += 4; 17 | } 18 | i = 0; 19 | 20 | std.debug.print("\n000 ", .{}); 21 | while (i < len) { 22 | std.debug.print("{} ", .{std.fmt.fmtSliceHexLower(mem[i .. i + 4])}); 23 | // add a newline every 16 bytes 24 | i += 4; 25 | if (i % 64 == 0) { 26 | std.debug.print("\n{x:0>3} ", .{i}); 27 | } 28 | } 29 | std.debug.print("\n", .{}); 30 | } 31 | 32 | const Fy = struct { 33 | fyalloc: std.mem.Allocator, 34 | userWords: std.StringHashMap(Word), 35 | dataStack: [DATASTACKSIZE]Value = undefined, 36 | image: Image, 37 | 38 | const version = "v0.0.1"; 39 | const DATASTACKSIZE = 512; 40 | 41 | fn init(allocator: std.mem.Allocator) Fy { 42 | const image = Image.init() catch @panic("failed to allocate image"); 43 | return Fy{ 44 | .userWords = std.StringHashMap(Word).init(allocator), 45 | .fyalloc = allocator, 46 | .image = image, 47 | }; 48 | } 49 | 50 | fn deinit(self: *Fy) void { 51 | self.image.deinit(); 52 | deinitUserWords(self); 53 | return; 54 | } 55 | 56 | fn deinitUserWords(self: *Fy) void { 57 | var keys = self.userWords.keyIterator(); 58 | while (keys.next()) |k| { 59 | if (self.userWords.getPtr(k.*)) |v| { 60 | self.fyalloc.free(v.code); 61 | self.fyalloc.free(k.*); 62 | } 63 | } 64 | self.userWords.deinit(); 65 | return; 66 | } 67 | 68 | const Value = i64; 69 | 70 | const Word = struct { 71 | code: []const u32, //machine code 72 | c: usize, //consumes 73 | p: usize, //produces 74 | callSlot: ?*const void, 75 | 76 | const DEFINE = ":"; 77 | const END = ";"; 78 | const QUOTE_OPEN = "["; 79 | const QUOTE_END = "]"; 80 | }; 81 | 82 | fn fnToWord(comptime fun: anytype) Word { 83 | const T = @TypeOf(fun); 84 | const typeinfo = @typeInfo(T).Fn; 85 | const paramCount = typeinfo.params.len; 86 | var returnCount = 0; 87 | for (0..paramCount) |i| { 88 | if (typeinfo.params[i].type.? != Value) { 89 | @compileError("fnToWord: param {} must be Value"); 90 | } 91 | } 92 | if (typeinfo.return_type) |rt| { 93 | if (rt != Value and rt != void) { 94 | @compileError("fnToWord: return type must be Value or void"); 95 | } 96 | if (rt == Value) { 97 | returnCount = 1; 98 | } 99 | } 100 | 101 | // 1 for call slot, 1 for each param, 1 for each return value 102 | const codeLen: usize = 1 + paramCount + returnCount; 103 | 104 | var code = [1]u32{undefined} ** codeLen; 105 | 106 | // pop all params 107 | for (0..paramCount) |i| { 108 | code[i] = Asm.@".pop Xn"(i); 109 | } 110 | 111 | // call the function and push the result 112 | if (returnCount == 1) { 113 | code[codeLen - 2] = Asm.CALLSLOT; 114 | code[codeLen - 1] = Asm.@".push x0"; 115 | } else { 116 | code[codeLen - 1] = Asm.CALLSLOT; 117 | } 118 | 119 | return Word{ 120 | .code = &code, 121 | .c = paramCount, 122 | .p = returnCount, 123 | .callSlot = @ptrCast(&fun), 124 | }; 125 | } 126 | 127 | fn binOp(comptime op: u32, comptime swap: bool) Word { 128 | var p1 = Asm.@".pop x0, x1"; 129 | if (swap) { 130 | p1 = Asm.@".pop x1, x0"; 131 | } 132 | const code = &[_]u32{ 133 | p1, 134 | op, 135 | Asm.@".push x0", 136 | }; 137 | return Word{ 138 | .code = code, 139 | .c = 2, 140 | .p = 1, 141 | .callSlot = null, 142 | }; 143 | } 144 | 145 | fn cmpOp(comptime op: u32) Word { 146 | return inlineWord(&[_]u32{ 147 | Asm.@".pop x0, x1", 148 | Asm.@"cmp x0, x1", 149 | op, 150 | Asm.@"mov x0, #0", 151 | Asm.@"b 2", 152 | Asm.@"mov x0, #1", 153 | Asm.@".push x0", 154 | }, 2, 1); 155 | } 156 | 157 | fn inlineWord(comptime code: []const u32, comptime c: usize, comptime p: usize) Word { 158 | return Word{ 159 | .code = code, 160 | .c = c, 161 | .p = p, 162 | .callSlot = null, 163 | }; 164 | } 165 | 166 | const Builtins = struct { 167 | fn print(a: Value) void { 168 | std.io.getStdOut().writer().print("{d}\n", .{a}) catch std.debug.print("{d}\n", .{a}); 169 | } 170 | fn printHex(a: Value) void { 171 | std.io.getStdOut().writer().print("0x{x}\n", .{a}) catch std.debug.print("0x{x}\n", .{a}); 172 | } 173 | fn printNewline() void { 174 | std.io.getStdOut().writer().print("\n", .{}) catch std.debug.print("\n", .{}); 175 | } 176 | fn printChar(a: Value) void { 177 | std.io.getStdOut().writer().print("{c}", .{@as(u8, @intCast(a))}) catch std.debug.print("{c}", .{@as(u8, @intCast(a))}); 178 | } 179 | fn spy(a: Value) Value { 180 | print(a); 181 | return a; 182 | } 183 | fn spyStack(base: Value, end: Value) void { 184 | const w = std.io.getStdOut().writer(); 185 | const p: [*]Value = @ptrFromInt(@as(usize, @intCast(base))); 186 | const l: usize = @intCast(end - base); 187 | const len: usize = l / @sizeOf(Value); 188 | const s: []Value = p[0..len]; 189 | w.print("--| ", .{}) catch std.debug.print("--| ", .{}); 190 | for (2..len + 1) |v| { 191 | w.print("{d} ", .{s[len - v]}) catch std.debug.print("{d} ", .{s[len - v]}); 192 | } 193 | w.print("\n", .{}) catch std.debug.print("\n", .{}); 194 | } 195 | }; 196 | 197 | const words = std.ComptimeStringMap(Word, .{ 198 | // a b -- a+b 199 | .{ "+", binOp(Asm.@"add x0, x0, x1", false) }, 200 | // a b -- a-b 201 | .{ "-", binOp(Asm.@"sub x0, x1, x0", true) }, 202 | // a b -- a-b 203 | .{ "!-", binOp(Asm.@"sub x0, x1, x0", false) }, 204 | // a b -- a*b 205 | .{ "*", binOp(Asm.@"mul x0, x0, x1", false) }, 206 | // a b -- a&b 207 | .{ "&", binOp(Asm.@"and x0, x0, x1", false) }, 208 | // a b -- a/b 209 | .{ "/", binOp(Asm.@"sdiv x0, x1, x0", true) }, 210 | .{ "=", cmpOp(Asm.@"beq #2") }, 211 | .{ "!=", cmpOp(Asm.@"bne #2") }, 212 | .{ ">", cmpOp(Asm.@"blt #2") }, 213 | .{ "<", cmpOp(Asm.@"bgt #2") }, 214 | .{ ">=", cmpOp(Asm.@"ble #2") }, 215 | .{ "<=", cmpOp(Asm.@"bge #2") }, 216 | // a -- a a 217 | .{ "dup", inlineWord(&[_]u32{ Asm.@".pop x0", Asm.@".push x0", Asm.@".push x0" }, 1, 2) }, 218 | // a b -- b a 219 | .{ "swap", inlineWord(&[_]u32{ Asm.@".pop x0, x1", Asm.@".push x0, x1" }, 2, 2) }, 220 | // a -- 221 | .{ "drop", inlineWord(&[_]u32{Asm.@".pop x0"}, 1, 0) }, 222 | // a b -- a b a 223 | .{ "over", inlineWord(&[_]u32{ Asm.@".pop x0, x1", Asm.@".push x1, x0", Asm.@".push x1" }, 2, 3) }, 224 | // c d a b -- c d a b c d 225 | .{ "over2", inlineWord(&[_]u32{ 226 | Asm.@".pop x0, x1", 227 | Asm.@".pop x2, x3", 228 | Asm.@".push x2, x3", 229 | Asm.@".push x0, x1", 230 | Asm.@".push x2, x3", 231 | }, 4, 6) }, 232 | // a b -- b 233 | .{ "nip", inlineWord(&[_]u32{ Asm.@".pop x0, x1", Asm.@".push x0" }, 2, 1) }, 234 | // a b -- b a b 235 | .{ "tuck", inlineWord(&[_]u32{ Asm.@".pop x0, x1", Asm.@".push x0, x1", Asm.@".push x0" }, 2, 3) }, 236 | // -- a 237 | .{ "depth", inlineWord(&[_]u32{ Asm.@"sub x0, x22, x21", Asm.@"asr x0, x0, #3", Asm.@"sub x0, x0, #1", Asm.@".push x0" }, 0, 1) }, 238 | // a -- 239 | .{ ".", fnToWord(Builtins.print) }, 240 | // -- 241 | .{ ".nl", fnToWord(Builtins.printNewline) }, 242 | // a -- 243 | .{ ".c", fnToWord(Builtins.printChar) }, 244 | // a -- 245 | .{ ".hex", fnToWord(Builtins.printHex) }, 246 | // a -- a 247 | .{ "spy", fnToWord(Builtins.spy) }, 248 | // -- 249 | .{ ".dbg", .{ 250 | .code = &[_]u32{ Asm.@"mov x0, x21", Asm.@"mov x1, x22", Asm.CALLSLOT }, 251 | .c = 0, 252 | .p = 0, 253 | .callSlot = @as(*const void, @ptrCast(&Builtins.spyStack)), 254 | } }, 255 | // a -- a + 1 256 | .{ "1+", inlineWord(&[_]u32{ Asm.@".pop x0", Asm.@"add x0, x0, #1", Asm.@".push x0" }, 1, 1) }, 257 | // a -- a - 1 258 | .{ "1-", inlineWord(&[_]u32{ Asm.@".pop x0", Asm.@"sub x0, x0, #1", Asm.@".push x0" }, 1, 1) }, 259 | // ... f -- f(...) 260 | .{ "do", inlineWord(&[_]u32{ Asm.@".pop x0", Asm.@"blr Xn"(0) }, 0, 0) }, 261 | // ... ft -- ft(...) | ... 262 | .{ 263 | "do?", inlineWord(&[_]u32{ 264 | Asm.@".pop x1, x0", // 265 | Asm.@"cbz Xn, offset"(0, 2), 266 | Asm.@"blr Xn"(1), 267 | }, 0, 1), 268 | }, 269 | // ... c ft ff -- ft(...) | ff(...) 270 | .{ 271 | "ifte", inlineWord(&[_]u32{ 272 | Asm.@".pop x1, x0", // 273 | Asm.@".pop Xn"(2), 274 | Asm.@"cmp x2, #0", 275 | Asm.@"csel x0, x0, x1, ne", 276 | Asm.@"blr Xn"(0), 277 | }, 0, 1), 278 | }, 279 | // ... n f -- ... 280 | .{ 281 | "dotimes", inlineWord(&[_]u32{ 282 | Asm.@".pop x0, x1", 283 | Asm.@"cbz Xn, offset"(1, 7), 284 | Asm.@".push x0, x1", 285 | Asm.@"blr Xn"(0), 286 | Asm.@".pop x0, x1", 287 | Asm.@"sub x0, x0, #1", 288 | Asm.@".push x0, x1", 289 | Asm.@"b offset"(-7), 290 | }, 2, 0), 291 | }, 292 | // ... f -- ... 293 | // repeat the quote until the top of the stack is 0 294 | .{ "repeat", inlineWord(&[_]u32{ 295 | Asm.@".pop x0", 296 | Asm.@"cbz Xn, offset"(0, 2), 297 | Asm.@"blr Xn"(0), 298 | Asm.@"b offset"(-2), 299 | }, 1, 0) }, 300 | }); 301 | 302 | fn findWord(self: *Fy, word: []const u8) ?Word { 303 | return self.userWords.get(word) orelse words.get(word); 304 | } 305 | 306 | // parseromptime c: usize, comptime p: usize 307 | const Parser = struct { 308 | code: []const u8, 309 | pos: usize, 310 | autoclose: bool, 311 | 312 | const Token = union(enum) { 313 | Number: Value, 314 | Word: []const u8, 315 | }; 316 | 317 | fn init(code: []const u8) Parser { 318 | return Parser{ 319 | .code = code, 320 | .pos = 0, 321 | .autoclose = false, 322 | }; 323 | } 324 | 325 | fn isDigit(c: u8) bool { 326 | return c >= '0' and c <= '9'; 327 | } 328 | 329 | fn isWhitespace(c: u8) bool { 330 | return c == ' ' or c == '\n' or c == '\r' or c == '\t'; 331 | } 332 | 333 | fn nextToken(self: *Parser) ?Token { 334 | while (self.pos < self.code.len and isWhitespace(self.code[self.pos])) { 335 | if (self.autoclose) { 336 | self.autoclose = false; 337 | return Token{ .Word = Word.QUOTE_END }; 338 | } 339 | self.pos += 1; 340 | } 341 | if (self.pos >= self.code.len) { 342 | return null; 343 | } 344 | var c = self.code[self.pos]; 345 | const start = self.pos; 346 | if (c == '(') { 347 | self.pos += 1; 348 | var depth: usize = 1; 349 | while (self.pos < self.code.len and depth > 0) { 350 | switch (self.code[self.pos]) { 351 | '(' => depth += 1, 352 | ')' => depth -= 1, 353 | else => {}, 354 | } 355 | self.pos += 1; 356 | } 357 | if (depth != 0) { 358 | @panic("Unbalanced parentheses in comment"); 359 | } 360 | return self.nextToken(); 361 | } 362 | if (c == '\'') { 363 | self.pos += 1; 364 | if (self.pos >= self.code.len) { 365 | @panic("Expected character after '\\''"); 366 | } 367 | c = self.code[self.pos]; 368 | const charValue = @as(Value, c); 369 | self.pos += 1; 370 | return Token{ .Number = charValue }; 371 | } 372 | if (c == ':') { 373 | self.pos += 1; 374 | return Token{ .Word = Word.DEFINE }; 375 | } 376 | if (c == ';') { 377 | self.pos += 1; 378 | return Token{ .Word = Word.END }; 379 | } 380 | if (c == '\\') { 381 | self.pos += 1; 382 | self.autoclose = true; 383 | return Token{ .Word = Word.QUOTE_OPEN }; 384 | } 385 | if (c == '[') { 386 | self.pos += 1; 387 | return Token{ .Word = Word.QUOTE_OPEN }; 388 | } 389 | if (c == ']') { 390 | self.pos += 1; 391 | return Token{ .Word = Word.QUOTE_END }; 392 | } 393 | if (isDigit(c) or c == '-') { 394 | var negative = false; 395 | var number = true; 396 | if (c == '-') { 397 | negative = true; 398 | self.pos += 1; 399 | if (self.pos >= self.code.len or !isDigit(self.code[self.pos])) { 400 | number = false; 401 | } 402 | } 403 | 404 | if (number) { 405 | while (self.pos < self.code.len and isDigit(self.code[self.pos])) { 406 | self.pos += 1; 407 | } 408 | return Token{ .Number = std.fmt.parseInt(Value, self.code[start..self.pos], 10) catch 2137 }; 409 | } 410 | } 411 | while (self.pos < self.code.len and !isWhitespace(self.code[self.pos]) and self.code[self.pos] != ';' and self.code[self.pos] != ']') { 412 | self.pos += 1; 413 | } 414 | 415 | return Token{ .Word = self.code[start..self.pos] }; 416 | } 417 | }; 418 | 419 | // compiler 420 | const Compiler = struct { 421 | parser: *Parser, 422 | code: std.ArrayList(u32), 423 | prev: u32 = 0, 424 | fy: *Fy, 425 | 426 | const Error = error{ 427 | ExpectedWord, 428 | UnexpectedEndOfInput, 429 | UnknownWord, 430 | OutOfMemory, 431 | }; 432 | 433 | fn init(fy: *Fy, parser: *Parser) Compiler { 434 | return Compiler{ 435 | .code = std.ArrayList(u32).init(fy.fyalloc), 436 | .parser = parser, 437 | .fy = fy, 438 | }; 439 | } 440 | 441 | fn emit(self: *Compiler, instr: u32) !void { 442 | // if (self.prev == Asm.@".push x0" and instr == Asm.@".pop x0") { 443 | // _ = self.code.pop(); 444 | // if (self.code.items.len == 0) { 445 | // self.prev = 0; 446 | // } else { 447 | // self.prev = self.code.getLast(); 448 | // } 449 | // return; 450 | // } 451 | try self.code.append(instr); 452 | self.prev = instr; 453 | } 454 | 455 | fn emitWord(self: *Compiler, word: Word) !void { 456 | var i: usize = 0; 457 | while (true) { 458 | const instr = word.code[i]; 459 | if (instr == Asm.CALLSLOT) { 460 | const fun: u64 = @intFromPtr(word.callSlot); 461 | try self.emitNumber(fun, Asm.REGCALL); 462 | try self.emitCall(Asm.REGCALL); 463 | i += 1; 464 | } else { 465 | try self.emit(instr); 466 | i += 1; 467 | } 468 | if (i >= word.code.len) { 469 | break; 470 | } 471 | } 472 | } 473 | 474 | fn emitPush(self: *Compiler) !void { 475 | try self.emit(Asm.@".push x0"); 476 | } 477 | 478 | fn emitPop(self: *Compiler) !void { 479 | try self.emit(Asm.@".pop x0"); 480 | } 481 | 482 | fn seg16(x: u64, shift: u6) u32 { 483 | return @as(u32, @truncate(x >> shift)) & 0xffff; 484 | } 485 | 486 | fn emitNumber(self: *Compiler, n: u64, r: u5) !void { 487 | const rr: u32 = r; 488 | // movz x0, #token.Number 489 | try self.emit(0xd2800000 | rr | seg16(n, 0) << 5); 490 | if (n > 0xffff) { 491 | // movk x0, #token.Number, lsl #16 492 | try self.emit(0xf2a00000 | rr | seg16(n, 16) << 5); 493 | } 494 | if (n > 0xffffffff) { 495 | // movk x0, #token.Number, lsl #32 496 | try self.emit(0xf2c00000 | rr | seg16(n, 32) << 5); 497 | } 498 | if (n > 0xffffffffffff) { 499 | // movk x0, #token.Number, lsl #48 500 | try self.emit(0xf2e00000 | rr | seg16(n, 48) << 5); 501 | } 502 | } 503 | 504 | fn emitCall(self: *Compiler, r: u5) !void { 505 | try self.emit(Asm.@"blr Xn"(r)); 506 | } 507 | 508 | fn compileToken(self: *Compiler, token: Parser.Token) Error!void { 509 | switch (token) { 510 | .Number => { 511 | const n = @as(u64, @bitCast(token.Number)); 512 | try self.emitNumber(n, 0); 513 | try self.emitPush(); 514 | }, 515 | .Word => { 516 | const word = self.fy.findWord(token.Word); 517 | if (word) |w| { 518 | try self.emitWord(w); 519 | } else { 520 | std.debug.print("Unknown word: {s}\n", .{token.Word}); 521 | return Error.UnknownWord; 522 | } 523 | }, 524 | } 525 | } 526 | 527 | fn defineWord(self: *Compiler, name: []const u8, code: []u32) !void { 528 | var key: []u8 = undefined; 529 | if (self.fy.userWords.getPtr(name)) |oldword| { 530 | self.fy.fyalloc.free(oldword.code); 531 | key = @constCast(name); 532 | } else { 533 | key = try self.fy.fyalloc.dupe(u8, name); 534 | } 535 | try self.fy.userWords.put(key, Word{ 536 | .code = code, 537 | .c = 0, 538 | .p = 0, 539 | .callSlot = null, 540 | }); 541 | } 542 | 543 | const Wrap = enum { 544 | None, 545 | Quote, 546 | Function, 547 | }; 548 | 549 | fn compileDefinition(self: *Compiler) Error!void { 550 | const name = self.parser.nextToken(); 551 | if (name) |n| { 552 | switch (n) { 553 | .Word => |w| { 554 | var compiler = Compiler.init(self.fy, self.parser); 555 | const code = try compiler.compile(.None); 556 | 557 | try self.defineWord(w, code); 558 | }, 559 | else => { 560 | return Error.ExpectedWord; 561 | }, 562 | } 563 | } else { 564 | return Error.UnexpectedEndOfInput; 565 | } 566 | } 567 | 568 | fn compileQuote(self: *Compiler) Error!u64 { 569 | var compiler = Compiler.init(self.fy, self.parser); 570 | const code = try compiler.compile(.Quote); 571 | const executable = self.fy.image.link(code); 572 | compiler.fy.fyalloc.free(code); 573 | return @intFromPtr(executable.ptr); 574 | } 575 | 576 | fn enter(self: *Compiler) !void { 577 | try self.emit(Asm.@"stp x29, x30, [sp, #0x10]!"); 578 | //try self.emit(Asm.@"mov x29, sp"); 579 | 580 | } 581 | 582 | fn leave(self: *Compiler) !void { 583 | //try self.emit(Asm.@"mov sp, x29"); 584 | try self.emit(Asm.@"ldp x29, x30, [sp], #0x10"); 585 | try self.emit(Asm.ret); 586 | } 587 | 588 | fn compile(self: *Compiler, wrap: Wrap) Error![]u32 { 589 | switch (wrap) { 590 | .None => {}, 591 | .Quote => { 592 | try self.enter(); 593 | }, 594 | .Function => { 595 | try self.enter(); 596 | try self.emitNumber(@intFromPtr(&self.fy.dataStack) + DATASTACKSIZE, 21); 597 | try self.emitNumber(@intFromPtr(&self.fy.dataStack) + DATASTACKSIZE, 22); 598 | try self.emitNumber(0, 0); 599 | try self.emitPush(); 600 | }, 601 | } 602 | var token = self.parser.nextToken(); 603 | while (token != null) : (token = self.parser.nextToken()) { 604 | switch (token.?) { 605 | .Word => |w| { 606 | if (std.mem.eql(u8, w, Word.END) or std.mem.eql(u8, w, Word.QUOTE_END)) { 607 | break; 608 | } 609 | if (std.mem.eql(u8, w, Word.DEFINE)) { 610 | try self.compileDefinition(); 611 | continue; 612 | } 613 | if (std.mem.eql(u8, w, Word.QUOTE_OPEN)) { 614 | const target = try self.compileQuote(); 615 | try self.emitNumber(target, 0); 616 | try self.emitPush(); 617 | continue; 618 | } 619 | }, 620 | else => {}, 621 | } 622 | try self.compileToken(token.?); 623 | } 624 | switch (wrap) { 625 | .None => {}, 626 | .Quote => { 627 | try self.leave(); 628 | }, 629 | .Function => { 630 | try self.emitPop(); 631 | try self.leave(); 632 | }, 633 | } 634 | return self.code.toOwnedSlice(); 635 | } 636 | 637 | fn compileFn(self: *Compiler) ![]u32 { 638 | return self.compile(.Function); 639 | } 640 | }; 641 | 642 | // const TableEntry = struct { 643 | // length: usize, 644 | // }; 645 | 646 | const Image = struct { 647 | mem: []align(std.mem.page_size) u8, 648 | end: usize, 649 | // table: std.AutoHashMap(usize, TableEntry), 650 | fn init() !Image { 651 | // flags: std.os.MAP.PRIVATE | std.os.MAP.ANONYMOUS = 3 652 | const mem = try std.os.mmap(null, std.mem.page_size, std.os.PROT.READ | std.os.PROT.WRITE, .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, -1, 0); 653 | return Image{ 654 | .mem = mem, 655 | .end = 0, 656 | }; 657 | } 658 | 659 | fn deinit(self: *Image) void { 660 | std.os.munmap(self.mem); 661 | } 662 | 663 | fn grow(self: *Image) !void { 664 | const oldlen = self.mem.len; 665 | const newlen = oldlen + std.mem.page_size; 666 | // flags: std.os.MAP.PRIVATE | std.os.MAP.ANONYMOUS = 3 667 | const new = try std.os.mmap(null, newlen, std.os.PROT.READ | std.os.PROT.WRITE, .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, -1, 0); 668 | @memcpy(new, self.mem); 669 | std.os.munmap(self.mem); 670 | self.mem = new; 671 | } 672 | 673 | fn protect(self: *Image, executable: bool) !void { 674 | if (executable) { 675 | try std.os.mprotect(self.mem, std.os.PROT.READ | std.os.PROT.EXEC); 676 | } else { 677 | try std.os.mprotect(self.mem, std.os.PROT.READ | std.os.PROT.WRITE); 678 | } 679 | } 680 | 681 | fn link(self: *Image, code: []u32) []u8 { 682 | const len: usize = code.len * @sizeOf(u32); 683 | const memlen = self.mem.len; 684 | if (self.end + len > memlen) { 685 | self.grow() catch @panic("failed to grow image"); 686 | } else { 687 | self.protect(false) catch @panic("failed to set image writable"); 688 | } 689 | const new = self.end; 690 | self.end += len; 691 | @memcpy(self.mem[new..self.end], std.mem.sliceAsBytes(code)); 692 | self.protect(true) catch @panic("failed to set image executable"); 693 | __clear_cache(@intFromPtr(self.mem.ptr), @intFromPtr(self.mem.ptr) + self.end); 694 | //debugSlice(self.mem[new..self.end], self.end - new); 695 | return self.mem[new..self.end]; 696 | } 697 | 698 | fn reset(self: *Image) void { 699 | self.end = 0; 700 | } 701 | }; 702 | 703 | const Fn = struct { 704 | call: *const fn () Value, 705 | }; 706 | 707 | fn jit(self: *Fy, code: []u32) !Fn { 708 | const executable: []u8 = self.image.link(code); 709 | // free the original code buffer as we already have machine code in executable memory 710 | self.fyalloc.free(code); 711 | // cast the memory to a function pointer and call 712 | const fun: *const fn () Value = @alignCast(@ptrCast(executable)); 713 | return Fn{ .call = fun }; 714 | } 715 | 716 | fn run(self: *Fy, src: []const u8) !Fy.Value { 717 | var parser = Fy.Parser.init(src); 718 | var compiler = Fy.Compiler.init(self, &parser); 719 | const code = compiler.compileFn(); 720 | 721 | if (code) |c| { 722 | var fyfn = try self.jit(c); 723 | const x = fyfn.call(); 724 | //self.image.reset(); 725 | return x; 726 | } else |err| { 727 | return err; 728 | } 729 | } 730 | 731 | fn debugDump(self: *Fy) void { 732 | // show userWords 733 | var keys = self.userWords.keyIterator(); 734 | std.debug.print("user words: ", .{}); 735 | while (keys.next()) |k| { 736 | std.debug.print("{s} ", .{k.*}); 737 | } 738 | std.debug.print("\n", .{}); 739 | } 740 | }; 741 | 742 | pub fn repl(allocator: std.mem.Allocator, fy: *Fy) !void { 743 | const stdout = std.io.getStdOut().writer(); 744 | var editor = Editor.init(allocator, .{}); 745 | defer editor.deinit(); 746 | 747 | var handler: struct { 748 | editor: *Editor, 749 | pub fn paste(self: *@This(), text: []const u32) void { 750 | self.editor.insertUtf32(text); 751 | } 752 | } = .{ .editor = &editor }; 753 | editor.setHandler(&handler); 754 | 755 | try stdout.print("fy! {s}\n", .{Fy.version}); 756 | 757 | while (true) { 758 | const line: []const u8 = editor.getLine("fy> ") catch |err| switch (err) { 759 | error.Eof => break, 760 | else => return err, 761 | }; 762 | defer allocator.free(line); 763 | try editor.addToHistory(line); 764 | if (line.len == 0) { 765 | allocator.free(line); 766 | continue; 767 | } 768 | const result = fy.run(line); 769 | if (result) |r| { 770 | try stdout.print(" {d}\n", .{r}); 771 | } else |err| { 772 | try stdout.print("error: {}\n", .{err}); 773 | } 774 | } 775 | return; 776 | } 777 | 778 | pub fn runFile(allocator: std.mem.Allocator, fy: *Fy, path: []const u8) !void { 779 | const file = try std.fs.cwd().openFile(path, .{}); 780 | defer file.close(); 781 | const stat = try file.stat(); 782 | const fileSize = stat.size; 783 | const src = try file.reader().readAllAlloc(allocator, fileSize); 784 | var cleanSrc = src; 785 | // get rid of shebang from src if present 786 | if (src.len > 2 and src[0] == '#' and src[1] == '!') { 787 | var i: usize = 2; 788 | while (i < src.len and src[i] != '\n') { 789 | i += 1; 790 | } 791 | cleanSrc = src[i..]; 792 | } 793 | const result = fy.run(cleanSrc); 794 | allocator.free(src); 795 | if (result) |_| { 796 | //std.debug.print("{d}\n", .{r}); 797 | } else |err| { 798 | std.debug.print("error: {}\n", .{err}); 799 | } 800 | } 801 | 802 | pub fn dumpImage(fy: *Fy) !void { 803 | const path = "fy.out"; 804 | const file = try std.fs.cwd().createFile(path, .{}); 805 | defer file.close(); 806 | _ = try file.write(fy.image.mem); 807 | } 808 | 809 | pub fn main() !void { 810 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 811 | const allocator = gpa.allocator(); 812 | defer { 813 | _ = gpa.deinit(); 814 | } 815 | 816 | var argIt = try std.process.ArgIterator.initWithAllocator(allocator); 817 | _ = argIt.skip(); // skip the program name 818 | var parsedArgs = try Args.parseArgs(allocator, &argIt); 819 | defer { 820 | parsedArgs.deinit(); 821 | argIt.deinit(); 822 | } 823 | 824 | if (parsedArgs.help) { 825 | std.debug.print("Usage: fy [options] [files]\n", .{}); 826 | std.debug.print("Options:\n", .{}); 827 | std.debug.print(" -e, --eval Evaluate expr\n", .{}); 828 | std.debug.print(" -r, --repl Launch interactive REPL\n", .{}); 829 | std.debug.print(" -i, --image Dump executable memory image to fy.out\n", .{}); 830 | std.debug.print(" -v, --version Display version and exit\n", .{}); 831 | std.debug.print(" -h, --help Display this help and exit\n", .{}); 832 | return; 833 | } 834 | 835 | if (parsedArgs.version) { 836 | std.debug.print("fy {s}\n", .{Fy.version}); 837 | return; 838 | } 839 | 840 | if (parsedArgs.eval == null and parsedArgs.files == 0) { 841 | parsedArgs.repl = true; 842 | } 843 | 844 | var fy = Fy.init(allocator); 845 | defer { 846 | fy.deinit(); 847 | } 848 | 849 | if (parsedArgs.files > 0) { 850 | for (parsedArgs.other_args.items) |file| { 851 | //std.debug.print("file: {s}\n", .{file}); 852 | try runFile(allocator, &fy, file); 853 | } 854 | } 855 | 856 | if (parsedArgs.eval) |e| { 857 | //std.debug.print("eval: '{s}'\n", .{e}); 858 | const result = fy.run(e); 859 | if (result) |r| { 860 | std.debug.print("{d}\n", .{r}); 861 | } else |err| { 862 | std.debug.print("error: {}\n", .{err}); 863 | } 864 | } 865 | 866 | if (parsedArgs.repl) { 867 | //std.debug.print("repl\n", .{}); 868 | return repl(allocator, &fy); 869 | } 870 | 871 | if (parsedArgs.image) { 872 | try dumpImage(&fy); 873 | } 874 | return; 875 | } 876 | 877 | // Tests below 878 | 879 | const TestCase = struct { 880 | input: []const u8, 881 | expected: Fy.Value, 882 | 883 | fn run(self: *const TestCase, fy: *Fy) !void { 884 | std.debug.print("\nfy> {s}\n", .{self.input}); 885 | const input = self.input; 886 | const result = try fy.run(input); 887 | std.debug.print("exp {d}\n {d}\n", .{ self.expected, result }); 888 | try std.testing.expectEqual(self.expected, result); 889 | } 890 | }; 891 | 892 | fn runCases(fy: *Fy, testCases: []const TestCase) !void { 893 | for (testCases) |testCase| { 894 | try testCase.run(fy); 895 | } 896 | } 897 | 898 | test "Basic expressions and built-in words" { 899 | var fy = Fy.init(std.testing.allocator); 900 | defer fy.deinit(); 901 | 902 | try runCases(&fy, &[_]TestCase{ 903 | .{ .input = "", .expected = 0 }, // 904 | .{ .input = "1", .expected = 1 }, 905 | .{ .input = "-1", .expected = -1 }, 906 | .{ .input = "1 2", .expected = 2 }, 907 | .{ .input = "1 2 +", .expected = 3 }, 908 | .{ .input = "10 -10 +", .expected = 0 }, 909 | .{ .input = "-5 0 - 6 +", .expected = 1 }, 910 | .{ .input = "1 2 -", .expected = -1 }, 911 | .{ .input = "1 2 !-", .expected = 1 }, 912 | .{ .input = "2 2 *", .expected = 4 }, 913 | .{ .input = "12 3 /", .expected = 4 }, 914 | .{ .input = "12 5 &", .expected = 4 }, 915 | .{ .input = "1 2 =", .expected = 0 }, 916 | .{ .input = "1 1 =", .expected = 1 }, 917 | .{ .input = "1 2 !=", .expected = 1 }, 918 | .{ .input = "1 1 !=", .expected = 0 }, 919 | .{ .input = "1 2 >", .expected = 0 }, 920 | .{ .input = "2 1 >", .expected = 1 }, 921 | .{ .input = "1 2 <", .expected = 1 }, 922 | .{ .input = "2 1 <", .expected = 0 }, 923 | .{ .input = "1 2 >=", .expected = 0 }, 924 | .{ .input = "2 1 >=", .expected = 1 }, 925 | .{ .input = "2 2 >=", .expected = 1 }, 926 | .{ .input = "1 2 <=", .expected = 1 }, 927 | .{ .input = "2 1 <=", .expected = 0 }, 928 | .{ .input = "2 2 <=", .expected = 1 }, 929 | .{ .input = "2 dup", .expected = 2 }, 930 | .{ .input = "2 3 swap", .expected = 2 }, 931 | .{ .input = "2 3 over", .expected = 2 }, 932 | .{ .input = "2 3 4 5 over2", .expected = 3 }, 933 | .{ .input = "2 3 nip", .expected = 3 }, 934 | .{ .input = "2 3 tuck", .expected = 3 }, 935 | .{ .input = "2 3 drop", .expected = 2 }, 936 | .{ .input = "2 1+ 4 1- =", .expected = 1 }, 937 | .{ .input = "depth", .expected = 0 }, 938 | .{ .input = "5 6 7 8 depth", .expected = 4 }, 939 | }); 940 | } 941 | 942 | test "User defined words" { 943 | var fy = Fy.init(std.testing.allocator); 944 | defer fy.deinit(); 945 | 946 | try runCases(&fy, &[_]TestCase{ 947 | .{ .input = ": sqr dup * ;", .expected = 0 }, 948 | .{ .input = "2 sqr", .expected = 4 }, 949 | .{ .input = ":sqr dup *; 2 sqr", .expected = 4 }, 950 | .{ .input = ": sqr dup * ; 2 sqr", .expected = 4 }, 951 | .{ .input = ":a 1; a a +", .expected = 2 }, 952 | .{ .input = ": a 2 +; :b 3 +; 1 a b 6 =", .expected = 1 }, 953 | .{ .input = "1 a b", .expected = 6 }, 954 | .{ .input = "2 dup :dup *; dup", .expected = 4 }, // warning: this breaks dup in this Fy instance forever 955 | }); 956 | } 957 | 958 | test "Quotes" { 959 | var fy = Fy.init(std.testing.allocator); 960 | defer fy.deinit(); 961 | 962 | try runCases(&fy, &[_]TestCase{ 963 | .{ 964 | .input = "2 [dup +] do", // 965 | .expected = 4, 966 | }, 967 | .{ .input = "[dup +] 3 swap do", .expected = 6 }, 968 | .{ .input = ":dup+ [dup +]; 5 dup+ do", .expected = 10 }, 969 | .{ .input = "10 dup+ do", .expected = 20 }, 970 | .{ .input = "2 [2 *] over over do over do nip nip", .expected = 8 }, 971 | .{ .input = "[2 *] 1 [1 +] do swap do", .expected = 4 }, 972 | .{ .input = "2 4 [spy 30 .] dotimes", .expected = 2 }, 973 | .{ .input = "5 [4 [spy] dotimes] dotimes", .expected = 0 }, 974 | .{ .input = "100 50 > [5 5 10 > [7] do?] do?", .expected = 5 }, 975 | .{ .input = "2 3 over over < [*] do?", .expected = 6 }, 976 | .{ .input = "3 dup 1 > [3 *] [3 /] ifte", .expected = 9 }, 977 | .{ .input = "10 [dup 5 <= [1 .] [0 .] ifte] dotimes", .expected = 0 }, 978 | .{ .input = "[1 2 3] do + + 1 2 3 + + =", .expected = 1 }, 979 | .{ .input = "2 3 \\* do", .expected = 6 }, 980 | }); 981 | } 982 | 983 | test "Print functions compile" { 984 | var fy = Fy.init(std.testing.allocator); 985 | defer fy.deinit(); 986 | 987 | _ = try fy.run("1 ."); // Test print 988 | _ = try fy.run("1 .hex"); // Test printHex 989 | _ = try fy.run("1 . .nl 2 ."); // Test printNewline 990 | _ = try fy.run("65 .c .nl"); // Test printChar 991 | _ = try fy.run("1 spy"); // Test spy 992 | } 993 | 994 | test "Comments are ignored" { 995 | var fy = Fy.init(std.testing.allocator); 996 | defer fy.deinit(); 997 | 998 | try runCases(&fy, &[_]TestCase{ 999 | .{ .input = "( Comment before code ) 1 2 +", .expected = 3 }, 1000 | .{ .input = "1 2 + ( Comment ) ( Another comment )", .expected = 3 }, 1001 | .{ .input = "1 ( Comment ) 2 +", .expected = 3 }, 1002 | .{ .input = "(Comment before code) 1 2 +", .expected = 3 }, 1003 | .{ .input = "1 2 + ( Co(mm)ent ) (Another comment )", .expected = 3 }, 1004 | .{ .input = "1 ( Comment) 2 +", .expected = 3 }, 1005 | }); 1006 | } 1007 | 1008 | test "Character literals" { 1009 | var fy = Fy.init(std.testing.allocator); 1010 | defer fy.deinit(); 1011 | 1012 | try runCases(&fy, &[_]TestCase{ 1013 | .{ .input = "'a", .expected = 'a' }, 1014 | .{ .input = "'b 'c +", .expected = 'b' + 'c' }, 1015 | .{ .input = "'0 '9 +", .expected = '0' + '9' }, 1016 | .{ .input = "'x 'y swap", .expected = 'x' }, 1017 | .{ .input = "'z 1 +", .expected = 'z' + 1 }, 1018 | .{ .input = "'a 'a =", .expected = 1 }, 1019 | .{ .input = "'a 'b !=", .expected = 1 }, 1020 | .{ .input = "'m 'n >", .expected = 0 }, 1021 | .{ .input = "'p 'o <", .expected = 0 }, 1022 | .{ .input = "''", .expected = '\'' }, 1023 | .{ .input = "' ' drop", .expected = ' ' }, 1024 | .{ .input = "'a'b swap", .expected = 'a' }, 1025 | }); 1026 | } 1027 | --------------------------------------------------------------------------------