Compare commits

..

No commits in common. "e5fafd03ba2e37a84bb0ad308d5a25661acc146a" and "2d95a58ce76248f7efb44b7a719519724c30694f" have entirely different histories.

23 changed files with 658 additions and 1511 deletions

3
.gitignore vendored
View File

@ -10,7 +10,6 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
# Miscellaneous # Miscellaneous files
.DS_Store .DS_Store
tarpaulin-report.html tarpaulin-report.html
*.profraw

View File

@ -1,14 +1,10 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["onihime", "tools/*"] members = ["onihime"]
[workspace.package] [workspace.package]
authors = ["Jesse Braham <jesse@beta7.io>"] authors = ["Jesse Braham <jesse@hatebit.org>"]
edition = "2021" edition = "2021"
homepage = "https://onihime.org" homepage = "https://onihime.org"
repository = "https://hatebit.org/jesse/onihime" repository = "https://hatebit.org/jesse/onihime"
license = "BSD-3-Clause" license = "BSD-3-Clause"
[profile.release]
strip = true
lto = "fat"

View File

@ -1,6 +1,6 @@
BSD 3-Clause License 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 Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

View File

@ -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

View File

@ -7,8 +7,8 @@ homepage.workspace = true
repository.workspace = true repository.workspace = true
license.workspace = true license.workspace = true
[dependencies] [dev-dependencies]
unicode-segmentation = "1.12.0" proptest = "1.6.0"
[lints.rust] [lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }

1
onihime/LICENSE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE

1
onihime/README.md Normal file
View File

@ -0,0 +1 @@
# onihime

View File

