Skip to content

repl: decide command or code in one place, and take only full command names in a multi-line input - #46364

Open
robobun wants to merge 1 commit into
mainfrom
robobun/6e8078cb/repl-dot-line-is-code
Open

robobun wants to merge 1 commit into
mainfrom
robobun/6e8078cb/repl-dot-line-is-code

Conversation

@robobun

@robobun robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun repl takes every line whose first byte is . as a REPL command. .5 + 1 prints Unknown command: .5.
  • In a multi-line input it drops such a line and runs the rest. (ev / .e / ) exits, and (o / .s = 1) writes a file named = 1).
  • The cause is line[0] == b'.' in Repl::handle_enter (src/runtime/cli/repl.rs:2535), ahead of the continuation check. handle_tab and update_suggestion repeat it.

Fix

  • ReplCommand::classify decides what a line is: code, a command, or an unknown name. Enter and Tab ask it, the inline suggestion asks its byte rule may_name.
  • A line that starts with . and a digit, or with .., is code. In a multi-line input only a full command name is a command.
  • Verified: test/js/bun/repl/repl.test.ts, 11 new tests, all fail on main.
  • Self-reviewed: 12 concerns raised, 11 addressed, 1 in part (Notes).

Background

  • At the prompt a command name can be cut short: .e runs .exit.
  • A multi-line input is open while brackets are not closed. .break cancels it.
  • Node's REPL and Bun's node:repl have this rule (src/js/node/repl.js:824).
  • Considered a fall-through in the unknown-name arm only: .e and .s = 1) still run commands.

Downsides

  • In a multi-line input a short name (.br) or a mistyped one (.brek) is code: no message, no Tab completion.
  • Dot lines of a multi-line input now reach the history file. After a restart, Up then Enter on .s = 1) runs .save (repl: keep multi-line history entries on one line of the history file #38023 prevents it).
  • Enter and Tab call classify: +32 instructions per Enter (393 to 425), no allocation, +256 bytes of text.
Notes

Origin. There is no issue. A check of the REPL docs page (#46308) found it. No user reported it.

When it triggers. A . in column 0: a number such as .5 at the prompt, or a line of a multi-line input that starts with .. A line indented with spaces already works on main, because its first byte is a space. A line indented with TAB does not: a TAB on an empty line is the Tab key and inserts nothing, so a pasted or piped <TAB>...defaults, is entered as ...defaults,.

Repro. Piped stdin, release builds of main (9b4284f) and of this PR, node -i 26.3. / is Enter.

Input main this PR node
.5 + 1 Unknown command: .5 1.5 1.5
[ / ...[1, 2], / ...[3] / ] Unknown command twice, then [] [ 1, 2, 3 ] [ 1, 2, 3 ]
await (Promise.resolve([3, 1, 2]) / .then(a => a.sort()) / .then(a => a.slice(1)) / ) Unknown command twice, then [ 3, 1, 2 ] [ 2, 3 ] [ 2, 3 ]
const o = { s: 0 } / (o / .s = 1) Session saved to = 1), a file = 1) in the cwd 1, no file 1
const ev = { e: 4 } / (ev / .e / ) the REPL exits 4 4
const defaults = { retries: 3 } / const cfg = { / <TAB>...defaults, / <TAB>timeout: 50, / } / cfg Unknown command: ...defaults,, then { timeout: 50 } { retries: 3, timeout: 50 } { retries: 3, timeout: 50 }
[3, 1, 2] / .sort() Unknown command: .sort() Unknown command: .sort() Invalid REPL keyword

The rule

  • The second byte decides as in node's line listener: a digit or a second . makes the line code. For a line that starts with ., Number.parseFloat(line) is a number exactly when the second character is an ASCII digit (checked for every string of 2 and 3 characters).
  • In a multi-line input a full name as the first word of the line runs, as in node with piped stdin. Every other dot line is code. Node on a terminal takes every continuation line as code, .break included. Bun keeps .break there, because the docs give it as the way out.
  • In .editor mode Enter already stored every line as text. Tab now agrees: it completes no command name, and it completes a property on a line that starts with .. Node on a terminal completes .b to .break there.
  • The inline suggestion runs only at the prompt. There may_name alone gives the answer of classify, with no table lookup on each key.
  • Differences from node that stay: the text Unknown command, short names at the prompt, and no trim of leading blanks ( .help is code, and that is why chains indented with spaces work).

What changes beyond the rows above

  • The 31 short spellings (.b, .br, ... .histor) are code in a multi-line input. First words that run a command there: 40 on main, 9 with this change.
  • A name that is no command is code in a multi-line input. function foo() { / .brek / } / typeof foo: main prints Unknown command: .brek, drops the line and defines foo. This PR prints SyntaxError: Unexpected token '.' at the } and does not define foo. Node prints the same SyntaxError at the .brek line, because it parses the input at each Enter. Bun counts brackets.
  • An input that cannot close shows this most. // don't do this opens a string for the bracket counter (repl: recognize regex literals and comments in continuation detection #37327 teaches it comments). Then .quit and .q print Unknown command on main and .ex exits. With this change the three lines join the input and print nothing. .exit, .break, .help and Ctrl+C work as before.
  • Tab on a dot line that Enter takes as code goes to property completion, or to the two-space indent when there is nothing to complete. Main did nothing on such a line.
  • The inline suggestion appears on a line such as .5 + Mat.
  • Dot lines of a multi-line input now reach ~/.bun_repl_history, as part of their entry. The file holds one line per row, so after a restart .s = 1) is an entry of its own, and Up then Enter on it runs .save at the prompt. repl: keep multi-line history entries on one line of the history file #38023 stores a multi-line entry on one row.

