Compare commits

..

6 commits

Author SHA1 Message Date
33c4edf91d
feat(grammar): meta-commands must be followed by end-of-file 2025-06-04 14:06:57 +02:00
64d93e9a27
refactor(location): deduplicated usage of Location::new with same value into default 2025-06-03 22:00:18 +02:00
567aa31c07
feat(errors): improve error messages with example values
Add token type examples to make error messages more helpful. Created an
ExpectedToken enum to replace string literals for better type safety,
added example values for each token type, and enhanced error display to
show concrete examples of valid syntax.
2025-06-03 22:00:03 +02:00
e78511f692
feat(parser): implement semicolon-separated statements
Add support for semicolon-terminated statements according to the
updated grammar. This change enables executing multiple SQL statements
in a single input by separating them with semicolons. Key improvements
include:
- Update grammar to require semicolons after statements
- Add Semicolon token to the tokenizer
- Implement error recovery by skipping to next semicolon on parse errors
- Create helper functions for checking semicolons in statement parsers
- Add tests for multiple statements and error conditions
2025-06-03 19:06:54 +02:00
28cb288eaf
feat(parser): implement insert command parsing
Implements the parse_insert_command function to handle the insert
statement according to the grammar definition. The function now
correctly parses the command format insert int string string and creates
a structured Statement::Insert with the id, username, and email fields.
2025-06-03 17:48:36 +02:00
315703d46b
feat(meta): version command 2025-05-31 16:04:56 +02:00
18 changed files with 647 additions and 80 deletions

View file

@ -1,32 +1,43 @@
token ::= insert
| select
| meta-command
| int
| string
| end-of-file
/* token is first stage of parsing */
token ::= insert
| select
| meta-command
| int
| string
| semicolon
| end-of-file
insert ::= "insert"
select ::= "select"
/* command is second stage of parsing */
command ::= cmd-insert semicolon
| cmd-select semicolon
cmd-insert ::= insert int string string
cmd-select ::= select
meta-command ::= "." "exit"
| "about"
insert ::= "insert"
select ::= "select"
semicolon ::= ";"
int ::= sign? digit+
sign ::= "+"
| "-"
digit ::= "0"
| "1"
| "2"
| "3"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
meta-command ::= meta-command-verb end-of-file
meta-command-verb ::= ".exit"
| ".about"
| ".version"
string ::= '"' string-char* '"'
string-char ::= '\' utf8-char
| utf8-char-not-dbl-quote
int ::= sign? digit+
sign ::= "+"
| "-"
digit ::= "0"
| "1"
| "2"
| "3"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
string ::= '"' string-char* '"'
string-char ::= '\' utf8-char
| utf8-char-not-dbl-quote

View file

@ -226,17 +226,53 @@ i will use rustyline, since it seems like the most feature-complete
* DONE write a proper grammar
* TODO .about meta-command
* TODO .version meta-command
* DONE .about meta-command
* DONE .version meta-command
* TODO .license meta-command
* TODO .help meta-command
* TODO parse insert statements in the form
* DONE parse insert statements in the form
insert <id:int> <username:string> <email:string>
** TODO Row struct
** TODO parse row insert
** TODO serialize/deserialize row to/from raw bytes
*** TODO look for best practices for creating binary formats
** DONE parse row insert
* DONE separate statements with semicolons
* DONE this error message could be better
#+begin example
Error: unexpected token
╭─[ <stdin>:1:24 ]
1 │ insert 0 "user" "email"
│ │
│ ╰─ found end of file ""
│ Note: expected token type to be one of ["semicolon"]
───╯
#+end example
** plan
1. Create an example mapping system
- Define a mapping of token types to example values
- Example: "integer" → "42", "string" → "example", "semicolon" → ";"
2. Enhance CommandParseError
- Add a method to generate user-friendly error messages
- Include both the expected token type and concrete examples
3. Implementation approach
- Create a static lookup table or function that returns examples
- Extend existing error handling to include examples in messages
- Make sure the examples follow SQL syntax conventions
4. Error display refinement
- Update error_display.rs to include these examples
- Format error messages to show both what was expected and example syntax
5. Testing
- Add tests that verify the error messages include helpful examples
- Ensure examples are contextually appropriate
This will make errors like "expected semicolon" more helpful by showing "expected semicolon (example: ;)".
* DONE correct all instances of <unknown> in locations
* DONE meta-commands must be followed by end-of-file
* TODO project code documentation
* TODO project usage documentation
* DONE in case of parse error, skip until next semicolon to better recover
* TODO serialize/deserialize row to/from raw bytes
** TODO look for best practices for creating binary formats
* WAIT cli tests using insta-cmd
https://insta.rs/docs/cmd/

