The REPL
A loop that reads a line, answers it, and never dies
Build the loop every later stage plugs into: prompt, read one line, decide what it is, answer it, and keep going — including when the answer is an error.
- The loop as the shell of the program: everything built later is reached through it
- Two kinds of input: meta commands the loop answers itself, and everything else
- An error is data, not a crash: it is printed and the loop reads the next line
- One-shot mode: the same evaluator, one command, no session around it
- The exit status is part of the contract, not an afterthought
Why this stage exists
Every other stage in this tutorial is reached through this loop. The lexer, the pager, the B+Tree, transactions — none of them has a user interface of its own. They are all things that happen between a line arriving and an answer being printed.
That makes the loop worth getting right once, because two of its properties are load-bearing for everything after it. The first: an error is an answer, not a stop. A database that exits when you type a bad query is a database you cannot use, and every later stage produces errors — a malformed statement, a full page, a duplicate key, a transaction that cannot commit. If the loop dies on the first one, none of those stages can be tested. The second: the exit status is a promise to whoever is running you. The tests for every later stage run your program as a subprocess and check that status.
There is a third reason, smaller but immediate: this stage is where you print your first error, so it is where the error format is settled. One line, a code that says what went wrong, and a message you are free to word however you like.
What the loop does
Notice what is not in the diagram: there is no path from an error to the end. Every answer, including an error, leads back to the prompt. The only two ways out are .exit and end of input, and both of them take the same exit.
The two kinds of input
A line beginning with . is a meta command: an instruction to the program itself rather than to the data. .exit and .help are the two you implement here; later stages add .mode. They are answered by the loop and never reach the query machinery.
Everything else is a statement, and eventually goes to the lexer and the parser. Not yet, though — there is nowhere to send it. So at this stage the loop recognises meta commands and rejects everything else, which is a real answer and not a placeholder: an unknown command is exactly what a line of SQL is to a program that has no SQL in it.
The algorithm in words
The loop
Three steps repeated until the input runs out. The order matters: the prompt belongs before the read, not after the answer, or a piped session shows its prompts in the wrong places.
- 1.Print the prompt: the single character
>, with no space and no newline after it. Make sure it reaches the terminal before you block on the read — output with no newline can sit in a buffer, and a prompt nobody sees is a program that looks hung. - 2.Read one line. End of input is not an error: it ends the session exactly as
.exitdoes. - 3.Trim whitespace from both ends of the line, and drop the line ending. A line that still carries its newline will never equal
.exit. - 4.Answer the line, then repeat.
- 5.Invariants: one prompt per attempted read, no matter what the line turned out to be; the loop ends only at
.exitor end of input; nothing a user can type ends it any other way.
Answering one line
Everything the loop can be given falls into four cases, and later stages only ever add to the last one.
- 1.An empty line: print nothing at all, and read again. Not an error, not a blank line of output.
- 2.
.exit: end the session. - 3.
.help: print the three lines of help text, exactly as the contract gives them. - 4.Any other line starting with
.: it is a meta command you do not have. Report error 00103. - 5.Anything else: at this stage there is nothing else to try, so report error 00105.
- 6.Invariant: a line produces either its own output or exactly one error line, never both and never neither — except the empty line, which produces nothing.
The command line, and what the exit status means
The same evaluator serves a session and a single command. What differs is the wrapping and the status.
- 1.With
-c "<command>", answer that one command and stop. Print no welcome line, no prompt, and no goodbye: the output is meant for a script reading it. - 2.Exit 0 after a session that ended with
.exitor end of input, even if commands inside it failed. The session did its job. - 3.Exit 1 when a
-ccommand printed an error, when an option is missing its argument, or when the option is not one you know. - 4.Invariant: the status answers 'did the thing I asked for work', not 'did the program survive'.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, sections 2 and 3.
| Input | Output |
|---|---|
| a session starting | one line beginning Welcome to droid |
| before each read | the prompt >, no space, no newline after it |
.help | Available commands:, .exit - Exit the program, .help - Show this help message |
.exit or end of input | exiting.. good bye!, then exit 0 |
| an empty line | nothing |
.foo | [ERROR:00103] <message> |
| any other unrecognised line | [ERROR:00105] <message> |
-c with no argument | [ERROR:00102] <message>, exit 1 |
| an unknown option | [ERROR:00101] <message>, exit 1 |
An error is one line: [ERROR:SSCCC] message. The five digits are fixed by the contract and the tests assert them; the message after them is yours to word, and a second line starting HINT: may follow if you want to be helpful. Errors go to standard output, like everything else, so that a test reading one stream sees the session in the order it happened.
Why numbers rather than sentences? Because a message is for a person and a code is for everything else — a test, a log, a support question, a future you grepping the source. Sentences get reworded; 00103 does not. The first two digits say which part of the program noticed (01 is the command line and the loop), the last three say what it noticed.
What you should see when done
Welcome to droid!>.helpAvailable commands:.exit - Exit the program.help - Show this help message>> .helpAvailable commands:.exit - Exit the program.help - Show this help message>.foo[ERROR:00103] Unknown meta command: .foo>select * from users;[ERROR:00105] Unknown command: select>.exitexiting.. good bye!
Three things to notice. The empty line printed nothing — the prompt after it is the only trace. The indented .help worked, because the line was trimmed before it was compared. And the session ended normally after two errors: they were answers, not accidents.
From a script, without a session around it:
$droid -c ".help"Available commands:.exit - Exit the program.help - Show this help message$echo $?0$droid -c "select * from users;"[ERROR:00105] Unknown command: select$echo $?1
Common bugs in this stage
- The prompt appears in the wrong place. Printing it after the answer instead of before the read produces output that looks right on a terminal and wrong everywhere else. Print, then read.
- The prompt never appears. With no newline to flush it, output can sit in a buffer while the program blocks on the read. Flush after printing the prompt.
.exitnever matches. The line still carries its newline, or its leading spaces. Trim before comparing, and thetrimmed-help-with-spacestest is the one that catches it.- An empty line prints an error, or a blank line. It should print nothing at all and go straight back to the prompt.
- End of input is treated as a failure. A closed input is not an error: say goodbye and exit 0, exactly as
.exitdoes. Piped input hits this every time. - An error exits the program. Print it and read the next line. If your error path calls the exit routine, every later stage becomes untestable.
- The status is always 0, or always 1. A session with errors in it still exits 0; a
-cwhose command failed exits 1. The status describes the request, not the crash. - A fixed-size input buffer. A statement may be 4096 bytes long. Read the whole line however long it is, or at minimum refuse it without overrunning anything.
- Errors printed to standard error. Everything goes to standard output, so the order of a session is preserved for whoever is reading it.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C:
getlineallocates and grows the buffer for you;fgetswith a fixed array is where the 4096-byte case goes wrong.fflush(stdout)after the prompt. - C++:
std::getline(std::cin, line)returns a falsy stream at end of input, which is the loop's exit condition.std::cout << std::flushafter the prompt. - Rust:
BufRead::read_linereturnsOk(0)at end of input — a zero-length read, not an error.io::stdout().flush()after the prompt. - Zig:
readUntilDelimiterOrEofreturnsnullat end of input. Remember the allocator for the line, and flush the writer after the prompt.
Checklist
One line per thing the tests check. The tags name the test cases.
- A session prints the welcome line, then a prompt, then
exiting.. good bye!, and exits 0welcome-prompt-and-exit .helpprints the three contract lines exactly, with or without surrounding spaceshelp-command-works · trimmed-help-with-spaces- An unknown meta command prints one
[ERROR:00103]line and the session continuesinvalid-meta-command-error-code - Any other unrecognised line prints one
[ERROR:00105]lineunknown-line-error-code - Two bad lines in a row do not stop the loop: the command after them still worksloop-continues-after-errors
- An empty line prints nothing, not even an errorempty-line-prints-nothing
- End of input ends the session the way
.exitdoes, with status 0eof-ends-the-session - A 4096-byte line is answered with one error and the next command still workslong-line-is-safe
-cprints no welcome line and no prompt, and exits 1 when its command printed an errorcli-c-runs-one-command-cwith a command that succeeded exits 0cli-c-help-exits-zero-cwith no argument prints[ERROR:00102]and exits 1cli-c-missing-argument-fails- An unknown command line option prints
[ERROR:00101]and exits 1cli-unknown-option-fails