Project Setup
Create the Project
cargo new toml-parser --lib
cd toml-parser
Dependencies
[package]
name = "toml-parser"
version = "0.1.0"
edition = "2024"
[dependencies]
synkit = "0.1"
thiserror = "2"
logos = "0.15"
Error Type
Define an error type that implements Default (required by logos):
#[derive(Error, Debug, Clone, Default, PartialEq)]
pub enum TomlError {
#[default]
#[error("unknown lexing error")]
Unknown,
#[error("expected {expect}, found {found}")]
Expected { expect: &'static str, found: String },
#[error("expected {expect}, found EOF")]
Empty { expect: &'static str },
#[error("unclosed string")]
UnclosedString,
#[error("{source}")]
Spanned {
#[source]
source: Box<TomlError>,
span: Span,
},
}
Key requirements:
#[default]variant for unknown tokensExpectedvariant withexpectandfoundfieldsEmptyvariant for EOF errorsSpannedvariant wrapping errors with location
parser_kit! Invocation
The macro generates all parsing infrastructure:
synkit::parser_kit! {
error: TomlError,
skip_tokens: [Space, Tab],
tokens: {
// Whitespace
#[token(" ", priority = 0)]
Space,
#[token("\t", priority = 0)]
Tab,
#[regex(r"\r?\n")]
#[fmt("newline")]
#[no_to_tokens]
Newline,
// Comments
#[regex(r"#[^\n]*", allow_greedy = true)]
#[fmt("comment")]
Comment,
// Punctuation
#[token("=")]
Eq,
#[token(".")]
Dot,
#[token(",")]
Comma,
#[token("[")]
LBracket,
#[token("]")]
RBracket,
#[token("{")]
LBrace,
#[token("}")]
RBrace,
// Keywords/literals
#[token("true")]
True,
#[token("false")]
False,
// Bare keys: alphanumeric, underscores, dashes
#[regex(r"[A-Za-z0-9_-]+", |lex| lex.slice().to_string(), priority = 1)]
#[fmt("bare key")]
#[derive(PartialOrd, Ord, Hash, Eq)]
BareKey(String),
// Basic strings (double-quoted) - needs custom ToTokens for quote handling
#[regex(r#""([^"\\]|\\.)*""#, |lex| {
let s = lex.slice();
// Remove surrounding quotes
s[1..s.len()-1].to_string()
})]
#[fmt("string")]
#[no_to_tokens]
BasicString(String),
// Integers
#[regex(r"-?[0-9]+", |lex| lex.slice().parse::<i64>().ok())]
#[fmt("integer")]
Integer(i64),
},
delimiters: {
Bracket => (LBracket, RBracket),
Brace => (LBrace, RBrace),
},
span_derives: [Debug, Clone, PartialEq, Eq, Hash, Copy],
token_derives: [Clone, PartialEq, Debug],
}
This generates:
spanmodule withSpan,Spanned<T>tokensmodule withTokenenum and*Tokenstructsstreammodule withTokenStreamtraitsmodule withParse,Peek,ToTokensdelimitersmodule withBracket,Brace
Error Helpers
Add convenience methods for error creation:
impl TomlError {
pub fn expected<D: Diagnostic>(found: &Token) -> Self {
Self::Expected {
expect: D::fmt(),
found: format!("{}", found),
}
}
pub fn empty<D: Diagnostic>() -> Self {
Self::Empty { expect: D::fmt() }
}
}
impl synkit::SpannedError for TomlError {
type Span = Span;
fn with_span(self, span: Span) -> Self {
Self::Spanned {
source: Box::new(self),
span,
}
}
fn span(&self) -> Option<&Span> {
match self {
Self::Spanned { span, .. } => Some(span),
_ => None,
}
}
}
Module Structure
// lib.rs
mod ast;
mod parse;
mod print;
mod visitor;
pub use ast::*;
pub use parse::*;
pub use visitor::*;
Verify Setup
cargo check
The macro should expand without errors. If you see errors about missing traits, ensure your error type has the required variants.