diff --git a/Cargo.lock b/Cargo.lock index f18d411..8f09537 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -879,8 +879,6 @@ dependencies = [ [[package]] name = "sexp" version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8fa7ac9df84000b0238cf497cb2d3056bac2ff2a7d8cf179d2803b4b58571f" [[package]] name = "sha-1" diff --git a/crsn/Cargo.toml b/crsn/Cargo.toml index 369e7aa..15c3d4e 100644 --- a/crsn/Cargo.toml +++ b/crsn/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -sexp = "1.1.4" +sexp = { path = "../lib/spanned_sexp" } thiserror = "1.0.20" anyhow = "1.0.32" dyn-clonable = "0.9.0" diff --git a/lib/spanned_sexp/.cargo-ok b/lib/spanned_sexp/.cargo-ok new file mode 100644 index 0000000..b5754e2 --- /dev/null +++ b/lib/spanned_sexp/.cargo-ok @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/lib/spanned_sexp/.editorconfig b/lib/spanned_sexp/.editorconfig new file mode 100644 index 0000000..fa9becf --- /dev/null +++ b/lib/spanned_sexp/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 +tab_width = 4 +trim_trailing_whitespace = true diff --git a/lib/spanned_sexp/.gitignore b/lib/spanned_sexp/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/lib/spanned_sexp/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/lib/spanned_sexp/.travis.yml b/lib/spanned_sexp/.travis.yml new file mode 100644 index 0000000..a4430b7 --- /dev/null +++ b/lib/spanned_sexp/.travis.yml @@ -0,0 +1,36 @@ +language: rust +sudo: false +# necessary for `travis-cargo coveralls --no-sudo` +addons: + apt: + packages: + - libcurl4-openssl-dev + - libelf-dev + - libdw-dev +rust: +- nightly +os: +- linux + +env: + global: + - TRAVIS_CARGO_NIGHTLY_FEATURE="" + - secure: C2P1wLHzBxccS3jrimsG2TaDy4sAhYiKSq1g+cwYHhAKZKkiIpL7Ez5iEHH6BbEvvg4HiUJy4j0w83luZ/FXUuxkD2GZsXWoG+20DFBTLQvCJE/LPahVNbb5i+NdmyIsZPHLloXNvT63hXwu8KNV4U0hrYAgViIXkumoLnOiQD/jim81i7gxUOSe65AzMHcfPRaAwKHn+NGIvUfwMzU2hKZbnH/BPIi2PNtQ6e0VZEvAqA5Ad3hRV0YaBKZ3HZn8tr8UnHKmLbPffb/01EVWAFBU+rFMVYrdzDsiVp7UHMPtVV9aNXUVszB+a/ASWHsAZEdX8XsbmH9RSEBCzsUq2j2HFM2R7yYZnkL3FPcpf/ZKgy4ZVw6gKO42DCvBRGwhI1JMjeKBmrzCGZHE70FxD0zAZRwX9n9M7mUKhakzMvs/LSKMQKlOJslSR+OLEUpr3MCBthpKIiajNYDrJL5P/3KrFOF2R4H/2Z91/3osEIRqzYiEKdeJU01Yef5FCI+H6SLvbhIlVAQTM0IJKGAP0B2N6J4Ot7XrYuGDQag48oPzWzJ2dOGwYjwkda1rgW7pdjtWuullOi2ob1zdI6y/i/CdAS8AE0yRz7VCK4grwonUICzdVaaIAaTTd0yq9PRWAjSjZqNG5EOLADzABIihPnkBw4WygoDq18rSkk0pRbE= + +before_script: + - pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH + +script: +- travis-cargo build +- travis-cargo test +- travis-cargo bench +- travis-cargo doc +after_success: +- | + [ $TRAVIS_BRANCH = master ] && + [ $TRAVIS_PULL_REQUEST = false ] && + echo '' > target/doc/index.html && + git clone --depth 1 https://github.com/davisp/ghp-import && + ./ghp-import/ghp-import -n target/doc && + git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages +- travis-cargo coveralls --no-sudo diff --git a/lib/spanned_sexp/Cargo.toml b/lib/spanned_sexp/Cargo.toml new file mode 100644 index 0000000..26c6fc8 --- /dev/null +++ b/lib/spanned_sexp/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sexp" +version = "1.1.4" +authors = ["Clark Gaebel "] + +documentation = "https://cgaebel.github.io/sexp" +homepage = "https://github.com/cgaebel/sexp" +repository = "https://github.com/cgaebel/sexp" + +readme = "README.md" + +keywords = [ "sexp", "parsing", "s-expression", "file-format" ] + +description = "A small, simple, self-contained, s-expression parser and pretty-printer." + +license = "MIT" diff --git a/lib/spanned_sexp/LICENSE b/lib/spanned_sexp/LICENSE new file mode 100644 index 0000000..683f7fa --- /dev/null +++ b/lib/spanned_sexp/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Clark Gaebel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/spanned_sexp/README.md b/lib/spanned_sexp/README.md new file mode 100644 index 0000000..6da62fb --- /dev/null +++ b/lib/spanned_sexp/README.md @@ -0,0 +1,15 @@ +Sexp +===== + +A small, simple, self-contained, s-expression parser and pretty-printer. + +[![crates.io](https://img.shields.io/crates/v/sexp.svg)](https://crates.io/crates/sexp/) + +[![Build Status](https://travis-ci.org/cgaebel/sexp.svg?branch=master)](https://travis-ci.org/cgaebel/sexp) + +[![Coverage Status](https://coveralls.io/repos/cgaebel/sexp/badge.svg?branch=master&service=github)](https://coveralls.io/github/cgaebel/sexp?branch=master) + +Documentation +------------- + +See the [API Docs](https://cgaebel.github.io/sexp/). diff --git a/lib/spanned_sexp/src/lib.rs b/lib/spanned_sexp/src/lib.rs new file mode 100644 index 0000000..d514af7 --- /dev/null +++ b/lib/spanned_sexp/src/lib.rs @@ -0,0 +1,417 @@ +//! A lightweight, self-contained s-expression parser and data format. +//! Use `parse` to get an s-expression from its string representation, and the +//! `Display` trait to serialize it, potentially by doing `sexp.to_string()`. + +#![deny(missing_docs)] +#![deny(unsafe_code)] + +use std::borrow::Cow; +use std::cmp; +use std::error; +use std::fmt; +use std::str::{self, FromStr}; + +/// A single data element in an s-expression. Floats are excluded to ensure +/// atoms may be used as keys in ordered and hashed data structures. +/// +/// All strings must be valid utf-8. +#[derive(PartialEq, Clone, PartialOrd)] +#[allow(missing_docs)] +pub enum Atom { + S(String), + I(i64), + F(f64), +} + +/// An s-expression is either an atom or a list of s-expressions. This is +/// similar to the data format used by lisp. +#[derive(PartialEq, Clone, PartialOrd)] +#[allow(missing_docs)] +pub enum Sexp { + Atom(Atom), + List(Vec), +} + +#[test] +fn sexp_size() { + // I just want to see when this changes, in the diff. + use std::mem; + assert_eq!(mem::size_of::(), mem::size_of::()*5); +} + +/// The representation of an s-expression parse error. +pub struct Error { + /// The error message. + pub message: &'static str, + /// The line number on which the error occurred. + pub line: usize, + /// The column number on which the error occurred. + pub column: usize, + /// The index in the given string which caused the error. + pub index: usize, +} + +impl error::Error for Error { + fn description(&self) -> &str { self.message } + fn cause(&self) -> Option<&error::Error> { None } +} + +/// Since errors are the uncommon case, they're boxed. This keeps the size of +/// structs down, which helps performance in the common case. +/// +/// For example, an `ERes<()>` becomes 8 bytes, instead of the 24 bytes it would +/// be if `Err` were unboxed. +type Err = Box; + +/// Helps clean up type signatures, but shouldn't be exposed to the outside +/// world. +type ERes = Result; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}:{}: {}", self.line, self.column, self.message) + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self) + } +} + +#[test] +fn show_an_error() { + assert_eq!(format!("{:?}", parse("(aaaa").unwrap_err()), "1:4: unexpected eof"); +} + +fn get_line_and_column(s: &str, pos: usize) -> (usize, usize) { + let mut line: usize = 1; + let mut col: isize = -1; + for c in s.chars().take(pos+1) { + if c == '\n' { + line += 1; + col = -1; + } else { + col += 1; + } + } + (line, cmp::max(col, 0) as usize) +} + +#[test] +fn line_and_col_test() { + let s = "0123456789\n0123456789\n\n6"; + assert_eq!(get_line_and_column(s, 4), (1, 4)); + + assert_eq!(get_line_and_column(s, 10), (2, 0)); + assert_eq!(get_line_and_column(s, 11), (2, 0)); + assert_eq!(get_line_and_column(s, 15), (2, 4)); + + assert_eq!(get_line_and_column(s, 21), (3, 0)); + assert_eq!(get_line_and_column(s, 22), (4, 0)); + assert_eq!(get_line_and_column(s, 23), (4, 0)); + assert_eq!(get_line_and_column(s, 500), (4, 0)); +} + +#[cold] +fn err_impl(message: &'static str, s: &str, pos: &usize) -> Err { + let (line, column) = get_line_and_column(s, *pos); + Box::new(Error { + message: message, + line: line, + column: column, + index: *pos, + }) +} + +fn err(message: &'static str, s: &str, pos: &usize) -> ERes { + Err(err_impl(message, s, pos)) +} + +/// A helpful utility to trace the execution of a parser while testing. It will +/// be compiled out in release builds. +#[allow(unused_variables)] +fn dbg(msg: &str, pos: &usize) { + //println!("{} @ {}", msg, pos) +} + +fn atom_of_string(s: String) -> Atom { + match FromStr::from_str(&s) { + Ok(i) => return Atom::I(i), + Err(_) => {}, + }; + + match FromStr::from_str(&s) { + Ok(f) => return Atom::F(f), + Err(_) => {}, + }; + + Atom::S(s) +} + +// returns the char it found, and the new size if you wish to consume that char +fn peek(s: &str, pos: &usize) -> ERes<(char, usize)> { + dbg("peek", pos); + if *pos == s.len() { return err("unexpected eof", s, pos) } + if s.is_char_boundary(*pos) { + let ch = s[*pos..].chars().next().unwrap(); + let next = *pos + ch.len_utf8(); + Ok((ch, next)) + } else { + // strings must be composed of valid utf-8 chars. + unreachable!() + } +} + +fn expect(s: &str, pos: &mut usize, c: char) -> ERes<()> { + dbg("expect", pos); + let (ch, next) = try!(peek(s, pos)); + *pos = next; + if ch == c { Ok(()) } else { err("unexpected character", s, pos) } +} + +fn consume_until_newline(s: &str, pos: &mut usize) -> ERes<()> { + loop { + if *pos == s.len() { return Ok(()) } + let (ch, next) = try!(peek(s, pos)); + *pos = next; + if ch == '\n' { return Ok(()) } + } +} + +// zero or more spaces +fn zspace(s: &str, pos: &mut usize) -> ERes<()> { + dbg("zspace", pos); + loop { + if *pos == s.len() { return Ok(()) } + let (ch, next) = try!(peek(s, pos)); + + if ch == ';' { try!(consume_until_newline(s, pos)) } + else if ch.is_whitespace() { *pos = next; } + else { return Ok(()) } + } +} + +fn parse_quoted_atom(s: &str, pos: &mut usize) -> ERes { + dbg("parse_quoted_atom", pos); + let mut cs: String = String::new(); + + try!(expect(s, pos, '"')); + + loop { + let (ch, next) = try!(peek(s, pos)); + if ch == '"' { + *pos = next; + break; + } else if ch == '\\' { + let (postslash, nextnext) = try!(peek(s, &next)); + if postslash == '"' || postslash == '\\' { + cs.push(postslash); + } else { + cs.push(ch); + cs.push(postslash); + } + *pos = nextnext; + } else { + cs.push(ch); + *pos = next; + } + } + + // Do not try i64 conversion, since this atom was explicitly quoted. + Ok(Atom::S(cs)) +} + +fn parse_unquoted_atom(s: &str, pos: &mut usize) -> ERes { + dbg("parse_unquoted_atom", pos); + let mut cs: String = String::new(); + + loop { + if *pos == s.len() { break } + let (c, next) = try!(peek(s, pos)); + + if c == ';' { try!(consume_until_newline(s, pos)); break } + if c.is_whitespace() || c == '(' || c == ')' { break } + cs.push(c); + *pos = next; + } + + Ok(atom_of_string(cs)) +} + +fn parse_atom(s: &str, pos: &mut usize) -> ERes { + dbg("parse_atom", pos); + let (ch, _) = try!(peek(s, pos)); + + if ch == '"' { parse_quoted_atom (s, pos) } + else { parse_unquoted_atom(s, pos) } +} + +fn parse_list(s: &str, pos: &mut usize) -> ERes> { + dbg("parse_list", pos); + try!(zspace(s, pos)); + try!(expect(s, pos, '(')); + + let mut sexps: Vec = Vec::new(); + + loop { + try!(zspace(s, pos)); + let (c, next) = try!(peek(s, pos)); + if c == ')' { + *pos = next; + break; + } + sexps.push(try!(parse_sexp(s, pos))); + } + + try!(zspace(s, pos)); + + Ok(sexps) +} + +fn parse_sexp(s: &str, pos: &mut usize) -> ERes { + dbg("parse_sexp", pos); + try!(zspace(s, pos)); + let (c, _) = try!(peek(s, pos)); + let r = + if c == '(' { Ok(Sexp::List(try!(parse_list(s, pos)))) } + else { Ok(Sexp::Atom(try!(parse_atom(s, pos)))) }; + try!(zspace(s, pos)); + r +} + +/// Constructs an atomic s-expression from a string. +pub fn atom_s(s: &str) -> Sexp { + Sexp::Atom(Atom::S(s.to_owned())) +} + +/// Constructs an atomic s-expression from an int. +pub fn atom_i(i: i64) -> Sexp { + Sexp::Atom(Atom::I(i)) +} + +/// Constructs an atomic s-expression from a float. +pub fn atom_f(f: f64) -> Sexp { + Sexp::Atom(Atom::F(f)) +} + +/// Constructs a list s-expression given a slice of s-expressions. +pub fn list(xs: &[Sexp]) -> Sexp { + Sexp::List(xs.to_owned()) +} + +/// Reads an s-expression out of a `&str`. +#[inline(never)] +pub fn parse(s: &str) -> Result> { + let mut pos = 0; + let ret = try!(parse_sexp(s, &mut pos)); + if pos == s.len() { Ok(ret) } else { err("unrecognized post-s-expression data", s, &pos) } +} + +// TODO: Pretty print in lisp convention, instead of all on the same line, +// packed as tightly as possible. It's kinda ugly. + +fn is_num_string(s: &str) -> bool { + let x: Result = FromStr::from_str(&s); + let y: Result = FromStr::from_str(&s); + x.is_ok() || y.is_ok() +} + +fn string_contains_whitespace(s: &str) -> bool { + for c in s.chars() { + if c.is_whitespace() { return true } + } + false +} + +fn quote(s: &str) -> Cow { + if !s.contains("\"") + && !string_contains_whitespace(s) + && !is_num_string(s) { + Cow::Borrowed(s) + } else { + let mut r: String = "\"".to_string(); + r.push_str(&s.replace("\\", "\\\\").replace("\"", "\\\"")); + r.push_str("\""); + Cow::Owned(r) + } +} + +impl fmt::Display for Atom { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Atom::S(ref s) => write!(f, "{}", quote(s)), + Atom::I(i) => write!(f, "{}", i), + Atom::F(d) => write!(f, "{}", d), + } + } +} + +impl fmt::Display for Sexp { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Sexp::Atom(ref a) => write!(f, "{}", a), + Sexp::List(ref xs) => { + try!(write!(f, "(")); + for (i, x) in xs.iter().enumerate() { + let s = if i == 0 { "" } else { " " }; + try!(write!(f, "{}{}", s, x)); + } + write!(f, ")") + }, + } + } +} + +impl fmt::Debug for Atom { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self) + } +} + +impl fmt::Debug for Sexp { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self) + } +} + +#[test] +fn test_hello_world() { + assert_eq!( + parse("(hello -42\n\t -4.0 \"world\") ; comment").unwrap(), + list(&[ atom_s("hello"), atom_i(-42), atom_f(-4.0), atom_s("world") ])); +} + +#[test] +fn test_escaping() { + assert_eq!( + parse("(\"\\\"\\q\" \"1234\" 1234)").unwrap(), + list(&[ atom_s("\"\\q"), atom_s("1234"), atom_i(1234) ])); +} + +#[test] +fn test_pp() { + let s = "(hello world (what is (up) (4 6.4 you \"123\\\\ \\\"\")))"; + let sexp = parse(s).unwrap(); + assert_eq!(s, sexp.to_string()); + assert_eq!(s, format!("{:?}", sexp)); +} + +#[test] +fn test_tight_parens() { + let s = "(hello(world))"; + let sexp = parse(s).unwrap(); + assert_eq!(sexp, Sexp::List(vec![Sexp::Atom(Atom::S("hello".into())), + Sexp::List(vec![Sexp::Atom(Atom::S("world".into()))])])); + let s = "(this (has)tight(parens))"; + let s2 = "( this ( has ) tight ( parens ) )"; + assert_eq!(parse(s).unwrap(), parse(s2).unwrap()); +} + +#[test] +fn test_space_in_atom() { + let sexp = list(&[ atom_s("hello world")]); + let sexp_as_string = sexp.to_string(); + assert_eq!("(\"hello world\")", sexp_as_string); + assert_eq!(sexp, parse(&sexp_as_string).unwrap()); +}