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());
// }