Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to EigenScript are documented here.

## [Unreleased]

### Added
- **Interactive REPL line editor** (#392) — the REPL on a tty now has
history (in-memory + `EIGS_HISTORY` file, default
`~/.eigenscript_history`, created `0600`), arrow-key cursor editing,
Home/End/Delete, Ctrl-A/E/U/K/W/L, Ctrl-C line-and-block cancel, and tab
completion over builtins + session bindings (both are env bindings, so
one walk covers them). Zero new link dependencies: a raw-termios editor
in the new CLI-only `src/repl.c` (ported from the EigenOS console
editor), not readline/libedit — the embed/freestanding profiles never
see it. Piped/non-tty sessions keep the old fgets loop byte-for-byte
(`EIGS_REPL_PLAIN=1` forces it on a tty). Interactive sessions record
assignment history from the start, so `prev of x` works on session
bindings instead of silently answering from no history. Suite section
[42c] drives the editor on a real pty (`tests/test_repl.py`) and pins
the piped transcript as a byte-exact golden.

### Fixed
- **Four silent-wrong-answer builtin contracts** (#312, #314, #316, #317),
batch-fixed after the 2026-07 backlog triage:
Expand Down
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ LDFLAGS := -pie -Wl,-z,relro,-z,now -lm -lpthread
endif

SRC_DIR := src
SOURCES := $(SRC_DIR)/eigenscript.c $(SRC_DIR)/lexer.c $(SRC_DIR)/parser.c $(SRC_DIR)/builtins.c $(SRC_DIR)/builtins_tensor.c $(SRC_DIR)/hash.c $(SRC_DIR)/arena.c $(SRC_DIR)/state.c $(SRC_DIR)/strbuf.c $(SRC_DIR)/ext_store.c $(SRC_DIR)/fmt.c $(SRC_DIR)/lint.c $(SRC_DIR)/chunk.c $(SRC_DIR)/compiler.c $(SRC_DIR)/vm.c $(SRC_DIR)/jit.c $(SRC_DIR)/trace.c $(SRC_DIR)/eigs_embed.c $(SRC_DIR)/main.c
SOURCES := $(SRC_DIR)/eigenscript.c $(SRC_DIR)/lexer.c $(SRC_DIR)/parser.c $(SRC_DIR)/builtins.c $(SRC_DIR)/builtins_tensor.c $(SRC_DIR)/hash.c $(SRC_DIR)/arena.c $(SRC_DIR)/state.c $(SRC_DIR)/strbuf.c $(SRC_DIR)/ext_store.c $(SRC_DIR)/fmt.c $(SRC_DIR)/lint.c $(SRC_DIR)/chunk.c $(SRC_DIR)/compiler.c $(SRC_DIR)/vm.c $(SRC_DIR)/jit.c $(SRC_DIR)/trace.c $(SRC_DIR)/eigs_embed.c $(SRC_DIR)/repl.c $(SRC_DIR)/main.c
BINARY := $(SRC_DIR)/eigenscript

# CLI-only translation units: linked into the binary, never into the
# runtime library (repl.c pulls termios/isatty — banned in the
# freestanding/embed profile, same footing as main.c).
CLI_ONLY := $(SRC_DIR)/main.c $(SRC_DIR)/repl.c

FULL_SOURCES := $(SOURCES) $(SRC_DIR)/ext_http.c $(SRC_DIR)/ext_db.c \
$(SRC_DIR)/model_io.c $(SRC_DIR)/model_infer.c $(SRC_DIR)/model_train.c

Expand All @@ -30,7 +35,7 @@ PREFIX := $(HOME)/.local
# The LSP links the whole runtime (minus main.c): eigenscript.c calls into
# the VM/compiler/trace layers, so a hand-picked subset bitrots every time
# the runtime grows (it had, silently — nothing built this target in CI).
LSP_SOURCES := $(SRC_DIR)/eigenlsp.c $(filter-out $(SRC_DIR)/main.c,$(SOURCES))
LSP_SOURCES := $(SRC_DIR)/eigenlsp.c $(filter-out $(CLI_ONLY),$(SOURCES))
LSP_BINARY := $(SRC_DIR)/eigenlsp

.PHONY: all build full http gfx lib amalgamation tsan test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp jit-smoke embed-smoke asan valgrind pgo freestanding-check freestanding-libc-diff print-%
Expand Down Expand Up @@ -121,7 +126,7 @@ jit-smoke:
$(CC) -Wall -Wextra -O2 -o /tmp/jit_smoke $(SRC_DIR)/jit.c $(SRC_DIR)/jit_smoke.c -lm
/tmp/jit_smoke

EMBED_SOURCES := $(filter-out $(SRC_DIR)/main.c,$(SOURCES))
EMBED_SOURCES := $(filter-out $(CLI_ONLY),$(SOURCES))

# Single-file amalgamation (#397): "copy two files, call eigs_open".
# build/eigenscript_all.c (self-contained, system headers only) + the public
Expand Down Expand Up @@ -266,7 +271,7 @@ coverage: coverage-clean
# Like the LSP, the fuzz harness links the whole runtime minus main.c —
# the old hand-picked subset bitrotted when the bytecode VM replaced the
# tree-walking evaluator (eval_node), leaving `make fuzz` unbuildable.
FUZZ_SOURCES := $(filter-out $(SRC_DIR)/main.c,$(SOURCES))
FUZZ_SOURCES := $(filter-out $(CLI_ONLY),$(SOURCES))

fuzz: fuzz/fuzz_stdin.c $(FUZZ_SOURCES)
$(CC) -g -fsanitize=address,undefined -o fuzz/fuzz_stdin \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Image tags: `:latest`, `:X.Y.Z`, `:X.Y`, `:X` for releases; `:edge` tracks `main

```bash
eigenscript program.eigs # run a script
eigenscript # interactive REPL
eigenscript # interactive REPL (history, arrow keys, tab completion)
eigenscript --version
```

Expand Down
5 changes: 3 additions & 2 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ VERSION=$(cat ../VERSION)
# Compiler is overridable (e.g. CC=clang ./build.sh) so CI can exercise
# more than one toolchain; defaults to gcc.
CC="${CC:-gcc}"
SOURCES="eigenscript.c lexer.c parser.c builtins.c builtins_tensor.c hash.c arena.c state.c strbuf.c ext_store.c fmt.c lint.c chunk.c compiler.c vm.c jit.c trace.c eigs_embed.c main.c"
SOURCES="eigenscript.c lexer.c parser.c builtins.c builtins_tensor.c hash.c arena.c state.c strbuf.c ext_store.c fmt.c lint.c chunk.c compiler.c vm.c jit.c trace.c eigs_embed.c repl.c main.c"

# macOS Intel JIT: enabled via the Mach-O TLV-aware prologue (the JIT
# now calls eigs_jit_load_eigs_current instead of inlining %fs:
Expand All @@ -25,7 +25,8 @@ if [ "$1" = "lsp" ]; then
# toolchain. Links eigenlsp.c against the runtime (SOURCES minus main.c),
# minimal extensions, gcc-only like the rest of build.sh. Source list reused
# from SOURCES above so it can't drift.
LSP_SOURCES="${SOURCES/ main.c/} eigenlsp.c"
LSP_SOURCES="${SOURCES/ main.c/}"
LSP_SOURCES="${LSP_SOURCES/ repl.c/} eigenlsp.c" # repl.c is CLI-only, like main.c
$CC -Wall -Wextra -Werror=implicit-function-declaration -O2 -fstack-protector-strong -o eigenlsp $LSP_SOURCES \
-DEIGENSCRIPT_EXT_HTTP=0 \
-DEIGENSCRIPT_EXT_MODEL=0 \
Expand Down
132 changes: 2 additions & 130 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "state.h"
#include "vm.h"
#include "trace.h"
#include "repl.h"
#if EIGENSCRIPT_EXT_GFX
void register_gfx_builtins(Env *env);
#endif
Expand All @@ -14,137 +15,8 @@ void register_gfx_builtins(Env *env);
#define EIGENSCRIPT_VERSION "dev"
#endif

/* ---- REPL ---- */

static void eigenscript_repl(Env *env) {
printf("EigenScript %s\n", EIGENSCRIPT_VERSION);
printf("Type 'exit' or Ctrl-D to quit.\n\n");

char line_buf[4096];
strbuf input;
strbuf_init(&input);
int continuation = 0;

while (1) {
printf(continuation ? "... " : "eigs> ");
fflush(stdout);

if (!fgets(line_buf, sizeof(line_buf), stdin)) {
printf("\n");
break;
}

/* Exit commands */
if (!continuation) {
char *trimmed = line_buf;
while (*trimmed == ' ' || *trimmed == '\t') trimmed++;
if (strcmp(trimmed, "exit\n") == 0 || strcmp(trimmed, "quit\n") == 0 ||
strcmp(trimmed, "exit\r\n") == 0 || strcmp(trimmed, "quit\r\n") == 0) {
break;
}
}

int len = strlen(line_buf);
strbuf_append_n(&input, line_buf, (size_t)len);

/* Multi-line detection */
if (!continuation) {
/* Check if line ends with colon (block opener) */
char *end = line_buf + len - 1;
while (end > line_buf && (*end == '\n' || *end == '\r' || *end == ' ')) end--;
if (*end == ':') {
continuation = 1;
continue;
}
} else {
/* In continuation: blank line or unindented line ends block */
char *trimmed = line_buf;
while (*trimmed == ' ' || *trimmed == '\t') trimmed++;
if (*trimmed == '\n' || *trimmed == '\r' || *trimmed == '\0') {
continuation = 0;
/* fall through to execute */
} else if (line_buf[0] == ' ' || line_buf[0] == '\t') {
continue; /* still indented, keep accumulating */
} else {
continuation = 0;
/* unindented non-blank: end of block */
}
}

/* Skip empty input */
{
char *check = input.data;
while (*check == ' ' || *check == '\t' || *check == '\n' || *check == '\r') check++;
if (*check == '\0') {
input.len = 0;
input.data[0] = '\0';
continue;
}
}

/* Tokenize, parse, eval with error recovery */
g_parse_errors = 0;
TokenList tl = tokenize(input.data);
if (g_parse_errors > 0) {
free_tokenlist(&tl);
input.len = 0;
input.data[0] = '\0';
continue;
}

ASTNode *ast = parse(&tl);
if (g_parse_errors > 0) {
free_tokenlist(&tl);
input.len = 0;
input.data[0] = '\0';
continue;
}

g_returning = 0;
g_return_val = NULL;
g_has_error = 0; /* don't carry a prior line's error into this one */
EigsChunk *repl_chunk = compile_ast(ast, env);
if (g_parse_errors > 0) { /* e.g. an un-encodable jump/loop offset */
fprintf(stderr, "%d compile error(s) — line not run\n", g_parse_errors);
chunk_free(repl_chunk);
free_tokenlist(&tl);
free_ast(ast);
input.len = 0;
input.data[0] = '\0';
continue;
}
Value *result = vm_execute(repl_chunk, env);
if (g_exit_requested) { /* `exit of N` typed at the REPL */
g_has_error = 0;
chunk_free(repl_chunk);
if (result) val_decref(result);
free_tokenlist(&tl);
free_ast(ast);
break;
}
/* Chunks are refcounted: drop the creator ref. Functions defined
* on this line hold their own refs on their nested chunks. */
chunk_free(repl_chunk);

/* Print non-null results */
if (result && result->type != VAL_NULL) {
char *s = value_to_string(result);
printf("=> %s\n", s);
free(s);
}
if (result) val_decref(result);

free_tokenlist(&tl);
/* Function bodies are cloned (AST fns) or compiled into chunks
* (bytecode fns), so the per-line AST is safe to free. */
free_ast(ast);
input.len = 0;
input.data[0] = '\0';
}
strbuf_free(&input);
}

/* ---- Main ---- */
/* The REPL (piped loop + the #392 interactive line editor) lives in repl.c. */

static void set_exe_dir(const char *argv0) {
char exe_path[4096];
Expand Down
Loading
Loading