add sexp into source tree

pull/21/head
Ondřej Hruška 4 years ago
parent f404445003
commit 85911c8e99
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      Cargo.lock
  2. 2
      crsn/Cargo.toml
  3. 1
      lib/spanned_sexp/.cargo-ok
  4. 10
      lib/spanned_sexp/.editorconfig
  5. 2
      lib/spanned_sexp/.gitignore
  6. 36
      lib/spanned_sexp/.travis.yml
  7. 16
      lib/spanned_sexp/Cargo.toml
  8. 19
      lib/spanned_sexp/LICENSE
  9. 15
      lib/spanned_sexp/README.md
  10. 417
      lib/spanned_sexp/src/lib.rs

2
Cargo.lock generated

@ -879,8 +879,6 @@ dependencies = [
[[package]] [[package]]
name = "sexp" name = "sexp"
version = "1.1.4" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8fa7ac9df84000b0238cf497cb2d3056bac2ff2a7d8cf179d2803b4b58571f"
[[package]] [[package]]
name = "sha-1" name = "sha-1"

@ -6,7 +6,7 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
sexp = "1.1.4" sexp = { path = "../lib/spanned_sexp" }
thiserror = "1.0.20" thiserror = "1.0.20"
anyhow = "1.0.32" anyhow = "1.0.32"
dyn-clonable = "0.9.0" dyn-clonable = "0.9.0"

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

@ -0,0 +1,2 @@
target
Cargo.lock

@ -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 '<meta http-equiv=refresh content=0;url=sexp/index.html>' > 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

@ -0,0 +1,16 @@
[package]
name = "sexp"
version = "1.1.4"
authors = ["Clark Gaebel <cg.wowus.cg@gmail.com>"]
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"

@ -0,0 +1,19 @@
Copyright (c) 2015 Clark Gaebel <cg.wowus.cg@gmail.com>
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.

@ -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/).

@ -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<Sexp>),
}
#[test]
fn sexp_size() {
// I just want to see when this changes, in the diff.
use std::mem;
assert_eq!(mem::size_of::<Sexp>(), mem::size_of::<isize>()*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<Error>;
/// Helps clean up type signatures, but shouldn't be exposed to the outside
/// world.
type ERes<T> = Result<T, Err>;
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<T>(message: &'static str, s: &str, pos: &usize) -> ERes<T> {
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<Atom> {
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<Atom> {
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<Atom> {
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<Vec<Sexp>> {
dbg("parse_list", pos);
try!(zspace(s, pos));
try!(expect(s, pos, '('));
let mut sexps: Vec<Sexp> = 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<Sexp> {
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<Sexp, Box<Error>> {
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<i64, _> = FromStr::from_str(&s);
let y: Result<f64, _> = 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<str> {
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());
}
Loading…
Cancel
Save