View file

@ -13,12 +13,18 @@ pub fn startup_msg() -> String {
)
}
pub fn about_msg() -> String {
pub fn version_msg() -> String {
let name = env!("CARGO_PKG_NAME");
let version = env!("CARGO_PKG_VERSION");
format!("{name} v{version}")
}
pub fn about_msg() -> String {
let version = version_msg();
format!(
"{name} v{version} -- A database engine\n\
"{version} -- A database engine\n\
Note: This is experimental software. No maintenance is intendend."
)
}

View file

@ -1,6 +1,6 @@
use crate::meta_commands::{MetaCommand, MetaCommandExecuteResult};
use crate::statements::{Statement, StatementExecuteResult};
use crate::tokens::{ScanError, Token};
use crate::tokens::{Location, ScanError, Token};
#[derive(Debug)]
pub enum Command {
@ -47,10 +47,48 @@ impl Command {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpectedToken {
Integer,
String,
Semicolon,
Statement,
MetaCommand,
EndOfFile,
}
impl ExpectedToken {
/// Returns an example value for this token type
pub fn example(&self) -> &'static str {
match self {
ExpectedToken::Integer => "42",
ExpectedToken::String => "\"example\"",
ExpectedToken::Semicolon => ";",
ExpectedToken::Statement => "select",
ExpectedToken::MetaCommand => ".exit",
ExpectedToken::EndOfFile => "<end of input>",
}
}
}
impl std::fmt::Display for ExpectedToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExpectedToken::Integer => write!(f, "integer"),
ExpectedToken::String => write!(f, "string"),
ExpectedToken::Semicolon => write!(f, "semicolon"),
ExpectedToken::Statement => write!(f, "statement"),
ExpectedToken::MetaCommand => write!(f, "meta command"),
ExpectedToken::EndOfFile => write!(f, "end of file"),
}
}
}
#[derive(Debug)]
pub enum CommandParseError {
Scan(ScanError),
UnexpectedToken(Token, &'static [&'static str]),
UnexpectedToken(Token, &'static [ExpectedToken]),
UnexpectedEndOfFile(Location, &'static [ExpectedToken]),
}
impl From<MetaCommand> for Command {
@ -80,7 +118,12 @@ mod tests {
#[test]
fn test_execute_insert_statement() {
let statement: Command = Statement::Insert.into();
let statement: Command = Statement::Insert {
id: 45,
username: String::from("user"),
email: String::from("user@example.org"),
}
.into();
let result = statement.execute().display();
assert_snapshot!(result);
}

View file

@ -1,4 +1,7 @@
use crate::{command::CommandParseError, tokens::ScanError};
use crate::{
command::{CommandParseError, ExpectedToken},
tokens::ScanError,
};
use ariadne::{Color, Label, Report, ReportKind, Source};
pub trait OSDBError {
@ -11,24 +14,79 @@ impl OSDBError for CommandParseError {
CommandParseError::Scan(x) => {
x.display(file, input);
}
CommandParseError::UnexpectedToken(token, items) => {
CommandParseError::UnexpectedToken(token, expected_tokens) => {
let location = (file, Into::<std::ops::Range<usize>>::into(&token.location));
Report::build(ReportKind::Error, location.clone())
let mut report = Report::build(ReportKind::Error, location.clone())
.with_message("unexpected token")
.with_label(
Label::new(location.clone())
.with_color(Color::Red)
.with_message(format!("found {token}")),
)
.with_note(format!("expected token type to be one of {items:?}"))
.finish()
.eprint((file, Source::from(input)))
.unwrap()
);
report = add_expected_tokens_to_report(report, expected_tokens);
report.finish().eprint((file, Source::from(input))).unwrap()
}
CommandParseError::UnexpectedEndOfFile(location, expected_tokens) => {
let location = (file, Into::<std::ops::Range<usize>>::into(location));
let report = Report::build(ReportKind::Error, location)
.with_message("unexpected end of file");
let report = add_expected_tokens_to_report(report, expected_tokens);
report.finish().eprint((file, Source::from(input))).unwrap()
}
}
}
}
type OSDBReport<'a> = ariadne::ReportBuilder<'a, (&'a str, std::ops::Range<usize>)>;
fn add_expected_tokens_to_report<'a>(
mut report: OSDBReport<'a>,
expected_tokens: &'static [ExpectedToken],
) -> OSDBReport<'a> {
// If we have expected tokens, show an example for the first one
if let Some(help) = expected_token_example_msg(expected_tokens) {
report = report.with_help(help)
}
// If we have at least one expected token, show a message showing what type was expected
if let Some(note) = expected_token_msg(expected_tokens) {
report = report.with_note(note);
}
report
}
fn expected_token_msg(expected_tokens: &'static [ExpectedToken]) -> Option<String> {
if !expected_tokens.is_empty() {
// Add a note with all expected types
let expected_types: Vec<_> = expected_tokens.iter().map(|t| format!("{}", t)).collect();
// Use singular form when there's only one expected token type
Some(match expected_types.as_slice() {
[single_type] => {
format!("expected: {}", single_type)
}
_ => {
format!("expected one of: {}", expected_types.join(", "))
}
})
} else {
// there are no expected tokens
None
}
}
fn expected_token_example_msg(expected_tokens: &'static [ExpectedToken]) -> Option<String> {
if let Some(first_expected) = expected_tokens.first() {
let example = first_expected.example();
Some(format!("try a token of the expected type: {example}"))
} else {
None
}
}
impl OSDBError for ScanError {
fn display(&self, file: &str, input: &str) {
let location = (file, Into::<std::ops::Range<usize>>::into(&self.location));

View file

@ -1,9 +1,10 @@
use crate::branding;
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum MetaCommand {
Exit,
About,
Version,
}
impl std::fmt::Display for MetaCommand {
@ -11,6 +12,7 @@ impl std::fmt::Display for MetaCommand {
match self {
MetaCommand::Exit => write!(f, "exit"),
MetaCommand::About => write!(f, "about"),
MetaCommand::Version => write!(f, "version"),
}
}
}
@ -27,6 +29,10 @@ impl MetaCommand {
print!("{}", branding::about_msg());
MetaCommandExecuteResult { should_exit: false }
}
MetaCommand::Version => {
print!("{}", branding::version_msg());
MetaCommandExecuteResult { should_exit: false }
}
}
}
}

