diff --git a/CMakeLists.txt b/CMakeLists.txt index 11bbe0f..98dd6db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources(vm_lib src/opcode.cpp src/fio.cpp src/stdlib.cpp + src/lineedit.cpp PUBLIC FILE_SET HEADERS @@ -41,6 +42,7 @@ target_sources(vm_lib src/opcode.hpp src/fio.hpp src/stdlib.hpp + src/lineedit.hpp ) add_executable(vli src/vli.cpp) diff --git a/src/common.hpp b/src/common.hpp index 95af76b..c1ccaae 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -204,7 +204,7 @@ class Array : public Object { return Array(TRY(MkGcRoot(pod))); } - Result slice(uint64_t start, uint64_t end) { + Result slice(uint64_t start, uint64_t end) const { if (start > end) return ERROR(IndexOutOfRange); uint64_t res_size = end - start; auto pod = TRY(arena_alloc(res_size * sizeof(PodObject*))); @@ -315,7 +315,7 @@ class ByteArray : public Object { return ByteArray(TRY(MkGcRoot(pod))); } - Result slice(uint64_t start, uint64_t end) { + Result slice(uint64_t start, uint64_t end) const { if (start > end) return ERROR(IndexOutOfRange); uint64_t res_size = end - start; auto pod = TRY(arena_alloc(res_size * sizeof(char))); @@ -510,7 +510,7 @@ class String : public Object { return String(TRY(MkGcRoot(pod))); } - Result slice(uint64_t start, uint64_t end) { + Result slice(uint64_t start, uint64_t end) const { if (start > end) return ERROR(IndexOutOfRange); uint64_t res_size = end - start; auto pod = TRY(arena_alloc(res_size * sizeof(char32_t))); diff --git a/src/error.hpp b/src/error.hpp index dced58c..e3e68e1 100644 --- a/src/error.hpp +++ b/src/error.hpp @@ -16,6 +16,7 @@ enum class ErrorCode { CompilationError, ArgumentCountMismatch, IOError, + Interrupt, }; void seterr(const char* err); diff --git a/src/lineedit.cpp b/src/lineedit.cpp new file mode 100644 index 0000000..efb552a --- /dev/null +++ b/src/lineedit.cpp @@ -0,0 +1,394 @@ +#include "lineedit.hpp" + +#include +#include +#include + +#include + +#include "common.hpp" +#include "fio.hpp" + +enum class TtyEscape { + Null = 0, + CtrlA = 1, + CtrlB = 2, + CtrlC = 3, + CtrlD = 4, + CtrlE = 5, + CtrlF = 6, + CtrlH = 8, + Tab = 9, + CtrlK = 11, + CtrlL = 12, + Enter = 13, + CtrlN = 14, + CtrlP = 16, + CtrlT = 20, + CtrlU = 21, + CtrlW = 23, + Esc = 27, + Backspace = 127, +}; + +static struct termios orig_termios; +static int rawmode = 0; + +template +class CharBuf { + public: + CharBuf() : _len(0) {} + + Result append(char c) { + if (_len + 1 >= buflen) return ERROR(IndexOutOfRange); + _buf[_len] = c; + _len++; + + return Result(); + } + Result append(const char* s) { + uint64_t slen = strlen(s); + if (_len + slen >= buflen) return ERROR(IndexOutOfRange); + + memcpy(_buf + _len, s, slen); + _len += slen; + + return Result(); + } + + uint64_t len() { return _len; } + const char* buf() { return _buf; } + + private: + char _buf[buflen]; + uint64_t _len; +}; + +Result enableRawMode(int fd) { + struct termios raw; + + if (!isatty(STDIN_FILENO)) return ERROR(IOError); + if (tcgetattr(fd, &orig_termios) == -1) return ERROR(IOError); + + raw = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd, TCSAFLUSH, &raw) < 0) return ERROR(IOError); + rawmode = 1; + + return Result(); +} + +void disableRawMode(int fd) { + /* Don't even check the return value as it's too late. */ + if (rawmode && tcsetattr(fd, TCSAFLUSH, &orig_termios) != -1) rawmode = 0; +} + +class RawModeGuard { + public: + RawModeGuard() : fd(-1) {} + RawModeGuard(int fd) : fd(fd) {} + RawModeGuard(RawModeGuard&& rhs) { + fd = rhs.fd; + rhs.fd = -1; + } + RawModeGuard(const RawModeGuard&) = delete; + ~RawModeGuard() { + if (fd >= 0) disableRawMode(fd); + } + + static Result create(int fd) { + auto res = RawModeGuard(fd); + TRY(enableRawMode(fd)); + + return res; + } + + private: + int fd; +}; + +RawModeGuard raw_mode_guard(STDIN_FILENO); + +struct RefreshState { + uint64_t num_rows = 0; + uint64_t cursor_pos = 0; +}; + +class LineEdit { + public: + LineEdit() + : prompt(""), + cursor_pos(0), + line_len(0), + term_width(0), + input_fd(STDIN_FILENO), + output_fd(STDOUT_FILENO) { + buf[0] = 0; + } + + LineEdit(const char* prompt, int input_fd = STDIN_FILENO, + int output_fd = STDOUT_FILENO) + : prompt(prompt), + cursor_pos(0), + line_len(0), + term_width(0), + input_fd(input_fd), + output_fd(output_fd) { + buf[0] = 0; + } + + LineEdit(const LineEdit& rhs) { + prompt = rhs.prompt; + memcpy(buf, rhs.buf, buflen); + cursor_pos = rhs.cursor_pos; + line_len = rhs.line_len; + term_width = rhs.term_width; + input_fd = rhs.input_fd; + output_fd = rhs.output_fd; + last_refresh = rhs.last_refresh; + } + + static Result create(const char* prompt) { + LineEdit res(prompt); + + TRY(res.init()); + + return res; + } + + Result get_term_width() { + winsize ws; + + if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + /* ioctl() failed. Try to query the terminal itself. */ + int start, cols; + + /* Get the initial position so we can restore it later. */ + start = TRY(get_cursor_column()); + + /* Go to right margin and get position. */ + if (write(output_fd, "\x1b[999C", 6) != 6) return ERROR(IOError); + cols = TRY(get_cursor_column()); + + /* Restore position. */ + if (cols > start) { + char seq[32]; + snprintf(seq, 32, "\x1b[%dD", cols - start); + if (write(output_fd, seq, strlen(seq)) == -1) { + return ERROR(IOError); + } + } + return cols; + } else { + return ws.ws_col; + } + } + + Result get_cursor_column() { + char buf[32]; + int cols, rows; + unsigned int i = 0; + + /* Report cursor location */ + if (write(output_fd, "\x1b[6n", 4) != 4) return -1; + + /* Read the response: ESC [ rows ; cols R */ + while (i < sizeof(buf) - 1) { + if (read(input_fd, buf + i, 1) != 1) break; + if (buf[i] == 'R') break; + i++; + } + buf[i] = '\0'; + + /* Parse it. */ + if (buf[0] != (char)TtyEscape::Esc || buf[1] != '[') return -1; + if (sscanf(buf + 2, "%d;%d", &rows, &cols) != 2) return -1; + return cols; + } + + Result read_one() { + { + auto guard = TRY(RawModeGuard::create(input_fd)); + if (write(output_fd, prompt, strlen(prompt)) == -1) return ERROR(IOError); + + while (TRY(step())); + } + printf("\n"); + + auto res = TRY(String::create(buf)); + + return res; + } + + Result step() { + char c; + int nread; + char seq[3]; + + nread = read(input_fd, &c, 1); + if (nread <= 0) return false; + + switch (TtyEscape(c)) { + case TtyEscape::Enter: + return false; + break; + case TtyEscape::Backspace: + TRY(handle_backspace()); + break; + case TtyEscape::CtrlC: + return ERROR(Interrupt); + default: + TRY(handle_regular_char(c)); + break; + } + return true; + } + + Result handle_regular_char(char c) { + if (line_len >= buflen) return Result(); + + if (line_len == cursor_pos) { + buf[cursor_pos] = c; + cursor_pos++; + line_len++; + buf[cursor_pos] = 0; + + // if (strlen(prompt) + line_len < term_width) { + // if (write(output_fd, &c, 1) == -1) return ERROR(IOError); + // } else { + TRY(refresh_line()); + //} + } else { + memmove(buf + cursor_pos + 1, buf + cursor_pos, line_len - cursor_pos); + buf[cursor_pos] = c; + cursor_pos++; + line_len++; + buf[line_len] = 0; + + TRY(refresh_line()); + } + return Result(); + } + + Result handle_backspace() { + if (cursor_pos == 0) return Result(); + memmove(buf + cursor_pos - 1, buf + cursor_pos, line_len - cursor_pos); + cursor_pos--; + line_len--; + buf[line_len] = 0; + + TRY(refresh_line()); + + return Result(); + } + + Result refresh_line() { + char seq[64]; + CharBuf<1024> cbuf; + + RefreshState new_refresh = last_refresh; + + uint64_t prompt_len = strlen(prompt); + + uint64_t num_rows = (prompt_len + line_len + term_width - 1) / term_width; + uint64_t cursor_row = + (prompt_len + last_refresh.cursor_pos + term_width) / term_width; + uint64_t new_relative_row = 0; + + new_refresh.num_rows = num_rows; + new_refresh.cursor_pos = cursor_pos; + + // Go to last row + if (last_refresh.num_rows - cursor_row > 0) { + snprintf(seq, 64, "\x1b[%luB", last_refresh.num_rows - cursor_row); + TRY(cbuf.append(seq)); + } + + // Clear all rows one by one (except the first one) + if (last_refresh.num_rows > 1) { + for (uint64_t i = 0; i < last_refresh.num_rows - 1; i++) { + snprintf(seq, 64, "\r\x1b[0K\x1b[1A"); + TRY(cbuf.append(seq)); + } + } + + // Clear the first line + snprintf(seq, 64, "\r\x1b[0K"); + TRY(cbuf.append(seq)); + + // Write the prompt + cbuf.append(prompt); + + // Write the buffer content + cbuf.append(buf); + + if (cursor_pos > 0 && cursor_pos == line_len && + (cursor_pos + prompt_len) % term_width == 0) { + cbuf.append("\n"); + cbuf.append("\r"); + num_rows++; + if (num_rows > last_refresh.num_rows) new_refresh.num_rows = num_rows; + } + + cursor_row = (prompt_len + cursor_pos + term_width) / term_width; + + // Go up to the cursor row + if (num_rows - cursor_row > 0) { + snprintf(seq, 64, "\x1b[%luA", num_rows - cursor_row); + TRY(cbuf.append(seq)); + } + + // Go to the cursor column + uint64_t col = (prompt_len + cursor_pos) % term_width; + if (col > 0) { + snprintf(seq, 64, "\r\x1b[%luC", col); + TRY(cbuf.append(seq)); + } else { + TRY(cbuf.append("\r")); + } + + if (write(output_fd, cbuf.buf(), cbuf.len()) == -1) return ERROR(IOError); + + last_refresh = new_refresh; + return Result(); + } + + private: + Result init() { + term_width = TRY(get_term_width()); + + if (!stdin_isatty()) return ERROR(IOError); + return Result(); + } + + const char* prompt; + static const uint64_t buflen = 1024; + char buf[buflen + 1]; + uint64_t cursor_pos; + uint64_t line_len; + uint64_t term_width; + int input_fd; + int output_fd; + RefreshState last_refresh; +}; + +Result read_line(const char* prompt) { + auto rl = TRY(LineEdit::create(prompt)); + + return rl.read_one(); +} diff --git a/src/lineedit.hpp b/src/lineedit.hpp new file mode 100644 index 0000000..75583a2 --- /dev/null +++ b/src/lineedit.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include "result.hpp" + +class String; +Result read_line(const char* prompt); diff --git a/src/reader.cpp b/src/reader.cpp index 492d0f9..410ae60 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -409,12 +409,12 @@ bool Reader::forward_exponent() { return forward_decimal_number(); } -Result read_one(Value& value) { +Result read_one(const Value& value) { if (!value.is()) return ERROR(TypeMismatch); auto r = Reader(*value.to()); return r.read_one(); } -Result read_one(String& value) { +Result read_one(const String& value) { auto r = Reader(value); return r.read_one(); } @@ -424,7 +424,7 @@ Result read_one(const char* value) { return r.read_one(); } -Result read_multiple(String& value) { +Result read_multiple(const String& value) { auto r = Reader(value); return r.read_multiple(); } diff --git a/src/reader.hpp b/src/reader.hpp index eeba149..da0954c 100644 --- a/src/reader.hpp +++ b/src/reader.hpp @@ -5,7 +5,7 @@ class Reader { public: - Reader(String& str) : _str(str) {} + Reader(const String& str) : _str(str) {} Result read_one(); Result read_multiple(); @@ -38,12 +38,12 @@ class Reader { bool match(const char* str); bool match(char c); - String& _str; + const String& _str; SourcePosition position_{1, 1, 0}; }; -Result read_one(Value& value); -Result read_one(String& value); +Result read_one(const Value& value); +Result read_one(const String& value); Result read_one(const char* value); -Result read_multiple(String& value); +Result read_multiple(const String& value); diff --git a/src/vli.cpp b/src/vli.cpp index adacd1d..ed516c8 100644 --- a/src/vli.cpp +++ b/src/vli.cpp @@ -5,44 +5,51 @@ #include "compiler.hpp" #include "die.hpp" #include "fio.hpp" +#include "lineedit.hpp" #include "reader.hpp" #include "vm.hpp" #include "writer.hpp" StaticArena<64 * 1024 * 1024> arena; -Result run(int argc, const char* argv[]) { - String src = DIEX(String::create("")); - if (argc == 1) { - if (stdin_isatty()) { - die("Code expected at stdin, not a tty.\n"); - } - src = TRY(read_stdin()); - } else { - src = TRY(read_file(argv[1])); - } - - // auto code_str = TRY(String::create("(* (+ 1 2 3) (/ 4 2))")); - // auto code_str = - // TRY(String::create("((lambda (f y) (f y)) (lambda (x) (* x x)) 2)")); - +Result run_string(const String& src) { auto parsed = TRY(read_multiple(src)); - // auto code_str_written = TRY(write_multiple(parsed)); TRY(arena_gc()); - // TRY(debug_print(code_str_written)); - auto compiled = TRY(compile(parsed)); Module& mod = *compiled.to(); - // TRY(debug_print(TRY(mod.globals()))); - auto vm = TRY(VM::create()); auto res = TRY(vm.run(mod)); - // TRY(debug_print(res)); + return res; +} + +Result run_repl() { + while (true) { + auto src = TRY(read_line("vli> ")); + debug_print(src); + auto res = TRY(run_string(src)); + debug_print(res); + } + + return Result(); +} + +Result run(int argc, const char* argv[]) { + String src = DIEX(String::create("")); + if (argc == 1) { + if (stdin_isatty()) { + return run_repl(); + } + src = TRY(read_stdin()); + TRY(run_string(src)); + } else { + src = TRY(read_file(argv[1])); + TRY(run_string(src)); + } return Result(); }