Compare commits
No commits in common. "e5fafd03ba2e37a84bb0ad308d5a25661acc146a" and "2d95a58ce76248f7efb44b7a719519724c30694f" have entirely different histories.
e5fafd03ba
...
2d95a58ce7
3
.gitignore
vendored
3
.gitignore
vendored
@ -10,7 +10,6 @@ Cargo.lock
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Miscellaneous
|
||||
# Miscellaneous files
|
||||
.DS_Store
|
||||
tarpaulin-report.html
|
||||
*.profraw
|
||||
|
@ -1,14 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["onihime", "tools/*"]
|
||||
members = ["onihime"]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Jesse Braham <jesse@beta7.io>"]
|
||||
authors = ["Jesse Braham <jesse@hatebit.org>"]
|
||||
edition = "2021"
|
||||
homepage = "https://onihime.org"
|
||||
repository = "https://hatebit.org/jesse/onihime"
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = "fat"
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2024, Jesse Braham
|
||||
Copyright (c) 2025, Jesse Braham
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
52
justfile
52
justfile
@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env -S just --justfile
|
||||
|
||||
log := "warn"
|
||||
|
||||
export JUST_LOG := log
|
||||
|
||||
_default:
|
||||
@just --list --unsorted
|
||||
|
||||
# Build all packages
|
||||
[group('build')]
|
||||
build:
|
||||
cargo build --release --workspace
|
||||
|
||||
# Build the specified package
|
||||
[group('build')]
|
||||
build-package PACKAGE:
|
||||
cargo build --release --package={{PACKAGE}}
|
||||
|
||||
# Check test coverage of all packages in the workspace
|
||||
[group('test')]
|
||||
coverage:
|
||||
cargo tarpaulin --workspace --out=Html --exclude=onihime-macros
|
||||
|
||||
# Test all packages
|
||||
[group('test')]
|
||||
test:
|
||||
cargo test --workspace
|
||||
|
||||
# Test the specified package
|
||||
[group('test')]
|
||||
test-package PACKAGE:
|
||||
cargo test --package={{PACKAGE}}
|
||||
|
||||
# Check the formatting of all packages
|
||||
[group('lint')]
|
||||
check-format:
|
||||
cargo fmt --all -- --check
|
||||
|
||||
# Format all packages
|
||||
[group('lint')]
|
||||
format:
|
||||
cargo fmt --all
|
||||
|
||||
# Run clippy checks for all packages
|
||||
[group('lint')]
|
||||
clippy:
|
||||
cargo clippy --no-deps -- -D warnings -W clippy::all
|
||||
|
||||
# Check formatting and run clippy checks for all packages
|
||||
[group('lint')]
|
||||
lint: check-format clippy
|
@ -7,8 +7,8 @@ homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
unicode-segmentation = "1.12.0"
|
||||
[dev-dependencies]
|
||||
proptest = "1.6.0"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
|
1
onihime/LICENSE
Symbolic link
1
onihime/LICENSE
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE
|
1
onihime/README.md
Normal file
1
onihime/README.md
Normal file
@ -0,0 +1 @@
|
||||
# onihime
|
@ -1,56 +1,79 @@
|
||||
use crate::span::Span;
|
||||
use std::fmt;
|
||||
|
||||
/// Kinds of errors that may occur during lexical analysis.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
use crate::Span;
|
||||
|
||||
/// Kinds of errors which can occur during lexical analysis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum LexerErrorKind {
|
||||
/// An invalid escape sequence was encountered.
|
||||
InvalidEscape(String),
|
||||
/// An invalid numeric literal was encountered.
|
||||
InvalidNumber(String),
|
||||
/// An invalid string literal was encountered.
|
||||
InvalidString,
|
||||
/// An unclosed character literal was encountered.
|
||||
UnclosedChar,
|
||||
/// And unclosed string literal was encountered.
|
||||
/// An invalid character literal was encountered.
|
||||
InvalidChar,
|
||||
/// An invalid keyword was encountered.
|
||||
InvalidKeyword,
|
||||
/// An invalid number literal was encountered.
|
||||
InvalidNumber,
|
||||
/// An invalid symbol was encountered.
|
||||
InvalidSymbol,
|
||||
/// An unclosed string literal was encountered.
|
||||
UnclosedString,
|
||||
}
|
||||
|
||||
/// An error which occurred during lexical analysis.
|
||||
///
|
||||
/// `LexerError`s contain the kind of error which occurred, as well as a [Span]
|
||||
/// specifying the [Source] and [Location] of the error.
|
||||
///
|
||||
/// [Source]: crate::span::Source
|
||||
/// [Location]: crate::span::Location
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Display for LexerErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use LexerErrorKind::*;
|
||||
|
||||
match self {
|
||||
InvalidChar => write!(f, "Invalid character literal"),
|
||||
InvalidKeyword => write!(f, "Invalid keyword"),
|
||||
InvalidNumber => write!(f, "Invalid number literal"),
|
||||
InvalidSymbol => write!(f, "Invalid symbol"),
|
||||
UnclosedString => write!(f, "Unclosed string literal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors which occur during lexical analysis.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct LexerError {
|
||||
/// The kind of error encountered.
|
||||
/// The kind of lexer error.
|
||||
pub kind: LexerErrorKind,
|
||||
/// The span in which the error occurred.
|
||||
/// The span of the lexer error.
|
||||
pub span: Span,
|
||||
/// Additional context regarding the lexer error.
|
||||
pub context: Option<String>,
|
||||
}
|
||||
|
||||
impl LexerError {
|
||||
/// Construct a new instance of `LexerError`.
|
||||
/// Construct a new instance of a lexer error.
|
||||
#[must_use]
|
||||
pub const fn new(kind: LexerErrorKind, span: Span) -> Self {
|
||||
Self { kind, span }
|
||||
Self {
|
||||
kind,
|
||||
span,
|
||||
context: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide additional context for a lexer error.
|
||||
#[must_use]
|
||||
pub fn with_context<C>(mut self, f: impl FnOnce() -> C) -> Self
|
||||
where
|
||||
C: fmt::Display,
|
||||
{
|
||||
self.context = Some(f().to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LexerError {}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Display for LexerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use LexerErrorKind::*;
|
||||
|
||||
match &self.kind {
|
||||
InvalidEscape(c) => write!(f, "Invalid escape sequence '\\{c}'"),
|
||||
InvalidNumber(n) => write!(f, "Invalid numeric literal `{n}`"),
|
||||
InvalidString => write!(f, "Invalid string literal"),
|
||||
UnclosedChar => write!(f, "Unclosed character literal"),
|
||||
UnclosedString => write!(f, "Unclosed string literal"),
|
||||
impl fmt::Display for LexerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref context) = self.context {
|
||||
write!(f, "{}: {}", self.kind, context)
|
||||
} else {
|
||||
write!(f, "{}", self.kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +0,0 @@
|
||||
/// A symbol used to identify a function or variable.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub(crate) struct Symbol(String);
|
||||
|
||||
impl Symbol {
|
||||
/// Create a new `Symbol` from a string.
|
||||
pub(crate) fn from<S>(s: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Self(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Display for Symbol {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
use super::Symbol;
|
||||
use crate::span::Span;
|
||||
use crate::Span;
|
||||
|
||||
/// Possible kinds of a [Token].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum TokenKind {
|
||||
/// Block comment, e.g. `#| ... |#`
|
||||
BlockComment(String),
|
||||
/// Kinds of tokens which are valid in Onihime source code.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TokenKind {
|
||||
/// Line comment, e.g. `; ...`
|
||||
LineComment(String),
|
||||
Comment,
|
||||
/// Whitespace, e.g. ' ', '\t', '\n'
|
||||
Whitespace,
|
||||
|
||||
/// Opening parenthesis, e.g. `(`
|
||||
OpenParen,
|
||||
@ -21,46 +20,52 @@ pub(crate) enum TokenKind {
|
||||
OpenBracket,
|
||||
/// Closing bracket, e.g. `]`
|
||||
CloseBracket,
|
||||
/// Opening hash-brace, e.g. `#{`
|
||||
OpenHashBrace,
|
||||
|
||||
/// Boolean, e.g. `true`, `false`
|
||||
Bool(bool),
|
||||
/// Character, e.g. `'c'`, `'\n'`
|
||||
Char(String),
|
||||
/// Floating-point number, e.g. `-1.0`, `2.0`, `+0.003`
|
||||
Float(f64),
|
||||
/// Integer, e.g. `0`, `-1`, `+200`
|
||||
Integer(i64),
|
||||
/// Keyword, e.g. `:baz`
|
||||
Keyword(Symbol),
|
||||
Bool,
|
||||
/// Character, e.g. `\a`, `\x1e`, `\u03BB`, `\newline`
|
||||
Char,
|
||||
/// Keyword, e.g. `:foo-bar`, `:baz`, `:qux0`
|
||||
Keyword,
|
||||
/// Floating-point number, e.g. `-1.0`, `2.0`, `3.0e-4`
|
||||
Decimal,
|
||||
/// Integer, e.g. `0`, `-1`, `0b1010`, `0o7`, `0xDECAFBAD`
|
||||
Integer,
|
||||
/// Ratio, e.g. `1/3`, `-5/7`
|
||||
Ratio,
|
||||
/// String, e.g. `"foo bar"`
|
||||
String(String),
|
||||
/// Symbol, e.g. `qux`, `+`
|
||||
Symbol(Symbol),
|
||||
String,
|
||||
/// Symbol, e.g. `baz`, `*qux*`, `nil?`, `+`
|
||||
Symbol,
|
||||
/// Nil, e.g. `nil`
|
||||
Nil,
|
||||
}
|
||||
|
||||
/// A token encountered during lexical analysis.
|
||||
///
|
||||
/// `Token`s contain the kind of token which was found, as well as a [Span]
|
||||
/// specifying the [Source] and [Location] of the token.
|
||||
///
|
||||
/// [Source]: crate::span::Source
|
||||
/// [Location]: crate::span::Location
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Token {
|
||||
/// The kind of token.
|
||||
pub(crate) kind: TokenKind,
|
||||
/// The span in which the token occurs.
|
||||
pub(crate) span: Span,
|
||||
impl TokenKind {
|
||||
/// Returns `true` if the token type an atom.
|
||||
pub fn is_atom(&self) -> bool {
|
||||
use TokenKind::*;
|
||||
|
||||
matches!(
|
||||
self,
|
||||
Bool | Char | Keyword | Decimal | Integer | Ratio | String | Symbol | Nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid token found in Onihime source code.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Token {
|
||||
/// Kind of token which was found.
|
||||
pub kind: TokenKind,
|
||||
/// The token's span.
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
/// Construct a new instance of `Token`.
|
||||
/// Construct a new instance of a token.
|
||||
#[must_use]
|
||||
pub(crate) const fn new(kind: TokenKind, span: Span) -> Self {
|
||||
pub const fn new(kind: TokenKind, span: Span) -> Self {
|
||||
Self { kind, span }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
//! Onihime programming language.
|
||||
//! Onihime programming language
|
||||
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
@ -7,6 +7,8 @@
|
||||
unsafe_code
|
||||
)]
|
||||
|
||||
mod lexer;
|
||||
mod parser;
|
||||
pub use self::span::Span;
|
||||
|
||||
pub mod lexer;
|
||||
|
||||
mod span;
|
||||
|
@ -1,219 +0,0 @@
|
||||
use super::error::ParserError;
|
||||
use crate::{
|
||||
lexer::{Symbol, Token, TokenKind},
|
||||
span::Span,
|
||||
};
|
||||
|
||||
/// Abstract Syntax Tree (AST).
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub(crate) struct Ast {
|
||||
root: Vec<Node>,
|
||||
}
|
||||
|
||||
impl Ast {
|
||||
/// Construct a new instance of `Ast`.
|
||||
#[must_use]
|
||||
pub(crate) fn new(root: Vec<Node>) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Node>> for Ast {
|
||||
fn from(root: Vec<Node>) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Display for Ast {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for node in &self.root {
|
||||
writeln!(f, "{node}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in the Abstract Syntax Tree (AST).
|
||||
///
|
||||
/// `Nodes`s contain the kind of node which was found, as well as a [Span]
|
||||
/// specifying the [Source] and [Location] of the node.
|
||||
///
|
||||
/// [Source]: crate::span::Source
|
||||
/// [Location]: crate::span::Location
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Node {
|
||||
/// The kind of node.
|
||||
pub kind: Expr,
|
||||
/// The span in which the node occurs.
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Construct a new instance of `Node`.
|
||||
#[must_use]
|
||||
pub(crate) fn new(kind: Expr, span: Span) -> Self {
|
||||
Self { kind, span }
|
||||
}
|
||||
|
||||
/// Push a child node onto a list node.
|
||||
pub(crate) fn push_node(&mut self, child: Self) -> Result<(), ParserError> {
|
||||
match &mut self.kind {
|
||||
Expr::List(vec) | Expr::Map(vec) | Expr::Set(vec) | Expr::Vector(vec) => {
|
||||
vec.push(child);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[must_use]
|
||||
fn display(&self, indent: usize) -> String {
|
||||
let mut text = format!(
|
||||
"{}{}@{}..{}\n",
|
||||
" ".repeat(indent),
|
||||
self.kind,
|
||||
self.span.bytes().start,
|
||||
self.span.bytes().end
|
||||
);
|
||||
|
||||
match &self.kind {
|
||||
Expr::Atom(_) => {}
|
||||
Expr::List(vec) | Expr::Map(vec) | Expr::Set(vec) | Expr::Vector(vec) => {
|
||||
for node in vec {
|
||||
text.push_str(&format!("{}\n", node.display(indent + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text.trim_end().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Token> for Node {
|
||||
type Error = ParserError;
|
||||
|
||||
fn try_from(token: Token) -> Result<Self, Self::Error> {
|
||||
let span = token.span.clone();
|
||||
let kind = Expr::try_from(token)?;
|
||||
|
||||
Ok(Self::new(kind, span))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Display for Node {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.display(0))
|
||||
}
|
||||
}
|
||||
|
||||
/// An atomic value.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Atom {
|
||||
/// Boolean, e.g. `true`, `false`
|
||||
Bool(bool),
|
||||
/// Character, e.g. `'c'`, `'\n'`
|
||||
Char(String),
|
||||
/// Floating-point number, e.g. `-1.0`, `2.0`, `+0.003`
|
||||
Float(f64),
|
||||
/// Integer, e.g. `0`, `-1`, `+200`
|
||||
Integer(i64),
|
||||
/// Keyword, e.g. `:baz`
|
||||
Keyword(Symbol),
|
||||
/// String, e.g. `"foo bar"`
|
||||
String(String),
|
||||
/// Symbol, e.g. `qux`, `+`
|
||||
Symbol(Symbol),
|
||||
/// Nil, e.g. `nil`
|
||||
Nil,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Display for Atom {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use Atom::*;
|
||||
|
||||
match self {
|
||||
Bool(_) => write!(f, "BOOL"),
|
||||
Char(_) => write!(f, "CHAR"),
|
||||
Float(_) => write!(f, "FLOAT"),
|
||||
Integer(_) => write!(f, "INTEGER"),
|
||||
Keyword(_) => write!(f, "KEYWORD"),
|
||||
String(_) => write!(f, "STRING"),
|
||||
Symbol(_) => write!(f, "SYMBOL"),
|
||||
Nil => write!(f, "NIL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An expression.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Expr {
|
||||
/// An atomic value.
|
||||
Atom(Atom),
|
||||
/// A list of nodes.
|
||||
List(Vec<Node>),
|
||||
/// A map of nodes.
|
||||
Map(Vec<Node>),
|
||||
/// A set of nodes.
|
||||
Set(Vec<Node>),
|
||||
/// A vector of nodes.
|
||||
Vector(Vec<Node>),
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
/// Which closing delimiter is associated with the expression kind?
|
||||
pub(crate) fn closing_delimiter(&self) -> Option<TokenKind> {
|
||||
match self {
|
||||
Expr::List(_) => Some(TokenKind::CloseParen),
|
||||
Expr::Map(_) | Expr::Set(_) => Some(TokenKind::CloseBrace),
|
||||
Expr::Vector(_) => Some(TokenKind::CloseBracket),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Atom> for Expr {
|
||||
fn from(atom: Atom) -> Self {
|
||||
Self::Atom(atom)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Token> for Expr {
|
||||
type Error = ParserError;
|
||||
|
||||
fn try_from(token: Token) -> Result<Self, Self::Error> {
|
||||
let kind = match token.kind {
|
||||
TokenKind::Bool(b) => Atom::Bool(b),
|
||||
TokenKind::Char(c) => Atom::Char(c),
|
||||
TokenKind::Float(n) => Atom::Float(n),
|
||||
TokenKind::Integer(n) => Atom::Integer(n),
|
||||
TokenKind::Keyword(k) => Atom::Keyword(k),
|
||||
TokenKind::String(s) => Atom::String(s),
|
||||
TokenKind::Symbol(s) => Atom::Symbol(s),
|
||||
TokenKind::Nil => Atom::Nil,
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
Ok(kind.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Display for Expr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use Expr::*;
|
||||
|
||||
match self {
|
||||
Atom(atom) => write!(f, "{atom}"),
|
||||
List(_) => write!(f, "LIST"),
|
||||
Map(_) => write!(f, "MAP"),
|
||||
Set(_) => write!(f, "SET"),
|
||||
Vector(_) => write!(f, "VECTOR"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
use crate::{lexer::LexerError, span::Span};
|
||||
|
||||
/// Kinds of errors that can occur during parsing.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ParserErrorKind {
|
||||
/// An error which ocurred during lexical analysis.
|
||||
Lexer(LexerError),
|
||||
/// Key in map is missing its corresponding value.
|
||||
MissingValueInMap,
|
||||
/// Opening delimiter does not have a matching closing delimiter.
|
||||
UnclosedSequence,
|
||||
/// An unexpected closing delimiter was found.
|
||||
UnexpectedClosingDelimiter,
|
||||
/// Unexpectedly reached end of input.
|
||||
UnexpectedEof,
|
||||
/// An unmatched closing delimiter was found.
|
||||
UnmatchedClosingDelimiter,
|
||||
}
|
||||
|
||||
/// Parser error, with a start and end location.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ParserError {
|
||||
/// The type of error encountered.
|
||||
pub kind: ParserErrorKind,
|
||||
/// The span in which the error occurred.
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl ParserError {
|
||||
/// Construct a new instance of `ParserError`.
|
||||
#[must_use]
|
||||
pub const fn new(kind: ParserErrorKind, span: Span) -> Self {
|
||||
Self { kind, span }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LexerError> for ParserError {
|
||||
fn from(err: LexerError) -> Self {
|
||||
let span = err.span.clone();
|
||||
Self::new(ParserErrorKind::Lexer(err), span)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParserError {}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Display for ParserError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use ParserErrorKind::*;
|
||||
|
||||
match &self.kind {
|
||||
Lexer(err) => write!(f, "{err}"),
|
||||
MissingValueInMap => write!(f, "Key in map is missing its corresponding value"),
|
||||
UnclosedSequence => write!(
|
||||
f,
|
||||
"Opening delimiter does not have a matching closing delimiter"
|
||||
),
|
||||
UnexpectedClosingDelimiter => write!(f, "An unexpected closing delimiter was found"),
|
||||
UnexpectedEof => write!(f, "Unexpectedly reached end of input"),
|
||||
UnmatchedClosingDelimiter => write!(f, "An unmatched closing delimiter was found"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,327 +0,0 @@
|
||||
pub(crate) use self::{ast::Ast, error::ParserError};
|
||||
use self::{
|
||||
ast::{Expr, Node},
|
||||
error::ParserErrorKind,
|
||||
};
|
||||
use crate::{
|
||||
lexer::{Lexer, TokenKind},
|
||||
span::Span,
|
||||
};
|
||||
|
||||
mod ast;
|
||||
mod error;
|
||||
|
||||
pub(crate) struct Parser<'parser> {
|
||||
lexer: Lexer<'parser>,
|
||||
parents: Vec<Node>,
|
||||
current: Node,
|
||||
}
|
||||
|
||||
impl<'parser> Parser<'parser> {
|
||||
/// Create a new parser instance from a string.
|
||||
#[must_use]
|
||||
pub(crate) fn new(input: &'parser str) -> Self {
|
||||
let lexer = Lexer::new(input);
|
||||
let current = Node::new(Expr::List(Vec::new()), lexer.span());
|
||||
|
||||
Self {
|
||||
lexer,
|
||||
parents: Vec::new(),
|
||||
current,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the name of the lexer's source.
|
||||
pub(crate) fn set_name(&mut self, name: String) {
|
||||
self.lexer.set_name(name);
|
||||
}
|
||||
|
||||
/// Produce an Abstract Syntax Tree (AST) from the source input.
|
||||
pub(crate) fn parse(mut self) -> Result<Ast, ParserError> {
|
||||
// This parser is actually quite simple!Recursively parse expressions until we
|
||||
// run out of tokens, or an error occurs:
|
||||
while !self.lexer.eof() {
|
||||
if let Some(node) = self.expr()? {
|
||||
self.current.push_node(node)?;
|
||||
}
|
||||
}
|
||||
|
||||
// When we reach the end of input, there should be no remaining parent nodes; if
|
||||
// there are, that means that there is a missing closing delimiter somewhere:
|
||||
if !self.parents.is_empty() {
|
||||
return Err(ParserError::new(
|
||||
ParserErrorKind::UnclosedSequence,
|
||||
self.current.span,
|
||||
));
|
||||
}
|
||||
|
||||
// Since we created an initial `Expr::List` node to hold the parsed contents
|
||||
// (i.e. so that we had something to push nodes to), we can now rip out its guts
|
||||
// and return the newly constructed AST:
|
||||
if let Expr::List(root) = self.current.kind {
|
||||
Ok(Ast::new(root))
|
||||
} else {
|
||||
unreachable!() // TODO: Is this really true? It should be... right?
|
||||
}
|
||||
}
|
||||
|
||||
fn expr(&mut self) -> Result<Option<Node>, ParserError> {
|
||||
if let Some(token) = self.lexer.read()? {
|
||||
match token.kind {
|
||||
// Comments are simply ignored by the parser:
|
||||
TokenKind::BlockComment(_) | TokenKind::LineComment(_) => Ok(None),
|
||||
|
||||
// Any valid opening delimiters begins a new sequence:
|
||||
TokenKind::OpenParen => self.begin_sequence(Expr::List, token.span),
|
||||
TokenKind::OpenHashBrace => self.begin_sequence(Expr::Map, token.span),
|
||||
TokenKind::OpenBrace => self.begin_sequence(Expr::Set, token.span),
|
||||
TokenKind::OpenBracket => self.begin_sequence(Expr::Vector, token.span),
|
||||
|
||||
// Any valid closing delimiters end the current sequence:
|
||||
kind
|
||||
@ (TokenKind::CloseParen | TokenKind::CloseBrace | TokenKind::CloseBracket) => {
|
||||
self.end_sequence(kind, token.span)
|
||||
}
|
||||
|
||||
// Atoms are pushed to the current sequence:
|
||||
_ => {
|
||||
let node = Node::try_from(token)?;
|
||||
let span = node.span.clone();
|
||||
|
||||
self.current.push_node(node)?;
|
||||
self.current.span.extend(&span);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(ParserError::new(
|
||||
ParserErrorKind::UnexpectedEof,
|
||||
self.lexer.span(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_sequence(
|
||||
&mut self,
|
||||
init: impl FnOnce(Vec<Node>) -> Expr,
|
||||
span: Span,
|
||||
) -> Result<Option<Node>, ParserError> {
|
||||
self.current.span.extend(&span);
|
||||
self.parents.push(self.current.clone());
|
||||
|
||||
self.current = Node::new(init(Vec::new()), span.clone());
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn end_sequence(&mut self, kind: TokenKind, span: Span) -> Result<Option<Node>, ParserError> {
|
||||
// We will ultimately return the current expression, so clone it and update its
|
||||
// span first:
|
||||
let mut current = self.current.clone();
|
||||
current.span.extend(&span);
|
||||
|
||||
// Update the parser's current node to the previous parent, or return an error
|
||||
// if no parents exist:
|
||||
self.current = self.parents.pop().ok_or_else(|| {
|
||||
ParserError::new(ParserErrorKind::UnexpectedClosingDelimiter, span.clone())
|
||||
})?;
|
||||
|
||||
// Ensure that the appropriate closing delimiter was found for our current node:
|
||||
if current.kind.closing_delimiter() != Some(kind) {
|
||||
return Err(ParserError::new(
|
||||
ParserErrorKind::UnmatchedClosingDelimiter,
|
||||
span,
|
||||
));
|
||||
}
|
||||
|
||||
// For maps, ensure that each key has a corresponding value:
|
||||
match current.kind {
|
||||
Expr::Map(ref vec) if vec.len() % 2 != 0 => {
|
||||
return Err(ParserError::new(ParserErrorKind::MissingValueInMap, span));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Finally, return the current node so it can be added to the AST:
|
||||
Ok(Some(current))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
lexer::{LexerError, LexerErrorKind, Symbol},
|
||||
parser::ast::Atom,
|
||||
};
|
||||
|
||||
macro_rules! test {
|
||||
( $name:ident: $input:literal, $src:ident => $ast:expr ) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let parser = Parser::new($input);
|
||||
let $src = parser.lexer.source();
|
||||
assert_eq!(parser.parse(), $ast);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test!(empty: "", _src => Ok(Ast::default()));
|
||||
|
||||
test!(error_invalid_number: "(+ 1.2.3)", src => Err(ParserError::new(
|
||||
ParserErrorKind::Lexer(LexerError::new(
|
||||
LexerErrorKind::InvalidNumber("1.2.3".into()),
|
||||
Span::new(3..8, src.clone())
|
||||
)),
|
||||
Span::new(3..8, src)
|
||||
)));
|
||||
|
||||
test!(list: "(+ 1 2) ; sneaky comment :)", src => Ok(Ast::from(vec![
|
||||
Node::new(
|
||||
Expr::List(vec![
|
||||
Node::new(Atom::Symbol(Symbol::from("+")).into(), Span::new(1..2, src.clone())),
|
||||
Node::new(Atom::Integer(1).into(), Span::new(3..4, src.clone())),
|
||||
Node::new(Atom::Integer(2).into(), Span::new(5..6, src.clone())),
|
||||
]),
|
||||
Span::new(0..7, src)
|
||||
)
|
||||
])));
|
||||
|
||||
test!(error_list_unmatched_bracket: "(]", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnmatchedClosingDelimiter,
|
||||
Span::new(1..2, src),
|
||||
)));
|
||||
|
||||
test!(error_list_missing_close_paren: "(true", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnclosedSequence,
|
||||
Span::new(0..5, src),
|
||||
)));
|
||||
|
||||
test!(error_list_unexpected_close_paren: ")", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnexpectedClosingDelimiter,
|
||||
Span::new(0..1, src)
|
||||
)));
|
||||
|
||||
test!(map: "#{:a 0.0 :b 1.0}", src => Ok(Ast::from(vec![
|
||||
Node::new(
|
||||
Expr::Map(vec![
|
||||
Node::new(Atom::Keyword(Symbol::from("a")).into(), Span::new(2..4, src.clone())),
|
||||
Node::new(Atom::Float(0.0).into(), Span::new(5..8, src.clone())),
|
||||
Node::new(Atom::Keyword(Symbol::from("b")).into(), Span::new(9..11, src.clone())),
|
||||
Node::new(Atom::Float(1.0).into(), Span::new(12..15, src.clone())),
|
||||
]),
|
||||
Span::new(0..16, src)
|
||||
)
|
||||
])));
|
||||
|
||||
test!(error_map_missing_value: "#{:x}", src => Err(ParserError::new(
|
||||
ParserErrorKind::MissingValueInMap,
|
||||
Span::new(4..5, src.clone())
|
||||
)));
|
||||
|
||||
test!(error_map_unmatched_bracket: "#{)", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnmatchedClosingDelimiter,
|
||||
Span::new(2..3, src),
|
||||
)));
|
||||
|
||||
test!(error_map_missing_close_brace: "#{", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnclosedSequence,
|
||||
Span::new(0..2, src),
|
||||
)));
|
||||
|
||||
test!(error_map_set_unexpected_close_brace: "}", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnexpectedClosingDelimiter,
|
||||
Span::new(0..1, src)
|
||||
)));
|
||||
|
||||
test!(set: "{{} nil}", src => Ok(Ast::from(vec![
|
||||
Node::new(
|
||||
Expr::Set(vec![
|
||||
Node::new(Expr::Set(vec![]), Span::new(1..3, src.clone())),
|
||||
Node::new(Expr::Atom(Atom::Nil), Span::new(4..7, src.clone())),
|
||||
]),
|
||||
Span::new(0..8, src),
|
||||
)
|
||||
])));
|
||||
|
||||
test!(error_set_unmatched_bracket: "{]", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnmatchedClosingDelimiter,
|
||||
Span::new(1..2, src),
|
||||
)));
|
||||
|
||||
test!(error_set_missing_close_brace: "{", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnclosedSequence,
|
||||
Span::new(0..1, src),
|
||||
)));
|
||||
|
||||
test!(vector: "['a' 'b' 'c']", src => Ok(Ast::from(vec![
|
||||
Node::new(
|
||||
Expr::Vector(vec![
|
||||
Node::new(Expr::Atom(Atom::Char("a".into())), Span::new(1..4, src.clone())),
|
||||
Node::new(Expr::Atom(Atom::Char("b".into())), Span::new(5..8, src.clone())),
|
||||
Node::new(Expr::Atom(Atom::Char("c".into())), Span::new(9..12, src.clone())),
|
||||
]),
|
||||
Span::new(0..13, src),
|
||||
)
|
||||
])));
|
||||
|
||||
test!(error_vector_unmatched_bracket: "[}", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnmatchedClosingDelimiter,
|
||||
Span::new(1..2, src),
|
||||
)));
|
||||
|
||||
test!(error_vector_missing_close_bracket: "[", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnclosedSequence,
|
||||
Span::new(0..1, src),
|
||||
)));
|
||||
|
||||
test!(error_vector_unexpected_close_bracket: "]", src => Err(ParserError::new(
|
||||
ParserErrorKind::UnexpectedClosingDelimiter,
|
||||
Span::new(0..1, src)
|
||||
)));
|
||||
|
||||
test!(multiple_expressions: "(/ 6 3 (+ 1 2)) (* 2 5)\n(- 10 5)", src => Ok(Ast::from(vec![
|
||||
Node::new(
|
||||
Expr::List(vec![
|
||||
Node::new(Atom::Symbol(Symbol::from("/")).into(), Span::new(1..2, src.clone())),
|
||||
Node::new(Atom::Integer(6).into(), Span::new(3..4, src.clone())),
|
||||
Node::new(Atom::Integer(3).into(), Span::new(5..6, src.clone())),
|
||||
Node::new(
|
||||
Expr::List(vec![
|
||||
Node::new(Atom::Symbol(Symbol::from("+")).into(), Span::new(8..9, src.clone())),
|
||||
Node::new(Atom::Integer(1).into(), Span::new(10..11, src.clone())),
|
||||
Node::new(Atom::Integer(2).into(), Span::new(12..13, src.clone())),
|
||||
]),
|
||||
Span::new(7..14, src.clone())
|
||||
),
|
||||
]),
|
||||
Span::new(0..15, src.clone())
|
||||
),
|
||||
Node::new(
|
||||
Expr::List(vec![
|
||||
Node::new(Atom::Symbol(Symbol::from("*")).into(), Span::new(17..18, src.clone())),
|
||||
Node::new(Atom::Integer(2).into(), Span::new(19..20, src.clone())),
|
||||
Node::new(Atom::Integer(5).into(), Span::new(21..22, src.clone())),
|
||||
]),
|
||||
Span::new(16..23, src.clone())
|
||||
),
|
||||
Node::new(
|
||||
Expr::List(vec![
|
||||
Node::new(Atom::Symbol(Symbol::from("-")).into(), Span::new(25..26, src.clone())),
|
||||
Node::new(Atom::Integer(10).into(), Span::new(27..29, src.clone())),
|
||||
Node::new(Atom::Integer(5).into(), Span::new(30..31, src.clone())),
|
||||
]),
|
||||
Span::new(24..32, src)
|
||||
),
|
||||
])));
|
||||
|
||||
test!(function_application: "(join \"foo\" \"bar\")", src => Ok(Ast::from(vec![Node::new(
|
||||
Expr::List(vec![
|
||||
Node::new(Atom::Symbol(Symbol::from("join")).into(), Span::new(1..5, src.clone())),
|
||||
Node::new(Atom::String("foo".into()).into(), Span::new(6..11, src.clone())),
|
||||
Node::new(Atom::String("bar".into()).into(), Span::new(12..17, src.clone())),
|
||||
]),
|
||||
Span::new(0..18, src)
|
||||
)])));
|
||||
}
|
@ -1,156 +1,53 @@
|
||||
use std::{cmp::Ordering, iter, ops::Range, sync::Arc};
|
||||
|
||||
/// A location within some source text.
|
||||
/// A (half-open) range bounded inclusively below and exclusively above
|
||||
/// `(start..end)`.
|
||||
///
|
||||
/// The range `start..end` contains all values with `start <= x < end`. It is
|
||||
/// empty if `start >= end`.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Location {
|
||||
line: usize,
|
||||
column: usize,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
/// Construct a new instance of `Location`.
|
||||
#[must_use]
|
||||
pub(crate) const fn new(line: usize, column: usize) -> Self {
|
||||
Self { line, column }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Location {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match self.line.partial_cmp(&other.line) {
|
||||
Some(Ordering::Equal) => self.column.partial_cmp(&other.column),
|
||||
ord => ord,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Some (optionally named) source text.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Source {
|
||||
name: Option<String>,
|
||||
contents: String,
|
||||
lines: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
/// Construct a new instance of `Source`.
|
||||
#[must_use]
|
||||
pub(crate) fn new(name: Option<String>, contents: String) -> Self {
|
||||
let lines = contents
|
||||
.match_indices('\n')
|
||||
.map(|(i, _)| i)
|
||||
.chain(iter::once(contents.len()))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
name,
|
||||
contents,
|
||||
lines,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of the source.
|
||||
#[must_use]
|
||||
pub(crate) fn name(&self) -> Option<&str> {
|
||||
self.name.as_deref()
|
||||
}
|
||||
|
||||
/// Set the name of the source.
|
||||
pub(crate) fn set_name(&mut self, name: String) {
|
||||
self.name = Some(name);
|
||||
}
|
||||
|
||||
/// Get the [Location] of the specified byte in the source.
|
||||
#[must_use]
|
||||
pub(crate) fn location(&self, byte: usize) -> Location {
|
||||
let line = self.lines.partition_point(|&x| x < byte);
|
||||
let start = line.checked_sub(1).map_or(0, |n| self.lines[n] + 1);
|
||||
let column = self.contents[start..byte].chars().count();
|
||||
|
||||
Location::new(line, column)
|
||||
}
|
||||
|
||||
/// Get the full contents of the source.
|
||||
#[must_use]
|
||||
pub(crate) fn contents(&self) -> &str {
|
||||
&self.contents
|
||||
}
|
||||
|
||||
/// Get the specified line from the source.
|
||||
#[must_use]
|
||||
pub(crate) fn get_line(&self, line: usize) -> &str {
|
||||
let end = self.lines[line];
|
||||
let start = line.checked_sub(1).map_or(0, |n| self.lines[n] + 1);
|
||||
|
||||
&self.contents[start..end]
|
||||
}
|
||||
}
|
||||
|
||||
/// A contiguous sequence of bytes within some source.
|
||||
#[derive(Debug, Default, Clone, Eq)]
|
||||
pub struct Span {
|
||||
bytes: Range<usize>,
|
||||
source: Arc<Source>,
|
||||
/// The lower bound of the range (inclusive).
|
||||
pub start: usize,
|
||||
/// The upper bound of the range (exclusive).
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
/// Construct a new instance of `Span`.
|
||||
/// Construct a new instance of a span.
|
||||
#[must_use]
|
||||
pub(crate) fn new(bytes: Range<usize>, source: Arc<Source>) -> Self {
|
||||
Self { bytes, source }
|
||||
pub const fn new(start: usize, end: usize) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
/// Join two spans, creating a new span.
|
||||
/// Returns `true` if `item` is contained in the span.
|
||||
#[must_use]
|
||||
pub(crate) fn join(self, other: &Self) -> Self {
|
||||
debug_assert!(self.same_source(other));
|
||||
|
||||
Self::new(self.bytes.start..other.bytes.end, self.source)
|
||||
pub fn contains(&self, item: usize) -> bool {
|
||||
self.start <= item && item < self.end
|
||||
}
|
||||
|
||||
/// Extend one span to include another.
|
||||
pub(crate) fn extend(&mut self, other: &Self) {
|
||||
debug_assert!(self.same_source(other));
|
||||
self.bytes.end = other.bytes.end;
|
||||
/// Returns `true` if the span contains no items.
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.start >= self.end
|
||||
}
|
||||
|
||||
/// The start location of a span within some source.
|
||||
#[must_use]
|
||||
pub(crate) fn location(&self) -> Location {
|
||||
self.source.location(self.bytes.start)
|
||||
}
|
||||
|
||||
/// The end location of a span within some source.
|
||||
#[must_use]
|
||||
pub(crate) fn end_location(&self) -> Location {
|
||||
self.source.location(self.bytes.end)
|
||||
}
|
||||
|
||||
/// Do two spans share the same source?
|
||||
#[must_use]
|
||||
pub(crate) fn same_source(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.source, &other.source)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn bytes(&self) -> &Range<usize> {
|
||||
&self.bytes
|
||||
/// Extend the span's end bound to that of the provided span, if
|
||||
/// `other.end > self.end`.
|
||||
pub fn extend(&mut self, other: &Self) {
|
||||
if other.end > self.end {
|
||||
self.end = other.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Span {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.same_source(other) && self.bytes == other.bytes
|
||||
impl From<std::ops::Range<usize>> for Span {
|
||||
fn from(range: std::ops::Range<usize>) -> Self {
|
||||
Self::new(range.start, range.end)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Span {
|
||||
fn hash<H>(&self, state: &mut H)
|
||||
where
|
||||
H: std::hash::Hasher,
|
||||
{
|
||||
self.bytes.hash(state);
|
||||
self.source.hash(state);
|
||||
impl From<Span> for std::ops::Range<usize> {
|
||||
fn from(span: Span) -> Self {
|
||||
span.start..span.end
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,71 +56,35 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn location_partial_ord() {
|
||||
assert!(Location::new(1, 1) < Location::new(1, 2));
|
||||
assert!(Location::new(1, 10) < Location::new(2, 1));
|
||||
assert!(Location::new(5, 5) == Location::new(5, 5));
|
||||
assert!(Location::new(10, 1) > Location::new(9, 99));
|
||||
}
|
||||
fn span_equality() {
|
||||
let a = Span::new(0, 0);
|
||||
let b = Span::new(0, 1);
|
||||
|
||||
#[test]
|
||||
fn source_get_set_name() {
|
||||
let mut src = Source::new(None, "".into());
|
||||
assert!(src.name().is_none());
|
||||
|
||||
src.set_name("foo".into());
|
||||
assert!(src.name() == Some("foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_location() {
|
||||
let source = Source::new(None, "foo\nbar\nbaz".into());
|
||||
assert_eq!(source.location(0), Location::new(0, 0));
|
||||
assert_eq!(source.location(5), Location::new(1, 1));
|
||||
assert_eq!(source.location(10), Location::new(2, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_contents() {
|
||||
let contents = String::from("xxx");
|
||||
let source = Source::new(None, contents.clone());
|
||||
assert_eq!(source.contents(), &contents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_get_line() {
|
||||
let source = Source::new(None, "line 1\nline 2\nline 3\n".into());
|
||||
assert_eq!(source.get_line(0), "line 1");
|
||||
assert_eq!(source.get_line(1), "line 2");
|
||||
assert_eq!(source.get_line(2), "line 3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_partial_eq() {
|
||||
let source = Arc::new(Source::new(None, String::new()));
|
||||
|
||||
let a = Span::new(0..0, source.clone());
|
||||
assert_eq!(a, a);
|
||||
|
||||
let b = Span::new(1..10, source.clone());
|
||||
assert_ne!(a, b);
|
||||
|
||||
let source2 = Arc::new(Source::new(None, String::from("foo")));
|
||||
let c = Span::new(0..0, source2.clone());
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_start_end_location() {
|
||||
let source = Arc::new(Source::new(None, "foo\nbar\nbaz".into()));
|
||||
let span = Span::new(2..9, source);
|
||||
fn span_contains() {
|
||||
let s = Span::new(1, 3);
|
||||
|
||||
let start = span.location();
|
||||
assert_eq!(start.line, 0);
|
||||
assert_eq!(start.column, 2);
|
||||
assert!(s.contains(1));
|
||||
assert!(s.contains(2));
|
||||
|
||||
let end = span.end_location();
|
||||
assert_eq!(end.line, 2);
|
||||
assert_eq!(end.column, 1);
|
||||
assert!(!s.contains(0));
|
||||
assert!(!s.contains(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_extend() {
|
||||
let mut a = Span::new(0, 5);
|
||||
let b = Span::new(1, 10);
|
||||
let c = Span::new(5, 6);
|
||||
|
||||
assert_eq!(a.end, 5);
|
||||
a.extend(&b);
|
||||
assert_eq!(a.end, 10);
|
||||
a.extend(&c);
|
||||
assert_eq!(a.end, 10);
|
||||
}
|
||||
}
|
||||
|
16
rustfmt.toml
Normal file
16
rustfmt.toml
Normal file
@ -0,0 +1,16 @@
|
||||
# Edition
|
||||
edition = "2021"
|
||||
|
||||
# Comments
|
||||
format_code_in_doc_comments = true
|
||||
normalize_comments = true
|
||||
wrap_comments = true
|
||||
|
||||
# Imports
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
imports_layout = "HorizontalVertical"
|
||||
|
||||
# Miscellaneous
|
||||
enum_discrim_align_threshold = 25
|
||||
hex_literal_case = "Upper"
|
5
taplo.toml
Normal file
5
taplo.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[formatting]
|
||||
align_entries = true
|
||||
allowed_blank_lines = 1
|
||||
column_width = 100
|
||||
reorder_arrays = true
|
@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "fuga"
|
||||
version = "0.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "fuga"
|
||||
path = "src/bin/fuga.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.21", features = ["derive", "wrap_help"] }
|
||||
log = { version = "0.4.22", features = ["std"] }
|
@ -1,30 +0,0 @@
|
||||
use clap::{
|
||||
builder::{styling::Style, Styles},
|
||||
Parser,
|
||||
Subcommand,
|
||||
};
|
||||
use fuga::{color, command, AppResult};
|
||||
|
||||
const HEADER_STYLE: Style = Style::new().fg_color(Some(color::RED)).bold().underline();
|
||||
const LITERAL_STYLE: Style = Style::new().fg_color(Some(color::PURPLE)).bold();
|
||||
|
||||
const STYLES: Styles = Styles::styled()
|
||||
.usage(HEADER_STYLE)
|
||||
.header(HEADER_STYLE)
|
||||
.literal(LITERAL_STYLE);
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(styles = STYLES, version)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {}
|
||||
|
||||
fn main() -> AppResult<()> {
|
||||
fuga::logger::init()?;
|
||||
|
||||
match Cli::parse().command {}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
pub use clap::builder::styling::Reset;
|
||||
use clap::builder::styling::{Color, RgbColor};
|
||||
|
||||
pub const RED: Color = Color::Rgb(RgbColor(225, 55, 55)); // Red
|
||||
pub const ORANGE: Color = Color::Rgb(RgbColor(215, 140, 100)); // Orange
|
||||
pub const WHITE: Color = Color::Rgb(RgbColor(255, 255, 255)); // White
|
||||
pub const BLUE: Color = Color::Rgb(RgbColor(60, 140, 185)); // Blue
|
||||
pub const PURPLE: Color = Color::Rgb(RgbColor(180, 130, 215)); // Purple
|
||||
|
||||
pub trait EscapeSequence {
|
||||
fn to_escape_sequence(&self) -> String;
|
||||
}
|
||||
|
||||
impl EscapeSequence for Color {
|
||||
fn to_escape_sequence(&self) -> String {
|
||||
match self {
|
||||
Color::Rgb(RgbColor(r, g, b)) => format!("\x1b[1;38;2;{r};{g};{b}m"),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
#![deny(rust_2018_idioms, unsafe_code)]
|
||||
|
||||
pub mod color;
|
||||
pub mod logger;
|
||||
|
||||
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
@ -1,47 +0,0 @@
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use crate::color::{self, EscapeSequence as _};
|
||||
|
||||
struct FugaLogger {
|
||||
level: log::LevelFilter,
|
||||
}
|
||||
|
||||
impl log::Log for FugaLogger {
|
||||
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
|
||||
metadata.level() <= self.level
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record<'_>) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let style = match record.level() {
|
||||
log::Level::Error => color::RED.to_escape_sequence(),
|
||||
log::Level::Warn => color::ORANGE.to_escape_sequence(),
|
||||
log::Level::Info => color::WHITE.to_escape_sequence(),
|
||||
log::Level::Debug => color::BLUE.to_escape_sequence(),
|
||||
log::Level::Trace => color::PURPLE.to_escape_sequence(),
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"{style}{: <5}{} {}",
|
||||
record.level(),
|
||||
color::Reset.render(),
|
||||
record.args()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
pub fn init() -> Result<(), log::SetLoggerError> {
|
||||
let level = if let Some(level) = std::option_env!("FUGA_LOG") {
|
||||
log::LevelFilter::from_str(level).unwrap_or(log::LevelFilter::Off)
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
};
|
||||
|
||||
let logger = FugaLogger { level };
|
||||
log::set_boxed_logger(Box::new(logger)).map(|()| log::set_max_level(level))?;
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user