add sexp into source tree
This commit is contained in:
Generated
-2
@@ -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"
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ok
|
||||
@@ -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.
|
||||
|
||||
[](https://crates.io/crates/sexp/)
|
||||
|
||||
[](https://travis-ci.org/cgaebel/sexp)
|
||||
|
||||
[](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());
|
||||
}
|
||||
Reference in New Issue
Block a user