@ -1,56 +1,79 @@
use crate::span::Span; use std::fmt;
/// Kinds of errors that may occur during lexical analysis. use crate::Span;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// Kinds of errors which can occur during lexical analysis.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LexerErrorKind { pub enum LexerErrorKind {
/// An invalid escape sequence was encountered. /// An invalid character literal was encountered.
InvalidEscape(String), InvalidChar,
/// An invalid numeric literal was encountered. /// An invalid keyword was encountered.
InvalidNumber(String), InvalidKeyword,
/// An invalid string literal was encountered. /// An invalid number literal was encountered.
InvalidString, InvalidNumber,
/// An unclosed character literal was encountered. /// An invalid symbol was encountered.
UnclosedChar, InvalidSymbol,
/// And unclosed string literal was encountered. /// An unclosed string literal was encountered.
UnclosedString, UnclosedString,
} }
/// An error which occurred during lexical analysis. #[cfg(not(tarpaulin_include))]
/// impl fmt::Display for LexerErrorKind {
/// `LexerError`s contain the kind of error which occurred, as well as a [Span] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// specifying the [Source] and [Location] of the error. use LexerErrorKind::*;
///
/// [Source]: crate::span::Source match self {
/// [Location]: crate::span::Location InvalidChar => write!(f, "Invalid character literal"),
#[derive(Debug, Clone, PartialEq, Hash)] 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 { pub struct LexerError {
/// The kind of error encountered. /// The kind of lexer error.
pub kind: LexerErrorKind, pub kind: LexerErrorKind,
/// The span in which the error occurred. /// The span of the lexer error.
pub span: Span, pub span: Span,
/// Additional context regarding the lexer error.
pub context: Option<String>,
} }
impl LexerError { impl LexerError {
/// Construct a new instance of `LexerError`. /// Construct a new instance of a lexer error.
#[must_use] #[must_use]
pub const fn new(kind: LexerErrorKind, span: Span) -> Self { 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 {} impl std::error::Error for LexerError {}
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
impl std::fmt::Display for LexerError { impl fmt::Display for LexerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use LexerErrorKind::*; if let Some(ref context) = self.context {
write!(f, "{}: {}", self.kind, context)
match &self.kind { } else {
InvalidEscape(c) => write!(f, "Invalid escape sequence '\\{c}'"), write!(f, "{}", self.kind)
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"),
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -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)
}
}

View File

@ -1,13 +1,12 @@
use super::Symbol; use crate::Span;
use crate::span::Span;
/// Possible kinds of a [Token]. /// Kinds of tokens which are valid in Onihime source code.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum TokenKind { pub enum TokenKind {
/// Block comment, e.g. `#| ... |#`
BlockComment(String),
/// Line comment, e.g. `; ...` /// Line comment, e.g. `; ...`
LineComment(String), Comment,
/// Whitespace, e.g. ' ', '\t', '\n'
Whitespace,
/// Opening parenthesis, e.g. `(` /// Opening parenthesis, e.g. `(`
OpenParen, OpenParen,
@ -21,46 +20,52 @@ pub(crate) enum TokenKind {
OpenBracket, OpenBracket,
/// Closing bracket, e.g. `]` /// Closing bracket, e.g. `]`
CloseBracket, CloseBracket,
/// Opening hash-brace, e.g. `#{`
OpenHashBrace,
/// Boolean, e.g. `true`, `false` /// Boolean, e.g. `true`, `false`
Bool(bool), Bool,
/// Character, e.g. `'c'`, `'\n'` /// Character, e.g. `\a`, `\x1e`, `\u03BB`, `\newline`
Char(String), Char,
/// Floating-point number, e.g. `-1.0`, `2.0`, `+0.003` /// Keyword, e.g. `:foo-bar`, `:baz`, `:qux0`
Float(f64), Keyword,
/// Integer, e.g. `0`, `-1`, `+200` /// Floating-point number, e.g. `-1.0`, `2.0`, `3.0e-4`
Integer(i64), Decimal,
/// Keyword, e.g. `:baz` /// Integer, e.g. `0`, `-1`, `0b1010`, `0o7`, `0xDECAFBAD`
Keyword(Symbol), Integer,
/// Ratio, e.g. `1/3`, `-5/7`
Ratio,
/// String, e.g. `"foo bar"` /// String, e.g. `"foo bar"`
String(String), String,
/// Symbol, e.g. `qux`, `+` /// Symbol, e.g. `baz`, `*qux*`, `nil?`, `+`
Symbol(Symbol), Symbol,
/// Nil, e.g. `nil` /// Nil, e.g. `nil`
Nil, Nil,
} }
/// A token encountered during lexical analysis. impl TokenKind {
/// /// Returns `true` if the token type an atom.
/// `Token`s contain the kind of token which was found, as well as a [Span] pub fn is_atom(&self) -> bool {
/// specifying the [Source] and [Location] of the token. use TokenKind::*;
///
/// [Source]: crate::span::Source matches!(
/// [Location]: crate::span::Location self,
#[derive(Debug, Clone, PartialEq)] Bool | Char | Keyword | Decimal | Integer | Ratio | String | Symbol | Nil
pub(crate) struct Token { )
/// The kind of token. }
pub(crate) kind: TokenKind, }
/// The span in which the token occurs.
pub(crate) span: Span, /// 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 { impl Token {
/// Construct a new instance of `Token`. /// Construct a new instance of a token.
#[must_use] #[must_use]
pub(crate) const fn new(kind: TokenKind, span: Span) -> Self { pub const fn new(kind: TokenKind, span: Span) -> Self {
Self { kind, span } Self { kind, span }
} }
} }

View File

@ -1,4 +1,4 @@
//! Onihime programming language. //! Onihime programming language
#![deny( #![deny(
missing_debug_implementations, missing_debug_implementations,
@ -7,6 +7,8 @@
unsafe_code unsafe_code
)] )]
mod lexer; pub use self::span::Span;
mod parser;
pub mod lexer;
mod span; mod span;

View File

@ -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"),
}
}
}

View File

@ -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"),
}
}
}

View File

@ -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)
)])));
}

View File

