Make every error the program can produce travel the same way: detected where it happens, carried up as a value with its code and position, printed once by the loop in one format, and followed by the next prompt.
Count the places that can go wrong so far. The loop rejects unknown meta commands. The lexer rejects characters and unterminated strings. The parser rejects statements that do not fit the grammar and INSERTs whose counts do not match. Three layers, written at three different times, and it is likely that they report their problems in three different ways: one prints and carries on, one prints and returns a flag, one prints two lines.
The next stages add the pager, the file, the B+Tree, and statement execution. Each brings errors of its own — a page out of bounds, a file that cannot be opened, a duplicate key, a full node. If every one of them chooses its own way to fail, the program ends up with errors printed twice, errors printed in the middle of other output, errors that end the session, and errors that vanish. Now, with only three layers, is the cheapest moment to decide how an error travels. After this stage, adding a new error means adding a code and one line that detects it, and nothing else.
Until now each layer had its own debug command, tokenize or ast, and its tests talked to it directly. From this stage on, a line that is not a meta command is a statement, and it goes through every layer in turn. A statement the parser accepts has nowhere to go yet: what it prints is not tested until the stages that store rows, so printing nothing is fine. Here is one bad statement making the whole trip:
The lexer found the problem, but the loop printed it. Between them the error was a value — a code, a column, a message — returned instead of a token list. The parser was never called, because there was nothing for it to parse. And the loop went back to the prompt, as it does after every answer.
This has two consequences you can see from outside. First, a line produces at most one error. launch #; is wrong twice: # is not a character the lexer accepts, and launch is not a statement. Only the first layer to fail gets to say so, and that is the lexer, so the answer is 00202 and never also 00301. Second, nothing but the error is printed. No token list, no half an AST, no second line saying "Syntax error". A person reading the output sees one clear sentence; a test reading it sees one line to check.
The code is SSCCC: two digits for the layer, three for the condition inside it.
SS | Layer | Codes so far |
|---|---|---|
01 | command line and loop | 00101 unknown option, 00102 option missing its argument, 00103 unknown meta command |
02 | lexer | 00201 unterminated string, 00202 unexpected character |
03 | parser | 00301 unknown statement, 00302 any other syntax error, 00303 INSERT count mismatch |
Two rules make this a system rather than a list. One condition has one code, so a code tells you exactly what happened without reading the message. And a code never changes meaning: a number is never reused, and the layers that come later get ranges of their own (06 for the pager, 07 for the B+Tree, and so on) instead of squeezing into these. The full table, including every code still to come, is in the contract.
The message is for a person and is yours to write. Make it say what was expected and what was found. If you have a suggestion, put it on a second line starting HINT:; it is optional, and nothing checks what it says.
Every error goes through the same four steps, whichever layer finds it. What matters is that each step happens in exactly one place.
-c, or from a debug command.HINT: line after it — then goes back to the prompt.Most errors are about one line. A few are about the program itself being unable to start.
-c mode, where there is no next line, exit 1.persistence stage, a database file that cannot be used is the same: nothing can work without it..exit, end of input, and the handful of errors that happen before the loop begins.The lexer is the last layer that sees characters, so it is the one that has to keep their positions for everyone else.
tokenize or ast it starts later.These are the exact lines the tests check. The full definition is on the output contract page, sections 2 and 3.
| Rule | What the tests check |
|---|---|
| an error is one line | [ERROR:SSCCC] <message>, the code from the contract |
| lexer and parser errors have a position | the line ends with at col N, N counted from 1 after trimming |
| a hint is optional | a HINT: <text> line may follow an error line, and only an error line |
| one error per line | the first layer to fail reports, and no other output comes from that line |
| an error ends the line, not the session | the next line is read and answered normally |
-c reports failure in its status | the error line alone, and exit 1; exit 0 when the command worked |
| everything on standard output | errors included, so the order of a session is kept |
Only two kinds of error end the program: a command line it cannot understand (00101, 00102), and — from the persistence stage — a database file it cannot use. Everything else is an answer to one line.
Welcome to droid!>select # from users;[ERROR:00202] Unexpected character: # at col 8> select # from users;[ERROR:00202] Unexpected character: # at col 8>select 'abc[ERROR:00201] Unterminated string literal at col 8>launch #;[ERROR:00202] Unexpected character: # at col 8>launch users;[ERROR:00301] Unrecognized statement: launch at col 1>insert into users (id, name) values (1);[ERROR:00303] Column count and value count must match at col 39>ast delete from users;Statement: DELETETable: users>.exitexiting.. good bye!
Things to notice. The indented statement reported the same column as the plain one. launch #; reported only the lexer's error, and launch users;, which the lexer accepts, reported the parser's. No line printed anything besides its error. And the ast command at the end worked normally after five errors in a row.
From a script:
$droid -c "select # from users;"[ERROR:00202] Unexpected character: # at col 8$echo $?1$droid -c "ast delete from users;"Statement: DELETETable: users$echo $?0
Syntax error line. The second line is either the optional HINT: or nothing.-c and kills every session. Only the loop decides what happens after an error, and it goes back to the prompt.-c exits 0 after an error. The status must say whether the command worked; a script has nothing else to go on.The page above is the same for every language. These are the only language-specific hints.
catch sits in the loop and nothing below it prints. std::expected (C++23) keeps the error in the return value instead, which makes the path visible in every signature.Result<T, E> with an error type that carries the code, the column and the message; ? is the whole of "pass on". Print only in the loop, from the Err it receives.try, but an error value carries no data. Keep the code, column and message in a small diagnostics value the caller provides, and let the error union say only that something failed.