Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

REPL (cmd/jennifer/repl.go)

The REPL drives a read-eval-print loop on top of the standard pipeline. Each input is lexed, preprocessed, parsed, and fed to Interpreter.EvalInteractive (not Run). EvalInteractive differs from Run in three documented ways: the global env is lazy-initialized and preserved across calls, library imports and method definitions are idempotent / re-assignable so the user can iterate, and the value of a trailing ExprStmt is returned so the loop can print it.

Both import kinds work at the prompt: use LIB; activates a library namespace, and import "PATH.j"; loads a module (runRepl calls EnableModules with the current directory as the local-import base and the system module dir as the search path, so ./mod.j resolves against the cwd and a bare mod.j through the search path). EvalInteractive calls loadModuleImports in REPL mode, which no-ops a re-submitted import of the same module under the same alias (a module is run-once / cached) while still rejecting an alias bound to a different module. Because caching is by resolved path, editing a module file and re-importing it in the same session serves the cached version - restart the REPL to pick up edits.

Echoing a value

Because EvalInteractive returns the trailing ExprStmt’s value, you can inspect any variable by typing its bare reference followed by ; - the REPL prints the value’s Display() form:

>>> def x as int init 41;
>>> $x;
41
>>> def doc as json.Value init json.decode("{\"a\":[1,2]}");
>>> $doc;
{"a":[1,2]}

This is a REPL-only convenience: Run (the batch path) evaluates an expression statement but discards its value, so a bare $x; in a .j script prints nothing. To show a value from a script, format it explicitly - io.printf("%v\n", $x); (or io.sprintf($x) / convert.toString($x)). Opaque values render through their registered displayer, so $doc; shows a json.Value as its JSON rather than <json.Value>.

Multi-line input is handled by a small inputComplete(tokens) helper that balances {/( against }/) (using the lexer’s tokens so string and comment contents are ignored) and requires the input to end in ; or }. Anything else triggers a ... continuation prompt. Unbalanced closing delimiters intentionally fall through to the parser for diagnosis since no amount of additional input would fix them.

REPL input is tagged with the synthetic file label <repl>. The cross-file-error snippet loader in printErrorContext treats <repl> like <stdin>: no external file lookup is attempted, and the current input buffer is used as the snippet source. Lex errors discard the buffer (since they cannot become valid by reading more); parse and runtime errors print and the loop continues.

:quit / :exit / EOF terminate cleanly; :help prints a short reminder. Directives are only recognized at a fresh prompt so a literal :quit inside a block doesn’t short-circuit.

Line editor (cmd/jennifer/lineedit.go, cmd/jennifer/history.go)

When stdin is a terminal the REPL installs raw mode via golang.org/x/term and reads lines through a small built-in editor. The editor is a single state machine over a rune buffer plus a cursor index; each keystroke updates the state and triggers a redraw().

Supported input:

KeyAction
Printable runeInsert at cursor
Backspace, Ctrl+HDelete char before cursor
Delete (CSI 3~)Delete char at cursor
Left / Right (CSI D / C)Move cursor by one char
Home / End (CSI H / F)Jump to line start / end
Ctrl+A / Ctrl+ESame as Home / End
Ctrl+Left / Ctrl+RightMove by word
Alt+B / Alt+FSame as Ctrl+Left / Ctrl+Right (macOS terminals send these for option-arrow)
Ctrl+W, Ctrl+BackspaceDelete word backward
Ctrl+UKill from line start to cursor
Ctrl+KKill from cursor to line end
Up / DownHistory navigation
Ctrl+CCancel current line (fresh prompt)
Ctrl+D on empty bufferEOF (exits the REPL)
Ctrl+D on non-empty bufferForward-delete

Word boundaries use a small punctuation + whitespace ruleset that’s predictable for source-code editing without needing a full Unicode word-break implementation. History is an in-memory ring (replHistory, 100 entries by default, adjacent duplicates collapsed); on-disk persistence is a future enhancement.

Non-TTY stdin falls back to the original bufio line reader. This keeps echo ... | jennifer repl and integration tests working unchanged - the editor would do nothing useful on a non-interactive stream anyway.

Raw mode disables the kernel’s OPOST flag, so \n written to stdout no longer auto-translates to \r\n. The REPL works around this with a tiny crlfWriter wrapper that performs the translation in user space for error/help/result prints. Cooked-mode output (the banner printed before raw mode is entered, and anything after raw mode is restored) goes to os.Stderr / os.Stdout directly.

The editor only handles single-line editing. Multi-line input via the continuation prompt (... ) is still driven by the surrounding REPL loop’s inputComplete() check, so unclosed { / ( accumulate across calls to editor.readLine.

Syntax highlighting on commit (cmd/jennifer/highlight.go)

While you type, the line is drawn plain. On Enter, if colour is enabled, the editor redraws the committed line one last time with syntax highlighting (redrawCommitted -> highlightLine) before the newline, so the source shows coloured just above its output. Highlighting only at commit (not per keystroke) keeps the edit path cheap and sidesteps recolouring half-typed, unlexable input.

highlightLine lexes the line and wraps each token’s source span in an ANSI SGR colour (keyword, type, string, number, $var, comment; other tokens stay default). It slices by each token’s 1-based rune column rather than its lexeme, so the user’s exact spacing is preserved and a processed TOKEN_STRING lexeme (quotes stripped, escapes resolved) can’t desync the offsets. A span runs to the next token’s start, so trailing whitespace inherits the token’s colour - invisible for foreground-only codes. The colours are zero-width escapes, so the editor’s cursor-column arithmetic is unaffected; the commit redraw skips the cursor-back step because a newline follows immediately. On any lex error (e.g. an unterminated string mid-edit) highlightLine returns the input unchanged, so the line always echoes verbatim.

Colour is gated by colorEnabled(): stdout must be a TTY and NO_COLOR (https://no-color.org) must be unset. The editor already requires a TTY stdin, so the two together mean colour appears only in a genuine interactive session; piped or redirected output stays plain.

Part of the CLI reference.