mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 13:26:49 -06:00
* AutoLISP: load and run .lsp routines (#758) CADCraft runs AutoLISP routines. `.lsp` files load with APPLOAD, (load) or by dropping them on the window, and at startup from the support folder (acad.lsp once per session or per drawing with ACADLSPASDOC, acaddoc.lsp per drawing, then S::STARTUP; never from a drawing's own folder). (defun c:NAME …) defines commands, (autoload) defers loading to first use, and (expression) and !variable work at the command line and at prompts. docs/autolisp.md lists what is supported. The interpreter (crates/engine/src/lisp) reads source with its own stack and evaluates on an explicit-stack machine, so a routine can stop for input (getpoint, pause in command) and carry on later on one thread, deep recursion is an error rather than a crash, and long loops run a slice per frame (Esc stops them). It follows AutoLISP: 32-bit wrapping integers, dynamic scope, *error*, vl-catch-all-apply, AutoLISP's error wording. About 200 built-ins cover the language, command (with pause and commands left open), entity lists (entget/entmake/entmod/entdel/entlast/ entnext/handent, through the DXF reader and writer's groups, exposed by cadcraft-io and handed to the engine through a hook), ssget with modes and filters, tables, system variables, get… input with initget, and units. A routine is one undo step; each drawing has its own symbols. Also: keywords typed as _KEYWORD match at prompts; TEXT run by a routine's command takes one line; DIMENSION records carry their measurement (42); Prompt::raw shows a get… prompt as written; the interface asks Session::prompting() (a command's or a routine's prompt) where it asked whether a command runs, so clicks, right-click and Delete work at a routine's prompts; getcorner and based get… prompts drag a rubber band. Tests: the language, errors, startup files, interactive input, hostile input (lisp/tests.rs); the #648 corpus, 18/18, when CADCRAFT_LISP_CORPUS points at it (tests/lisp_corpus.rs); drop, frames and canvas clicks in the app (ui-egui/tests/lisp_files.rs). * AutoLISP: bound memory, drop nested values iteratively, guard the per-frame tick - Cons cells drop with a work list over both car and cdr (and function bodies), so a list nested a million deep in its car no longer overflows the stack when it goes away. - Memory caps so a runaway routine fails with a LISP error instead of an uncatchable out-of-memory abort: at most 5,000,000 live cons cells (counted in Value::cons / Drop, checked every step; cells the drawing's variables already held at the run's start don't count against it), no string a built-in makes longer than 8 MiB (strcat checks before joining, printed text is cut), and a 2 GiB per-run budget for new strings, printed text and files read. - Session::lisp_tick runs under catch_unwind, and so does every slice of the LISP machine: a panic ends the routine with "internal error", puts the drawing back and leaves the session usable. - Regression tests for each (deep car nesting, nested functions, the cell cap, string length and budget, a panicking built-in in a blocking run and in a ticked one). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
563 lines
19 KiB
Rust
563 lines
19 KiB
Rust
//! AutoLISP values: integers (32-bit, wrapping as AutoLISP's do), reals, strings, symbols,
|
|
//! lists built of cons cells, entity names, selection sets, files and functions.
|
|
//!
|
|
//! Values are shared (`Arc`) so a session stays `Send`. Cons cells (and functions) are dropped
|
|
//! iteratively: a list a million long, or nested a million deep, would otherwise overflow the
|
|
//! stack when it goes away.
|
|
//!
|
|
//! Memory is bounded so a runaway routine fails with a LISP error instead of the process running
|
|
//! out of memory (which no panic guard can catch): live cons cells are counted ([`live_cells`],
|
|
//! checked by the evaluator at every step), no string a built-in makes may be longer than
|
|
//! [`MAX_STRING`], and the strings and printed text a run makes share a byte budget
|
|
//! ([`charge_bytes`]).
|
|
|
|
use std::cell::Cell;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use cadcraft_doc::Handle;
|
|
|
|
use super::LispError;
|
|
|
|
/// A symbol: its name in upper case (AutoLISP symbols are case-insensitive).
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
pub struct Sym(pub Arc<str>);
|
|
|
|
impl Sym {
|
|
pub fn new(name: &str) -> Sym {
|
|
Sym(Arc::from(name.to_ascii_uppercase().as_str()))
|
|
}
|
|
pub fn name(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
/// A cons cell.
|
|
#[derive(Debug)]
|
|
pub struct Cons {
|
|
pub car: Value,
|
|
pub cdr: Value,
|
|
}
|
|
|
|
/// Most cons cells alive at once (about 64 bytes each): far above real routines (a list of
|
|
/// every entity of a large drawing, its `entget` lists), well below memory trouble.
|
|
pub const MAX_CELLS: usize = 5_000_000;
|
|
/// Longest string a built-in may make, in bytes.
|
|
pub const MAX_STRING: usize = 8 << 20;
|
|
/// Bytes of new strings, printed text and files read one run may make in all.
|
|
pub const STRING_BUDGET: u64 = 2 << 30;
|
|
|
|
/// Cons cells alive in the process.
|
|
static LIVE_CELLS: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
/// Tests that make or cap many cells run one at a time: the count is shared by the process.
|
|
#[cfg(test)]
|
|
pub(crate) static HEAVY_CELLS: Mutex<()> = Mutex::new(());
|
|
|
|
/// Cons cells alive now (all sessions).
|
|
pub fn live_cells() -> usize {
|
|
LIVE_CELLS.load(Ordering::Relaxed)
|
|
}
|
|
|
|
thread_local! {
|
|
/// The running slice's string budget: bytes made so far and the most allowed.
|
|
static STRING_BYTES: Cell<(u64, u64)> = const { Cell::new((0, STRING_BUDGET)) };
|
|
/// The running slice's cap on live cons cells.
|
|
static CELL_LIMIT: Cell<usize> = const { Cell::new(MAX_CELLS) };
|
|
}
|
|
|
|
/// Start a slice of a run on this thread: it fails when more than `limit` cells are alive.
|
|
pub fn set_cell_limit(limit: usize) {
|
|
CELL_LIMIT.with(|c| c.set(limit));
|
|
}
|
|
|
|
/// Too many cons cells are alive: the error that stops the run.
|
|
pub fn check_cells() -> Result<(), LispError> {
|
|
if live_cells() > CELL_LIMIT.with(Cell::get) { Err(LispError::quit("out of memory: too many list cells")) } else { Ok(()) }
|
|
}
|
|
|
|
/// Start a slice of a run on this thread: it has made `used` bytes of its `budget`.
|
|
pub fn set_string_budget(used: u64, budget: u64) {
|
|
STRING_BYTES.with(|c| c.set((used, budget)));
|
|
}
|
|
|
|
/// The bytes the run has made so far on this thread.
|
|
pub fn string_bytes_used() -> u64 {
|
|
STRING_BYTES.with(|c| c.get().0)
|
|
}
|
|
|
|
/// Count `n` new bytes (a string, printed text, a file read) against the run's budget.
|
|
pub fn charge_bytes(n: usize) -> Result<(), LispError> {
|
|
STRING_BYTES.with(|c| {
|
|
let (used, budget) = c.get();
|
|
let used = used.saturating_add(u64::try_from(n).unwrap_or(u64::MAX));
|
|
c.set((used, budget));
|
|
if used > budget { Err(LispError::quit("out of memory: the routine made too much text")) } else { Ok(()) }
|
|
})
|
|
}
|
|
|
|
/// A string of `len` bytes may be made.
|
|
pub fn check_string_len(len: usize) -> Result<(), LispError> {
|
|
if len > MAX_STRING { Err(LispError::quit(format!("out of memory: string longer than {MAX_STRING} bytes"))) } else { Ok(()) }
|
|
}
|
|
|
|
/// A built-in's result: a new string (one nothing else holds) is checked and charged.
|
|
pub fn charge_result(v: &Value) -> Result<(), LispError> {
|
|
if let Value::Str(s) = v
|
|
&& Arc::strong_count(s) == 1
|
|
{
|
|
check_string_len(s.len())?;
|
|
charge_bytes(s.len())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
impl Drop for Cons {
|
|
fn drop(&mut self) {
|
|
// Cells are only made by `Value::cons`, which counts them; never wrap below zero.
|
|
if LIVE_CELLS.fetch_sub(1, Ordering::Relaxed) == 0 {
|
|
LIVE_CELLS.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
// Unlink everything this cell alone holds (the rest of the list, nested lists in its
|
|
// car, function bodies) one cell at a time, so nothing recurses on the stack.
|
|
let mut work = Vec::new();
|
|
keep_for_drop(&mut work, std::mem::take(&mut self.car));
|
|
keep_for_drop(&mut work, std::mem::take(&mut self.cdr));
|
|
while let Some(v) = work.pop() {
|
|
match v {
|
|
Value::Cons(c) => {
|
|
if let Ok(mut cell) = Arc::try_unwrap(c) {
|
|
keep_for_drop(&mut work, std::mem::take(&mut cell.car));
|
|
keep_for_drop(&mut work, std::mem::take(&mut cell.cdr));
|
|
}
|
|
}
|
|
Value::Func(f) => {
|
|
if let Ok(mut func) = Arc::try_unwrap(f) {
|
|
keep_for_drop(&mut work, std::mem::take(&mut func.body));
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Values that can hold others go on the work list; atoms are dropped here.
|
|
fn keep_for_drop(work: &mut Vec<Value>, v: Value) {
|
|
if matches!(v, Value::Cons(_) | Value::Func(_)) {
|
|
work.push(v);
|
|
}
|
|
}
|
|
|
|
/// A selection set: the entities it holds, in order. `ssadd` and `ssdel` change it in place.
|
|
#[derive(Clone, Debug)]
|
|
pub struct PickSet {
|
|
pub id: u32,
|
|
pub items: Arc<Mutex<Vec<Handle>>>,
|
|
}
|
|
|
|
/// A function defined in LISP (`defun`, `lambda`).
|
|
#[derive(Debug)]
|
|
pub struct Func {
|
|
pub name: Option<Sym>,
|
|
pub params: Vec<Sym>,
|
|
/// Local variables (after the `/` in the argument list): bound to nil during a call.
|
|
pub locals: Vec<Sym>,
|
|
/// The body forms, as a list.
|
|
pub body: Value,
|
|
}
|
|
|
|
/// A built-in function. `special` forms get their arguments unevaluated.
|
|
pub struct Builtin {
|
|
pub name: &'static str,
|
|
pub kind: BuiltinKind,
|
|
}
|
|
|
|
impl std::fmt::Debug for Builtin {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "<Subr: {}>", self.name)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub enum BuiltinKind {
|
|
/// Evaluated arguments to a value.
|
|
Pure(fn(&[Value]) -> Result<Value, LispError>),
|
|
/// Evaluated arguments, with the interpreter (and its session) at hand.
|
|
Ctx(fn(&mut super::eval::Ctx<'_>, Vec<Value>) -> Result<super::eval::Flow, LispError>),
|
|
/// A special form, handled by the evaluator itself.
|
|
Special(super::eval::Special),
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub enum Value {
|
|
#[default]
|
|
Nil,
|
|
Int(i32),
|
|
Real(f64),
|
|
Str(Arc<str>),
|
|
Sym(Sym),
|
|
Cons(Arc<Cons>),
|
|
Ename(Handle),
|
|
PickSet(PickSet),
|
|
File(u32),
|
|
Subr(&'static Builtin),
|
|
Func(Arc<Func>),
|
|
}
|
|
|
|
impl Value {
|
|
pub fn t() -> Value {
|
|
Value::Sym(Sym::new("T"))
|
|
}
|
|
pub fn bool(b: bool) -> Value {
|
|
if b { Value::t() } else { Value::Nil }
|
|
}
|
|
pub fn str(s: &str) -> Value {
|
|
Value::Str(Arc::from(s))
|
|
}
|
|
pub fn sym(s: &str) -> Value {
|
|
Value::Sym(Sym::new(s))
|
|
}
|
|
pub fn cons(car: Value, cdr: Value) -> Value {
|
|
LIVE_CELLS.fetch_add(1, Ordering::Relaxed);
|
|
Value::Cons(Arc::new(Cons { car, cdr }))
|
|
}
|
|
pub fn is_nil(&self) -> bool {
|
|
matches!(self, Value::Nil)
|
|
}
|
|
/// A proper list of `items`.
|
|
pub fn list(items: impl IntoIterator<Item = Value, IntoIter: DoubleEndedIterator>) -> Value {
|
|
items.into_iter().rev().fold(Value::Nil, |acc, v| Value::cons(v, acc))
|
|
}
|
|
/// `items` followed by `tail` (a dotted list when `tail` is an atom).
|
|
pub fn list_with_tail(items: Vec<Value>, tail: Value) -> Value {
|
|
items.into_iter().rev().fold(tail, |acc, v| Value::cons(v, acc))
|
|
}
|
|
/// A point as a list of reals.
|
|
pub fn point(p: &[f64]) -> Value {
|
|
Value::list(p.iter().map(|v| Value::Real(*v)).collect::<Vec<_>>())
|
|
}
|
|
pub fn car(&self) -> Value {
|
|
match self {
|
|
Value::Cons(c) => c.car.clone(),
|
|
_ => Value::Nil,
|
|
}
|
|
}
|
|
pub fn cdr(&self) -> Value {
|
|
match self {
|
|
Value::Cons(c) => c.cdr.clone(),
|
|
_ => Value::Nil,
|
|
}
|
|
}
|
|
/// The elements of a list (a dotted tail is left out).
|
|
pub fn iter(&self) -> ListIter {
|
|
ListIter { next: self.clone() }
|
|
}
|
|
pub fn to_vec(&self) -> Vec<Value> {
|
|
self.iter().collect()
|
|
}
|
|
/// A proper list (nil or cons cells ending in nil).
|
|
pub fn is_list(&self) -> bool {
|
|
let mut v = self.clone();
|
|
loop {
|
|
match v {
|
|
Value::Nil => return true,
|
|
Value::Cons(c) => v = c.cdr.clone(),
|
|
_ => return false,
|
|
}
|
|
}
|
|
}
|
|
pub fn as_number(&self) -> Option<f64> {
|
|
match self {
|
|
Value::Int(i) => Some(f64::from(*i)),
|
|
Value::Real(r) => Some(*r),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub fn as_sym(&self) -> Option<&Sym> {
|
|
match self {
|
|
Value::Sym(s) => Some(s),
|
|
_ => None,
|
|
}
|
|
}
|
|
/// The type name `type` returns (nil for nil).
|
|
pub fn type_name(&self) -> Option<&'static str> {
|
|
Some(match self {
|
|
Value::Nil => return None,
|
|
Value::Int(_) => "INT",
|
|
Value::Real(_) => "REAL",
|
|
Value::Str(_) => "STR",
|
|
Value::Sym(_) => "SYM",
|
|
Value::Cons(_) => "LIST",
|
|
Value::Ename(_) => "ENAME",
|
|
Value::PickSet(_) => "PICKSET",
|
|
Value::File(_) => "FILE",
|
|
Value::Subr(_) => "SUBR",
|
|
Value::Func(_) => "USUBR",
|
|
})
|
|
}
|
|
/// A 2D or 3D point given as a list of two or three numbers.
|
|
pub fn as_point(&self) -> Option<[f64; 3]> {
|
|
let mut it = self.iter();
|
|
let x = it.next()?.as_number()?;
|
|
let y = it.next()?.as_number()?;
|
|
let z = match it.next() {
|
|
Some(v) => v.as_number()?,
|
|
None => 0.0,
|
|
};
|
|
(it.next().is_none() && self.is_list()).then_some([x, y, z])
|
|
}
|
|
}
|
|
|
|
pub struct ListIter {
|
|
next: Value,
|
|
}
|
|
|
|
impl Iterator for ListIter {
|
|
type Item = Value;
|
|
fn next(&mut self) -> Option<Value> {
|
|
match std::mem::take(&mut self.next) {
|
|
Value::Cons(c) => {
|
|
self.next = c.cdr.clone();
|
|
Some(c.car.clone())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Deepest nesting `equal`, printing and reading follow before giving up (hostile input).
|
|
pub const MAX_DEPTH: usize = 2_000;
|
|
|
|
/// `eq`: the same atom or the same cons cell.
|
|
pub fn eq(a: &Value, b: &Value) -> bool {
|
|
match (a, b) {
|
|
(Value::Cons(x), Value::Cons(y)) => Arc::ptr_eq(x, y),
|
|
(Value::Str(x), Value::Str(y)) => Arc::ptr_eq(x, y) || x == y,
|
|
(Value::PickSet(x), Value::PickSet(y)) => x.id == y.id,
|
|
(Value::Func(x), Value::Func(y)) => Arc::ptr_eq(x, y),
|
|
(Value::Subr(x), Value::Subr(y)) => std::ptr::eq(*x, *y),
|
|
_ => atom_equal(a, b, 0.0),
|
|
}
|
|
}
|
|
|
|
/// `equal`: the same structure, numbers within `fuzz`.
|
|
pub fn equal(a: &Value, b: &Value, fuzz: f64) -> bool {
|
|
equal_depth(a, b, fuzz, 0)
|
|
}
|
|
|
|
fn equal_depth(a: &Value, b: &Value, fuzz: f64, depth: usize) -> bool {
|
|
if depth > MAX_DEPTH {
|
|
return false;
|
|
}
|
|
let (mut a, mut b) = (a.clone(), b.clone());
|
|
loop {
|
|
match (&a, &b) {
|
|
(Value::Cons(x), Value::Cons(y)) => {
|
|
if !equal_depth(&x.car, &y.car, fuzz, depth + 1) {
|
|
return false;
|
|
}
|
|
let (na, nb) = (x.cdr.clone(), y.cdr.clone());
|
|
a = na;
|
|
b = nb;
|
|
}
|
|
_ => return atom_equal(&a, &b, fuzz),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn atom_equal(a: &Value, b: &Value, fuzz: f64) -> bool {
|
|
match (a, b) {
|
|
(Value::Nil, Value::Nil) => true,
|
|
(Value::Int(x), Value::Int(y)) => x == y,
|
|
(Value::Str(x), Value::Str(y)) => x == y,
|
|
(Value::Sym(x), Value::Sym(y)) => x == y,
|
|
(Value::Ename(x), Value::Ename(y)) => x == y,
|
|
(Value::File(x), Value::File(y)) => x == y,
|
|
(Value::PickSet(x), Value::PickSet(y)) => x.id == y.id,
|
|
(Value::Subr(x), Value::Subr(y)) => std::ptr::eq(*x, *y),
|
|
(Value::Func(x), Value::Func(y)) => Arc::ptr_eq(x, y),
|
|
(x, y) => match (x.as_number(), y.as_number()) {
|
|
(Some(p), Some(q)) if matches!(x, Value::Real(_)) || matches!(y, Value::Real(_)) || fuzz > 0.0 => (p - q).abs() <= fuzz,
|
|
_ => false,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// A real as AutoLISP prints it: six significant digits, always with a decimal point.
|
|
pub fn real_text(r: f64) -> String {
|
|
if !r.is_finite() {
|
|
return if r.is_nan() {
|
|
"-1.#IND".into()
|
|
} else if r > 0.0 {
|
|
"1.#INF".into()
|
|
} else {
|
|
"-1.#INF".into()
|
|
};
|
|
}
|
|
if r == 0.0 {
|
|
return "0.0".into();
|
|
}
|
|
let exp = r.abs().log10().floor() as i32;
|
|
// As C's %g with six digits: exponent form outside 1e-5 ..< 1e6.
|
|
if !(-5..6).contains(&exp) {
|
|
// Exponent form: d.ddddde+NNN
|
|
let m = r / 10f64.powi(exp);
|
|
let mut s = trim_fraction(&format!("{m:.5}"));
|
|
if !s.contains('.') {
|
|
s.push_str(".0");
|
|
}
|
|
return format!("{s}e{}{:03}", if exp < 0 { '-' } else { '+' }, exp.abs());
|
|
}
|
|
let decimals = (5 - exp).max(0) as usize;
|
|
let mut s = trim_fraction(&format!("{r:.decimals$}"));
|
|
if !s.contains('.') {
|
|
s.push_str(".0");
|
|
}
|
|
s
|
|
}
|
|
|
|
fn trim_fraction(s: &str) -> String {
|
|
if s.contains('.') {
|
|
let t = s.trim_end_matches('0');
|
|
t.strip_suffix('.').map_or(t.to_string(), |u| format!("{u}.0"))
|
|
} else {
|
|
s.to_string()
|
|
}
|
|
}
|
|
|
|
/// `prin1`/`print` text (`escape`: strings quoted with their escapes) or `princ` text.
|
|
pub fn to_text(v: &Value, escape: bool) -> String {
|
|
let mut out = String::new();
|
|
write_value(&mut out, v, escape, 0);
|
|
if out.len() > MAX_STRING {
|
|
let mut end = MAX_STRING;
|
|
while !out.is_char_boundary(end) {
|
|
end -= 1;
|
|
}
|
|
out.truncate(end);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn write_value(out: &mut String, v: &Value, escape: bool, depth: usize) {
|
|
// Text past the string cap is cut (a list sharing its parts can print exponentially long).
|
|
if out.len() > MAX_STRING {
|
|
return;
|
|
}
|
|
if depth > MAX_DEPTH {
|
|
out.push_str("...");
|
|
return;
|
|
}
|
|
match v {
|
|
Value::Nil => out.push_str("nil"),
|
|
Value::Int(i) => out.push_str(&i.to_string()),
|
|
Value::Real(r) => out.push_str(&real_text(*r)),
|
|
Value::Str(s) => {
|
|
if escape {
|
|
out.push('"');
|
|
for c in s.chars() {
|
|
match c {
|
|
'"' => out.push_str("\\\""),
|
|
'\\' => out.push_str("\\\\"),
|
|
'\n' => out.push_str("\\n"),
|
|
'\t' => out.push_str("\\t"),
|
|
'\r' => out.push_str("\\r"),
|
|
'\u{1b}' => out.push_str("\\e"),
|
|
c => out.push(c),
|
|
}
|
|
}
|
|
out.push('"');
|
|
} else {
|
|
out.push_str(s);
|
|
}
|
|
}
|
|
Value::Sym(s) => out.push_str(s.name()),
|
|
Value::Cons(_) => {
|
|
// (quote x) prints as 'x.
|
|
if let (Value::Sym(s), Value::Cons(rest)) = (v.car(), v.cdr())
|
|
&& s.name() == "QUOTE"
|
|
&& rest.cdr.is_nil()
|
|
{
|
|
out.push('\'');
|
|
write_value(out, &rest.car, escape, depth + 1);
|
|
return;
|
|
}
|
|
out.push('(');
|
|
let mut cur = v.clone();
|
|
let mut first = true;
|
|
let mut count = 0usize;
|
|
loop {
|
|
match cur {
|
|
Value::Cons(c) => {
|
|
if !first {
|
|
out.push(' ');
|
|
}
|
|
first = false;
|
|
write_value(out, &c.car, escape, depth + 1);
|
|
cur = c.cdr.clone();
|
|
count += 1;
|
|
if count > 1_000_000 || out.len() > MAX_STRING {
|
|
out.push_str(" ...");
|
|
break;
|
|
}
|
|
}
|
|
Value::Nil => break,
|
|
other => {
|
|
out.push_str(" . ");
|
|
write_value(out, &other, escape, depth + 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
out.push(')');
|
|
}
|
|
Value::Ename(h) => out.push_str(&format!("<Entity name: {:x}>", h.0)),
|
|
Value::PickSet(p) => out.push_str(&format!("<Selection set: {:x}>", p.id)),
|
|
Value::File(f) => out.push_str(&format!("#<FILE {f:x}>")),
|
|
Value::Subr(b) => out.push_str(&format!("#<SUBR @{} {}>", "0", b.name)),
|
|
Value::Func(f) => out.push_str(&format!("#<USUBR {}>", f.name.as_ref().map_or("-lambda-", |s| s.name()))),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn reals_print_with_six_significant_digits() {
|
|
assert_eq!(real_text(1.0), "1.0");
|
|
assert_eq!(real_text(std::f64::consts::PI), "3.14159");
|
|
assert_eq!(real_text(1.23456789), "1.23457");
|
|
assert_eq!(real_text(0.1), "0.1");
|
|
assert_eq!(real_text(12.5), "12.5");
|
|
assert_eq!(real_text(-2.0), "-2.0");
|
|
assert_eq!(real_text(100000.0), "100000.0");
|
|
assert_eq!(real_text(123456.7), "123457.0");
|
|
assert_eq!(real_text(0.0001), "0.0001");
|
|
assert_eq!(real_text(1_000_000.0), "1.0e+006");
|
|
assert_eq!(real_text(0.000001), "1.0e-006");
|
|
}
|
|
|
|
#[test]
|
|
fn a_long_list_drops_without_overflowing_the_stack() {
|
|
let _heavy = HEAVY_CELLS.lock().unwrap_or_else(|e| e.into_inner());
|
|
let mut l = Value::Nil;
|
|
for i in 0..2_000_000 {
|
|
l = Value::cons(Value::Int(i), l);
|
|
}
|
|
drop(l);
|
|
}
|
|
|
|
#[test]
|
|
fn printing_and_equality() {
|
|
let l = Value::list(vec![Value::Int(1), Value::str("a\"b"), Value::cons(Value::Int(8), Value::str("0"))]);
|
|
assert_eq!(to_text(&l, true), "(1 \"a\\\"b\" (8 . \"0\"))");
|
|
assert_eq!(to_text(&l, false), "(1 a\"b (8 . 0))");
|
|
assert!(equal(&l, &l.clone(), 0.0));
|
|
assert!(equal(&Value::Real(1.0), &Value::Real(1.0000001), 1e-3));
|
|
assert!(!equal(&Value::Real(1.0), &Value::Real(1.1), 1e-3));
|
|
assert!(eq(&Value::sym("a"), &Value::sym("A")));
|
|
}
|
|
}
|