const std = @import("std"); const Linenoise = @import("linenoize").Linenoise; const Scanner = @import("./Scanner.zig"); const Token = @import("./Token.zig"); const Error = @import("./Error.zig"); const readline = @cImport({ @cInclude("stdio.h"); @cInclude("stdlib.h"); @cInclude("readline/readline.h"); @cInclude("readline/history.h"); }); pub fn main() !u8 { var gen_alloc = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 128, }){}; defer std.debug.assert(!gen_alloc.deinit()); const gpa = gen_alloc.allocator(); const args = try std.process.argsAlloc(gpa); defer std.process.argsFree(gpa, args); switch (args.len) { 1 => { try runPrompt(gpa); }, 2 => { return runFile(gpa, args[1]); }, else => { std.debug.print("Usage: jlox [script]\n", .{}); }, } return 0; } // No Mutex shenanigans, as there is no async yet. // var stdout_mutex: std.event.Lock = .{}; pub fn print(comptime fmt: []const u8, args: anytype) !void { var bw = std.io.bufferedWriter(std.io.getStdOut().writer()); const stdout = bw.writer(); stdout.print(fmt, args) catch return; try bw.flush(); } pub fn readLine(allocator: std.mem.Allocator) !?[]const u8 { var br = std.io.bufferedReader(std.io.getStdIn().reader()); const stdin = br.reader(); return stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', std.math.maxInt(u8)); } fn init_readline() !void { readline.rl_completion_entry_function = &lox_completion; // if (readline.rl_bind_key('\t', readline.rl_insert) != 0) { // return error.ReadlineInitError; // } } fn lox_completion(text: [*c]const u8, state: c_int) callconv(.C) [*c]u8 { const alloc = std.heap.c_allocator; const textptr = @ptrCast(?[*:0]const u8, text); if (textptr) |curr| { var slice = lox_completion_internal(alloc, std.mem.span(curr), state) catch return null; if (slice) |comp| { // Assume that C will properly deallocate our given pointers? var ret = alloc.allocSentinel(u8, comp.len, 0) catch @panic("OOM"); std.mem.copy(u8, ret, comp); return ret; } return null; } else { // Could not cast Readline text to proper pointer, so it's unsafe, so exit. return null; } } const CompletionDbGoal = enum { // General Name, Value, // Syntax Equal, }; const CompletionDbEntry = struct { token: Token.Type, goal: CompletionDbGoal, }; const completion_db = [_]CompletionDbEntry{ .{ .token = .kw_var, .goal = .Name }, // recent names that have been used .{ .token = .identifier, .goal = .Equal }, .{ .token = .equal, .goal = .Value }, // recent values that have been used }; var completion_db_index: usize = 0; fn lox_completion_internal(allocator: std.mem.Allocator, text: []const u8, state: c_int) !?[]const u8 { // So we are only given a single // std.log.debug("{d}", .{state}); if (state == 0) { completion_db_index = 0; } // Lex the text, then check the last token // Can't init Scanner? No completion. var scanner = Scanner.init(allocator, text) catch return null; defer scanner.deinit(); Error.can_output = false; defer Error.can_output = true; // Error in scanning tokens? No completion. const tokens = scanner.scanTokens() catch return null; defer { for (tokens) |token| { allocator.free(token.lexeme); } allocator.free(tokens); } const lastToken = tokens[tokens.len -| 2]; // Last Token before EOF var output = std.ArrayList(u8).init(allocator); defer output.deinit(); const writer = output.writer(); while (completion_db_index < completion_db.len) : (completion_db_index += 1) { const entry = completion_db[completion_db_index]; if (lastToken.token_type == entry.token) { completion_db_index += 1; // Bump so that next call doesn't find us. switch (entry.goal) { .Name => @panic("TODO get identifiers in scope?"), .Equal => try writer.print("{s} =", .{text}), .Value => @panic("TODO get values that have been used"), } return try output.toOwnedSlice(); } } return null; } var line_read: ?[*:0]u8 = null; fn rl_gets(prompt: []const u8) !?[]const u8 { if (line_read) |_| { std.c.free(@ptrCast(?*anyopaque, line_read)); line_read = null; } line_read = readline.readline(@ptrCast([*c]const u8, prompt)); if (line_read) |read_line| { readline.add_history(read_line); return std.mem.span(read_line); } return null; } // TODO Make environment static, so that completion can access fn runPrompt(allocator: std.mem.Allocator) !void { try init_readline(); while (true) { // Quit on errors var line = try rl_gets("> "); // Handle EOF gracefully, and dealloc each line when done. // TODO Maybe store lines if they don't form a full statement, until they do. if (line) |uline| { if (uline.len == 0) { break; } try run(allocator, uline); } else { break; } } } fn runFile(allocator: std.mem.Allocator, file_name: [:0]u8) !u8 { std.debug.print("Running file {s}\n", .{file_name}); const cwd = std.fs.cwd(); const file = try cwd.openFileZ(file_name, .{ .mode = .read_only }); const contents = try file.readToEndAlloc(allocator, std.math.maxInt(u32)); defer allocator.free(contents); try run(allocator, contents); if (Error.errored) { return 65; } return 0; } fn run(allocator: std.mem.Allocator, source: []const u8) !void { // std.debug.print("{s}\n", .{source}); var scanner = try Scanner.init(allocator, source); defer scanner.deinit(); const tokens = try scanner.scanTokens(); defer { for (tokens) |token| { allocator.free(token.lexeme); } allocator.free(tokens); } for (tokens) |token| { std.debug.print("[line {d}] {any}\n", .{ token.line, token }); } } // test "simple test" { // var list = std.ArrayList(i32).init(std.testing.allocator); // defer list.deinit(); // try commenting this out and see if zig detects the memory leak! // try list.append(42); // try std.testing.expectEqual(@as(i32, 42), list.pop()); // }