@ -1,156 +1,53 @@
use std::{cmp::Ordering, iter, ops::Range, sync::Arc}; /// A (half-open) range bounded inclusively below and exclusively above
/// `(start..end)`.
/// A location within some source text. ///
/// 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)] #[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 { pub struct Span {
bytes: Range<usize>, /// The lower bound of the range (inclusive).
source: Arc<Source>, pub start: usize,
/// The upper bound of the range (exclusive).
pub end: usize,
} }
impl Span { impl Span {
/// Construct a new instance of `Span`. /// Construct a new instance of a span.
#[must_use] #[must_use]
pub(crate) fn new(bytes: Range<usize>, source: Arc<Source>) -> Self { pub const fn new(start: usize, end: usize) -> Self {
Self { bytes, source } Self { start, end }
} }
/// Join two spans, creating a new span. /// Returns `true` if `item` is contained in the span.
#[must_use] #[must_use]
pub(crate) fn join(self, other: &Self) -> Self { pub fn contains(&self, item: usize) -> bool {
debug_assert!(self.same_source(other)); self.start <= item && item < self.end
Self::new(self.bytes.start..other.bytes.end, self.source)
} }
/// Extend one span to include another. /// Returns `true` if the span contains no items.
pub(crate) fn extend(&mut self, other: &Self) {
debug_assert!(self.same_source(other));
self.bytes.end = other.bytes.end;
}
/// The start location of a span within some source.
#[must_use] #[must_use]
pub(crate) fn location(&self) -> Location { pub fn is_empty(&self) -> bool {
self.source.location(self.bytes.start) self.start >= self.end
} }
/// The end location of a span within some source. /// Extend the span's end bound to that of the provided span, if
#[must_use] /// `other.end > self.end`.
pub(crate) fn end_location(&self) -> Location { pub fn extend(&mut self, other: &Self) {
self.source.location(self.bytes.end) if other.end > self.end {
self.end = other.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
} }
} }
impl PartialEq for Span { impl From<std::ops::Range<usize>> for Span {
fn eq(&self, other: &Self) -> bool { fn from(range: std::ops::Range<usize>) -> Self {
self.same_source(other) && self.bytes == other.bytes Self::new(range.start, range.end)
} }
} }
impl std::hash::Hash for Span { impl From<Span> for std::ops::Range<usize> {
fn hash<H>(&self, state: &mut H) fn from(span: Span) -> Self {
where span.start..span.end
H: std::hash::Hasher,
{
self.bytes.hash(state);
self.source.hash(state);
} }
} }
@ -159,71 +56,35 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn location_partial_ord() { fn span_equality() {
assert!(Location::new(1, 1) < Location::new(1, 2)); let a = Span::new(0, 0);
assert!(Location::new(1, 10) < Location::new(2, 1)); let b = Span::new(0, 1);
assert!(Location::new(5, 5) == Location::new(5, 5));
assert!(Location::new(10, 1) > Location::new(9, 99));
}
#[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); assert_eq!(a, a);
let b = Span::new(1..10, source.clone());
assert_ne!(a, b); 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] #[test]
fn span_start_end_location() { fn span_contains() {
let source = Arc::new(Source::new(None, "foo\nbar\nbaz".into())); let s = Span::new(1, 3);
let span = Span::new(2..9, source);
let start = span.location(); assert!(s.contains(1));
assert_eq!(start.line, 0); assert!(s.contains(2));
assert_eq!(start.column, 2);
let end = span.end_location(); assert!(!s.contains(0));
assert_eq!(end.line, 2); assert!(!s.contains(3));
assert_eq!(end.column, 1); }
#[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
View 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
View File

@ -0,0 +1,5 @@
[formatting]
align_entries = true
allowed_blank_lines = 1
column_width = 100
reorder_arrays = true

View File

@ -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"] }

View File

@ -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 {}
}

View File

@ -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!(),
}
}
}

View File

@ -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>>;

View File

@ -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(())
}