View file

@ -1,38 +1,241 @@
use std::collections::VecDeque;
use crate::{
command::{Command, CommandParseError},
command::{Command, CommandParseError, ExpectedToken},
statements::Statement,
tokens::tokenize,
tokens::{Location, Token, TokenData, tokenize},
};
// Helper function to skip tokens until reaching a semicolon or end of file
// This helps with error recovery when a statement has a syntax error
fn skip_to_next_statement(tokens: &mut VecDeque<Token>) {
while let Some(token) = tokens.front() {
match token.data {
TokenData::Semicolon | TokenData::EndOfFile => break,
_ => {
tokens.pop_front();
}
}
}
// Consume the semicolon if that's what we stopped at
if tokens
.front()
.is_some_and(|t| matches!(t.data, TokenData::Semicolon))
{
tokens.pop_front();
}
}
// Helper function to check for a semicolon after a statement
fn expect_semicolon(tokens: &mut VecDeque<crate::tokens::Token>) -> Result<(), CommandParseError> {
if let Some(next_token) = tokens.front() {
match next_token.data {
TokenData::Semicolon => {
tokens.pop_front(); // Consume the semicolon
Ok(())
}
_ => Err(CommandParseError::UnexpectedToken(
next_token.clone(),
&[ExpectedToken::Semicolon],
)),
}
} else {
// Even at the end of input, we need a semicolon
Err(CommandParseError::UnexpectedToken(
Token {
location: tokens
.back()
.map_or_else(Location::default, |t| t.location.clone()),
data: TokenData::EndOfFile,
lexeme: String::new(),
},
&[ExpectedToken::Semicolon],
))
}
}
fn parse_select_command(
tokens: &mut VecDeque<crate::tokens::Token>,
) -> Result<Command, CommandParseError> {
// Parse the select command (currently doesn't require additional tokens)
let cmd = Command::Statement(Statement::Select);
// Check for semicolon after select command
expect_semicolon(tokens)?;
Ok(cmd)
}
pub fn parse(file: String, input: String) -> Result<Vec<Command>, Vec<CommandParseError>> {
let mut tokens: VecDeque<_> = tokenize(input, file)
.map_err(|x| x.into_iter().map(|x| x.into()).collect::<Vec<_>>())?
.into();
let mut cmds = Vec::new();
let mut errs = Vec::new();
while let Some(token) = tokens.pop_front() {
match token.data {
crate::tokens::TokenData::Insert => cmds.push(Command::Statement(Statement::Insert)),
crate::tokens::TokenData::Select => cmds.push(Command::Statement(Statement::Select)),
crate::tokens::TokenData::MetaCommand(meta_command) => {
cmds.push(Command::MetaCommand(meta_command))
TokenData::Insert => match parse_insert_command(&mut tokens) {
Ok(cmd) => cmds.push(cmd),
Err(err) => {
errs.push(err);
skip_to_next_statement(&mut tokens); // Skip to next statement for error recovery
}
},
TokenData::Select => match parse_select_command(&mut tokens) {
Ok(cmd) => cmds.push(cmd),
Err(err) => {
errs.push(err);
skip_to_next_statement(&mut tokens); // Skip to next statement for error recovery
}
},
TokenData::MetaCommand(meta_command) => {
match parse_meta_command(meta_command, &mut tokens) {
Ok(cmd) => cmds.push(cmd),
Err(err) => {
errs.push(err);
skip_to_next_statement(&mut tokens); // Skip to next statement for error recovery
}
}
}
crate::tokens::TokenData::Int(_) => errs.push(CommandParseError::UnexpectedToken(
token,
&["statement", "meta command", "eof"],
)),
crate::tokens::TokenData::String(_) => errs.push(CommandParseError::UnexpectedToken(
token,
&["statement", "meta command", "eof"],
)),
crate::tokens::TokenData::EndOfFile => (),
TokenData::Semicolon => {
// Empty statement (just a semicolon) - ignore it
}
TokenData::Int(_) => {
errs.push(CommandParseError::UnexpectedToken(
token,
&[
ExpectedToken::Statement,
ExpectedToken::MetaCommand,
ExpectedToken::EndOfFile,
],
));
skip_to_next_statement(&mut tokens);
}
TokenData::String(_) => {
errs.push(CommandParseError::UnexpectedToken(
token,
&[
ExpectedToken::Statement,
ExpectedToken::MetaCommand,
ExpectedToken::EndOfFile,
],
));
skip_to_next_statement(&mut tokens);
}
TokenData::EndOfFile => (), // End of parsing
}
}
if errs.is_empty() { Ok(cmds) } else { Err(errs) }
}
fn parse_meta_command(
meta_command: crate::meta_commands::MetaCommand,
tokens: &mut VecDeque<Token>,
) -> Result<Command, CommandParseError> {
if let Some(token) = tokens.pop_front() {
if matches!(token.data, TokenData::EndOfFile) {
Ok(Command::MetaCommand(meta_command))
} else {
Err(CommandParseError::UnexpectedToken(
token,
&[ExpectedToken::EndOfFile],
))
}
} else {
Ok(Command::MetaCommand(meta_command))
}
}
fn parse_insert_command(
tokens: &mut VecDeque<crate::tokens::Token>,
) -> Result<Command, CommandParseError> {
// According to grammar.ebnf, insert command should be: insert int string string semicolon
// Parse the id (integer)
let id_token = tokens.pop_front().ok_or_else(|| {
CommandParseError::UnexpectedToken(
Token {
location: tokens
.back()
.map_or_else(Location::default, |t| t.location.clone()),
data: TokenData::EndOfFile,
lexeme: String::new(),
},
&[ExpectedToken::Integer],
)
})?;
let id = match id_token.data {
TokenData::Int(id) => id,
_ => {
return Err(CommandParseError::UnexpectedToken(
id_token,
&[ExpectedToken::Integer],
));
}
};
// Parse the username (string)
let username_token = tokens.pop_front().ok_or_else(|| {
CommandParseError::UnexpectedToken(
Token {
location: tokens
.back()
.map_or_else(Location::default, |t| t.location.clone()),
data: TokenData::EndOfFile,
lexeme: String::new(),
},
&[ExpectedToken::String],
)
})?;
let username = match username_token.data {
TokenData::String(username) => username,
_ => {
return Err(CommandParseError::UnexpectedToken(
username_token,
&[ExpectedToken::String],
));
}
};
// Parse the email (string)
let email_token = tokens.pop_front().ok_or_else(|| {
CommandParseError::UnexpectedToken(
Token {
location: tokens
.back()
.map_or_else(Location::default, |t| t.location.clone()),
data: TokenData::EndOfFile,
lexeme: String::new(),
},
&[ExpectedToken::String],
)
})?;
let email = match email_token.data {
TokenData::String(email) => email,
_ => {
return Err(CommandParseError::UnexpectedToken(
email_token,
&[ExpectedToken::String],
));
}
};
// Check for semicolon after the insert command
expect_semicolon(tokens)?;
Ok(Command::Statement(Statement::Insert {
id,
username,
email,
}))
}
#[cfg(test)]
mod tests {
use super::*;
@ -42,10 +245,32 @@ mod tests {
fn test_parse_single_correct() {
let file = String::from("<stdin>");
assert_debug_snapshot!(parse(file.clone(), String::from(".exit")));
assert_debug_snapshot!(parse(file.clone(), String::from("select;")));
assert_debug_snapshot!(parse(file.clone(), String::from("sElEcT;")));
}
#[test]
fn test_parse_insert_command() {
let file = String::from("<stdin>");
assert_debug_snapshot!(parse(
file.clone(),
String::from(r#"insert 1 "username" "email@example.com";"#)
));
assert_debug_snapshot!(parse(
file.clone(),
String::from(r#"insert "not_an_id" "username" "email@example.com";"#)
));
assert_debug_snapshot!(parse(file.clone(), String::from(r#"insert 1 "username";"#)));
}
#[test]
fn test_parse_missing_semicolon() {
let file = String::from("<stdin>");
assert_debug_snapshot!(parse(file.clone(), String::from("select")));
assert_debug_snapshot!(parse(file.clone(), String::from("sElEcT")));
assert_debug_snapshot!(parse(file.clone(), String::from("INSERT")));
assert_debug_snapshot!(parse(file.clone(), String::from("InSErT")));
assert_debug_snapshot!(parse(
file.clone(),
String::from(r#"insert 1 "username" "email@example.com""#)
));
}
#[test]
@ -60,10 +285,22 @@ mod tests {
#[test]
fn test_parse_multiple_correct() {
let file = String::from("<stdin>");
assert_debug_snapshot!(parse(file.clone(), String::from("select; select; select;")));
}
#[test]
fn test_meta_command_require_eof() {
let file = String::from("<stdin>");
assert_debug_snapshot!(parse(file.clone(), String::from(".exit select; select;")));
}
#[test]
fn test_parse_multiple_statements_with_insert() {
let file = String::from("<stdin>");
assert_debug_snapshot!(parse(
file.clone(),
String::from(".exit select select insert select")
String::from(r#"select; insert 1 "user" "email@test.com"; select;"#)
));
}

View file

@ -2,4 +2,4 @@
source: src/command.rs
expression: result
---
insert
insert 45 "user" "user@example.org"

View file

@ -0,0 +1,22 @@
---
source: src/parser.rs
expression: "parse(file.clone(), String::from(\".exit select; select;\"))"
---
Err(
[
UnexpectedToken(
Token {
location: Location {
file: "<stdin>",
offset: 6,
length: 6,
},
data: Select,
lexeme: "select",
},
[
EndOfFile,
],
),
],
)

View file

@ -0,0 +1,24 @@
---
source: src/parser.rs
expression: "parse(file.clone(),\nString::from(r#\"insert \"not_an_id\" \"username\" \"email@example.com\";\"#))"
---
Err(
[
UnexpectedToken(
Token {
location: Location {
file: "<stdin>",
offset: 7,
length: 11,
},
data: String(
"not_an_id",
),
lexeme: "\"not_an_id\"",
},
[
Integer,
],
),
],
)

View file

@ -0,0 +1,22 @@
---
source: src/parser.rs
expression: "parse(file.clone(), String::from(r#\"insert 1 \"username\";\"#))"
---
Err(
[
UnexpectedToken(
Token {
location: Location {
file: "<stdin>",
offset: 19,
length: 1,
},
data: Semicolon,
lexeme: ";",
},
[
String,
],
),
],
)

View file

@ -0,0 +1,15 @@
---
source: src/parser.rs
expression: "parse(file.clone(),\nString::from(r#\"insert 1 \"username\" \"email@example.com\"\"#))"
---
Ok(
[
Statement(
Insert {
id: 1,
username: "username",
email: "email@example.com",
},
),
],
)

View file

@ -0,0 +1,22 @@
---
source: src/parser.rs
expression: "parse(file.clone(),\nString::from(r#\"insert 1 \"username\" \"email@example.com\"\"#))"
---
Err(
[
UnexpectedToken(
Token {
location: Location {
file: "<stdin>",
offset: 39,
length: 0,
},
data: EndOfFile,
lexeme: "",
},
[
Semicolon,
],
),
],
)

View file

@ -0,0 +1,22 @@
---
source: src/parser.rs
expression: "parse(file.clone(), String::from(\"select\"))"
---
Err(
[
UnexpectedToken(
Token {
location: Location {
file: "<stdin>",
offset: 6,
length: 0,
},
data: EndOfFile,
lexeme: "",
},
[
Semicolon,
],
),
],
)

View file

@ -1,21 +1,15 @@
---
source: src/parser.rs
expression: "parse(file.clone(), String::from(\".exit select select insert select\"))"
expression: "parse(file.clone(), String::from(\"select; select; select;\"))"
---
Ok(
[
MetaCommand(
Exit,
),
Statement(
Select,
),
Statement(
Select,
),
Statement(
Insert,
),
Statement(
Select,
),

View file

@ -0,0 +1,21 @@
---
source: src/parser.rs
expression: "parse(file.clone(),\nString::from(r#\"select; insert 1 \"user\" \"email@test.com\"; select;\"#))"
---
Ok(
[
Statement(
Select,
),
Statement(
Insert {
id: 1,
username: "user",
email: "email@test.com",
},
),
Statement(
Select,
),
],
)

View file

@ -1,6 +1,10 @@
#[derive(Debug)]
pub enum Statement {
Insert,
Insert {
id: i64,
username: String,
email: String,
},
Select,
}
@ -11,8 +15,12 @@ pub struct StatementExecuteResult {
impl Statement {
pub fn execute(&self) -> StatementExecuteResult {
match self {
Statement::Insert => StatementExecuteResult {
msg: String::from("insert"),
Statement::Insert {
id,
username,
email,
} => StatementExecuteResult {
msg: format!("insert {id:?} {username:?} {email:?}"),
},
Statement::Select => StatementExecuteResult {
msg: String::from("select"),

View file

@ -1,6 +1,6 @@
use crate::meta_commands::MetaCommand;
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum TokenData {
Insert,
Select,
@ -8,9 +8,10 @@ pub enum TokenData {
EndOfFile,
Int(i64),
String(String),
Semicolon,
}
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Location {
/// file name
pub file: String,
@ -29,6 +30,12 @@ impl From<&Location> for std::ops::Range<usize> {
}
}
impl Default for Location {
fn default() -> Self {
Self::new(String::from("<unknown>"), 0, 0)
}
}
impl Location {
/// ```
/// use osdb::tokens::Location;
@ -46,7 +53,7 @@ impl Location {
}
}
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Token {
/// Where in the input was this token found?
pub location: Location,
@ -65,6 +72,7 @@ impl std::fmt::Display for Token {
TokenData::EndOfFile => write!(f, "end of file"),
TokenData::Int(x) => write!(f, "integer {x}"),
TokenData::String(x) => write!(f, "string {x:?}"),
TokenData::Semicolon => write!(f, "semicolon"),
}?;
let lexeme = &self.lexeme;
write!(f, " {lexeme:?}")
@ -161,6 +169,7 @@ impl Tokenizer {
match word.to_lowercase().as_str() {
".exit" => Some(TokenData::MetaCommand(MetaCommand::Exit)),
".about" => Some(TokenData::MetaCommand(MetaCommand::About)),
".version" => Some(TokenData::MetaCommand(MetaCommand::Version)),
_ => None,
}
}
@ -306,6 +315,15 @@ impl Tokenizer {
}
}
fn scan_semicolon(&mut self) -> Result<Token, ScanError> {
self.advance();
Ok(Token {
location: self.previous_location(1),
data: TokenData::Semicolon,
lexeme: String::from(";"),
})
}
fn scan_token(&mut self) -> Result<Option<Token>, ScanError> {
loop {
if let Some(c) = self.peek() {
@ -317,6 +335,8 @@ impl Tokenizer {
return self.scan_integer().map(Some);
} else if c == '"' {
return self.scan_string().map(Some);
} else if c == ';' {
return self.scan_semicolon().map(Some);
} else if c.is_whitespace() {
self.advance();
} else {