Not changed

Relation to open PRs

Measurements. Release builds of main (9b4284f) and of this change on it, same directory and toolchain, Linux x64. perf, valgrind, strace and bloaty are not on the machine. The tools are size, llvm-nm, llvm-objdump, perf_event_open and gdb.

  • Size: text 107,318,912 to 107,319,168 bytes (+256). Data and bss do not change. Repl::run_with_vm 14,350 to 13,271 bytes, Repl::update_suggestion 1,297 to 1,446, new ReplCommand::classify 1,329. Symbols of the repl module: 35 to 36.
  • Instructions before the branch that a line with no dot takes: Enter 2 to 38, Tab 2 to 37 (the call to classify), edit key 2 to 2 (llvm-objdump).
  • Instructions per Enter (instructions:u, (200k - 100k rows) / 100k, median of 5 interleaved runs, A/A floor 0.11): empty line 393.1 to 425.0, .nonexistent 3,121.7 to 3,159.7, .break 1,292.4 to 1,344.4, [ then .break 1,817.7 to 1,863.8.
  • Allocator calls and write(2) calls per key (gdb counters, (2000 - 1000 keys) / 1000), the same on both builds: empty Enter 0 and 2, .nope Enter 1 and 10, .break Enter 1 and 2, a Tab 103 and 27, .he Tab 2 and 1, .zz Tab 1 and 1, .load ./proc Tab 1 and 1. 1 Enter: 51.2 and 3 on both (A/A spread 51.12 to 51.27).
  • Corpus: the 123 lines of test/** that start with . in column 0, each entered as a line of an open template literal (116 usable). Main keeps 0, reports 112 as unknown and runs 4 as commands (.b { and .c {, twice each). This PR keeps 116.
  • Behaviour matrix, piped, 443 cells (52 lines at the prompt, after [1,, after (o and in a template literal, with LF and CRLF, plus 27 Tab cells): 282 differ from main. 40 are .digit and .. lines, 186 are short names in a multi-line input, 42 are unknown names in a multi-line input, 14 are Tab cells (1 at the prompt, 7 in a multi-line input, 6 in .editor mode). 0 other cells differ: every line at the prompt that is not a number or a spread, and the nine full names in every context, give the same output.
  • History file after const o = {b: 7} / (o / .b / ) / .5 + 1: lines that start with .: 0 on main, 2 with this PR.

Tests

  • 11 new tests: 6 piped, 5 on a pty. A release build of main fails all 11 and passes the other 129. The debug build of this PR passes 140 of 140.
  • Each clause is pinned. A debug build with a run-time switch took out one clause at a time: the digit test (3 tests fail), the .. test (1), full names only in a multi-line input (3), unknown name is code there (6), the .editor guard (1), Tab asks for more than Command (1), the suggestion asks may_name (1).
  • Also run: test/regression/issue/26058.test.ts, test/internal/source-lints/byte-search.test.ts.
  • Nothing was run on macOS or Windows. The change has no platform code. The 6 piped tests are not gated. The 5 pty tests are in the existing describe.todoIf(isWindows) block.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/repl/repl.test.ts

`bun repl` took every line whose first byte is `.` as a REPL command.
`.5 + 1` printed `Unknown command: .5`. Inside a pending multi-line
input, a line such as `.then(f)` or `...rest` was dropped with the same
message and the rest of the input still ran.

`ReplCommand::classify` now decides what a line is. Enter, Tab and the
inline suggestion ask it.

- A line that starts with `.` and a digit, or with `..`, is JavaScript.
- In a pending input, only a full command name is a command. Any other
  line that starts with `.` is JavaScript and joins the input.
- At the prompt, a name that is no command still prints
  `Unknown command`, and a name cut short (`.ex`) still runs.
- In `.editor` mode every line is JavaScript, for Tab as for Enter.
@robobun

robobun commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Status: the fix is in this PR (#46364).

How I reproduced it, on 1.4.3 and on a release build of main (9b4284f), Linux x64, piped stdin:

printf '.5 + 1\n' | bun repl                     # Unknown command: .5
printf '[1,\n.5]\n' | bun repl                   # Unknown command: .5]
printf 'const o = {b: 7}\n(o\n.b)\n' | bun repl  # Unknown command: .b)

node -i 26.3 prints 1.5, [ 1, 0.5 ] and 7 for the same input. The 11 new tests in test/js/bun/repl/repl.test.ts fail on main and pass with this change.

@robobun

robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:39 PM PT - Oct 21st, 2026

✅ @robobun, your commit 5ec1b146c65198fcb26bb1f59d5656f4787b9fed passed in Build #130226! 🎉


🧪   To try this PR locally:

bunx bun-pr 46364

That installs a local version of the PR into your bun-46364 executable, so you can run:

bun-46364 --bun

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant