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
|
# 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
|
|
||||||
|
@ -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"
|
|
||||||
|
2
LICENSE
2
LICENSE
@ -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:
|
||||||
|
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
|
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
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.
|
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
@ -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;
|
||||||
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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 (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) {
|
#[must_use]
|
||||||
debug_assert!(self.same_source(other));
|
pub fn is_empty(&self) -> bool {
|
||||||
self.bytes.end = other.bytes.end;
|
self.start >= self.end
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The start 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 location(&self) -> Location {
|
pub fn extend(&mut self, other: &Self) {
|
||||||
self.source.location(self.bytes.start)
|
if other.end > self.end {
|
||||||
}
|
self.end = other.end;
|
||||||
|
}
|
||||||
/// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
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