From 1ad836cedbe2aeabdd919159ca89e5cbb8cc9dbd Mon Sep 17 00:00:00 2001 From: fubark Date: Fri, 12 Apr 2024 19:55:07 -0400 Subject: [PATCH] Implement the REPL for the CLI. --- build.zig | 51 +- docs/docs.md | 72 +- lib/linenoise/linenoise.c | 1349 +++++++++++++++++++++++++++++++++++ lib/linenoise/linenoise.h | 113 +++ lib/linenoise/linenoise.zig | 7 + src/bc_gen.zig | 17 +- src/capi.zig | 1 + src/cli.zig | 8 +- src/compiler.zig | 109 ++- src/debug.zig | 172 +++++ src/include/cyber.h | 4 + src/lib.zig | 7 +- src/main.zig | 172 ++++- src/parser.zig | 56 +- src/sema.zig | 24 +- src/value.zig | 52 -- src/vm.h | 2 +- src/vm.zig | 40 +- test/setup.zig | 1 + 19 files changed, 2101 insertions(+), 156 deletions(-) create mode 100644 lib/linenoise/linenoise.c create mode 100644 lib/linenoise/linenoise.h create mode 100644 lib/linenoise/linenoise.zig diff --git a/build.zig b/build.zig index 9ad49ab62..b9a7b420c 100644 --- a/build.zig +++ b/build.zig @@ -20,6 +20,7 @@ var optRT: ?config.Runtime = undefined; var stdx: *std.build.Module = undefined; var tcc: *std.build.Module = undefined; +var linenoise: *std.build.Module = undefined; var mimalloc: *std.build.Module = undefined; pub fn build(b: *std.build.Builder) !void { @@ -42,6 +43,7 @@ pub fn build(b: *std.build.Builder) !void { }); tcc = tcc_lib.createModule(b); mimalloc = mimalloc_lib.createModule(b); + linenoise = createLinenoiseModule(b); { const step = b.step("cli", "Build main cli."); @@ -303,6 +305,15 @@ pub fn buildAndLinkDeps(step: *std.build.Step.Compile, opts: Options) !void { step.stack_protector = false; } + if (opts.cli and opts.target.getOsTag() != .windows) { + addLinenoiseModule(step, "linenoise", linenoise); + buildAndLinkLinenoise(b, step); + } else { + step.addAnonymousModule("tcc", .{ + .source_file = .{ .path = thisDir() ++ "/src/nopkg.zig" }, + }); + } + if (opts.jit) { // step.addIncludePath(.{ .path = "/opt/homebrew/Cellar/llvm/17.0.1/include" }); // step.addLibraryPath(.{ .path = "/opt/homebrew/Cellar/llvm/17.0.1/lib" }); @@ -377,7 +388,7 @@ fn getDefaultOptions(target: std.zig.CrossTarget, optimize: std.builtin.Optimize fn createBuildOptions(b: *std.build.Builder, opts: Options) !*std.build.Step.Options { const buildTag = std.process.getEnvVarOwned(b.allocator, "BUILD") catch |err| b: { if (err == error.EnvironmentVariableNotFound) { - break :b "local"; + break :b "0"; } else { return err; } @@ -571,4 +582,40 @@ fn buildLib(b: *std.Build, opts: Options) !*std.build.Step.Compile { try buildAndLinkDeps(lib, opts); return lib; -} \ No newline at end of file +} + +pub fn createLinenoiseModule(b: *std.Build) *std.build.Module { + return b.createModule(.{ + .source_file = .{ .path = thisDir() ++ "/lib/linenoise/linenoise.zig" }, + }); +} + +pub fn addLinenoiseModule(step: *std.build.CompileStep, name: []const u8, mod: *std.build.Module) void { + step.addModule(name, mod); + step.addIncludePath(.{ .path = thisDir() ++ "/lib/linenoise" }); +} + +pub fn buildAndLinkLinenoise(b: *std.Build, step: *std.build.CompileStep) void { + const lib = b.addStaticLibrary(.{ + .name = "linenoise", + .target = step.target, + .optimize = step.optimize, + }); + lib.addIncludePath(.{ .path = thisDir() ++ "/lib/linenoise" }); + lib.linkLibC(); + // lib.disable_sanitize_c = true; + + var c_flags = std.ArrayList([]const u8).init(b.allocator); + + var sources = std.ArrayList([]const u8).init(b.allocator); + sources.appendSlice(&.{ + "/lib/linenoise/linenoise.c", + }) catch @panic("error"); + for (sources.items) |src| { + lib.addCSourceFile(.{ + .file = .{ .path = b.fmt("{s}{s}", .{thisDir(), src}) }, + .flags = c_flags.items, + }); + } + step.linkLibrary(lib); +} diff --git a/docs/docs.md b/docs/docs.md index d2da785a1..5c2ee990a 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -13,7 +13,7 @@ - [Metaprogramming.](#metaprogramming) - [Embedding.](#embedding) - [Memory.](#memory) -- [Backends.](#backends) +- [CLI.](#cli) @@ -3261,20 +3261,76 @@ print res['numCycFreed'] -- Output: 2 print res['numObjFreed'] -- Output: 2 ``` -# Backends. +# CLI. -* [JIT.](#jit) -* [AOT.](#aot) +* [REPL.](#repl) +* [JIT compiler.](#jit-compiler) +* [AOT compiler.](#aot-compiler) [^top](#table-of-contents) -## JIT. +## REPL. +The REPL is started by running the CLI without any arguments: +```bash +cyber +``` + +The REPL starts new sessions with [`use $global`](#use-global). This allows undeclared variables to be used similar to other dynamic languages: +```bash +> a = 123 +> a * 2 +`int` 246 +``` + +When the first input ends with `:`, the REPL will automatically indent the next line. To recede the indentation, provide an empty input. Once the indent returns to the beginning, the entire code block is submitted for evaluation: +```bash +> if true: + | print 'hello!' + | +hello! +``` + +Top level declarations such as imports, types, and functions can be referenced in subsequent evals: +```bash +> use math +> math.random() +`float` 0.3650744641604983 +``` + +```bash +> type Foo: + | a int + | +> f = Foo{a: 123} +> f.a +`int` 123 +``` + +Local variables **can not** be referenced in subsequent evals, since their scope ends with each eval input: +```bash +> let a = 123 +> a +panic: Variable is not defined in `$global`. + +input:1:1 main: +a +^ +``` + +## JIT compiler. Cyber's just-in-time compiler is incomplete and unstable. To run your script with JIT enabled: ```bash cyber -jit <script> ``` -The JIT compiler is just as fast as the bytecode generation so when it's enabled, the entire script is compiled from the start. +The goal of the JIT compiler is to be fast at compilation while still being significantly faster than the interpreter. The codegen involves stitching together pregenerated machine code that targets the same runtime stack slots used by the VM. This technique is also known as `copy-and-patch`. As the VM transitions to unboxed data types, the generated code will see more performance gains. + +## AOT compiler. +The ahead-of-time compiler generates a static or dynamic binary from Cyber source code. +This is done by first compiling Cyber code into C code, and using a C compiler to generate the final binary. +The user can specify the system's `cc` compiler or the builtin `tinyc` compiler that is bundled with the CLI. +*This is currently in progress.* + +  -## AOT -Work on the ahead-of-time compiler has not begun. \ No newline at end of file +  \ No newline at end of file diff --git a/lib/linenoise/linenoise.c b/lib/linenoise/linenoise.c new file mode 100644 index 000000000..574ab1f12 --- /dev/null +++ b/lib/linenoise/linenoise.c @@ -0,0 +1,1349 @@ +/* linenoise.c -- guerrilla line editing library against the idea that a + * line editing lib needs to be 20,000 lines of C code. + * + * You can find the latest source code at: + * + * http://github.com/antirez/linenoise + * + * Does a number of crazy assumptions that happen to be true in 99.9999% of + * the 2010 UNIX computers around. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2023, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ------------------------------------------------------------------------ + * + * References: + * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html + * + * Todo list: + * - Filter bogus Ctrl+ combinations. + * - Win32 support + * + * Bloat: + * - History search like Ctrl+r in readline? + * + * List of escape sequences used by this program, we do everything just + * with three sequences. In order to be so cheap we may have some + * flickering effect with some slow terminal, but the lesser sequences + * the more compatible. + * + * EL (Erase Line) + * Sequence: ESC [ n K + * Effect: if n is 0 or missing, clear from cursor to end of line + * Effect: if n is 1, clear from beginning of line to cursor + * Effect: if n is 2, clear entire line + * + * CUF (CUrsor Forward) + * Sequence: ESC [ n C + * Effect: moves cursor forward n chars + * + * CUB (CUrsor Backward) + * Sequence: ESC [ n D + * Effect: moves cursor backward n chars + * + * The following is used to get the terminal width if getting + * the width with the TIOCGWINSZ ioctl fails + * + * DSR (Device Status Report) + * Sequence: ESC [ 6 n + * Effect: reports the current cusor position as ESC [ n ; m R + * where n is the row and m is the column + * + * When multi line mode is enabled, we also use an additional escape + * sequence. However multi line editing is disabled by default. + * + * CUU (Cursor Up) + * Sequence: ESC [ n A + * Effect: moves cursor up of n chars. + * + * CUD (Cursor Down) + * Sequence: ESC [ n B + * Effect: moves cursor down of n chars. + * + * When linenoiseClearScreen() is called, two additional escape sequences + * are used in order to clear the screen and position the cursor at home + * position. + * + * CUP (Cursor position) + * Sequence: ESC [ H + * Effect: moves the cursor to upper left corner + * + * ED (Erase display) + * Sequence: ESC [ 2 J + * Effect: clear the whole screen + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "linenoise.h" + +#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 +#define LINENOISE_MAX_LINE 4096 +static char *unsupported_term[] = {"dumb","cons25","emacs",NULL}; +static linenoiseCompletionCallback *completionCallback = NULL; +static linenoiseHintsCallback *hintsCallback = NULL; +static linenoiseFreeHintsCallback *freeHintsCallback = NULL; +static char *linenoiseNoTTY(void); +static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags); +static void refreshLineWithFlags(struct linenoiseState *l, int flags); + +static struct termios orig_termios; /* In order to restore at exit.*/ +static int maskmode = 0; /* Show "***" instead of input. For passwords. */ +static int rawmode = 0; /* For atexit() function to check if restore is needed*/ +static int mlmode = 0; /* Multi line mode. Default is single line. */ +static int atexit_registered = 0; /* Register atexit just 1 time. */ +static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; +static int history_len = 0; +static char **history = NULL; + +enum KEY_ACTION{ + KEY_NULL = 0, /* NULL */ + CTRL_A = 1, /* Ctrl+a */ + CTRL_B = 2, /* Ctrl-b */ + CTRL_C = 3, /* Ctrl-c */ + CTRL_D = 4, /* Ctrl-d */ + CTRL_E = 5, /* Ctrl-e */ + CTRL_F = 6, /* Ctrl-f */ + CTRL_H = 8, /* Ctrl-h */ + TAB = 9, /* Tab */ + CTRL_K = 11, /* Ctrl+k */ + CTRL_L = 12, /* Ctrl+l */ + ENTER = 13, /* Enter */ + CTRL_N = 14, /* Ctrl-n */ + CTRL_P = 16, /* Ctrl-p */ + CTRL_T = 20, /* Ctrl-t */ + CTRL_U = 21, /* Ctrl+u */ + CTRL_W = 23, /* Ctrl+w */ + ESC = 27, /* Escape */ + BACKSPACE = 127 /* Backspace */ +}; + +static void linenoiseAtExit(void); +int linenoiseHistoryAdd(const char *line); +#define REFRESH_CLEAN (1<<0) // Clean the old prompt from the screen +#define REFRESH_WRITE (1<<1) // Rewrite the prompt on the screen. +#define REFRESH_ALL (REFRESH_CLEAN|REFRESH_WRITE) // Do both. +static void refreshLine(struct linenoiseState *l); + +/* Debugging macro. */ +#if 0 +FILE *lndebug_fp = NULL; +#define lndebug(...) \ + do { \ + if (lndebug_fp == NULL) { \ + lndebug_fp = fopen("/tmp/lndebug.txt","a"); \ + fprintf(lndebug_fp, \ + "[%d %d %d] p: %d, rows: %d, rpos: %d, max: %d, oldmax: %d\n", \ + (int)l->len,(int)l->pos,(int)l->oldpos,plen,rows,rpos, \ + (int)l->oldrows,old_rows); \ + } \ + fprintf(lndebug_fp, ", " __VA_ARGS__); \ + fflush(lndebug_fp); \ + } while (0) +#else +#define lndebug(fmt, ...) +#endif + +/* ======================= Low level terminal handling ====================== */ + +/* Enable "mask mode". When it is enabled, instead of the input that + * the user is typing, the terminal will just display a corresponding + * number of asterisks, like "****". This is useful for passwords and other + * secrets that should not be displayed. */ +void linenoiseMaskModeEnable(void) { + maskmode = 1; +} + +/* Disable mask mode. */ +void linenoiseMaskModeDisable(void) { + maskmode = 0; +} + +/* Set if to use or not the multi line mode. */ +void linenoiseSetMultiLine(int ml) { + mlmode = ml; +} + +/* Return true if the terminal name is in the list of terminals we know are + * not able to understand basic escape sequences. */ +static int isUnsupportedTerm(void) { + char *term = getenv("TERM"); + int j; + + if (term == NULL) return 0; + for (j = 0; unsupported_term[j]; j++) + if (!strcasecmp(term,unsupported_term[j])) return 1; + return 0; +} + +/* Raw mode: 1960 magic shit. */ +static int enableRawMode(int fd) { + struct termios raw; + + if (!isatty(STDIN_FILENO)) goto fatal; + if (!atexit_registered) { + atexit(linenoiseAtExit); + atexit_registered = 1; + } + if (tcgetattr(fd,&orig_termios) == -1) goto fatal; + + 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) goto fatal; + rawmode = 1; + return 0; + +fatal: + errno = ENOTTY; + return -1; +} + +static 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; +} + +/* Use the ESC [6n escape sequence to query the horizontal cursor position + * and return it. On error -1 is returned, on success the position of the + * cursor. */ +static int getCursorPosition(int ifd, int ofd) { + char buf[32]; + int cols, rows; + unsigned int i = 0; + + /* Report cursor location */ + if (write(ofd, "\x1b[6n", 4) != 4) return -1; + + /* Read the response: ESC [ rows ; cols R */ + while (i < sizeof(buf)-1) { + if (read(ifd,buf+i,1) != 1) break; + if (buf[i] == 'R') break; + i++; + } + buf[i] = '\0'; + + /* Parse it. */ + if (buf[0] != ESC || buf[1] != '[') return -1; + if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1; + return cols; +} + +/* Try to get the number of columns in the current terminal, or assume 80 + * if it fails. */ +static int getColumns(int ifd, int ofd) { + struct 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 = getCursorPosition(ifd,ofd); + if (start == -1) goto failed; + + /* Go to right margin and get position. */ + if (write(ofd,"\x1b[999C",6) != 6) goto failed; + cols = getCursorPosition(ifd,ofd); + if (cols == -1) goto failed; + + /* Restore position. */ + if (cols > start) { + char seq[32]; + snprintf(seq,32,"\x1b[%dD",cols-start); + if (write(ofd,seq,strlen(seq)) == -1) { + /* Can't recover... */ + } + } + return cols; + } else { + return ws.ws_col; + } + +failed: + return 80; +} + +/* Clear the screen. Used to handle ctrl+l */ +void linenoiseClearScreen(void) { + if (write(STDOUT_FILENO,"\x1b[H\x1b[2J",7) <= 0) { + /* nothing to do, just to avoid warning. */ + } +} + +/* Beep, used for completion when there is nothing to complete or when all + * the choices were already shown. */ +static void linenoiseBeep(void) { + fprintf(stderr, "\x7"); + fflush(stderr); +} + +/* ============================== Completion ================================ */ + +/* Free a list of completion option populated by linenoiseAddCompletion(). */ +static void freeCompletions(linenoiseCompletions *lc) { + size_t i; + for (i = 0; i < lc->len; i++) + free(lc->cvec[i]); + if (lc->cvec != NULL) + free(lc->cvec); +} + +/* Called by completeLine() and linenoiseShow() to render the current + * edited line with the proposed completion. If the current completion table + * is already available, it is passed as second argument, otherwise the + * function will use the callback to obtain it. + * + * Flags are the same as refreshLine*(), that is REFRESH_* macros. */ +static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags) { + /* Obtain the table of completions if the caller didn't provide one. */ + linenoiseCompletions ctable = { 0, NULL }; + if (lc == NULL) { + completionCallback(ls->buf,&ctable); + lc = &ctable; + } + + /* Show the edited line with completion if possible, or just refresh. */ + if (ls->completion_idx < lc->len) { + struct linenoiseState saved = *ls; + ls->len = ls->pos = strlen(lc->cvec[ls->completion_idx]); + ls->buf = lc->cvec[ls->completion_idx]; + refreshLineWithFlags(ls,flags); + ls->len = saved.len; + ls->pos = saved.pos; + ls->buf = saved.buf; + } else { + refreshLineWithFlags(ls,flags); + } + + /* Free the completions table if needed. */ + if (lc != &ctable) freeCompletions(&ctable); +} + +/* This is an helper function for linenoiseEdit*() and is called when the + * user types the key in order to complete the string currently in the + * input. + * + * The state of the editing is encapsulated into the pointed linenoiseState + * structure as described in the structure definition. + * + * If the function returns non-zero, the caller should handle the + * returned value as a byte read from the standard input, and process + * it as usually: this basically means that the function may return a byte + * read from the termianl but not processed. Otherwise, if zero is returned, + * the input was consumed by the completeLine() function to navigate the + * possible completions, and the caller should read for the next characters + * from stdin. */ +static int completeLine(struct linenoiseState *ls, int keypressed) { + linenoiseCompletions lc = { 0, NULL }; + int nwritten; + char c = keypressed; + + completionCallback(ls->buf,&lc); + if (lc.len == 0) { + linenoiseBeep(); + ls->in_completion = 0; + } else { + switch(c) { + case 9: /* tab */ + if (ls->in_completion == 0) { + ls->in_completion = 1; + ls->completion_idx = 0; + } else { + ls->completion_idx = (ls->completion_idx+1) % (lc.len+1); + if (ls->completion_idx == lc.len) linenoiseBeep(); + } + c = 0; + break; + case 27: /* escape */ + /* Re-show original buffer */ + if (ls->completion_idx < lc.len) refreshLine(ls); + ls->in_completion = 0; + c = 0; + break; + default: + /* Update buffer and return */ + if (ls->completion_idx < lc.len) { + nwritten = snprintf(ls->buf,ls->buflen,"%s", + lc.cvec[ls->completion_idx]); + ls->len = ls->pos = nwritten; + } + ls->in_completion = 0; + break; + } + + /* Show completion or original buffer */ + if (ls->in_completion && ls->completion_idx < lc.len) { + refreshLineWithCompletion(ls,&lc,REFRESH_ALL); + } else { + refreshLine(ls); + } + } + + freeCompletions(&lc); + return c; /* Return last read character */ +} + +/* Register a callback function to be called for tab-completion. */ +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) { + completionCallback = fn; +} + +/* Register a hits function to be called to show hits to the user at the + * right of the prompt. */ +void linenoiseSetHintsCallback(linenoiseHintsCallback *fn) { + hintsCallback = fn; +} + +/* Register a function to free the hints returned by the hints callback + * registered with linenoiseSetHintsCallback(). */ +void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *fn) { + freeHintsCallback = fn; +} + +/* This function is used by the callback function registered by the user + * in order to add completion options given the input string when the + * user typed . See the example.c source code for a very easy to + * understand example. */ +void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) { + size_t len = strlen(str); + char *copy, **cvec; + + copy = malloc(len+1); + if (copy == NULL) return; + memcpy(copy,str,len+1); + cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1)); + if (cvec == NULL) { + free(copy); + return; + } + lc->cvec = cvec; + lc->cvec[lc->len++] = copy; +} + +/* =========================== Line editing ================================= */ + +/* We define a very simple "append buffer" structure, that is an heap + * allocated string where we can append to. This is useful in order to + * write all the escape sequences in a buffer and flush them to the standard + * output in a single call, to avoid flickering effects. */ +struct abuf { + char *b; + int len; +}; + +static void abInit(struct abuf *ab) { + ab->b = NULL; + ab->len = 0; +} + +static void abAppend(struct abuf *ab, const char *s, int len) { + char *new = realloc(ab->b,ab->len+len); + + if (new == NULL) return; + memcpy(new+ab->len,s,len); + ab->b = new; + ab->len += len; +} + +static void abFree(struct abuf *ab) { + free(ab->b); +} + +/* Helper of refreshSingleLine() and refreshMultiLine() to show hints + * to the right of the prompt. */ +void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int plen) { + char seq[64]; + if (hintsCallback && plen+l->len < l->cols) { + int color = -1, bold = 0; + char *hint = hintsCallback(l->buf,&color,&bold); + if (hint) { + int hintlen = strlen(hint); + int hintmaxlen = l->cols-(plen+l->len); + if (hintlen > hintmaxlen) hintlen = hintmaxlen; + if (bold == 1 && color == -1) color = 37; + if (color != -1 || bold != 0) + snprintf(seq,64,"\033[%d;%d;49m",bold,color); + else + seq[0] = '\0'; + abAppend(ab,seq,strlen(seq)); + abAppend(ab,hint,hintlen); + if (color != -1 || bold != 0) + abAppend(ab,"\033[0m",4); + /* Call the function to free the hint returned. */ + if (freeHintsCallback) freeHintsCallback(hint); + } + } +} + +/* Single line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. + * + * Flags is REFRESH_* macros. The function can just remove the old + * prompt, just write it, or both. */ +static void refreshSingleLine(struct linenoiseState *l, int flags) { + char seq[64]; + size_t plen = strlen(l->prompt); + int fd = l->ofd; + char *buf = l->buf; + size_t len = l->len; + size_t pos = l->pos; + struct abuf ab; + + while((plen+pos) >= l->cols) { + buf++; + len--; + pos--; + } + while (plen+len > l->cols) { + len--; + } + + abInit(&ab); + /* Cursor to left edge */ + snprintf(seq,sizeof(seq),"\r"); + abAppend(&ab,seq,strlen(seq)); + + if (flags & REFRESH_WRITE) { + /* Write the prompt and the current buffer content */ + abAppend(&ab,l->prompt,strlen(l->prompt)); + if (maskmode == 1) { + while (len--) abAppend(&ab,"*",1); + } else { + abAppend(&ab,buf,len); + } + /* Show hits if any. */ + refreshShowHints(&ab,l,plen); + } + + /* Erase to right */ + snprintf(seq,sizeof(seq),"\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + + if (flags & REFRESH_WRITE) { + /* Move cursor to original position. */ + snprintf(seq,sizeof(seq),"\r\x1b[%dC", (int)(pos+plen)); + abAppend(&ab,seq,strlen(seq)); + } + + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +/* Multi line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. + * + * Flags is REFRESH_* macros. The function can just remove the old + * prompt, just write it, or both. */ +static void refreshMultiLine(struct linenoiseState *l, int flags) { + char seq[64]; + int plen = strlen(l->prompt); + int rows = (plen+l->len+l->cols-1)/l->cols; /* rows used by current buf. */ + int rpos = (plen+l->oldpos+l->cols)/l->cols; /* cursor relative row. */ + int rpos2; /* rpos after refresh. */ + int col; /* colum position, zero-based. */ + int old_rows = l->oldrows; + int fd = l->ofd, j; + struct abuf ab; + + l->oldrows = rows; + + /* First step: clear all the lines used before. To do so start by + * going to the last row. */ + abInit(&ab); + + if (flags & REFRESH_CLEAN) { + if (old_rows-rpos > 0) { + lndebug("go down %d", old_rows-rpos); + snprintf(seq,64,"\x1b[%dB", old_rows-rpos); + abAppend(&ab,seq,strlen(seq)); + } + + /* Now for every row clear it, go up. */ + for (j = 0; j < old_rows-1; j++) { + lndebug("clear+up"); + snprintf(seq,64,"\r\x1b[0K\x1b[1A"); + abAppend(&ab,seq,strlen(seq)); + } + } + + if (flags & REFRESH_ALL) { + /* Clean the top line. */ + lndebug("clear"); + snprintf(seq,64,"\r\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + } + + if (flags & REFRESH_WRITE) { + /* Write the prompt and the current buffer content */ + abAppend(&ab,l->prompt,strlen(l->prompt)); + if (maskmode == 1) { + unsigned int i; + for (i = 0; i < l->len; i++) abAppend(&ab,"*",1); + } else { + abAppend(&ab,l->buf,l->len); + } + + /* Show hits if any. */ + refreshShowHints(&ab,l,plen); + + /* If we are at the very end of the screen with our prompt, we need to + * emit a newline and move the prompt to the first column. */ + if (l->pos && + l->pos == l->len && + (l->pos+plen) % l->cols == 0) + { + lndebug(""); + abAppend(&ab,"\n",1); + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + rows++; + if (rows > (int)l->oldrows) l->oldrows = rows; + } + + /* Move cursor to right position. */ + rpos2 = (plen+l->pos+l->cols)/l->cols; /* Current cursor relative row */ + lndebug("rpos2 %d", rpos2); + + /* Go up till we reach the expected positon. */ + if (rows-rpos2 > 0) { + lndebug("go-up %d", rows-rpos2); + snprintf(seq,64,"\x1b[%dA", rows-rpos2); + abAppend(&ab,seq,strlen(seq)); + } + + /* Set column. */ + col = (plen+(int)l->pos) % (int)l->cols; + lndebug("set col %d", 1+col); + if (col) + snprintf(seq,64,"\r\x1b[%dC", col); + else + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + } + + lndebug("\n"); + l->oldpos = l->pos; + + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +/* Calls the two low level functions refreshSingleLine() or + * refreshMultiLine() according to the selected mode. */ +static void refreshLineWithFlags(struct linenoiseState *l, int flags) { + if (mlmode) + refreshMultiLine(l,flags); + else + refreshSingleLine(l,flags); +} + +/* Utility function to avoid specifying REFRESH_ALL all the times. */ +static void refreshLine(struct linenoiseState *l) { + refreshLineWithFlags(l,REFRESH_ALL); +} + +/* Hide the current line, when using the multiplexing API. */ +void linenoiseHide(struct linenoiseState *l) { + if (mlmode) + refreshMultiLine(l,REFRESH_CLEAN); + else + refreshSingleLine(l,REFRESH_CLEAN); +} + +/* Show the current line, when using the multiplexing API. */ +void linenoiseShow(struct linenoiseState *l) { + if (l->in_completion) { + refreshLineWithCompletion(l,NULL,REFRESH_WRITE); + } else { + refreshLineWithFlags(l,REFRESH_WRITE); + } +} + +/* Insert the character 'c' at cursor current position. + * + * On error writing to the terminal -1 is returned, otherwise 0. */ +int linenoiseEditInsert(struct linenoiseState *l, char c) { + if (l->len < l->buflen) { + if (l->len == l->pos) { + l->buf[l->pos] = c; + l->pos++; + l->len++; + l->buf[l->len] = '\0'; + if ((!mlmode && l->plen+l->len < l->cols && !hintsCallback)) { + /* Avoid a full update of the line in the + * trivial case. */ + char d = (maskmode==1) ? '*' : c; + if (write(l->ofd,&d,1) == -1) return -1; + } else { + refreshLine(l); + } + } else { + memmove(l->buf+l->pos+1,l->buf+l->pos,l->len-l->pos); + l->buf[l->pos] = c; + l->len++; + l->pos++; + l->buf[l->len] = '\0'; + refreshLine(l); + } + } + return 0; +} + +/* Move cursor on the left. */ +void linenoiseEditMoveLeft(struct linenoiseState *l) { + if (l->pos > 0) { + l->pos--; + refreshLine(l); + } +} + +/* Move cursor on the right. */ +void linenoiseEditMoveRight(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos++; + refreshLine(l); + } +} + +/* Move cursor to the start of the line. */ +void linenoiseEditMoveHome(struct linenoiseState *l) { + if (l->pos != 0) { + l->pos = 0; + refreshLine(l); + } +} + +/* Move cursor to the end of the line. */ +void linenoiseEditMoveEnd(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos = l->len; + refreshLine(l); + } +} + +/* Substitute the currently edited line with the next or previous history + * entry as specified by 'dir'. */ +#define LINENOISE_HISTORY_NEXT 0 +#define LINENOISE_HISTORY_PREV 1 +void linenoiseEditHistoryNext(struct linenoiseState *l, int dir) { + if (history_len > 1) { + /* Update the current history entry before to + * overwrite it with the next one. */ + free(history[history_len - 1 - l->history_index]); + history[history_len - 1 - l->history_index] = strdup(l->buf); + /* Show the new entry */ + l->history_index += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1; + if (l->history_index < 0) { + l->history_index = 0; + return; + } else if (l->history_index >= history_len) { + l->history_index = history_len-1; + return; + } + strncpy(l->buf,history[history_len - 1 - l->history_index],l->buflen); + l->buf[l->buflen-1] = '\0'; + l->len = l->pos = strlen(l->buf); + refreshLine(l); + } +} + +/* Delete the character at the right of the cursor without altering the cursor + * position. Basically this is what happens with the "Delete" keyboard key. */ +void linenoiseEditDelete(struct linenoiseState *l) { + if (l->len > 0 && l->pos < l->len) { + memmove(l->buf+l->pos,l->buf+l->pos+1,l->len-l->pos-1); + l->len--; + l->buf[l->len] = '\0'; + refreshLine(l); + } +} + +/* Backspace implementation. */ +void linenoiseEditBackspace(struct linenoiseState *l) { + if (l->pos > 0 && l->len > 0) { + memmove(l->buf+l->pos-1,l->buf+l->pos,l->len-l->pos); + l->pos--; + l->len--; + l->buf[l->len] = '\0'; + refreshLine(l); + } +} + +/* Delete the previosu word, maintaining the cursor at the start of the + * current word. */ +void linenoiseEditDeletePrevWord(struct linenoiseState *l) { + size_t old_pos = l->pos; + size_t diff; + + while (l->pos > 0 && l->buf[l->pos-1] == ' ') + l->pos--; + while (l->pos > 0 && l->buf[l->pos-1] != ' ') + l->pos--; + diff = old_pos - l->pos; + memmove(l->buf+l->pos,l->buf+old_pos,l->len-old_pos+1); + l->len -= diff; + refreshLine(l); +} + +/* This function is part of the multiplexed API of Linenoise, that is used + * in order to implement the blocking variant of the API but can also be + * called by the user directly in an event driven program. It will: + * + * 1. Initialize the linenoise state passed by the user. + * 2. Put the terminal in RAW mode. + * 3. Show the prompt. + * 4. Return control to the user, that will have to call linenoiseEditFeed() + * each time there is some data arriving in the standard input. + * + * The user can also call linenoiseEditHide() and linenoiseEditShow() if it + * is required to show some input arriving asyncronously, without mixing + * it with the currently edited line. + * + * When linenoiseEditFeed() returns non-NULL, the user finished with the + * line editing session (pressed enter CTRL-D/C): in this case the caller + * needs to call linenoiseEditStop() to put back the terminal in normal + * mode. This will not destroy the buffer, as long as the linenoiseState + * is still valid in the context of the caller. + * + * The function returns 0 on success, or -1 if writing to standard output + * fails. If stdin_fd or stdout_fd are set to -1, the default is to use + * STDIN_FILENO and STDOUT_FILENO. + */ +int linenoiseEditStart(struct linenoiseState *l, int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) { + /* Populate the linenoise state that we pass to functions implementing + * specific editing functionalities. */ + l->in_completion = 0; + l->ifd = stdin_fd != -1 ? stdin_fd : STDIN_FILENO; + l->ofd = stdout_fd != -1 ? stdout_fd : STDOUT_FILENO; + l->buf = buf; + l->buflen = buflen; + l->prompt = prompt; + l->plen = strlen(prompt); + l->oldpos = l->pos = 0; + l->len = 0; + + /* Enter raw mode. */ + if (enableRawMode(l->ifd) == -1) return -1; + + l->cols = getColumns(stdin_fd, stdout_fd); + l->oldrows = 0; + l->history_index = 0; + + /* Buffer starts empty. */ + l->buf[0] = '\0'; + l->buflen--; /* Make sure there is always space for the nulterm */ + + /* If stdin is not a tty, stop here with the initialization. We + * will actually just read a line from standard input in blocking + * mode later, in linenoiseEditFeed(). */ + if (!isatty(l->ifd)) return 0; + + /* The latest history entry is always our current buffer, that + * initially is just an empty string. */ + linenoiseHistoryAdd(""); + + if (write(l->ofd,prompt,l->plen) == -1) return -1; + return 0; +} + +char *linenoiseEditMore = "If you see this, you are misusing the API: when linenoiseEditFeed() is called, if it returns linenoiseEditMore the user is yet editing the line. See the README file for more information."; + +/* This function is part of the multiplexed API of linenoise, see the top + * comment on linenoiseEditStart() for more information. Call this function + * each time there is some data to read from the standard input file + * descriptor. In the case of blocking operations, this function can just be + * called in a loop, and block. + * + * The function returns linenoiseEditMore to signal that line editing is still + * in progress, that is, the user didn't yet pressed enter / CTRL-D. Otherwise + * the function returns the pointer to the heap-allocated buffer with the + * edited line, that the user should free with linenoiseFree(). + * + * On special conditions, NULL is returned and errno is populated: + * + * EAGAIN if the user pressed Ctrl-C + * ENOENT if the user pressed Ctrl-D + * + * Some other errno: I/O error. + */ +char *linenoiseEditFeed(struct linenoiseState *l) { + /* Not a TTY, pass control to line reading without character + * count limits. */ + if (!isatty(l->ifd)) return linenoiseNoTTY(); + + char c; + int nread; + char seq[3]; + + nread = read(l->ifd,&c,1); + if (nread <= 0) return NULL; + + /* Only autocomplete when the callback is set. It returns < 0 when + * there was an error reading from fd. Otherwise it will return the + * character that should be handled next. */ + if ((l->in_completion || c == 9) && completionCallback != NULL) { + c = completeLine(l,c); + /* Return on errors */ + if (c < 0) return NULL; + /* Read next character when 0 */ + if (c == 0) return linenoiseEditMore; + } + + switch(c) { + case ENTER: /* enter */ + history_len--; + free(history[history_len]); + if (mlmode) linenoiseEditMoveEnd(l); + if (hintsCallback) { + /* Force a refresh without hints to leave the previous + * line as the user typed it after a newline. */ + linenoiseHintsCallback *hc = hintsCallback; + hintsCallback = NULL; + refreshLine(l); + hintsCallback = hc; + } + return strdup(l->buf); + case CTRL_C: /* ctrl-c */ + errno = EAGAIN; + return NULL; + case BACKSPACE: /* backspace */ + case 8: /* ctrl-h */ + linenoiseEditBackspace(l); + break; + case CTRL_D: /* ctrl-d, remove char at right of cursor, or if the + line is empty, act as end-of-file. */ + if (l->len > 0) { + linenoiseEditDelete(l); + } else { + history_len--; + free(history[history_len]); + errno = ENOENT; + return NULL; + } + break; + case CTRL_T: /* ctrl-t, swaps current character with previous. */ + if (l->pos > 0 && l->pos < l->len) { + int aux = l->buf[l->pos-1]; + l->buf[l->pos-1] = l->buf[l->pos]; + l->buf[l->pos] = aux; + if (l->pos != l->len-1) l->pos++; + refreshLine(l); + } + break; + case CTRL_B: /* ctrl-b */ + linenoiseEditMoveLeft(l); + break; + case CTRL_F: /* ctrl-f */ + linenoiseEditMoveRight(l); + break; + case CTRL_P: /* ctrl-p */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_PREV); + break; + case CTRL_N: /* ctrl-n */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_NEXT); + break; + case ESC: /* escape sequence */ + /* Read the next two bytes representing the escape sequence. + * Use two calls to handle slow terminals returning the two + * chars at different times. */ + if (read(l->ifd,seq,1) == -1) break; + if (read(l->ifd,seq+1,1) == -1) break; + + /* ESC [ sequences. */ + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + /* Extended escape, read additional byte. */ + if (read(l->ifd,seq+2,1) == -1) break; + if (seq[2] == '~') { + switch(seq[1]) { + case '3': /* Delete key. */ + linenoiseEditDelete(l); + break; + } + } + } else { + switch(seq[1]) { + case 'A': /* Up */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_PREV); + break; + case 'B': /* Down */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_NEXT); + break; + case 'C': /* Right */ + linenoiseEditMoveRight(l); + break; + case 'D': /* Left */ + linenoiseEditMoveLeft(l); + break; + case 'H': /* Home */ + linenoiseEditMoveHome(l); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(l); + break; + } + } + } + + /* ESC O sequences. */ + else if (seq[0] == 'O') { + switch(seq[1]) { + case 'H': /* Home */ + linenoiseEditMoveHome(l); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(l); + break; + } + } + break; + default: + if (linenoiseEditInsert(l,c)) return NULL; + break; + case CTRL_U: /* Ctrl+u, delete the whole line. */ + l->buf[0] = '\0'; + l->pos = l->len = 0; + refreshLine(l); + break; + case CTRL_K: /* Ctrl+k, delete from current to end of line. */ + l->buf[l->pos] = '\0'; + l->len = l->pos; + refreshLine(l); + break; + case CTRL_A: /* Ctrl+a, go to the start of the line */ + linenoiseEditMoveHome(l); + break; + case CTRL_E: /* ctrl+e, go to the end of the line */ + linenoiseEditMoveEnd(l); + break; + case CTRL_L: /* ctrl+l, clear screen */ + linenoiseClearScreen(); + refreshLine(l); + break; + case CTRL_W: /* ctrl+w, delete previous word */ + linenoiseEditDeletePrevWord(l); + break; + } + return linenoiseEditMore; +} + +/* This is part of the multiplexed linenoise API. See linenoiseEditStart() + * for more information. This function is called when linenoiseEditFeed() + * returns something different than NULL. At this point the user input + * is in the buffer, and we can restore the terminal in normal mode. */ +void linenoiseEditStop(struct linenoiseState *l) { + if (!isatty(l->ifd)) return; + disableRawMode(l->ifd); + printf("\n"); +} + +/* This just implements a blocking loop for the multiplexed API. + * In many applications that are not event-drivern, we can just call + * the blocking linenoise API, wait for the user to complete the editing + * and return the buffer. */ +static char *linenoiseBlockingEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) +{ + struct linenoiseState l; + + /* Editing without a buffer is invalid. */ + if (buflen == 0) { + errno = EINVAL; + return NULL; + } + + linenoiseEditStart(&l,stdin_fd,stdout_fd,buf,buflen,prompt); + char *res; + while((res = linenoiseEditFeed(&l)) == linenoiseEditMore); + linenoiseEditStop(&l); + return res; +} + +/* This special mode is used by linenoise in order to print scan codes + * on screen for debugging / development purposes. It is implemented + * by the linenoise_example program using the --keycodes option. */ +void linenoisePrintKeyCodes(void) { + char quit[4]; + + printf("Linenoise key codes debugging mode.\n" + "Press keys to see scan codes. Type 'quit' at any time to exit.\n"); + if (enableRawMode(STDIN_FILENO) == -1) return; + memset(quit,' ',4); + while(1) { + char c; + int nread; + + nread = read(STDIN_FILENO,&c,1); + if (nread <= 0) continue; + memmove(quit,quit+1,sizeof(quit)-1); /* shift string to left. */ + quit[sizeof(quit)-1] = c; /* Insert current char on the right. */ + if (memcmp(quit,"quit",sizeof(quit)) == 0) break; + + printf("'%c' %02x (%d) (type quit to exit)\n", + isprint(c) ? c : '?', (int)c, (int)c); + printf("\r"); /* Go left edge manually, we are in raw mode. */ + fflush(stdout); + } + disableRawMode(STDIN_FILENO); +} + +/* This function is called when linenoise() is called with the standard + * input file descriptor not attached to a TTY. So for example when the + * program using linenoise is called in pipe or with a file redirected + * to its standard input. In this case, we want to be able to return the + * line regardless of its length (by default we are limited to 4k). */ +static char *linenoiseNoTTY(void) { + char *line = NULL; + size_t len = 0, maxlen = 0; + + while(1) { + if (len == maxlen) { + if (maxlen == 0) maxlen = 16; + maxlen *= 2; + char *oldval = line; + line = realloc(line,maxlen); + if (line == NULL) { + if (oldval) free(oldval); + return NULL; + } + } + int c = fgetc(stdin); + if (c == EOF || c == '\n') { + if (c == EOF && len == 0) { + free(line); + return NULL; + } else { + line[len] = '\0'; + return line; + } + } else { + line[len] = c; + len++; + } + } +} + +/* The high level function that is the main API of the linenoise library. + * This function checks if the terminal has basic capabilities, just checking + * for a blacklist of stupid terminals, and later either calls the line + * editing function or uses dummy fgets() so that you will be able to type + * something even in the most desperate of the conditions. */ +char *linenoise(const char *prompt) { + char buf[LINENOISE_MAX_LINE]; + + if (!isatty(STDIN_FILENO)) { + /* Not a tty: read from file / pipe. In this mode we don't want any + * limit to the line size, so we call a function to handle that. */ + return linenoiseNoTTY(); + } else if (isUnsupportedTerm()) { + size_t len; + + printf("%s",prompt); + fflush(stdout); + if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL; + len = strlen(buf); + while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) { + len--; + buf[len] = '\0'; + } + return strdup(buf); + } else { + char *retval = linenoiseBlockingEdit(STDIN_FILENO,STDOUT_FILENO,buf,LINENOISE_MAX_LINE,prompt); + return retval; + } +} + +/* This is just a wrapper the user may want to call in order to make sure + * the linenoise returned buffer is freed with the same allocator it was + * created with. Useful when the main program is using an alternative + * allocator. */ +void linenoiseFree(void *ptr) { + if (ptr == linenoiseEditMore) return; // Protect from API misuse. + free(ptr); +} + +/* ================================ History ================================= */ + +/* Free the history, but does not reset it. Only used when we have to + * exit() to avoid memory leaks are reported by valgrind & co. */ +static void freeHistory(void) { + if (history) { + int j; + + for (j = 0; j < history_len; j++) + free(history[j]); + free(history); + } +} + +/* At exit we'll try to fix the terminal to the initial conditions. */ +static void linenoiseAtExit(void) { + disableRawMode(STDIN_FILENO); + freeHistory(); +} + +/* This is the API call to add a new entry in the linenoise history. + * It uses a fixed array of char pointers that are shifted (memmoved) + * when the history max length is reached in order to remove the older + * entry and make room for the new one, so it is not exactly suitable for huge + * histories, but will work well for a few hundred of entries. + * + * Using a circular buffer is smarter, but a bit more complex to handle. */ +int linenoiseHistoryAdd(const char *line) { + char *linecopy; + + if (history_max_len == 0) return 0; + + /* Initialization on first call. */ + if (history == NULL) { + history = malloc(sizeof(char*)*history_max_len); + if (history == NULL) return 0; + memset(history,0,(sizeof(char*)*history_max_len)); + } + + /* Don't add duplicated lines. */ + if (history_len && !strcmp(history[history_len-1], line)) return 0; + + /* Add an heap allocated copy of the line in the history. + * If we reached the max length, remove the older line. */ + linecopy = strdup(line); + if (!linecopy) return 0; + if (history_len == history_max_len) { + free(history[0]); + memmove(history,history+1,sizeof(char*)*(history_max_len-1)); + history_len--; + } + history[history_len] = linecopy; + history_len++; + return 1; +} + +/* Set the maximum length for the history. This function can be called even + * if there is already some history, the function will make sure to retain + * just the latest 'len' elements if the new history length value is smaller + * than the amount of items already inside the history. */ +int linenoiseHistorySetMaxLen(int len) { + char **new; + + if (len < 1) return 0; + if (history) { + int tocopy = history_len; + + new = malloc(sizeof(char*)*len); + if (new == NULL) return 0; + + /* If we can't copy everything, free the elements we'll not use. */ + if (len < tocopy) { + int j; + + for (j = 0; j < tocopy-len; j++) free(history[j]); + tocopy = len; + } + memset(new,0,sizeof(char*)*len); + memcpy(new,history+(history_len-tocopy), sizeof(char*)*tocopy); + free(history); + history = new; + } + history_max_len = len; + if (history_len > history_max_len) + history_len = history_max_len; + return 1; +} + +/* Save the history in the specified file. On success 0 is returned + * otherwise -1 is returned. */ +int linenoiseHistorySave(const char *filename) { + mode_t old_umask = umask(S_IXUSR|S_IRWXG|S_IRWXO); + FILE *fp; + int j; + + fp = fopen(filename,"w"); + umask(old_umask); + if (fp == NULL) return -1; + chmod(filename,S_IRUSR|S_IWUSR); + for (j = 0; j < history_len; j++) + fprintf(fp,"%s\n",history[j]); + fclose(fp); + return 0; +} + +/* Load the history from the specified file. If the file does not exist + * zero is returned and no operation is performed. + * + * If the file exists and the operation succeeded 0 is returned, otherwise + * on error -1 is returned. */ +int linenoiseHistoryLoad(const char *filename) { + FILE *fp = fopen(filename,"r"); + char buf[LINENOISE_MAX_LINE]; + + if (fp == NULL) return -1; + + while (fgets(buf,LINENOISE_MAX_LINE,fp) != NULL) { + char *p; + + p = strchr(buf,'\r'); + if (!p) p = strchr(buf,'\n'); + if (p) *p = '\0'; + linenoiseHistoryAdd(buf); + } + fclose(fp); + return 0; +} diff --git a/lib/linenoise/linenoise.h b/lib/linenoise/linenoise.h new file mode 100644 index 000000000..3f0270e3e --- /dev/null +++ b/lib/linenoise/linenoise.h @@ -0,0 +1,113 @@ +/* linenoise.h -- VERSION 1.0 + * + * Guerrilla line editing library against the idea that a line editing lib + * needs to be 20,000 lines of C code. + * + * See linenoise.c for more information. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2023, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __LINENOISE_H +#define __LINENOISE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include /* For size_t. */ + +extern char *linenoiseEditMore; + +/* The linenoiseState structure represents the state during line editing. + * We pass this state to functions implementing specific editing + * functionalities. */ +struct linenoiseState { + int in_completion; /* The user pressed TAB and we are now in completion + * mode, so input is handled by completeLine(). */ + size_t completion_idx; /* Index of next completion to propose. */ + int ifd; /* Terminal stdin file descriptor. */ + int ofd; /* Terminal stdout file descriptor. */ + char *buf; /* Edited line buffer. */ + size_t buflen; /* Edited line buffer size. */ + const char *prompt; /* Prompt to display. */ + size_t plen; /* Prompt length. */ + size_t pos; /* Current cursor position. */ + size_t oldpos; /* Previous refresh cursor position. */ + size_t len; /* Current edited line length. */ + size_t cols; /* Number of columns in terminal. */ + size_t oldrows; /* Rows used by last refrehsed line (multiline mode) */ + int history_index; /* The history index we are currently editing. */ +}; + +typedef struct linenoiseCompletions { + size_t len; + char **cvec; +} linenoiseCompletions; + +/* Non blocking API. */ +int linenoiseEditStart(struct linenoiseState *l, int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt); +char *linenoiseEditFeed(struct linenoiseState *l); +void linenoiseEditStop(struct linenoiseState *l); +void linenoiseHide(struct linenoiseState *l); +void linenoiseShow(struct linenoiseState *l); + +/* Blocking API. */ +char *linenoise(const char *prompt); +void linenoiseFree(void *ptr); + +/* Completion API. */ +typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); +typedef char*(linenoiseHintsCallback)(const char *, int *color, int *bold); +typedef void(linenoiseFreeHintsCallback)(void *); +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); +void linenoiseSetHintsCallback(linenoiseHintsCallback *); +void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *); +void linenoiseAddCompletion(linenoiseCompletions *, const char *); + +/* History API. */ +int linenoiseHistoryAdd(const char *line); +int linenoiseHistorySetMaxLen(int len); +int linenoiseHistorySave(const char *filename); +int linenoiseHistoryLoad(const char *filename); + +/* Other utilities. */ +void linenoiseClearScreen(void); +void linenoiseSetMultiLine(int ml); +void linenoisePrintKeyCodes(void); +void linenoiseMaskModeEnable(void); +void linenoiseMaskModeDisable(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __LINENOISE_H */ diff --git a/lib/linenoise/linenoise.zig b/lib/linenoise/linenoise.zig new file mode 100644 index 000000000..ca180294b --- /dev/null +++ b/lib/linenoise/linenoise.zig @@ -0,0 +1,7 @@ +const std = @import("std"); + +const c = @cImport({ + @cInclude("linenoise.h"); +}); + +pub usingnamespace c; \ No newline at end of file diff --git a/src/bc_gen.zig b/src/bc_gen.zig index 4dc76a173..57e2c2b16 100644 --- a/src/bc_gen.zig +++ b/src/bc_gen.zig @@ -26,12 +26,12 @@ pub fn genAll(c: *cy.Compiler) !void { try c.vm.staticObjects.append(c.alloc, c.vm.emptyArray.asHeapObject()); // Prepare types. - for (c.sema.types.items, 0..) |stype, typeId| { + for (c.newTypes(), c.type_start..) |stype, typeId| { if (stype.kind == .null) { // Skip placeholders. continue; } - log.tracev("bc prepare type: {s}", .{stype.sym.name()}); + log.tracev("prep type: {s}", .{stype.sym.name()}); const sym = stype.sym; switch (sym.type) { @@ -61,7 +61,7 @@ pub fn genAll(c: *cy.Compiler) !void { } } - for (c.chunks.items) |chunk| { + for (c.newChunks()) |chunk| { log.tracev("prep chunk", .{}); for (chunk.syms.items) |sym| { try prepareSym(c, sym); @@ -74,7 +74,7 @@ pub fn genAll(c: *cy.Compiler) !void { // After rt funcs are set up, prepare the overloaded func table. // Methods entries are also registered at this point // since they depend on overloaded func entries. - for (c.chunks.items) |chunk| { + for (c.newChunks()) |chunk| { for (chunk.syms.items) |sym| { if (sym.type == .func) { const func_sym = sym.cast(.func); @@ -120,7 +120,7 @@ pub fn genAll(c: *cy.Compiler) !void { // Bind the rest that aren't in sema. try @call(.never_inline, cy.bindings.bindCore, .{c.vm}); - for (c.chunks.items) |chunk| { + for (c.newChunks()) |chunk| { log.tracev("Perform codegen for chunk{}: {s}", .{chunk.id, chunk.srcUri}); chunk.buf = &c.buf; try genChunk(chunk); @@ -134,9 +134,7 @@ pub fn genAll(c: *cy.Compiler) !void { } const constAddr = std.mem.alignForward(usize, @intFromPtr(c.buf.ops.items.ptr) + c.buf.ops.items.len, @alignOf(cy.Value)); const constDst = @as([*]cy.Value, @ptrFromInt(constAddr))[0..c.buf.consts.items.len]; - const constSrc = try c.buf.consts.toOwnedSlice(c.alloc); - std.mem.copy(cy.Value, constDst, constSrc); - c.alloc.free(constSrc); + std.mem.copy(cy.Value, constDst, c.buf.consts.items); c.buf.mconsts = constDst; // Final op address is known. Patch pc offsets. @@ -166,6 +164,7 @@ pub fn genAll(c: *cy.Compiler) !void { } fn prepareSym(c: *cy.Compiler, sym: *cy.Sym) !void { + log.tracev("prep sym: {s}", .{sym.name()}); switch (sym.type) { .hostVar => { const id = c.vm.varSyms.len; @@ -211,7 +210,7 @@ fn prepareFunc(c: *cy.Compiler, func: *cy.Func) !void { if (cy.Trace) { const symPath = try func.sym.?.head.allocAbsPath(c.alloc); defer c.alloc.free(symPath); - log.tracev("bc prepare func: {s}", .{symPath}); + log.tracev("prep func: {s}", .{symPath}); } if (func.type == .hostFunc) { const funcSig = c.sema.getFuncSig(func.funcSigId); diff --git a/src/capi.zig b/src/capi.zig index 400ea38ef..1e1734df4 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -42,6 +42,7 @@ pub const deinit = c.csDeinit; pub const destroy = c.csDestroy; pub const validate = c.csValidate; pub const defaultEvalConfig = c.csDefaultEvalConfig; +pub const reset = c.csReset; pub const eval = c.csEval; pub const defaultCompileConfig = c.csDefaultCompileConfig; pub const compile = c.csCompile; diff --git a/src/cli.zig b/src/cli.zig index 44768e801..51c742d76 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -207,10 +207,14 @@ fn zResolve(vm: *cy.VM, alloc: std.mem.Allocator, chunkId: cy.ChunkId, buf: []u8 var pathBuf: [4096]u8 = undefined; var fbuf = std.io.fixedBufferStream(&pathBuf); - if (cur_uri_opt) |cur_uri| { + if (cur_uri_opt) |cur_uri| b: { // Create path from the current script. // There should always be a parent directory since `curUri` should be absolute when dealing with file modules. - const dir = std.fs.path.dirname(cur_uri) orelse return error.NoParentDir; + const dir = std.fs.path.dirname(cur_uri) orelse { + // Likely from an in-memory chunk uri such as `main` or `input` (repl). + _ = try fbuf.write(spec); + break :b; + }; try fbuf.writer().print("{s}/{s}", .{dir, spec}); } else { _ = try fbuf.write(spec); diff --git a/src/compiler.zig b/src/compiler.zig index e3be7554e..5fa84be3a 100644 --- a/src/compiler.zig +++ b/src/compiler.zig @@ -82,6 +82,12 @@ pub const Compiler = struct { global_sym: ?*cy.sym.UserVar, get_global: ?*cy.Func, + /// Whether this is a subsequent compilation reusing the same state. + cont: bool, + + chunk_start: u32, + type_start: u32, + pub fn init(self: *Compiler, vm: *cy.VM) !void { self.* = .{ .alloc = vm.alloc, @@ -102,8 +108,11 @@ pub const Compiler = struct { .main_chunk = undefined, .global_sym = null, .get_global = null, + .cont = false, + .chunk_start = 0, + .type_start = 0, }; - try self.reinit(); + try self.reinitPerRun(); } pub fn deinitModRetained(self: *Compiler) void { @@ -163,7 +172,7 @@ pub const Compiler = struct { self.apiError = ""; } - pub fn reinit(self: *Compiler) !void { + pub fn reinitPerRun(self: *Compiler) !void { self.clearReports(); } @@ -174,11 +183,17 @@ pub const Compiler = struct { self.reports.clearRetainingCapacity(); } + pub fn newTypes(self: *Compiler) []cy.types.Type { + return self.sema.types.items[self.type_start..]; + } + + pub fn newChunks(self: *Compiler) []*cy.Chunk { + return self.chunks.items[self.chunk_start..]; + } + pub fn compile(self: *Compiler, uri: []const u8, src: ?[]const u8, config: cc.CompileConfig) !CompileResult { - defer { - // Update VM types view. - self.vm.types = self.sema.types.items; - } + self.chunk_start = @intCast(self.chunks.items.len); + self.type_start = @intCast(self.sema.types.items.len); const res = self.compileInner(uri, src, config) catch |err| { if (dumpCompileErrorStackTrace and !cc.silent()) { std.debug.dumpStackTrace(@errorReturnTrace().?.*); @@ -194,6 +209,12 @@ pub const Compiler = struct { return error.CompileError; } }; + + // Update VM types view. + self.vm.types = self.sema.types.items; + + // Successful. + self.cont = true; return res; } @@ -217,7 +238,9 @@ pub const Compiler = struct { src = res.src[0..res.srcLen]; } - try loadBuiltins(self); + if (!self.cont) { + try loadBuiltins(self); + } // TODO: Types and symbols should be loaded recursively for single-threaded. // Separate into two passes one for types and function signatures, and @@ -225,22 +248,26 @@ pub const Compiler = struct { // Load core module first since the members are imported into each user module. var core_sym: *cy.sym.Chunk = undefined; - if (self.importBuiltins) { - const importCore = ImportTask{ - .type = .nop, - .from = null, - .nodeId = cy.NullNode, - .resolved_spec = try self.alloc.dupe(u8, "builtins"), - .data = undefined, - }; - try self.import_tasks.append(self.alloc, importCore); - const core_chunk = performImportTask(self, importCore) catch |err| { - return err; - }; - core_sym = core_chunk.sym; - _ = self.import_tasks.orderedRemove(0); - try reserveCoreTypes(self); - try createDynMethodIds(self); + if (self.importCore) { + if (!self.cont) { + const importCore = ImportTask{ + .type = .nop, + .from = null, + .nodeId = cy.NullNode, + .resolved_spec = try self.alloc.dupe(u8, "core"), + .data = undefined, + }; + try self.import_tasks.append(self.alloc, importCore); + const core_chunk = performImportTask(self, importCore) catch |err| { + return err; + }; + core_sym = core_chunk.sym; + _ = self.import_tasks.orderedRemove(0); + try reserveCoreTypes(self); + try createDynMethodIds(self); + } else { + core_sym = self.chunk_map.get("core").?.sym; + } } // Main chunk. @@ -248,10 +275,32 @@ pub const Compiler = struct { var mainChunk = try self.alloc.create(cy.Chunk); mainChunk.* = try cy.Chunk.init(self, nextId, r_uri, src); mainChunk.sym = try mainChunk.createChunkSym(r_uri); - self.main_chunk = mainChunk; try self.chunks.append(self.alloc, mainChunk); try self.chunk_map.put(self.alloc, r_uri, mainChunk); + if (self.cont) { + // Use all top level syms from previous main. + var iter = self.main_chunk.sym.getMod().symMap.iterator(); + while (iter.next()) |e| { + const name = e.key_ptr.*; + const sym = e.value_ptr.*; + + const alias = try mainChunk.reserveUseAlias(@ptrCast(mainChunk.sym), name, cy.NullNode); + if (sym.type == .use_alias) { + alias.sym = sym.cast(.use_alias).sym; + } else { + alias.sym = sym; + } + alias.resolved = true; + } + + if (self.main_chunk.use_global) { + mainChunk.use_global = true; + } + } + + self.main_chunk = mainChunk; + // All symbols are reserved by loading all modules and looking at the declarations. try reserveSyms(self, core_sym); @@ -262,7 +311,7 @@ pub const Compiler = struct { try resolveSyms(self); // Pass through type syms. - for (self.sema.types.items) |*type_e| { + for (self.newTypes()) |*type_e| { if (type_e.sym.getMod().?.getSym("$get") != null) { type_e.has_get_method = true; } @@ -279,7 +328,7 @@ pub const Compiler = struct { // Perform sema on static initializers. log.tracev("Perform init sema.", .{}); - for (self.chunks.items) |chunk| { + for (self.newChunks()) |chunk| { // First stmt is root at index 0. _ = try chunk.ir.pushEmptyStmt2(chunk.alloc, .root, chunk.parserAstRootId, false); try chunk.ir.pushStmtBlock2(chunk.alloc, chunk.rootStmtBlock); @@ -295,7 +344,7 @@ pub const Compiler = struct { // Perform sema on all chunks. log.tracev("Perform sema.", .{}); - for (self.chunks.items) |chunk| { + for (self.newChunks()) |chunk| { performChunkSema(self, chunk) catch |err| { if (err == error.CompileError) { return err; @@ -308,7 +357,7 @@ pub const Compiler = struct { } // Perform deferred sema. - for (self.chunks.items) |chunk| { + for (self.newChunks()) |chunk| { try chunk.ir.pushStmtBlock2(chunk.alloc, chunk.rootStmtBlock); for (chunk.variantFuncSyms.items) |func| { if (func.isMethod) { @@ -673,7 +722,7 @@ fn performImportTask(self: *Compiler, task: ImportTask) !*cy.Chunk { fn reserveSyms(self: *Compiler, core_sym: *cy.sym.Chunk) !void { log.tracev("Reserve symbols.", .{}); - var id: u32 = 0; + var id: u32 = self.chunk_start; while (true) { while (id < self.chunks.items.len) : (id += 1) { var last_type_sym: *cy.Sym = undefined; @@ -891,7 +940,7 @@ fn createDynMethodIds(self: *Compiler) !void { fn resolveSyms(self: *Compiler) !void { log.tracev("Resolve syms.", .{}); - for (self.chunks.items) |chunk| { + for (self.newChunks()) |chunk| { // Process static declarations. var last_type_sym: *cy.Sym = undefined; for (chunk.parser.staticDecls.items) |*decl| { diff --git a/src/debug.zig b/src/debug.zig index 4ea8efdf0..d80194a47 100644 --- a/src/debug.zig +++ b/src/debug.zig @@ -655,6 +655,178 @@ pub fn dumpInst(vm: *cy.VM, pcOffset: u32, code: cy.OpCode, pc: [*]const cy.Inst try bytecode.dumpInst(vm, pcOffset, code, pc, .{ .extra = extra, .prefix = prefix }); } +const DumpValueConfig = struct { + show_rc: bool = false, + show_ptr: bool = false, + max_depth: u32 = 4, + force_types: bool = false, +}; + +const DumpValueState = struct { + depth: u32, +}; + +pub fn dumpValue(vm: *cy.VM, w: anytype, val: cy.Value, config: DumpValueConfig) !void { + var state = DumpValueState{ + .depth = 1, + }; + try dumpValue2(vm, &state, w, val, config); +} + +fn getTypeName(vm: *cy.VM, type_id: cy.TypeId) []const u8 { + return vm.sema.getTypeBaseName(type_id); +} + +fn dumpValue2(vm: *cy.VM, state: *DumpValueState, w: anytype, val: cy.Value, config: DumpValueConfig) !void { + const type_id = val.getTypeId(); + switch (type_id) { + bt.Map => { + try w.writeByte('`'); + const name = getTypeName(vm, type_id); + _ = try w.writeAll(name); + _ = try w.print("[{}]` ", .{val.asHeapObject().map.map().size}); + }, + bt.Table => { + if (state.depth == 1 or config.force_types) { + try w.writeByte('`'); + const name = getTypeName(vm, type_id); + _ = try w.writeAll(name); + _ = try w.print("[{}]` ", .{val.asHeapObject().table.map().size}); + } + }, + bt.List => { + if (state.depth == 1 or config.force_types) { + try w.writeByte('`'); + const name = getTypeName(vm, type_id); + _ = try w.writeAll(name); + _ = try w.print("[{}]` ", .{val.asHeapObject().list.list.len}); + } + }, + bt.Error, + bt.String, + bt.Integer, + bt.Float => { + if (state.depth == 1 or config.force_types) { + const name = getTypeName(vm, type_id); + try w.writeByte('`'); + _ = try w.writeAll(name); + _ = try w.writeAll("` "); + } + }, + else => { + const name = getTypeName(vm, type_id); + try w.writeByte('`'); + _ = try w.writeAll(name); + if (type_id == bt.Void) { + _ = try w.writeAll("`"); + } else { + _ = try w.writeAll("` "); + } + }, + } + + switch (type_id) { + bt.Integer => try w.print("{}", .{val.asInteger()}), + bt.Float => { + const f = val.asF64(); + if (cy.Value.floatCanBeInteger(f)) { + try std.fmt.format(w, "{d:.0}", .{f}); + } else { + try std.fmt.format(w, "{d}", .{f}); + } + }, + bt.Void => {}, + bt.Error => { + try w.print(".{}", .{val.asErrorSymbol()}); + }, + bt.Symbol => { + try w.print(".{}", .{val.asSymbolId()}); + }, + else => { + if (val.isPointer()) { + const obj = val.asHeapObject(); + if (config.show_ptr) { + try w.print(" {*}", .{obj}); + } + if (config.show_rc) { + try w.print(" rc={}", .{obj.head.rc}); + } + switch (type_id) { + bt.List => { + if (state.depth < config.max_depth) { + state.depth += 1; + _ = try w.writeAll("["); + const children = obj.list.items(); + for (children, 0..) |childv, i| { + try dumpValue2(vm, state, w, childv, config); + if (i < children.len - 1) { + _ = try w.writeAll(", "); + } + } + _ = try w.writeByte(']'); + } + }, + bt.Table => { + const size = obj.table.map().size; + if (state.depth < config.max_depth) { + state.depth += 1; + _ = try w.writeAll("{"); + var iter = obj.table.map().iterator(); + var i: u32 = 0; + while (iter.next()) |e| { + try dumpValue2(vm, state, w, e.key, config); + _ = try w.writeAll(": "); + try dumpValue2(vm, state, w, e.value, config); + if (i < size - 1) { + _ = try w.writeAll(", "); + } + i += 1; + } + _ = try w.writeByte('}'); + } + }, + bt.Map => { + const size = obj.map.map().size; + if (state.depth < config.max_depth) { + state.depth += 1; + _ = try w.writeAll("{"); + var iter = obj.map.map().iterator(); + var i: u32 = 0; + while (iter.next()) |e| { + try dumpValue2(vm, state, w, e.key, config); + _ = try w.writeAll(": "); + try dumpValue2(vm, state, w, e.value, config); + if (i < size - 1) { + _ = try w.writeAll(", "); + } + i += 1; + } + _ = try w.writeByte('}'); + } + }, + bt.String => { + const MaxStrLen = 30; + const str = obj.string.getSlice(); + if (str.len > MaxStrLen) { + try w.print("'{s}'...", .{str[0..MaxStrLen]}); + } else { + try w.print("'{s}'", .{str}); + } + }, + // bt.Lambda => try w.print("Lambda {*}", .{obj}), + // bt.Closure => try w.print("Closure {*}", .{obj}), + // bt.Fiber => try w.print("Fiber {*}", .{obj}), + // bt.HostFunc => try w.print("NativeFunc {*}", .{obj}), + // bt.Pointer => try w.print("Pointer {*} ptr={*}", .{obj, obj.pointer.ptr}), + else => {}, + } + } else { + try w.print("unsupported", .{}); + } + }, + } +} + const EnableTimerTrace = builtin.os.tag != .freestanding and builtin.mode == .Debug; const TimerTrace = struct { diff --git a/src/include/cyber.h b/src/include/cyber.h index f5bd705cd..9d0896433 100644 --- a/src/include/cyber.h +++ b/src/include/cyber.h @@ -362,7 +362,11 @@ void csSetErrorPrinter(CsVM* vm, CsPrintErrorFn print); CsEvalConfig csDefaultEvalConfig(void); CsCompileConfig csDefaultCompileConfig(void); +// Resets the compiler and runtime state. +void csReset(CsVM* vm); + // Evalutes the source code and returns the result code. +// Subsequent evals will reuse the same compiler and runtime state. // If the last statement of the script is an expression, `outVal` will contain the value. CsResultCode csEval(CsVM* vm, CsStr src, CsValue* outVal); diff --git a/src/lib.zig b/src/lib.zig index 37d1f1d87..1c4e715d7 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -93,8 +93,11 @@ export fn csDefaultEvalConfig() c.EvalConfig { }; } +export fn csReset(vm: *cy.VM) void { + vm.resetVM() catch cy.fatal(); +} + export fn csEval(vm: *cy.VM, src: c.Str, outVal: *cy.Value) c.ResultCode { - vm.deinit(true); const uri: []const u8 = "main"; return csEvalExt(vm, c.toStr(uri), src, c.defaultEvalConfig(), outVal); } @@ -625,7 +628,7 @@ test "csGetTypeId()" { export fn csNewValueDump(vm: *cy.VM, val: Value) c.Str { var buf: std.ArrayListUnmanaged(u8) = .{}; const w = buf.writer(vm.alloc); - val.writeDump(w) catch cy.fatal(); + cy.debug.dumpValue(vm, w, val, .{}) catch cy.fatal(); return c.toStr(buf.toOwnedSlice(vm.alloc) catch cy.fatal()); } diff --git a/src/main.zig b/src/main.zig index c205894da..22764c514 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,6 +7,7 @@ const log = cy.log.scoped(.main); const cli = @import("cli.zig"); const build_options = @import("build_options"); const fmt = @import("fmt.zig"); +const ln = @import("linenoise"); comptime { const lib = @import("lib.zig"); std.testing.refAllDecls(lib); @@ -41,7 +42,7 @@ pub fn main() !void { const args = try std.process.argsAlloc(alloc); defer std.process.argsFree(alloc, args); - var cmd = Command.none; + var cmd = Command.repl; var arg0: ?[]const u8 = null; var i: usize = 1; @@ -78,7 +79,7 @@ pub fn main() !void { // Ignore unrecognized options so a script can use them. } } else { - if (cmd == .none) { + if (cmd == .repl) { if (std.mem.eql(u8, arg, "compile")) { cmd = .compile; } else if (std.mem.eql(u8, arg, "version")) { @@ -122,8 +123,8 @@ pub fn main() !void { .version => { version(); }, - .none => { - help(); + .repl => { + try repl(alloc); }, } } @@ -140,7 +141,7 @@ const Command = enum { compile, help, version, - none, + repl, }; fn compilePath(alloc: std.mem.Allocator, path: []const u8) !void { @@ -170,6 +171,166 @@ fn compilePath(alloc: std.mem.Allocator, path: []const u8) !void { try cy.debug.dumpBytecode(&vm, .{ .pcContext = pc }); } +const use_ln = builtin.os.tag != .windows; + +fn getReplInput(alloc: std.mem.Allocator, indent: u32) ![]const u8 { + if (use_ln) { + var linez: [*c]const u8 = undefined; + if (indent == 0) { + linez = ln.linenoise("> "); + if (linez == null) { + return error.EndOfInput; + } + } else { + var buf: [1024]u8 = undefined; + var fbuf = std.io.fixedBufferStream(&buf); + const w = fbuf.writer(); + try w.writeByteNTimes(' ', indent * 4); + try w.writeAll("| \x00"); + linez = ln.linenoise(&buf); + } + + _ = ln.linenoiseHistoryAdd(linez); + return std.mem.sliceTo(linez, 0); + } else { + const stdin = std.io.getStdIn(); + const stdout = std.io.getStdOut(); + if (indent == 0) { + try stdout.writeAll("> "); + } else { + try stdout.writer().writeByteNTimes(' ', indent * 4); + try stdout.writeAll("| "); + } + return stdin.reader().readUntilDelimiterAlloc(alloc, '\n', 10e8); + } +} + +fn freeReplInput(alloc: std.mem.Allocator, input: []const u8) void { + if (use_ln) { + ln.linenoiseFree(@constCast(input.ptr)); + } else { + alloc.free(input); + } +} + +fn repl(alloc: std.mem.Allocator) !void { + const stdout = std.io.getStdOut(); + + c.setVerbose(verbose); + try vm.init(alloc); + cli.csSetupForCLI(@ptrCast(&vm)); + defer vm.deinit(false); + + var config = c.defaultEvalConfig(); + config.single_run = builtin.mode == .ReleaseFast; + config.file_modules = true; + config.reload = reload; + config.backend = c.BackendVM; + config.spawn_exe = false; + + // TODO: Record inputs that successfully compiled. Can then be exported to file. + + // Initial input includes `use $global`. + // Can also include additional source if needed. + const init_src = + \\use $global + \\ + ; + _ = vm.eval("repl_init", init_src, config) catch return error.Unexpected; + + // Build multi-line input. + var input_builder: std.ArrayListUnmanaged(u8) = .{}; + defer input_builder.deinit(alloc); + var indent: u32 = 0; + + try stdout.writer().print("{s} REPL\n", .{build_options.full_version}); + try stdout.writeAll("Commands: .exit\n"); + while (true) { + var input = try getReplInput(alloc, indent); + defer freeReplInput(alloc, input); + + if (std.mem.eql(u8, ".exit", input)) { + break; + } + + if (std.mem.endsWith(u8, input, ":")) { + try input_builder.appendSlice(alloc, input); + indent += 1; + continue; + } + + if (input_builder.items.len > 0) { + if (input.len == 0) { + indent -= 1; + if (indent > 0) { + continue; + } else { + // Build input and submit. + freeReplInput(alloc, input); + if (use_ln) { + // malloc so that `freeReplInput` performs `free`. + const inputz: [*]u8 = @ptrCast(std.c.malloc(input_builder.items.len + 1).?); + @memcpy(inputz[0..input_builder.items.len], input_builder.items); + inputz[input_builder.items.len] = 0; + input = inputz[0..input_builder.items.len]; + input_builder.clearRetainingCapacity(); + } else { + input = try input_builder.toOwnedSlice(alloc); + } + } + } else { + try input_builder.append(alloc, '\n'); + try input_builder.appendNTimes(alloc, ' ', indent * 4); + try input_builder.appendSlice(alloc, input); + continue; + } + } + + const val_opt: ?cy.Value = vm.eval("input", input, config) catch |err| b: { + switch (err) { + error.Panic => { + if (!c.silent()) { + const report = c.newPanicSummary(@ptrCast(&vm)); + defer c.freeStr(@ptrCast(&vm), report); + try std.io.getStdErr().writeAll(c.fromStr(report)); + } + }, + error.CompileError => { + if (!c.silent()) { + const report = c.newErrorReportSummary(@ptrCast(&vm)); + defer c.freeStr(@ptrCast(&vm), report); + try std.io.getStdErr().writeAll(c.fromStr(report)); + } + }, + else => { + std.debug.print("unexpected {}\n", .{err}); + }, + } + break :b null; + }; + + if (val_opt) |val| { + if (!val.isVoid()) { + try cy.debug.dumpValue(&vm, stdout.writer(), val, .{}); + try stdout.writeAll("\n"); + } + } + } + + if (verbose) { + std.debug.print("\n==VM Info==\n", .{}); + try vm.dumpInfo(); + } + if (cy.Trace and dumpStats) { + vm.dumpStats(); + } + if (cy.TrackGlobalRC) { + vm.deinitRtObjects(); + vm.compiler.deinitModRetained(); + try cy.arc.checkGlobalRC(&vm); + } +} + fn evalPath(alloc: std.mem.Allocator, path: []const u8) !void { c.setVerbose(verbose); @@ -232,6 +393,7 @@ fn help() void { \\Usage: cyber [command?] [options] [source] \\ \\Commands: + \\ cyber Run the REPL. \\ cyber [source] Compile and run. \\ cyber compile [source] Compile and dump the code. \\ cyber help Print usage. diff --git a/src/parser.zig b/src/parser.zig index 672cc64b6..3c5b099f2 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1521,31 +1521,37 @@ pub const Parser = struct { name = try self.pushSpanNode(.ident, self.next_pos); self.advance(); - if (self.peek().tag() == .raw_string) { - spec = try self.pushSpanNode(.raw_string_lit, self.next_pos); - self.advance(); - try self.consumeNewLineOrEnd(); - } else if (self.peek().tag() == .new_line) { - self.advance(); - } else if (self.peek().tag() == .minus_right_angle) { - self.advance(); - const target = (try self.parseExpr(.{})) orelse { - return self.reportError("Expected condition expression.", &.{}); - }; - const alias = try self.pushNode(.use_alias, start); - self.ast.setNodeData(alias, .{ .use_alias = .{ - .name = name, - .target = target, - }}); - try self.staticDecls.append(self.alloc, .{ - .declT = .use_alias, - .nodeId = alias, - .data = undefined, - }); - try self.consumeNewLineOrEnd(); - return alias; - } else { - return self.reportError("Expected a module specifier.", &.{}); + switch (self.peek().tag()) { + .raw_string => { + spec = try self.pushSpanNode(.raw_string_lit, self.next_pos); + self.advance(); + try self.consumeNewLineOrEnd(); + }, + .new_line => { + self.advance(); + }, + .null => {}, + .minus_right_angle => { + self.advance(); + const target = (try self.parseExpr(.{})) orelse { + return self.reportError("Expected condition expression.", &.{}); + }; + const alias = try self.pushNode(.use_alias, start); + self.ast.setNodeData(alias, .{ .use_alias = .{ + .name = name, + .target = target, + }}); + try self.staticDecls.append(self.alloc, .{ + .declT = .use_alias, + .nodeId = alias, + .data = undefined, + }); + try self.consumeNewLineOrEnd(); + return alias; + }, + else => { + return self.reportError("Expected a module specifier.", &.{}); + }, } } else if (self.peek().tag() == .star) { name = try self.pushNode(.all, self.next_pos); diff --git a/src/sema.zig b/src/sema.zig index 617d97d28..c05dacd6a 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -2755,21 +2755,23 @@ pub fn semaMainBlock(compiler: *cy.Compiler, mainc: *cy.Chunk) !u32 { const id = try pushProc(mainc, null); mainc.mainSemaProcId = id; - if (compiler.global_sym) |global| { - const set = try mainc.ir.pushEmptyStmt(compiler.alloc, .setVarSym, cy.NullNode); - const sym = try mainc.ir.pushExpr(.varSym, compiler.alloc, bt.Map, cy.NullNode, .{ .sym = @as(*cy.Sym, @ptrCast(global)) }); - const map = try mainc.semaMap(cy.NullNode); - mainc.ir.setStmtData(set, .setVarSym, .{ .generic = .{ - .left_t = CompactType.initStatic(bt.Map), - .right_t = CompactType.initStatic(bt.Map), - .left = sym, - .right = map.irIdx, - }}); + if (!compiler.cont) { + if (compiler.global_sym) |global| { + const set = try mainc.ir.pushEmptyStmt(compiler.alloc, .setVarSym, cy.NullNode); + const sym = try mainc.ir.pushExpr(.varSym, compiler.alloc, bt.Map, cy.NullNode, .{ .sym = @as(*cy.Sym, @ptrCast(global)) }); + const map = try mainc.semaMap(cy.NullNode); + mainc.ir.setStmtData(set, .setVarSym, .{ .generic = .{ + .left_t = CompactType.initStatic(bt.Map), + .right_t = CompactType.initStatic(bt.Map), + .left = sym, + .right = map.irIdx, + }}); + } } // Emit IR for initializers. DFS order. Stop at circular dependency. // TODO: Allow circular dependency between chunks but not symbols. - for (compiler.chunks.items) |c| { + for (compiler.newChunks()) |c| { if (c.hasStaticInit and !c.initializerVisited) { try visitChunkInit(compiler, c, ); } diff --git a/src/value.zig b/src/value.zig index 97bfbd9a6..75fade597 100644 --- a/src/value.zig +++ b/src/value.zig @@ -445,58 +445,6 @@ pub const Value = packed union { return @intCast(self.val & 0xffffffff); } - pub fn writeDump(self: *const Value, w: anytype) !void { - switch (self.getTypeId()) { - bt.Void => { - _ = try w.writeAll("void"); - }, - bt.Float => { - try w.print("Float {}", .{self.asF64()}); - }, - bt.Integer => { - try w.print("Integer {}", .{self.asInteger()}); - }, - bt.Error => { - try w.print("Error {}", .{self.asErrorSymbol()}); - }, - bt.Symbol => { - try w.print("Symbol {}", .{self.asSymbolId()}); - }, - else => |typeId| { - if (self.isPointer()) { - const obj = self.asHeapObject(); - switch (obj.getTypeId()) { - bt.List => try w.print("List {*} rc={} len={}", .{obj, obj.head.rc, obj.list.list.len}), - bt.Map => try w.print("Map {*} rc={} size={}", .{obj, obj.head.rc, obj.map.inner.size}), - bt.String => { - const str = obj.string.getSlice(); - if (str.len > 20) { - try w.print("String {*} rc={} len={} str=\"{s}\"...", .{obj, obj.head.rc, str.len, str[0..20]}); - } else { - try w.print("String {*} rc={} len={} str={s}", .{obj, obj.head.rc, str.len, str}); - } - }, - bt.Lambda => try w.print("Lambda {*} rc={}", .{obj, obj.head.rc}), - bt.Closure => try w.print("Closure {*} rc={}", .{obj, obj.head.rc}), - bt.Fiber => try w.print("Fiber {*} rc={}", .{obj, obj.head.rc}), - bt.HostFunc => try w.print("NativeFunc {*} rc={}", .{obj, obj.head.rc}), - bt.Pointer => try w.print("Pointer {*} rc={} ptr={*}", .{obj, obj.head.rc, obj.pointer.ptr}), - else => { - try w.print("HeapObject {*} type={} rc={}", .{obj, obj.getTypeId(), obj.head.rc}); - }, - } - } else { - if (self.isEnum()) { - try w.print("Enum {}", .{typeId}); - } else { - try w.print("Unknown {}", .{self.getTag()}); - } - } - } - } - } - - pub fn getUserTag(self: *const Value) ValueUserTag { const typeId = self.getTypeId(); switch (typeId) { diff --git a/src/vm.h b/src/vm.h index 9fd6af959..3ddc50464 100644 --- a/src/vm.h +++ b/src/vm.h @@ -1023,7 +1023,7 @@ char* zOpCodeName(OpCode code); PcSpResult zCallSym(VM* vm, Inst* pc, Value* stack, u16 symId, u8 startLocal, u8 numArgs); PcSpResult zCallSymDyn(VM* vm, Inst* pc, Value* stack, u16 symId, u8 startLocal, u8 numArgs); void zDumpEvalOp(VM* vm, Inst* pc); -void zDumpValue(Value val); +void zDumpValue(VM* vm, Value val); void zFreeObject(VM* vm, HeapObject* obj); void zEnd(VM* vm, Inst* pc); ValueResult zAllocList(VM* vm, Value* elemStart, uint8_t nelems); diff --git a/src/vm.zig b/src/vm.zig index 7a8721aeb..61d1702dc 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -170,6 +170,13 @@ pub const VM = struct { lastExeError: []const u8, last_res: cc.ResultCode, + num_evals: u32, + + /// Save vm state at the end of execution so that a subsequent eval knows where it left off + /// from a the previous bytecode buffer. + num_cont_evals: u32, + last_bc_len: u32, + pub fn init(self: *VM, alloc: std.mem.Allocator) !void { self.* = .{ .alloc = alloc, @@ -237,6 +244,9 @@ pub const VM = struct { .tempBuf = undefined, .lastExeError = "", .last_res = cc.Success, + .num_evals = 0, + .num_cont_evals = 0, + .last_bc_len = 0, }; self.mainFiber.panicType = vmc.PANIC_NONE; self.curFiber = &self.mainFiber; @@ -481,8 +491,9 @@ pub const VM = struct { return res; } - fn resetVM(self: *VM) !void { + pub fn resetVM(self: *VM) !void { self.deinit(true); + self.num_cont_evals = 0; // Reset flags for next reset/deinit. self.deinitedRtObjects = false; @@ -490,14 +501,16 @@ pub const VM = struct { // Before reinit, everything in VM, VMcompiler should be cleared. - try self.compiler.reinit(); + try self.compiler.reinitPerRun(); + self.compiler.cont = false; } pub fn eval(self: *VM, src_uri: []const u8, src: ?[]const u8, config: cc.EvalConfig) !Value { - try self.resetVM(); - self.config = config; var tt = cy.debug.timer(); + self.config = config; + try self.compiler.reinitPerRun(); + var compile_c = cc.defaultCompileConfig(); compile_c.single_run = config.single_run; compile_c.file_modules = config.file_modules; @@ -590,7 +603,7 @@ pub const VM = struct { return error.Panic; } } - return Value.initInt(0); + return Value.Void; } } @@ -701,7 +714,11 @@ pub const VM = struct { self.unwindTempPrevIndexes = buf.unwindTempPrevIndexes.items; // Set these last to hint location to cache before eval. - self.pc = @ptrCast(buf.ops.items.ptr); + if (self.num_cont_evals > 0) { + self.pc = @ptrCast(&buf.ops.items[self.last_bc_len]); + } else { + self.pc = @ptrCast(buf.ops.items.ptr); + } try cy.fiber.stackEnsureTotalCapacity(self, buf.mainStackSize); self.framePtr = @ptrCast(self.stack.ptr); @@ -709,11 +726,16 @@ pub const VM = struct { self.consts = buf.mconsts; self.types = self.compiler.sema.types.items; + defer { + self.num_cont_evals += 1; + self.num_evals += 1; + self.last_bc_len = @intCast(self.ops.len); + } try @call(.never_inline, evalLoopGrowStack, .{self, true}); logger.tracev("main stack size: {}", .{buf.mainStackSize}); if (self.endLocal == 255) { - return Value.initInt(0); + return Value.Void; } else { return self.stack[self.endLocal]; } @@ -4259,10 +4281,10 @@ pub export fn zAllocStringTemplate2(vm: *cy.VM, strs: [*]cy.Value, strCount: u8, }; } -export fn zDumpValue(val: Value) void { +export fn zDumpValue(vm: *VM, val: Value) void { var buf: [1024]u8 = undefined; var fbuf = std.io.fixedBufferStream(&buf); - val.writeDump(fbuf.writer()) catch cy.fatal(); + cy.debug.dumpValue(vm, fbuf.writer(), val, .{}) catch cy.fatal(); cy.rt.log(fbuf.getWritten()); } diff --git a/test/setup.zig b/test/setup.zig index 5e61a55ae..59b2276cd 100644 --- a/test/setup.zig +++ b/test/setup.zig @@ -213,6 +213,7 @@ pub const VMrunner = struct { .spawn_exe = false, .reload = config.reload, }; + c.reset(vm); const res_code = c.evalExt(vm, c.toStr(r_uri), c.toStr(src), c_config, @ptrCast(&resv)); if (optCb) |cb| {