const std = @import("std"); const langs = @import("lang.zig"); const runners = @import("runner.zig"); const printer = @import("printer.zig"); test "scritcher" { _ = @import("lang.zig"); _ = @import("runner.zig"); } const readline = @cImport({ @cInclude("stdio.h"); @cInclude("stdlib.h"); @cInclude("readline/readline.h"); @cInclude("readline/history.h"); }); fn wrapInCmdList(allocator: *std.mem.Allocator, cmd: langs.Command) !langs.CommandList { var cmds = langs.CommandList.init(allocator); try cmds.append(cmd); return cmds; } pub fn doRepl(allocator: *std.mem.Allocator, args_it: var) !void { var stdout_file = std.io.getStdOut(); const stdout = &stdout_file.outStream(); const scri_path = try (args_it.next(allocator) orelse @panic("expected scri path")); var file_read_opt: ?std.fs.File = std.fs.cwd().openFile(scri_path, .{}) catch |err| blk: { if (err == error.FileNotFound) break :blk null; return err; }; const total_bytes = if (file_read_opt) |file_read| try file_read.getEndPos() else 0; var cmds = langs.CommandList.init(allocator); defer cmds.deinit(); var lang = langs.Lang.init(allocator); defer lang.deinit(); if (total_bytes > 0) { // this MUST BE long lived (a reference to it is kept inside // existing_cmds, and then passed along to cmds), // we can't defer them here var scri_existing = try allocator.alloc(u8, total_bytes); _ = try file_read_opt.?.read(scri_existing); // we can defer this because we copy the Command structs back to cmds var existing_cmds = try lang.parse(scri_existing); defer existing_cmds.deinit(); // copy the existing command list into the repl's command list for (existing_cmds.items) |existing_cmd| { try cmds.append(existing_cmd); } } else { // if there isn't any commands on the file, we load our default // 'load :0' command // TODO: deliberate memleak here. we only allocate this // command once, for the start of the file, so. var load_cmd = try allocator.create(langs.Command.Load); std.mem.copy(u8, load_cmd.path, ":0"); load_cmd.base.tag = langs.Command.Tag.load; // taking address is fine, because load_cmd lives in the lifetime // of the allocator. try cmds.append(&load_cmd.base); } if (file_read_opt) |file_read| { file_read.close(); } var file = try std.fs.cwd().openFile(scri_path, .{ .write = true, .read = false, }); defer file.close(); var out = file.outStream(); var stream = &out; // since we opened the file for writing, it becomes empty, so, to ensure // we don't fuck up later on, we print cmds before starting the repl try printer.printList(cmds, stdout); try printer.printList(cmds, stream); // we keep // - a CommandList with the full commands we have right now // - a Command with the current last typed successful command // - one runner that contains the current full state of the image // as if the current cmds was ran over it (TODO better wording) // - one runner that gets copied from the original on every new // command the user issues var current: langs.Command = undefined; var runner = runners.Runner.init(allocator, true); defer runner.deinit(); // run the load command try runner.runCommands(cmds, true); var runqs_args = langs.ArgList.init(allocator); defer runqs_args.deinit(); const wanted_runner: []const u8 = std.os.getenv("SCRITCHER_RUNNER") orelse "ristretto"; try runqs_args.append(wanted_runner); while (true) { lang.reset(); var rd_line = readline.readline("> "); if (rd_line == null) { std.debug.warn("leaving from eof\n", .{}); break; } readline.add_history(rd_line); //defer std.heap.c_allocator.destroy(rd_line); var line = rd_line[0..std.mem.len(rd_line)]; if (std.mem.eql(u8, line, "push")) { try cmds.append(current); // run the current added command to main cmds list // with the main parent runner var cmds_wrapped = try wrapInCmdList(allocator, current); defer cmds_wrapped.deinit(); try runner.runCommands(cmds_wrapped, true); continue; } else if (std.mem.eql(u8, line, "list")) { try printer.printList(cmds, stdout); continue; } else if (std.mem.eql(u8, line, "save")) { // seek to 0 instead of appending the new command // NOTE appending single command might be faster try file.seekTo(0); try printer.printList(cmds, stream); continue; } else if (std.mem.eql(u8, line, "quit") or std.mem.eql(u8, line, "q")) { std.debug.warn("leaving\n", .{}); break; } else if (std.mem.startsWith(u8, line, "#")) { continue; } var cmds_parsed = lang.parse(line) catch |err| { std.debug.warn("repl: error while parsing: {}\n", .{err}); continue; }; current = cmds_parsed.items[0]; // by cloning the parent runner, we can iteratively write // whatever command we want and only commit the good results // back to the parent runner var runner_clone = try runner.clone(); defer runner_clone.deinit(); try cmds_parsed.append(langs.Command{ .command = .RunQS, .args = runqs_args, }); try runner_clone.runCommands(cmds_parsed, true); _ = try stdout.write("\n"); } } pub fn main() !void { const allocator = std.heap.page_allocator; var lang = langs.Lang.init(allocator); defer lang.deinit(); var runner = runners.Runner.init(allocator, false); defer runner.deinit(); var args_it = std.process.args(); // TODO print help _ = try (args_it.next(allocator) orelse @panic("expected exe name")); const scri_path = try (args_it.next(allocator) orelse @panic("expected scri path or 'repl'")); if (std.mem.eql(u8, scri_path, "repl")) { return try doRepl(allocator, &args_it); } var file = try std.fs.cwd().openFile(scri_path, .{}); defer file.close(); // sadly, we read it all into memory. such is life const total_bytes = try file.getEndPos(); var data = try allocator.alloc(u8, total_bytes); defer allocator.free(data); _ = try file.read(data); var cmds = try lang.parse(data); defer cmds.deinit(); try runner.runCommands(cmds, true); }