forked from MightyPork/crsn
add better string and number parsing to sexp. cleaning, more tests
This commit is contained in:
@@ -374,7 +374,7 @@ Many instructions have two forms:
|
|||||||
(asl DST B)
|
(asl DST B)
|
||||||
|
|
||||||
; Delete an object by its handle. Objects are used by some extensions.
|
; Delete an object by its handle. Objects are used by some extensions.
|
||||||
(drop @REG)
|
(del @REG)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stacks Module
|
## Stacks Module
|
||||||
@@ -398,7 +398,7 @@ This module defines data stacks. Stacks can be shared by routines by passing a h
|
|||||||
(rpop DST @REG)
|
(rpop DST @REG)
|
||||||
```
|
```
|
||||||
|
|
||||||
To delete a stack, drop its handle - `(drop @REG)`
|
To delete a stack, use the `del` instruction - `(del @REG)`
|
||||||
|
|
||||||
## Screen module
|
## Screen module
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ edition = "2018"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sexp = { path = "../lib/spanned_sexp" }
|
sexp = { path = "crsn-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"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
Copyright (c) 2015 Clark Gaebel <cg.wowus.cg@gmail.com>
|
Copyright (c) 2015 Clark Gaebel <cg.wowus.cg@gmail.com>
|
||||||
|
Copyright (c) 2020 Ondřej Hruška <ondra@ondrovo.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
CRSN Sexp
|
||||||
|
=========
|
||||||
|
|
||||||
|
This is an updated and extended version of the "sexp" crate by Clark Gaebel: [https://github.com/cgaebel/sexp](https://github.com/cgaebel/sexp).
|
||||||
|
|
||||||
|
## Changes from "cgaebel/sexp"
|
||||||
|
|
||||||
|
- Updated to the 2018 Rust edition (that is, removed `try!()` and such)
|
||||||
|
- All parsed atoms now track their source location. This enables better error reporting in subsequent parsing and processing.
|
||||||
|
- Quoted strings now support C-style escapes other than `\"`, such as `\n`, `\t` and `\\`.
|
||||||
|
- Unrecognized escapes result in the slash being removed and the next character being taken literally. Use `\\` to enter a backslash.
|
||||||
|
- Added special parsing of "character literals" that use single quotes (`'A'`) and may contain backslash escapes (`'\n'`).
|
||||||
|
- This does not interfere with "apostrophe tokens" like `'foo`, that becomes an unquoted string atom.
|
||||||
|
- Added "quoted string", "unsigned" and "character" atom types
|
||||||
|
- Added parsing for `0x123`, `#ff00ff` and `0b123456`
|
||||||
|
- Numeric literals may now contain underscores to separate digit groups
|
||||||
|
- Numbers are preferably parsed as the unsigned atom (u64). The signed atom (i64) is only used for negative numbers.
|
||||||
|
|
||||||
|
.
|
||||||
@@ -63,23 +63,23 @@ pub(crate) fn get_line_and_column(s: &str, pos: usize) -> SourcePosition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cold]
|
#[cold]
|
||||||
fn err_impl(message: &'static str, s: &str, pos: &usize) -> Err {
|
fn err_impl(message: &'static str, s: &str, pos: usize) -> Err {
|
||||||
Box::new(Error {
|
Box::new(Error {
|
||||||
message,
|
message,
|
||||||
pos: get_line_and_column(s, *pos),
|
pos: get_line_and_column(s, pos),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an error with span information
|
/// Build an error with span information
|
||||||
pub(crate) fn err<T>(message: &'static str, s: &str, pos: &usize) -> ERes<T> {
|
pub(crate) fn err<T>(message: &'static str, s: &str, pos: usize) -> ERes<T> {
|
||||||
Err(err_impl(message, s, pos))
|
Err(err_impl(message, s, pos))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a span
|
/// Build a span
|
||||||
pub(crate) fn spos(s: &str, pos: &usize) -> SourcePosition {
|
pub(crate) fn spos(s: &str, pos: usize) -> SourcePosition {
|
||||||
if *pos >= s.len() {
|
if pos >= s.len() {
|
||||||
Default::default()
|
Default::default()
|
||||||
} else {
|
} else {
|
||||||
get_line_and_column(s, *pos)
|
get_line_and_column(s, pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
//! 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(unsafe_code)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
use error::{ERes, err, spos};
|
||||||
|
pub use error::Error;
|
||||||
|
pub use error::SourcePosition;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
|
||||||
|
/// 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(Debug, PartialEq, Clone, PartialOrd)]
|
||||||
|
pub enum Atom {
|
||||||
|
/// Simple string atom
|
||||||
|
S(String),
|
||||||
|
/// Quoted string
|
||||||
|
QS(String),
|
||||||
|
/// Character literal (with single quotes)
|
||||||
|
C(char),
|
||||||
|
/// Signed integer (normally only used for negative values)
|
||||||
|
I(i64),
|
||||||
|
/// Unsigned integer
|
||||||
|
U(u64),
|
||||||
|
/// Float
|
||||||
|
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(Debug, Clone)]
|
||||||
|
pub enum Sexp {
|
||||||
|
/// Atom
|
||||||
|
Atom(Atom, SourcePosition),
|
||||||
|
/// List of expressions
|
||||||
|
List(Vec<Sexp>, SourcePosition),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sexp {
|
||||||
|
pub fn pos(&self) -> &SourcePosition {
|
||||||
|
match self {
|
||||||
|
Sexp::List(_, pos) | Sexp::Atom(_, pos) => pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check fi thsi Sexp is an atom
|
||||||
|
pub fn is_atom(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Sexp::Atom(_, _) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check fi thsi Sexp is a list
|
||||||
|
pub fn is_list(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Sexp::List(_, _) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Sexp {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Sexp::Atom(a, _), Sexp::Atom(b, _)) => {
|
||||||
|
a == b
|
||||||
|
}
|
||||||
|
(Sexp::List(a, _), Sexp::List(b, _)) => {
|
||||||
|
a == b
|
||||||
|
}
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn without_underscores(s: &str) -> Cow<str> {
|
||||||
|
if s.contains('_') {
|
||||||
|
s.chars()
|
||||||
|
.filter(|c| *c != '_')
|
||||||
|
.collect::<String>().into()
|
||||||
|
} else {
|
||||||
|
s.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atom_of_string(s: String) -> Atom {
|
||||||
|
if s.starts_with('#') {
|
||||||
|
match u64::from_str_radix(&without_underscores(&s[1..]), 16) {
|
||||||
|
Ok(u) => return Atom::U(u),
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.starts_with("0x") {
|
||||||
|
match u64::from_str_radix(&without_underscores(&s[2..]), 16) {
|
||||||
|
Ok(u) => return Atom::U(u),
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.starts_with("0b") {
|
||||||
|
match u64::from_str_radix(&without_underscores(&s[2..]), 2) {
|
||||||
|
Ok(u) => return Atom::U(u),
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.starts_with('_') {
|
||||||
|
let filtered = without_underscores(&s);
|
||||||
|
|
||||||
|
match FromStr::from_str(&filtered) {
|
||||||
|
Ok(u) => return Atom::U(u),
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
match FromStr::from_str(&filtered) {
|
||||||
|
Ok(i) => return Atom::I(i),
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
match FromStr::from_str(&filtered) {
|
||||||
|
Ok(f) => return Atom::F(f),
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Atom::S(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the char it found, and the new pos if you wish to consume that char
|
||||||
|
fn peek(s: &str, pos: usize) -> ERes<(char, usize)> {
|
||||||
|
trace!("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!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the char it found, and the new pos if you wish to consume that char
|
||||||
|
fn peekn(s: &str, nth: usize, pos: usize) -> Option<(char, usize)> {
|
||||||
|
trace!("peekn {}", pos);
|
||||||
|
if nth == 0 {
|
||||||
|
panic!("peekn with nth=0");
|
||||||
|
}
|
||||||
|
if s.is_char_boundary(pos) {
|
||||||
|
let mut iter = s[pos..].chars();
|
||||||
|
let mut bytelen = 0;
|
||||||
|
for n in 0..nth {
|
||||||
|
if let Some(ch) = iter.next() {
|
||||||
|
bytelen += ch.len_utf8();
|
||||||
|
if n == (nth - 1) {
|
||||||
|
return Some((ch, pos + bytelen));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable!()
|
||||||
|
} else {
|
||||||
|
// strings must be composed of valid utf-8 chars.
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect(s: &str, pos: &mut usize, c: char) -> ERes<()> {
|
||||||
|
trace!("expect {}", pos);
|
||||||
|
let (ch, next) = 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) = peek(s, *pos)?;
|
||||||
|
*pos = next;
|
||||||
|
if ch == '\n' {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// zero or more spaces
|
||||||
|
fn zspace(s: &str, pos: &mut usize) -> ERes<()> {
|
||||||
|
trace!("zspace {}", pos);
|
||||||
|
loop {
|
||||||
|
if *pos == s.len() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let (ch, next) = peek(s, *pos)?;
|
||||||
|
|
||||||
|
if ch == ';' {
|
||||||
|
consume_until_newline(s, pos)?
|
||||||
|
} else if ch.is_whitespace() {
|
||||||
|
*pos = next;
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_quoted_atom(s: &str, quote: char, pos: &mut usize) -> ERes<Atom> {
|
||||||
|
trace!("parse_quoted_atom {}", pos);
|
||||||
|
let pos0 = *pos;
|
||||||
|
let mut cs: String = String::new();
|
||||||
|
|
||||||
|
expect(s, pos, quote)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (ch, next) = peek(s, *pos)?;
|
||||||
|
if ch == quote {
|
||||||
|
*pos = next;
|
||||||
|
break;
|
||||||
|
} else if ch == '\\' {
|
||||||
|
let (postslash, nextnext) = peek(s, next)?;
|
||||||
|
match postslash {
|
||||||
|
'r' => cs.push('\r'),
|
||||||
|
'n' => cs.push('\n'),
|
||||||
|
't' => cs.push('\t'),
|
||||||
|
other => cs.push(other)
|
||||||
|
}
|
||||||
|
*pos = nextnext;
|
||||||
|
} else {
|
||||||
|
cs.push(ch);
|
||||||
|
*pos = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if quote == '\'' {
|
||||||
|
// This is a character literal
|
||||||
|
if cs.chars().count() == 1 {
|
||||||
|
return Ok(Atom::C(cs.chars().next().unwrap()));
|
||||||
|
} else {
|
||||||
|
return err("Too long character literal!", s, pos0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not try i64 conversion, since this atom was explicitly quoted.
|
||||||
|
Ok(Atom::QS(cs))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_unquoted_atom(s: &str, pos: &mut usize) -> ERes<Atom> {
|
||||||
|
trace!("parse_unquoted_atom {}", pos);
|
||||||
|
let mut cs: String = String::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if *pos == s.len() { break; }
|
||||||
|
let (c, next) = peek(s, *pos)?;
|
||||||
|
|
||||||
|
if c == ';' {
|
||||||
|
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> {
|
||||||
|
trace!("parse_atom {}", pos);
|
||||||
|
let (ch, _) = peek(s, *pos)?;
|
||||||
|
|
||||||
|
if ch == '"' {
|
||||||
|
return parse_quoted_atom(s, ch, pos);
|
||||||
|
} else if ch == '\'' {
|
||||||
|
if let Some(('\\', _)) = peekn(s, 2, *pos) {
|
||||||
|
if let Some(('\'', _)) = peekn(s, 4, *pos) {
|
||||||
|
// Character literal with an escape sequence
|
||||||
|
return parse_quoted_atom(s, '\'', pos);
|
||||||
|
}
|
||||||
|
} else if let Some(('\'', _)) = peekn(s, 3, *pos) {
|
||||||
|
// Simple character literal
|
||||||
|
return parse_quoted_atom(s, '\'', pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_unquoted_atom(s, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_list(s: &str, pos: &mut usize) -> ERes<Vec<Sexp>> {
|
||||||
|
trace!("parse_list {}", pos);
|
||||||
|
zspace(s, pos)?;
|
||||||
|
expect(s, pos, '(')?;
|
||||||
|
|
||||||
|
let mut sexps: Vec<Sexp> = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
zspace(s, pos)?;
|
||||||
|
let (c, next) = peek(s, *pos)?;
|
||||||
|
if c == ')' {
|
||||||
|
*pos = next;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sexps.push(parse_sexp(s, pos)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
zspace(s, pos)?;
|
||||||
|
|
||||||
|
Ok(sexps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_sexp(s: &str, pos: &mut usize) -> ERes<Sexp> {
|
||||||
|
trace!("parse_sexp {}", pos);
|
||||||
|
zspace(s, pos)?;
|
||||||
|
let (c, _) = peek(s, *pos)?;
|
||||||
|
let r = if c == '(' {
|
||||||
|
Ok(Sexp::List(parse_list(s, pos)?, spos(s, *pos)))
|
||||||
|
} else {
|
||||||
|
Ok(Sexp::Atom(parse_atom(s, pos)?, spos(s, *pos)))
|
||||||
|
};
|
||||||
|
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()), Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an atomic s-expression from a string.
|
||||||
|
pub fn atom_qs(s: &str) -> Sexp {
|
||||||
|
Sexp::Atom(Atom::QS(s.to_owned()), Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an atomic s-expression from an int.
|
||||||
|
pub fn atom_i(i: i64) -> Sexp {
|
||||||
|
Sexp::Atom(Atom::I(i), Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an atomic s-expression from an unsigned int.
|
||||||
|
pub fn atom_u(u: u64) -> Sexp {
|
||||||
|
Sexp::Atom(Atom::U(u), Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an atomic s-expression from a char
|
||||||
|
pub fn atom_c(c: char) -> Sexp {
|
||||||
|
Sexp::Atom(Atom::C(c), Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an atomic s-expression from a float.
|
||||||
|
pub fn atom_f(f: f64) -> Sexp {
|
||||||
|
Sexp::Atom(Atom::F(f), Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a list s-expression given a slice of s-expressions.
|
||||||
|
pub fn list(xs: &[Sexp]) -> Sexp {
|
||||||
|
Sexp::List(xs.to_owned(), Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 = parse_sexp(s, &mut pos)?;
|
||||||
|
if pos == s.len() {
|
||||||
|
Ok(ret)
|
||||||
|
} else {
|
||||||
|
err("unrecognized post-s-expression data", s, pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quote(s: &str) -> String {
|
||||||
|
s.chars().fold(String::new(), |mut s, ch| {
|
||||||
|
match ch {
|
||||||
|
'\'' | '\\' | '"' => {
|
||||||
|
s.push('\\');
|
||||||
|
s.push(ch);
|
||||||
|
}
|
||||||
|
'\n' => {
|
||||||
|
s.push_str("\\n");
|
||||||
|
}
|
||||||
|
'\r' => {
|
||||||
|
s.push_str("\\n");
|
||||||
|
}
|
||||||
|
'\t' => {
|
||||||
|
s.push_str("\\t");
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
s.push(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Atom {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
match *self {
|
||||||
|
Atom::QS(ref s) => write!(f, "\"{}\"", quote(s)),
|
||||||
|
Atom::S(ref s) => write!(f, "{}", s),
|
||||||
|
Atom::C(c) => write!(f, "'{}'", quote(&c.to_string())),
|
||||||
|
Atom::I(i) => write!(f, "{}", i),
|
||||||
|
Atom::U(u) => write!(f, "{}", u),
|
||||||
|
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, _) => {
|
||||||
|
write!(f, "(")?;
|
||||||
|
for (i, x) in xs.iter().enumerate() {
|
||||||
|
let s = if i == 0 { "" } else { " " };
|
||||||
|
write!(f, "{}{}", s, x)?;
|
||||||
|
}
|
||||||
|
write!(f, ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,16 +3,33 @@ use super::error::get_line_and_column;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hello_world() {
|
fn test_hello_world() {
|
||||||
|
let parsed = parse("(hello #ffcc00 'c' ' ' '\\\\' '\\n' 'not_a_char 0xf_f 0b01_01 18_446_744_073_709_551_615 9223372036854775807 9223372036854775808 -42\n\t -4.0 \"world\") ; comment").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse("(hello -42\n\t -4.0 \"world\") ; comment").unwrap(),
|
parsed,
|
||||||
list(&[atom_s("hello"), atom_i(-42), atom_f(-4.0), atom_s("world")]));
|
list(&[
|
||||||
|
atom_s("hello"),
|
||||||
|
atom_u(0xffcc00),
|
||||||
|
atom_c('c'),
|
||||||
|
atom_c(' '),
|
||||||
|
atom_c('\\'),
|
||||||
|
atom_c('\n'),
|
||||||
|
atom_s("'not_a_char"),
|
||||||
|
atom_u(255),
|
||||||
|
atom_u(0b0101),
|
||||||
|
atom_u(18_446_744_073_709_551_615),
|
||||||
|
atom_u(9223372036854775807),
|
||||||
|
atom_u(9223372036854775808),
|
||||||
|
atom_i(-42),
|
||||||
|
atom_f(-4.0),
|
||||||
|
atom_qs("world")
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_escaping() {
|
fn test_escaping() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse("(\"\\\"\\q\" \"1234\" 1234)").unwrap(),
|
parse(r#"("\"\\q\t\n\r " "1234" 1234)"#).unwrap(),
|
||||||
list(&[atom_s("\"\\q"), atom_s("1234"), atom_i(1234)]));
|
list(&[atom_qs("\"\\q\t\n\r "), atom_qs("1234"), atom_u(1234)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -20,7 +37,7 @@ fn test_pp() {
|
|||||||
let s = "(hello world (what is (up) (4 6.4 you \"123\\\\ \\\"\")))";
|
let s = "(hello world (what is (up) (4 6.4 you \"123\\\\ \\\"\")))";
|
||||||
let sexp = parse(s).unwrap();
|
let sexp = parse(s).unwrap();
|
||||||
assert_eq!(s, sexp.to_string());
|
assert_eq!(s, sexp.to_string());
|
||||||
assert_eq!(s, format!("{:?}", sexp));
|
assert_eq!(s, format!("{}", sexp));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -36,7 +53,7 @@ fn test_tight_parens() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_space_in_atom() {
|
fn test_space_in_atom() {
|
||||||
let sexp = list(&[atom_s("hello world")]);
|
let sexp = list(&[atom_qs("hello world")]);
|
||||||
let sexp_as_string = sexp.to_string();
|
let sexp_as_string = sexp.to_string();
|
||||||
assert_eq!("(\"hello world\")", sexp_as_string);
|
assert_eq!("(\"hello world\")", sexp_as_string);
|
||||||
assert_eq!(sexp, parse(&sexp_as_string).unwrap());
|
assert_eq!(sexp, parse(&sexp_as_string).unwrap());
|
||||||
@@ -68,3 +85,19 @@ fn sexp_size() {
|
|||||||
use std::mem;
|
use std::mem;
|
||||||
assert_eq!(mem::size_of::<Sexp>(), mem::size_of::<isize>() * 6);
|
assert_eq!(mem::size_of::<Sexp>(), mem::size_of::<isize>() * 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_peek() {
|
||||||
|
assert_eq!(('a', 1), peek("ahoj", 0).unwrap());
|
||||||
|
assert_eq!(('j', 4), peek("ahoj", 3).unwrap());
|
||||||
|
assert!(peek("ahoj", 4).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_peekn() {
|
||||||
|
assert_eq!(Some(('a', 1)), peekn("ahoj", 1, 0));
|
||||||
|
assert_eq!(Some(('h', 2)), peekn("ahoj", 2, 0));
|
||||||
|
assert_eq!(Some(('j', 4)), peekn("ahoj", 4, 0));
|
||||||
|
assert_eq!(Some(('o', 3)), peekn("ahoj", 1, 2));
|
||||||
|
assert_eq!(None, peekn("ahoj", 3, 2));
|
||||||
|
}
|
||||||
@@ -78,36 +78,26 @@ pub fn parse_data_disp(tok: Sexp, pcx: &ParserContext) -> Result<DataDisp, CrsnE
|
|||||||
Sexp::Atom(Atom::I(val), _pos) => {
|
Sexp::Atom(Atom::I(val), _pos) => {
|
||||||
Ok(DataDisp::Immediate(unsafe { std::mem::transmute(val) }))
|
Ok(DataDisp::Immediate(unsafe { std::mem::transmute(val) }))
|
||||||
}
|
}
|
||||||
|
Sexp::Atom(Atom::F(val), _pos) => {
|
||||||
|
Ok(DataDisp::Immediate(unsafe { std::mem::transmute(val) }))
|
||||||
|
}
|
||||||
|
Sexp::Atom(Atom::U(val), _pos) => {
|
||||||
|
Ok(DataDisp::Immediate(val))
|
||||||
|
}
|
||||||
|
Sexp::Atom(Atom::C(val), _pos) => {
|
||||||
|
Ok(DataDisp::Immediate(val as u64))
|
||||||
|
}
|
||||||
|
Sexp::Atom(Atom::QS(_s), pos) => {
|
||||||
|
Err(CrsnError::Parse("Quoted string not expected here".into(), pos))
|
||||||
|
}
|
||||||
|
Sexp::List(_list, pos) => {
|
||||||
|
Err(CrsnError::Parse("List not expected here".into(), pos))
|
||||||
|
}
|
||||||
Sexp::Atom(Atom::S(s), pos) => {
|
Sexp::Atom(Atom::S(s), pos) => {
|
||||||
if s == "_" {
|
if s == "_" {
|
||||||
return Ok(DataDisp::Discard);
|
return Ok(DataDisp::Discard);
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.starts_with('\'') && s.ends_with('\'') {
|
|
||||||
if s.chars().count() == 3 {
|
|
||||||
let ch = s.chars().nth(1).unwrap();
|
|
||||||
if ch == '\'' {
|
|
||||||
return Err(CrsnError::Parse("Use '\\'' for apos".into(), pos));
|
|
||||||
}
|
|
||||||
return Ok(DataDisp::Immediate(ch as u64));
|
|
||||||
} else if s.chars().count() == 4 && s.chars().nth(1).unwrap() == '\\' {
|
|
||||||
let ch = s.chars().nth(2).unwrap();
|
|
||||||
let ch = match ch {
|
|
||||||
'\\' => '\\',
|
|
||||||
'n' => '\n',
|
|
||||||
'r' => '\r',
|
|
||||||
't' => '\t',
|
|
||||||
'\'' => '\'',
|
|
||||||
_ => {
|
|
||||||
return Err(CrsnError::Parse(format!("Unknown escape sequence: {}", s).into(), pos));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return Ok(DataDisp::Immediate(ch as u64));
|
|
||||||
} else {
|
|
||||||
return Err(CrsnError::Parse(format!("Invalid character literal synax {}", s).into(), pos));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(reference) = s.strip_prefix('@') {
|
if let Some(reference) = s.strip_prefix('@') {
|
||||||
/* extension constants (pre-defined handles) */
|
/* extension constants (pre-defined handles) */
|
||||||
for p in pcx.parsers {
|
for p in pcx.parsers {
|
||||||
@@ -129,8 +119,6 @@ pub fn parse_data_disp(tok: Sexp, pcx: &ParserContext) -> Result<DataDisp, CrsnE
|
|||||||
} else {
|
} else {
|
||||||
Ok(DataDisp::RegObject(reg))
|
Ok(DataDisp::RegObject(reg))
|
||||||
}
|
}
|
||||||
} else if s.starts_with(|c: char| c.is_ascii_digit() || c == '-') {
|
|
||||||
Ok(DataDisp::Immediate(unsafe { std::mem::transmute(parse_i64(&s, &pos)?) }))
|
|
||||||
} else {
|
} else {
|
||||||
/* extension constants */
|
/* extension constants */
|
||||||
for p in pcx.parsers {
|
for p in pcx.parsers {
|
||||||
@@ -161,9 +149,6 @@ pub fn parse_data_disp(tok: Sexp, pcx: &ParserContext) -> Result<DataDisp, CrsnE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
other => {
|
|
||||||
Err(CrsnError::Parse(format!("bad data disp: {:?}", other).into(), other.pos().clone()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,16 +158,28 @@ pub fn parse_value(tok: Sexp, pcx: &ParserContext) -> Result<Value, CrsnError> {
|
|||||||
Sexp::Atom(Atom::I(val), _pos) => {
|
Sexp::Atom(Atom::I(val), _pos) => {
|
||||||
Ok(unsafe { std::mem::transmute(val) })
|
Ok(unsafe { std::mem::transmute(val) })
|
||||||
}
|
}
|
||||||
|
Sexp::Atom(Atom::U(val), _pos) => {
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
Sexp::Atom(Atom::C(val), _pos) => {
|
||||||
|
Ok(val as u64)
|
||||||
|
}
|
||||||
|
Sexp::Atom(Atom::QS(_), pos) => {
|
||||||
|
Err(CrsnError::Parse("quoted string not expected here".into(), pos))
|
||||||
|
}
|
||||||
|
Sexp::Atom(Atom::F(val), _) => {
|
||||||
|
Ok(unsafe { std::mem::transmute(val) })
|
||||||
|
}
|
||||||
Sexp::Atom(Atom::S(s), pos) => {
|
Sexp::Atom(Atom::S(s), pos) => {
|
||||||
let pstate = pcx.state.borrow();
|
let pstate = pcx.state.borrow();
|
||||||
if let Some(val) = pstate.constants.get(&s) {
|
if let Some(val) = pstate.constants.get(&s) {
|
||||||
return Ok(*val);
|
return Ok(*val);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(unsafe { std::mem::transmute(parse_i64(&s, &pos)?) })
|
Err(CrsnError::Parse(format!("unknown constant: {}", s).into(), pos))
|
||||||
}
|
}
|
||||||
other => {
|
Sexp::List(_, pos) => {
|
||||||
Err(CrsnError::Parse(format!("bad value format: {:?}", other).into(), other.pos().clone()))
|
Err(CrsnError::Parse("expected a value".into(), pos))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,15 +201,6 @@ pub fn parse_u64(literal: &str, pos: &SourcePosition) -> Result<u64, CrsnError>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_i64(literal: &str, pos: &SourcePosition) -> Result<i64, CrsnError> {
|
|
||||||
// trace!("parse i64 from {}", literal);
|
|
||||||
if let Some(_value) = literal.strip_prefix("-") {
|
|
||||||
Ok(-1 * i64::try_from(parse_u64(literal, pos)?).err_pos(pos)?)
|
|
||||||
} else {
|
|
||||||
Ok(i64::try_from(parse_u64(literal, pos)?).err_pos(pos)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_rd(tok: Sexp, pcx: &ParserContext) -> Result<Rd, CrsnError> {
|
pub fn parse_rd(tok: Sexp, pcx: &ParserContext) -> Result<Rd, CrsnError> {
|
||||||
let pos = tok.pos().clone();
|
let pos = tok.pos().clone();
|
||||||
Ok(Rd::new(RdData::try_from(parse_data_disp(tok, pcx)?).err_pos(&pos)?))
|
Ok(Rd::new(RdData::try_from(parse_data_disp(tok, pcx)?).err_pos(&pos)?))
|
||||||
|
|||||||
@@ -36,10 +36,20 @@ pub fn expect_string_atom(expr: Sexp) -> Result<(String, SourcePosition), CrsnEr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn expect_int_atom(expr: Option<Sexp>) -> Result<i64, Error> {
|
pub fn expect_quoted_string_atom(expr: Sexp) -> Result<(String, SourcePosition), CrsnError> {
|
||||||
// match expect_atom(expr) {
|
match expect_atom(expr) {
|
||||||
// Ok(Atom::I(v)) => Ok(v),
|
Ok((Atom::QS(s), pos)) => Ok((s, pos)),
|
||||||
// Ok(atom) => Err(Error::ParseIn("Expected int atom".into(), Sexp::Atom(atom))),
|
Ok((_, pos)) => Err(CrsnError::Parse("Expected quoted string atom".into(), pos)),
|
||||||
// Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
/// Quoted or simple string
|
||||||
|
pub fn expect_any_string_atom(expr: Sexp) -> Result<(String, SourcePosition), CrsnError> {
|
||||||
|
match expect_atom(expr) {
|
||||||
|
Ok((Atom::S(s), pos)) => Ok((s, pos)),
|
||||||
|
Ok((Atom::QS(s), pos)) => Ok((s, pos)),
|
||||||
|
Ok((_, pos)) => Err(CrsnError::Parse("Expected quoted string atom".into(), pos)),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ pub enum BuiltinOp {
|
|||||||
Fault(Option<DebugMsg>),
|
Fault(Option<DebugMsg>),
|
||||||
/// Deallocate an extension object.
|
/// Deallocate an extension object.
|
||||||
/// The object is released and the handle becomes invalid.
|
/// The object is released and the handle becomes invalid.
|
||||||
Drop(RdObj),
|
Delete(RdObj),
|
||||||
/// Copy value
|
/// Copy value
|
||||||
Move { dst: Wr, src: Rd },
|
Move { dst: Wr, src: Rd },
|
||||||
/// Store runtime status to a register
|
/// Store runtime status to a register
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ impl OpTrait for BuiltinOp {
|
|||||||
BuiltinOp::Sleep { micros } => {
|
BuiltinOp::Sleep { micros } => {
|
||||||
std::thread::sleep(Duration::from_micros(state.read(*micros)?))
|
std::thread::sleep(Duration::from_micros(state.read(*micros)?))
|
||||||
}
|
}
|
||||||
BuiltinOp::Drop(obj) => {
|
BuiltinOp::Delete(obj) => {
|
||||||
let x = state.read(Rd::new(RdData::Register(obj.reg())))?;
|
let x = state.read(Rd::new(RdData::Register(obj.reg())))?;
|
||||||
|
|
||||||
trace!("Drop object: {:#x}", x);
|
trace!("Drop object: {:#x}", x);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::asm::error::CrsnError;
|
|||||||
use crate::asm::instr::op::OpKind;
|
use crate::asm::instr::op::OpKind;
|
||||||
use crate::asm::parse::arg_parser::TokenParser;
|
use crate::asm::parse::arg_parser::TokenParser;
|
||||||
use crate::asm::parse::parse_data::{parse_constant_name, parse_label, parse_label_str, parse_rd, parse_reg_alias, parse_value};
|
use crate::asm::parse::parse_data::{parse_constant_name, parse_label, parse_label_str, parse_rd, parse_reg_alias, parse_value};
|
||||||
use crate::asm::parse::sexp_expect::expect_string_atom;
|
use crate::asm::parse::sexp_expect::{expect_any_string_atom};
|
||||||
use crate::asm::patches::ErrWithPos;
|
use crate::asm::patches::ErrWithPos;
|
||||||
use crate::builtin::defs::{Barrier, BuiltinOp};
|
use crate::builtin::defs::{Barrier, BuiltinOp};
|
||||||
use crate::module::ParseRes;
|
use crate::module::ParseRes;
|
||||||
@@ -135,7 +135,7 @@ pub(crate) fn parse_op<'a>(op_pos: &SourcePosition, keyword: &str, mut args: Tok
|
|||||||
kind: Barrier::Standalone,
|
kind: Barrier::Standalone,
|
||||||
msg: match args.next() {
|
msg: match args.next() {
|
||||||
None => None,
|
None => None,
|
||||||
Some(s) => Some(expect_string_atom(s)?.0.into()),
|
Some(s) => Some(expect_any_string_atom(s)?.0.into()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ pub(crate) fn parse_op<'a>(op_pos: &SourcePosition, keyword: &str, mut args: Tok
|
|||||||
"fault" => {
|
"fault" => {
|
||||||
BuiltinOp::Fault(match args.next() {
|
BuiltinOp::Fault(match args.next() {
|
||||||
None => None,
|
None => None,
|
||||||
Some(s) => Some(expect_string_atom(s)?.0.into()),
|
Some(s) => Some(expect_any_string_atom(s)?.0.into()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,8 +180,8 @@ pub(crate) fn parse_op<'a>(op_pos: &SourcePosition, keyword: &str, mut args: Tok
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"drop" => {
|
"del" => {
|
||||||
BuiltinOp::Drop(args.next_rdobj()?)
|
BuiltinOp::Delete(args.next_rdobj()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
"far" => {
|
"far" => {
|
||||||
@@ -277,7 +277,7 @@ pub(crate) fn to_sexp(op: &BuiltinOp) -> Sexp {
|
|||||||
sexp::list(&[A("fault")])
|
sexp::list(&[A("fault")])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BuiltinOp::Drop(obj) => sexp::list(&[A("drop"), A(obj)]),
|
BuiltinOp::Delete(obj) => sexp::list(&[A("del"), A(obj)]),
|
||||||
BuiltinOp::Move { dst, src } => sexp::list(&[A("ld"), A(dst), A(src)]),
|
BuiltinOp::Move { dst, src } => sexp::list(&[A("ld"), A(dst), A(src)]),
|
||||||
BuiltinOp::StoreStatus { dst } => sexp::list(&[A("sst"), A(dst)]),
|
BuiltinOp::StoreStatus { dst } => sexp::list(&[A("sst"), A(dst)]),
|
||||||
BuiltinOp::LoadStatus { src } => sexp::list(&[A("sld"), A(src)])
|
BuiltinOp::LoadStatus { src } => sexp::list(&[A("sld"), A(src)])
|
||||||
@@ -303,6 +303,12 @@ mod test {
|
|||||||
("(nop)", "(nop)"),
|
("(nop)", "(nop)"),
|
||||||
("(halt)", "(halt)"),
|
("(halt)", "(halt)"),
|
||||||
("(sleep 1000)", "(sleep 1000)"),
|
("(sleep 1000)", "(sleep 1000)"),
|
||||||
|
("(sleep 1_0_0_0)", "(sleep 1000)"),
|
||||||
|
("(sleep ' ')", "(sleep 32)"),
|
||||||
|
("(sleep '\\n')", "(sleep 10)"),
|
||||||
|
("(sleep 0b111)", "(sleep 7)"),
|
||||||
|
("(sleep 0xab_cd)", "(sleep 43981)"),
|
||||||
|
("(sleep #ab_cd)", "(sleep 43981)"),
|
||||||
("(:x)", "(:x)"),
|
("(:x)", "(:x)"),
|
||||||
("(j :x)", "(j :x)"),
|
("(j :x)", "(j :x)"),
|
||||||
("(:#7)", "(:#7)"),
|
("(:#7)", "(:#7)"),
|
||||||
@@ -345,8 +351,8 @@ mod test {
|
|||||||
("(sst r0)", "(sst r0)"),
|
("(sst r0)", "(sst r0)"),
|
||||||
("(sld r0)", "(sld r0)"),
|
("(sld r0)", "(sld r0)"),
|
||||||
("(far :label)", "(far :label)"),
|
("(far :label)", "(far :label)"),
|
||||||
("(drop @r5)", "(drop @r5)"),
|
("(del @r5)", "(del @r5)"),
|
||||||
("(sym cat r0)(drop @cat)", "(drop @r0)"),
|
("(sym cat r0)(del @cat)", "(del @r0)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let parser = BuiltinOps::new();
|
let parser = BuiltinOps::new();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
|
// re-export our customized version of sexp
|
||||||
pub use sexp;
|
pub use sexp;
|
||||||
|
|
||||||
pub mod asm;
|
pub mod asm;
|
||||||
|
|||||||
+10
-1
@@ -11,6 +11,11 @@ mod option_ext;
|
|||||||
pub fn A(s: impl Display) -> Sexp {
|
pub fn A(s: impl Display) -> Sexp {
|
||||||
let s = s.to_string();
|
let s = s.to_string();
|
||||||
|
|
||||||
|
let x: Result<u64, _> = FromStr::from_str(&s);
|
||||||
|
if let Ok(x) = x {
|
||||||
|
return Sexp::Atom(Atom::U(x), Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
let x: Result<i64, _> = FromStr::from_str(&s);
|
let x: Result<i64, _> = FromStr::from_str(&s);
|
||||||
if let Ok(x) = x {
|
if let Ok(x) = x {
|
||||||
return Sexp::Atom(Atom::I(x), Default::default());
|
return Sexp::Atom(Atom::I(x), Default::default());
|
||||||
@@ -21,5 +26,9 @@ pub fn A(s: impl Display) -> Sexp {
|
|||||||
return Sexp::Atom(Atom::F(y), Default::default());
|
return Sexp::Atom(Atom::F(y), Default::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
Sexp::Atom(Atom::S(s), Default::default())
|
if s.contains(|c: char| " \t\"\\\n\t\r".contains(c)) {
|
||||||
|
Sexp::Atom(Atom::QS(s), Default::default())
|
||||||
|
} else {
|
||||||
|
Sexp::Atom(Atom::S(s), Default::default())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ use crsn::asm::instr::op::OpKind;
|
|||||||
use crsn::asm::parse::arg_parser::TokenParser;
|
use crsn::asm::parse::arg_parser::TokenParser;
|
||||||
use crsn::module::{CrsnExtension, ParseRes, CrsnUniq};
|
use crsn::module::{CrsnExtension, ParseRes, CrsnUniq};
|
||||||
use crsn::runtime::fault::Fault;
|
use crsn::runtime::fault::Fault;
|
||||||
use crsn::runtime::run_thread::{RunState, ThreadInfo};
|
use crsn::runtime::run_thread::{RunState};
|
||||||
use crsn::sexp::SourcePosition;
|
use crsn::sexp::SourcePosition;
|
||||||
use crsn::asm::data::Mask;
|
use crsn::asm::data::Mask;
|
||||||
use std::io::{Read, stdin, stdout, Write};
|
use std::io::{Write};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use crsn::asm::instr::Cond;
|
use crsn::asm::instr::Cond;
|
||||||
use console::Term;
|
use console::Term;
|
||||||
@@ -61,7 +61,7 @@ impl CrsnExtension for StdioOps {
|
|||||||
"stdio"
|
"stdio"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_op<'a>(&self, pos: &SourcePosition, keyword: &str, args: TokenParser<'a>) -> Result<ParseRes<'a, OpKind>, CrsnError> {
|
fn parse_op<'a>(&self, _pos: &SourcePosition, _keyword: &str, args: TokenParser<'a>) -> Result<ParseRes<'a, OpKind>, CrsnError> {
|
||||||
Ok(ParseRes::Unknown(args))
|
Ok(ParseRes::Unknown(args))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -32,5 +32,5 @@
|
|||||||
(rpop r1 @r0)
|
(rpop r1 @r0)
|
||||||
(cmp r1 1000 (ne? (fault)))
|
(cmp r1 1000 (ne? (fault)))
|
||||||
|
|
||||||
(drop @r0)
|
(del @r0)
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-4
@@ -1,8 +1,10 @@
|
|||||||
(
|
(
|
||||||
; we don't have strings yet 👌
|
; we don't have strings yet 👌
|
||||||
(ld @stdout 'H') (ld @stdout 'e') (ld @stdout 'l') (ld @stdout 'l') (ld @stdout 'o')
|
(ld @stdout 'P') (ld @stdout 'r') (ld @stdout 'e') (ld @stdout 's') (ld @stdout 's') (ld @stdout ' ')
|
||||||
(ld @stdout 32) (ld @stdout 'c') (ld @stdout 'r') (ld @stdout 's') (ld @stdout 'n')
|
(ld @stdout 'q') (ld @stdout ' ')
|
||||||
(ld @stdout '…') (ld @stdout 32)
|
(ld @stdout 't') (ld @stdout 'o') (ld @stdout ' ')
|
||||||
|
(ld @stdout 'q') (ld @stdout 'u') (ld @stdout 'i') (ld @stdout 't')
|
||||||
|
(ld @stdout '…') (ld @stdout ' ')
|
||||||
|
|
||||||
(:loop)
|
(:loop)
|
||||||
(ld r0 @stdin)
|
(ld r0 @stdin)
|
||||||
@@ -15,7 +17,7 @@
|
|||||||
; uppercase ASCII
|
; uppercase ASCII
|
||||||
(cmp r0 'a' (<? (j :badchar)))
|
(cmp r0 'a' (<? (j :badchar)))
|
||||||
(cmp r0 'z' (>? (j :badchar)))
|
(cmp r0 'z' (>? (j :badchar)))
|
||||||
(sub r0 32)
|
(sub r0 ' ')
|
||||||
(ld @stdout r0)
|
(ld @stdout r0)
|
||||||
(j :loop)
|
(j :loop)
|
||||||
|
|
||||||
|
|||||||
+16
-8
@@ -33,7 +33,7 @@ struct Config {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
program_file: String,
|
program_file: String,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
asm_only: bool,
|
assemble_only: bool,
|
||||||
#[serde(with = "serde_duration_millis")]
|
#[serde(with = "serde_duration_millis")]
|
||||||
cycle_time: Duration,
|
cycle_time: Duration,
|
||||||
}
|
}
|
||||||
@@ -42,11 +42,11 @@ impl Default for Config {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
log: LogConfig {
|
log: LogConfig {
|
||||||
level: "info".to_string(),
|
level: "warn".to_string(),
|
||||||
modules: Default::default(),
|
modules: Default::default(),
|
||||||
},
|
},
|
||||||
program_file: "".to_string(),
|
program_file: "".to_string(),
|
||||||
asm_only: false,
|
assemble_only: false,
|
||||||
cycle_time: Duration::default(),
|
cycle_time: Duration::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,14 @@ impl AppConfig for Config {
|
|||||||
Some(&self.log.modules)
|
Some(&self.log.modules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pre_log_println(_message: String) {
|
||||||
|
// shut up
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_banner(_name: &str, _version: &str) {
|
||||||
|
// No banner
|
||||||
|
}
|
||||||
|
|
||||||
/// Add args to later use in the `configure` method.
|
/// Add args to later use in the `configure` method.
|
||||||
fn add_args<'a: 'b, 'b>(clap: clap::App<'a, 'b>) -> clap::App<'a, 'b> {
|
fn add_args<'a: 'b, 'b>(clap: clap::App<'a, 'b>) -> clap::App<'a, 'b> {
|
||||||
// Default impl
|
// Default impl
|
||||||
@@ -106,7 +114,7 @@ impl AppConfig for Config {
|
|||||||
|
|
||||||
fn configure(mut self, clap: &ArgMatches) -> anyhow::Result<Self> {
|
fn configure(mut self, clap: &ArgMatches) -> anyhow::Result<Self> {
|
||||||
self.program_file = clap.value_of("input").unwrap().to_string();
|
self.program_file = clap.value_of("input").unwrap().to_string();
|
||||||
self.asm_only = clap.is_present("asm-only");
|
self.assemble_only = clap.is_present("asm-only");
|
||||||
if let Some(c) = clap.value_of("cycle") {
|
if let Some(c) = clap.value_of("cycle") {
|
||||||
self.cycle_time = Duration::from_millis(c.parse().unwrap());
|
self.cycle_time = Duration::from_millis(c.parse().unwrap());
|
||||||
}
|
}
|
||||||
@@ -118,7 +126,7 @@ impl AppConfig for Config {
|
|||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let config = Config::init("crsn", "crsn.json5", env!("CARGO_PKG_VERSION"))?;
|
let config = Config::init("crsn", "crsn.json5", env!("CARGO_PKG_VERSION"))?;
|
||||||
|
|
||||||
info!("Loading {}", config.program_file);
|
debug!("Loading {}", config.program_file);
|
||||||
|
|
||||||
let source = read_file::read_file(&config.program_file)?;
|
let source = read_file::read_file(&config.program_file)?;
|
||||||
|
|
||||||
@@ -131,7 +139,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
StdioOps::new(),
|
StdioOps::new(),
|
||||||
])?;
|
])?;
|
||||||
|
|
||||||
if config.asm_only {
|
if config.assemble_only {
|
||||||
for (n, op) in parsed.ops.iter().enumerate() {
|
for (n, op) in parsed.ops.iter().enumerate() {
|
||||||
println!("{:04} : {}", n, op.to_sexp());
|
println!("{:04} : {}", n, op.to_sexp());
|
||||||
}
|
}
|
||||||
@@ -144,7 +152,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
trace!("------------------------");
|
trace!("------------------------");
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Start runtime");
|
debug!("Start runtime");
|
||||||
|
|
||||||
let args = &[];
|
let args = &[];
|
||||||
let thread = RunThread::new(ThreadParams {
|
let thread = RunThread::new(ThreadParams {
|
||||||
@@ -159,7 +167,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
// run without spawning, so it is on the main thread - required by some extensions
|
// run without spawning, so it is on the main thread - required by some extensions
|
||||||
thread.run();
|
thread.run();
|
||||||
|
|
||||||
info!("Runtime shut down.");
|
debug!("Runtime shut down.");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
Source-location tracking Sexp
|
|
||||||
=====
|
|
||||||
|
|
||||||
**This is a fork of "sexp", updated to the 2018 edition, where each parsed node tracks its
|
|
||||||
source file position. This enables better error reporting in subsequent parsing and processing.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Original version by Clark Gaebel: [https://github.com/cgaebel/sexp](https://github.com/cgaebel/sexp).
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
//! 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(unsafe_code)]
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate log;
|
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::{self, FromStr};
|
|
||||||
|
|
||||||
use error::{ERes, err, spos};
|
|
||||||
pub use error::Error;
|
|
||||||
pub use error::SourcePosition;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test;
|
|
||||||
|
|
||||||
mod error;
|
|
||||||
|
|
||||||
/// 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(Clone)]
|
|
||||||
pub enum Sexp {
|
|
||||||
/// Atom
|
|
||||||
Atom(Atom, SourcePosition),
|
|
||||||
/// List of expressions
|
|
||||||
List(Vec<Sexp>, SourcePosition),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sexp {
|
|
||||||
pub fn pos(&self) -> &SourcePosition {
|
|
||||||
match self {
|
|
||||||
Sexp::List(_, pos) | Sexp::Atom(_, pos) => pos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check fi thsi Sexp is an atom
|
|
||||||
pub fn is_atom(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Sexp::Atom(_, _) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check fi thsi Sexp is a list
|
|
||||||
pub fn is_list(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Sexp::List(_, _) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for Sexp {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(Sexp::Atom(a, _), Sexp::Atom(b, _)) => {
|
|
||||||
a == b
|
|
||||||
}
|
|
||||||
(Sexp::List(a, _), Sexp::List(b, _)) => {
|
|
||||||
a == b
|
|
||||||
}
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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)> {
|
|
||||||
trace!("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<()> {
|
|
||||||
trace!("expect {}", pos);
|
|
||||||
let (ch, next) = 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) = peek(s, pos)?;
|
|
||||||
*pos = next;
|
|
||||||
if ch == '\n' { return Ok(()); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// zero or more spaces
|
|
||||||
fn zspace(s: &str, pos: &mut usize) -> ERes<()> {
|
|
||||||
trace!("zspace {}", pos);
|
|
||||||
loop {
|
|
||||||
if *pos == s.len() { return Ok(()); }
|
|
||||||
let (ch, next) = peek(s, pos)?;
|
|
||||||
|
|
||||||
if ch == ';' { 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> {
|
|
||||||
trace!("parse_quoted_atom {}", pos);
|
|
||||||
let mut cs: String = String::new();
|
|
||||||
|
|
||||||
expect(s, pos, '"')?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let (ch, next) = peek(s, pos)?;
|
|
||||||
if ch == '"' {
|
|
||||||
*pos = next;
|
|
||||||
break;
|
|
||||||
} else if ch == '\\' {
|
|
||||||
let (postslash, nextnext) = 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> {
|
|
||||||
trace!("parse_unquoted_atom {}", pos);
|
|
||||||
let mut cs: String = String::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if *pos == s.len() { break; }
|
|
||||||
let (c, next) = peek(s, pos)?;
|
|
||||||
|
|
||||||
if c == ';' {
|
|
||||||
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> {
|
|
||||||
trace!("parse_atom {}", pos);
|
|
||||||
let (ch, _) = 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>> {
|
|
||||||
trace!("parse_list {}", pos);
|
|
||||||
zspace(s, pos)?;
|
|
||||||
expect(s, pos, '(')?;
|
|
||||||
|
|
||||||
let mut sexps: Vec<Sexp> = Vec::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
zspace(s, pos)?;
|
|
||||||
let (c, next) = peek(s, pos)?;
|
|
||||||
if c == ')' {
|
|
||||||
*pos = next;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sexps.push(parse_sexp(s, pos)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
zspace(s, pos)?;
|
|
||||||
|
|
||||||
Ok(sexps)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_sexp(s: &str, pos: &mut usize) -> ERes<Sexp> {
|
|
||||||
trace!("parse_sexp {}", pos);
|
|
||||||
zspace(s, pos)?;
|
|
||||||
let (c, _) = peek(s, pos)?;
|
|
||||||
let r = if c == '(' {
|
|
||||||
Ok(Sexp::List(parse_list(s, pos)?, spos(s, pos)))
|
|
||||||
} else {
|
|
||||||
Ok(Sexp::Atom(parse_atom(s, pos)?, spos(s, pos)))
|
|
||||||
};
|
|
||||||
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()), Default::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs an atomic s-expression from an int.
|
|
||||||
pub fn atom_i(i: i64) -> Sexp {
|
|
||||||
Sexp::Atom(Atom::I(i), Default::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs an atomic s-expression from a float.
|
|
||||||
pub fn atom_f(f: f64) -> Sexp {
|
|
||||||
Sexp::Atom(Atom::F(f), Default::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a list s-expression given a slice of s-expressions.
|
|
||||||
pub fn list(xs: &[Sexp]) -> Sexp {
|
|
||||||
Sexp::List(xs.to_owned(), Default::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 = 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, _) => {
|
|
||||||
write!(f, "(")?;
|
|
||||||
for (i, x) in xs.iter().enumerate() {
|
|
||||||
let s = if i == 0 { "" } else { " " };
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user