mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
EPS files the reference app saves (its procsets in the prolog, a binary header with a TIFF preview) now open as editable art instead of failing with "undefinedresource in Generic". When their PostScript still can't be read, the palette preview they carry is placed with the usual warning instead of the file being refused. Root cause: five interpreter gaps and one preview format. The Generic and Category resource categories didn't exist, so categories couldn't be made from Generic (now Category holds the standard categories, defineresource in Category makes new ones, undefineresource is added); clipsave/cliprestore were unknown (now a clip stack in the graphics state); systemdict's languagelevel was the operator, not the integer 3; getinterval copied arrays and strings instead of sharing their storage, so astore into a subarray was lost (arrays and strings are now views into shared storage, and readstring/readline return the part they fill); and resourceforall was unknown (now enumerates matching names with * ? \ templates). The preview is an 8-bit palette TIFF with an alpha sample, which the image crate can't decode: a small bounds-checked reader handles that layout (either byte order, any strips). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1155 lines
44 KiB
Rust
1155 lines
44 KiB
Rust
//! The interpreter: the operand and dictionary stacks, running objects and procedures, and the
|
|
//! operators that don't draw (stack, arithmetic, control, composites, dictionaries).
|
|
//!
|
|
//! Every operation counts against a budget and procedures nest only so deep, so a looping or
|
|
//! recursing program ends with [`PsError::Limit`] instead of hanging.
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use vectorcraft_geom::Affine;
|
|
|
|
use super::graphics::{GState, Out};
|
|
use super::lex::Lexer;
|
|
use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err};
|
|
|
|
/// Most operations one file may run.
|
|
const MAX_OPS: u64 = 50_000_000;
|
|
/// Deepest procedure nesting.
|
|
const MAX_DEPTH: u32 = 400;
|
|
/// Most operands on the stack.
|
|
const MAX_STACK: usize = 100_000;
|
|
/// Largest array, string or dictionary a program may make.
|
|
pub(crate) const MAX_ALLOC: usize = 1 << 22;
|
|
/// Most dictionaries on the dictionary stack.
|
|
const MAX_DICTS: usize = 1000;
|
|
/// Most saved graphics states.
|
|
pub(crate) const MAX_GSAVE: usize = 1000;
|
|
/// Most memory (bytes) a program may ask for in all: strings, arrays, decoded data, images,
|
|
/// saved paths.
|
|
const MAX_MEMORY: usize = 1 << 30;
|
|
|
|
pub(crate) struct Interp<'a> {
|
|
pub lex: Lexer<'a>,
|
|
pub stack: Vec<Obj>,
|
|
pub dicts: Vec<DictRef>,
|
|
system: DictRef,
|
|
user: DictRef,
|
|
global: DictRef,
|
|
status: DictRef,
|
|
error: DictRef,
|
|
/// `FontDirectory`: fonts defined with `definefont`.
|
|
pub fonts: DictRef,
|
|
/// Resources by category (`defineresource`).
|
|
resources: DictRef,
|
|
depth: u32,
|
|
ops: u64,
|
|
/// Bytes asked for so far (see [`MAX_MEMORY`]).
|
|
allocated: usize,
|
|
seed: u32,
|
|
pub g: GState,
|
|
pub saved: Vec<GState>,
|
|
pub out: Out,
|
|
}
|
|
|
|
fn new_dict() -> DictRef {
|
|
Rc::new(RefCell::new(Dict::new()))
|
|
}
|
|
|
|
/// An encoding vector: 256 `/.notdef` names (type is read as Latin-1 whatever the encoding).
|
|
fn encoding() -> Obj {
|
|
Obj::array(vec![Obj::name(".notdef"); 256])
|
|
}
|
|
|
|
/// The resource categories there are to begin with (instances of `Category`).
|
|
const CATEGORIES: &str = "Category Generic Font CIDFont CMap FontSet Encoding Form Pattern ProcSet ColorSpace Halftone \r
|
|
ColorRendering IdiomSet InkParams TrapParams OutputDevice ControlLanguage Localization PDL HWOptions \r
|
|
Filter ColorSpaceFamily Emulator IODevice ColorRenderingType FMapType FontType FormType HalftoneType \r
|
|
ImageType PatternType FunctionType ShadingType";
|
|
|
|
impl<'a> Interp<'a> {
|
|
pub fn new(src: &'a [u8], g: GState, out: Out) -> Self {
|
|
let system = new_dict();
|
|
{
|
|
let mut d = system.borrow_mut();
|
|
for (op, name) in Op::ALL {
|
|
d.insert(Key::name(name), Obj::Op(*op));
|
|
}
|
|
for name in ["StandardEncoding", "ISOLatin1Encoding"] {
|
|
d.insert(Key::name(name), encoding());
|
|
}
|
|
// An integer, as interpreters have it (`systemdict /languagelevel get 2 ge`): run, the
|
|
// name pushes it all the same.
|
|
d.insert(Key::name("languagelevel"), Obj::Int(3));
|
|
}
|
|
// Each category's implementation dictionary (`/Generic /Category findresource`).
|
|
let categories: Dict =
|
|
CATEGORIES.split_whitespace().map(|c| (Key::name(c), Obj::dict(Dict::from([(Key::name("Category"), Obj::name(c))])))).collect();
|
|
let resources = new_dict();
|
|
resources.borrow_mut().insert(Key::name("Category"), Obj::dict(categories));
|
|
let fonts = new_dict();
|
|
system.borrow_mut().insert(Key::name("FontDirectory"), Obj::Dict(fonts.clone()));
|
|
system.borrow_mut().insert(Key::name("GlobalFontDirectory"), Obj::Dict(fonts.clone()));
|
|
let (user, global) = (new_dict(), new_dict());
|
|
Self {
|
|
lex: Lexer::new(src),
|
|
stack: vec![],
|
|
dicts: vec![system.clone(), global.clone(), user.clone()],
|
|
system,
|
|
user,
|
|
global,
|
|
status: new_dict(),
|
|
error: new_dict(),
|
|
fonts,
|
|
resources,
|
|
depth: 0,
|
|
ops: 0,
|
|
allocated: 0,
|
|
seed: 1,
|
|
g,
|
|
saved: vec![],
|
|
out,
|
|
}
|
|
}
|
|
|
|
/// Run the program to its end, its first `showpage` or `quit`.
|
|
pub fn run(&mut self) -> Res {
|
|
loop {
|
|
let Some(o) = self.lex.next()? else { return Ok(()) };
|
|
match self.exec_token(o) {
|
|
Err(PsError::Quit) => return Ok(()),
|
|
r => r?,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- the operand stack ----------
|
|
|
|
pub fn push(&mut self, o: Obj) -> Res {
|
|
if self.stack.len() >= MAX_STACK {
|
|
return Err(PsError::Limit("the operand stack overflowed"));
|
|
}
|
|
self.stack.push(o);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn push_num(&mut self, v: f64) -> Res {
|
|
self.push(if v.is_finite() { Obj::Real(v) } else { Obj::Real(0.0) })
|
|
}
|
|
|
|
pub fn pop(&mut self) -> Res<Obj> {
|
|
self.stack.pop().ok_or(PsError::Ps("stackunderflow", String::new()))
|
|
}
|
|
|
|
pub fn pop_num(&mut self) -> Res<f64> {
|
|
match self.pop()?.as_num() {
|
|
Some(v) if v.is_finite() => Ok(v),
|
|
Some(_) => ps_err("undefinedresult", ""),
|
|
None => ps_err("typecheck", "a number"),
|
|
}
|
|
}
|
|
|
|
/// `N` numbers, in the order they were pushed.
|
|
pub fn nums<const N: usize>(&mut self) -> Res<[f64; N]> {
|
|
let mut out = [0.0; N];
|
|
for slot in out.iter_mut().rev() {
|
|
*slot = self.pop_num()?;
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
pub fn pop_int(&mut self) -> Res<i64> {
|
|
match self.pop()? {
|
|
Obj::Int(i) => Ok(i),
|
|
Obj::Real(r) if r.fract() == 0.0 && r.abs() < 1e15 => Ok(r as i64),
|
|
_ => ps_err("typecheck", "an integer"),
|
|
}
|
|
}
|
|
|
|
/// A count or index: a non-negative integer, at most [`MAX_ALLOC`].
|
|
pub fn pop_count(&mut self) -> Res<usize> {
|
|
let i = self.pop_int()?;
|
|
if !(0..=MAX_ALLOC as i64).contains(&i) {
|
|
return ps_err("rangecheck", "a count");
|
|
}
|
|
Ok(i as usize)
|
|
}
|
|
|
|
pub fn pop_bool(&mut self) -> Res<bool> {
|
|
match self.pop()? {
|
|
Obj::Bool(b) => Ok(b),
|
|
_ => ps_err("typecheck", "a boolean"),
|
|
}
|
|
}
|
|
|
|
pub fn pop_dict(&mut self) -> Res<DictRef> {
|
|
match self.pop()? {
|
|
Obj::Dict(d) => Ok(d),
|
|
_ => ps_err("typecheck", "a dictionary"),
|
|
}
|
|
}
|
|
|
|
pub fn pop_str(&mut self) -> Res<Shared<u8>> {
|
|
match self.pop()? {
|
|
Obj::Str(s) => Ok(s),
|
|
_ => ps_err("typecheck", "a string"),
|
|
}
|
|
}
|
|
|
|
/// An array or procedure.
|
|
pub fn pop_array(&mut self) -> Res<Shared<Obj>> {
|
|
match self.pop()? {
|
|
Obj::Array { items, .. } => Ok(items),
|
|
_ => ps_err("typecheck", "an array"),
|
|
}
|
|
}
|
|
|
|
/// A matrix operand (an array of six numbers).
|
|
pub fn pop_matrix(&mut self) -> Res<Affine> {
|
|
let items = self.pop_array()?;
|
|
matrix_of(&items.borrow()).ok_or(PsError::Ps("typecheck", "a matrix".into()))
|
|
}
|
|
|
|
/// Store `m` into the matrix operand `items` and push it back.
|
|
pub fn put_matrix(&mut self, items: Shared<Obj>, m: Affine) -> Res {
|
|
if items.len() != 6 || !items.write(0, &m.as_coeffs().map(Obj::Real)) {
|
|
return ps_err("rangecheck", "a matrix");
|
|
}
|
|
self.push(Obj::Array { items, exec: false })
|
|
}
|
|
|
|
// ---------- names and running ----------
|
|
|
|
/// The value of name `name` on the dictionary stack.
|
|
pub fn lookup(&self, name: &Rc<str>) -> Option<Obj> {
|
|
let key = Key::Name(name.clone());
|
|
self.dicts.iter().rev().find_map(|d| d.borrow().get(&key).cloned())
|
|
}
|
|
|
|
/// Count `bytes` the program makes against [`MAX_MEMORY`].
|
|
pub fn alloc(&mut self, bytes: usize) -> Res {
|
|
self.allocated = self.allocated.saturating_add(bytes);
|
|
if self.allocated > MAX_MEMORY {
|
|
return Err(PsError::Limit("the program uses too much memory"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn tick(&mut self) -> Res {
|
|
self.ops += 1;
|
|
if self.ops > MAX_OPS {
|
|
return Err(PsError::Limit("the program runs too long"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// An object as read or met in a procedure body: executable names and operators run, the
|
|
/// rest (procedures too) is pushed.
|
|
fn exec_token(&mut self, o: Obj) -> Res {
|
|
self.tick()?;
|
|
match o {
|
|
Obj::Exec(name) => {
|
|
let v = self.lookup(&name).ok_or_else(|| PsError::Ps("undefined", name.to_string()))?;
|
|
self.call(v)
|
|
}
|
|
Obj::Op(op) => self.op(op),
|
|
o => self.push(o),
|
|
}
|
|
}
|
|
|
|
/// Run a value (what a name stands for, or `exec`'s operand): procedures and operators run,
|
|
/// executable names are looked up, the rest is pushed.
|
|
pub fn call(&mut self, v: Obj) -> Res {
|
|
match v {
|
|
Obj::Array { items, exec: true } => self.run_proc(&items),
|
|
Obj::Op(op) => self.op(op),
|
|
Obj::Exec(name) => {
|
|
self.enter()?;
|
|
let r = match self.lookup(&name) {
|
|
Some(v) => self.call(v),
|
|
None => ps_err("undefined", &name),
|
|
};
|
|
self.depth -= 1;
|
|
r
|
|
}
|
|
o => self.push(o),
|
|
}
|
|
}
|
|
|
|
fn enter(&mut self) -> Res {
|
|
if self.depth >= MAX_DEPTH {
|
|
return Err(PsError::Limit("procedures nest too deeply"));
|
|
}
|
|
self.depth += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn run_proc(&mut self, items: &Shared<Obj>) -> Res {
|
|
self.enter()?;
|
|
let r = self.run_items(items);
|
|
self.depth -= 1;
|
|
r
|
|
}
|
|
|
|
fn run_items(&mut self, items: &Shared<Obj>) -> Res {
|
|
let mut i = 0;
|
|
loop {
|
|
let Some(o) = items.get(i) else { return Ok(()) };
|
|
i += 1;
|
|
self.exec_token(o)?;
|
|
}
|
|
}
|
|
|
|
/// Run `proc` as a loop body: `Ok(false)` when it ran `exit`.
|
|
pub fn body(&mut self, proc: &Obj) -> Res<bool> {
|
|
self.tick()?;
|
|
match self.call(proc.clone()) {
|
|
Ok(()) => Ok(true),
|
|
Err(PsError::Exit) => Ok(false),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Run a procedure operand, executable or not (`exec`, `if`, loops).
|
|
pub fn pop_proc(&mut self) -> Res<Obj> {
|
|
match self.pop()? {
|
|
Obj::Array { items, .. } => Ok(Obj::Array { items, exec: true }),
|
|
o @ (Obj::Op(_) | Obj::Exec(_)) => Ok(o),
|
|
_ => ps_err("typecheck", "a procedure"),
|
|
}
|
|
}
|
|
|
|
// ---------- operators ----------
|
|
|
|
pub fn op(&mut self, op: Op) -> Res {
|
|
self.op_inner(op).map_err(|e| match e {
|
|
PsError::Ps(name, at) if at.is_empty() => PsError::Ps(name, op.name().to_string()),
|
|
e => e,
|
|
})
|
|
}
|
|
|
|
fn op_inner(&mut self, op: Op) -> Res {
|
|
use Op::*;
|
|
match op {
|
|
Pop => {
|
|
self.pop()?;
|
|
}
|
|
Exch => {
|
|
let (b, a) = (self.pop()?, self.pop()?);
|
|
self.push(b)?;
|
|
self.push(a)?;
|
|
}
|
|
Dup => {
|
|
let a = self.stack.last().cloned().ok_or(PsError::Ps("stackunderflow", String::new()))?;
|
|
self.push(a)?;
|
|
}
|
|
Copy => self.copy()?,
|
|
Index => {
|
|
let n = self.pop_count()?;
|
|
let at = self.stack.len().checked_sub(n + 1).ok_or(PsError::Ps("rangecheck", String::new()))?;
|
|
let o = self.stack.get(at).cloned().ok_or(PsError::Ps("rangecheck", String::new()))?;
|
|
self.push(o)?;
|
|
}
|
|
Roll => {
|
|
let j = self.pop_int()?;
|
|
let n = self.pop_count()?;
|
|
let at = self.stack.len().checked_sub(n).ok_or(PsError::Ps("rangecheck", String::new()))?;
|
|
if n > 0 {
|
|
let k = j.rem_euclid(n as i64) as usize;
|
|
if let Some(s) = self.stack.get_mut(at..) {
|
|
s.rotate_right(k);
|
|
}
|
|
}
|
|
}
|
|
Clear => self.stack.clear(),
|
|
Count => self.push(Obj::Int(self.stack.len() as i64))?,
|
|
Mark | MarkBracket | DictBegin => self.push(Obj::Mark)?,
|
|
CloseArray => {
|
|
let items = self.pop_to_mark()?;
|
|
self.push(Obj::array(items))?;
|
|
}
|
|
DictEnd => {
|
|
let items = self.pop_to_mark()?;
|
|
let mut d = Dict::new();
|
|
for pair in items.chunks(2) {
|
|
let [k, v] = pair else { return ps_err("rangecheck", ">>") };
|
|
d.insert(k.key().ok_or(PsError::Ps("typecheck", ">>".into()))?, v.clone());
|
|
}
|
|
self.push(Obj::dict(d))?;
|
|
}
|
|
ClearToMark => {
|
|
self.pop_to_mark()?;
|
|
}
|
|
CountToMark => {
|
|
let n = self.stack.iter().rev().position(|o| matches!(o, Obj::Mark)).ok_or(PsError::Ps("unmatchedmark", String::new()))?;
|
|
self.push(Obj::Int(n as i64))?;
|
|
}
|
|
Add | Sub | Mul => {
|
|
let (b, a) = (self.pop()?, self.pop()?);
|
|
let r = match (&a, &b) {
|
|
(Obj::Int(x), Obj::Int(y)) => match op {
|
|
Add => x.checked_add(*y).map(Obj::Int),
|
|
Sub => x.checked_sub(*y).map(Obj::Int),
|
|
_ => x.checked_mul(*y).map(Obj::Int),
|
|
},
|
|
_ => None,
|
|
};
|
|
let r = match r {
|
|
Some(r) => r,
|
|
None => {
|
|
let (x, y) =
|
|
(a.as_num().ok_or(PsError::Ps("typecheck", String::new()))?, b.as_num().ok_or(PsError::Ps("typecheck", String::new()))?);
|
|
Obj::Real(match op {
|
|
Add => x + y,
|
|
Sub => x - y,
|
|
_ => x * y,
|
|
})
|
|
}
|
|
};
|
|
self.push(r)?;
|
|
}
|
|
Div => {
|
|
let [a, b] = self.nums()?;
|
|
if b == 0.0 {
|
|
return ps_err("undefinedresult", "");
|
|
}
|
|
self.push_num(a / b)?;
|
|
}
|
|
Idiv | Mod => {
|
|
let (b, a) = (self.pop_int()?, self.pop_int()?);
|
|
let r = if op == Idiv { a.checked_div(b) } else { a.checked_rem(b) };
|
|
self.push(Obj::Int(r.ok_or(PsError::Ps("undefinedresult", String::new()))?))?;
|
|
}
|
|
Neg | Abs => match self.pop()? {
|
|
Obj::Int(i) => self.push(Obj::Int(if op == Neg { i.saturating_neg() } else { i.saturating_abs() }))?,
|
|
Obj::Real(r) => self.push(Obj::Real(if op == Neg { -r } else { r.abs() }))?,
|
|
_ => return ps_err("typecheck", ""),
|
|
},
|
|
Sqrt | Sin | Cos | Exp | Ln | Log | Atan => {
|
|
let r = match op {
|
|
Sqrt => self.pop_num()?.sqrt(),
|
|
Sin => self.pop_num()?.to_radians().sin(),
|
|
Cos => self.pop_num()?.to_radians().cos(),
|
|
Ln => self.pop_num()?.ln(),
|
|
Log => self.pop_num()?.log10(),
|
|
Exp => {
|
|
let [b, e] = self.nums()?;
|
|
b.powf(e)
|
|
}
|
|
_ => {
|
|
let [num, den] = self.nums()?;
|
|
num.atan2(den).to_degrees().rem_euclid(360.0)
|
|
}
|
|
};
|
|
if !r.is_finite() {
|
|
return ps_err("undefinedresult", "");
|
|
}
|
|
self.push(Obj::Real(r))?;
|
|
}
|
|
Round | Floor | Ceiling | Truncate => match self.pop()? {
|
|
Obj::Int(i) => self.push(Obj::Int(i))?,
|
|
Obj::Real(r) => self.push(Obj::Real(match op {
|
|
Round => (r + 0.5).floor(),
|
|
Floor => r.floor(),
|
|
Ceiling => r.ceil(),
|
|
_ => r.trunc(),
|
|
}))?,
|
|
_ => return ps_err("typecheck", ""),
|
|
},
|
|
Cvi => {
|
|
let v = match self.pop()? {
|
|
Obj::Str(s) => String::from_utf8_lossy(&s.borrow()).trim().parse::<f64>().map_err(|_| PsError::Ps("typecheck", String::new()))?,
|
|
o => o.as_num().ok_or(PsError::Ps("typecheck", String::new()))?,
|
|
};
|
|
if !(v.is_finite() && v.abs() < 9e15) {
|
|
return ps_err("rangecheck", "");
|
|
}
|
|
self.push(Obj::Int(v.trunc() as i64))?;
|
|
}
|
|
Cvr => {
|
|
let v = match self.pop()? {
|
|
Obj::Str(s) => String::from_utf8_lossy(&s.borrow()).trim().parse::<f64>().map_err(|_| PsError::Ps("typecheck", String::new()))?,
|
|
o => o.as_num().ok_or(PsError::Ps("typecheck", String::new()))?,
|
|
};
|
|
self.push_num(v)?;
|
|
}
|
|
Rand => {
|
|
// A small linear congruential generator: deterministic, like a fresh printer.
|
|
self.seed = self.seed.wrapping_mul(1_103_515_245).wrapping_add(12345);
|
|
self.push(Obj::Int(i64::from(self.seed >> 1)))?;
|
|
}
|
|
Srand => self.seed = self.pop_int()? as u32,
|
|
Rrand => self.push(Obj::Int(i64::from(self.seed)))?,
|
|
Eq | Ne => {
|
|
let (b, a) = (self.pop()?, self.pop()?);
|
|
let same = equal(&a, &b);
|
|
self.push(Obj::Bool(same == (op == Eq)))?;
|
|
}
|
|
Gt | Ge | Lt | Le => {
|
|
let (b, a) = (self.pop()?, self.pop()?);
|
|
let ord = match (&a, &b) {
|
|
(Obj::Str(x), Obj::Str(y)) => x.borrow().partial_cmp(&*y.borrow()),
|
|
_ => a.as_num().zip(b.as_num()).and_then(|(x, y)| x.partial_cmp(&y)),
|
|
}
|
|
.ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
let r = match op {
|
|
Gt => ord.is_gt(),
|
|
Ge => ord.is_ge(),
|
|
Lt => ord.is_lt(),
|
|
_ => ord.is_le(),
|
|
};
|
|
self.push(Obj::Bool(r))?;
|
|
}
|
|
And | Or | Xor => {
|
|
let (b, a) = (self.pop()?, self.pop()?);
|
|
let r = match (a, b) {
|
|
(Obj::Bool(x), Obj::Bool(y)) => Obj::Bool(match op {
|
|
And => x && y,
|
|
Or => x || y,
|
|
_ => x ^ y,
|
|
}),
|
|
(Obj::Int(x), Obj::Int(y)) => Obj::Int(match op {
|
|
And => x & y,
|
|
Or => x | y,
|
|
_ => x ^ y,
|
|
}),
|
|
_ => return ps_err("typecheck", ""),
|
|
};
|
|
self.push(r)?;
|
|
}
|
|
Not => match self.pop()? {
|
|
Obj::Bool(b) => self.push(Obj::Bool(!b))?,
|
|
Obj::Int(i) => self.push(Obj::Int(!i))?,
|
|
_ => return ps_err("typecheck", ""),
|
|
},
|
|
Bitshift => {
|
|
let (s, v) = (self.pop_int()?, self.pop_int()?);
|
|
let r = if s >= 0 { v.checked_shl(s.min(63) as u32) } else { v.checked_shr((-s).min(63) as u32) };
|
|
self.push(Obj::Int(r.unwrap_or(0)))?;
|
|
}
|
|
True => self.push(Obj::Bool(true))?,
|
|
False => self.push(Obj::Bool(false))?,
|
|
Null => self.push(Obj::Null)?,
|
|
Exec => {
|
|
let o = self.pop()?;
|
|
self.call(o)?;
|
|
}
|
|
If => {
|
|
let p = self.pop_proc()?;
|
|
if self.pop_bool()? {
|
|
self.call(p)?;
|
|
}
|
|
}
|
|
IfElse => {
|
|
let (no, yes) = (self.pop_proc()?, self.pop_proc()?);
|
|
let pick = if self.pop_bool()? { yes } else { no };
|
|
self.call(pick)?;
|
|
}
|
|
For => {
|
|
let p = self.pop_proc()?;
|
|
let (limit, inc, init) = (self.pop()?, self.pop()?, self.pop()?);
|
|
match (&init, &inc, &limit) {
|
|
(Obj::Int(i0), Obj::Int(d), Obj::Int(l)) => {
|
|
let (mut i, d, l) = (*i0, *d, *l);
|
|
while (d >= 0 && i <= l) || (d < 0 && i >= l) {
|
|
self.push(Obj::Int(i))?;
|
|
if !self.body(&p)? {
|
|
break;
|
|
}
|
|
i = i.checked_add(d).ok_or(PsError::Ps("rangecheck", String::new()))?;
|
|
if d == 0 {
|
|
self.tick()?;
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
let n = |o: &Obj| o.as_num().filter(|v| v.is_finite()).ok_or(PsError::Ps("typecheck", String::new()));
|
|
let (i0, d, l) = (n(&init)?, n(&inc)?, n(&limit)?);
|
|
let mut k = 0.0;
|
|
loop {
|
|
let i = i0 + d * k;
|
|
if !((d >= 0.0 && i <= l) || (d < 0.0 && i >= l)) {
|
|
break;
|
|
}
|
|
self.push(Obj::Real(i))?;
|
|
if !self.body(&p)? {
|
|
break;
|
|
}
|
|
k += 1.0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Repeat => {
|
|
let p = self.pop_proc()?;
|
|
let n = self.pop_int()?;
|
|
if n < 0 {
|
|
return ps_err("rangecheck", "");
|
|
}
|
|
for _ in 0..n {
|
|
if !self.body(&p)? {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Loop => {
|
|
let p = self.pop_proc()?;
|
|
while self.body(&p)? {}
|
|
}
|
|
Exit => return Err(PsError::Exit),
|
|
Forall => self.forall()?,
|
|
Stopped => {
|
|
let p = self.pop()?;
|
|
let depth = self.depth;
|
|
match self.call(p) {
|
|
Ok(()) => self.push(Obj::Bool(false))?,
|
|
Err(PsError::Ps(..) | PsError::Stop | PsError::Exit) => {
|
|
self.depth = depth;
|
|
self.push(Obj::Bool(true))?;
|
|
}
|
|
Err(e) => return Err(e),
|
|
}
|
|
}
|
|
Stop => return Err(PsError::Stop),
|
|
Quit => return Err(PsError::Quit),
|
|
CountExecStack => self.push(Obj::Int(i64::from(self.depth) + 1))?,
|
|
Type => {
|
|
let o = self.pop()?;
|
|
self.push(Obj::Exec(Rc::from(o.type_name())))?;
|
|
}
|
|
Cvx => {
|
|
let o = match self.pop()? {
|
|
Obj::Array { items, .. } => Obj::Array { items, exec: true },
|
|
Obj::Name(n) => Obj::Exec(n),
|
|
o => o,
|
|
};
|
|
self.push(o)?;
|
|
}
|
|
Cvlit => {
|
|
let o = match self.pop()? {
|
|
Obj::Array { items, .. } => Obj::Array { items, exec: false },
|
|
Obj::Exec(n) => Obj::Name(n),
|
|
o => o,
|
|
};
|
|
self.push(o)?;
|
|
}
|
|
Xcheck => {
|
|
let o = self.pop()?;
|
|
self.push(Obj::Bool(matches!(o, Obj::Array { exec: true, .. } | Obj::Exec(_) | Obj::Op(_))))?;
|
|
}
|
|
Cvn => {
|
|
let s = self.pop_str()?;
|
|
let n: Rc<str> = Rc::from(String::from_utf8_lossy(&s.borrow()).as_ref());
|
|
self.push(Obj::Name(n))?;
|
|
}
|
|
Cvs | Cvrs => {
|
|
let s = self.pop_str()?;
|
|
let text = if op == Cvrs {
|
|
let radix = self.pop_int()?;
|
|
let v = self.pop_num()?;
|
|
if !(2..=36).contains(&radix) {
|
|
return ps_err("rangecheck", "");
|
|
}
|
|
radix_text(v as i64, radix as u32)
|
|
} else {
|
|
show_text(&self.pop()?)
|
|
};
|
|
if !s.write(0, text.as_bytes()) {
|
|
return ps_err("rangecheck", "");
|
|
}
|
|
self.push_interval(Obj::Str(s), 0, text.len())?;
|
|
}
|
|
ReadOnly | ExecuteOnly | NoAccess | SetPacking | SetGlobal | SetObjectFormat => {
|
|
if matches!(op, SetPacking | SetGlobal | SetObjectFormat) {
|
|
self.pop()?;
|
|
}
|
|
}
|
|
Rcheck | Wcheck => {
|
|
self.pop()?;
|
|
self.push(Obj::Bool(true))?;
|
|
}
|
|
CurrentPacking | CurrentGlobal => self.push(Obj::Bool(false))?,
|
|
NewArray => {
|
|
let n = self.pop_count()?;
|
|
self.alloc(n * std::mem::size_of::<Obj>())?;
|
|
self.push(Obj::array(vec![Obj::Null; n]))?;
|
|
}
|
|
PackedArray => {
|
|
let n = self.pop_count()?;
|
|
let at = self.stack.len().checked_sub(n).ok_or(PsError::Ps("stackunderflow", String::new()))?;
|
|
let items = self.stack.split_off(at);
|
|
self.push(Obj::array(items))?;
|
|
}
|
|
NewString => {
|
|
let n = self.pop_count()?;
|
|
self.alloc(n)?;
|
|
self.push(Obj::string(vec![0; n]))?;
|
|
}
|
|
Length => {
|
|
let n = match self.pop()? {
|
|
Obj::Array { items, .. } => items.len(),
|
|
Obj::Str(s) => s.len(),
|
|
Obj::Dict(d) => d.borrow().len(),
|
|
Obj::Name(n) | Obj::Exec(n) => n.len(),
|
|
_ => return ps_err("typecheck", ""),
|
|
};
|
|
self.push(Obj::Int(n as i64))?;
|
|
}
|
|
MaxLength => {
|
|
let d = self.pop_dict()?;
|
|
let n = d.borrow().len().max(d.borrow().capacity());
|
|
self.push(Obj::Int(n as i64))?;
|
|
}
|
|
Get => {
|
|
let k = self.pop()?;
|
|
let o = match self.pop()? {
|
|
Obj::Array { items, .. } => items.get(index(&k)?).ok_or(PsError::Ps("rangecheck", String::new()))?,
|
|
Obj::Str(s) => Obj::Int(i64::from(s.get(index(&k)?).ok_or(PsError::Ps("rangecheck", String::new()))?)),
|
|
Obj::Dict(d) => {
|
|
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
let v = d.borrow().get(&key).cloned();
|
|
v.ok_or_else(|| PsError::Ps("undefined", show_text(&k)))?
|
|
}
|
|
_ => return ps_err("typecheck", ""),
|
|
};
|
|
self.push(o)?;
|
|
}
|
|
Put => {
|
|
let (v, k, c) = (self.pop()?, self.pop()?, self.pop()?);
|
|
let written = match c {
|
|
Obj::Array { items, .. } => items.write(index(&k)?, std::slice::from_ref(&v)),
|
|
Obj::Str(s) => {
|
|
let b = v.as_num().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
s.write(index(&k)?, &[b as i64 as u8])
|
|
}
|
|
Obj::Dict(d) => {
|
|
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
insert(&d, key, v)?;
|
|
true
|
|
}
|
|
_ => return ps_err("typecheck", ""),
|
|
};
|
|
if !written {
|
|
return ps_err("rangecheck", "");
|
|
}
|
|
}
|
|
GetInterval => {
|
|
let n = self.pop_count()?;
|
|
let at = self.pop_count()?;
|
|
let o = self.pop()?;
|
|
self.push_interval(o, at, n)?;
|
|
}
|
|
PutInterval => {
|
|
let src = self.pop()?;
|
|
let at = self.pop_count()?;
|
|
let written = match (self.pop()?, src) {
|
|
(Obj::Array { items, .. }, Obj::Array { items: from, .. }) => items.write(at, &from.to_vec()),
|
|
(Obj::Str(s), Obj::Str(from)) => s.write(at, &from.to_vec()),
|
|
_ => return ps_err("typecheck", ""),
|
|
};
|
|
if !written {
|
|
return ps_err("rangecheck", "");
|
|
}
|
|
}
|
|
Aload => {
|
|
let items = self.pop_array()?;
|
|
for o in items.to_vec() {
|
|
self.push(o)?;
|
|
}
|
|
self.push(Obj::Array { items, exec: false })?;
|
|
}
|
|
Astore => {
|
|
let items = self.pop_array()?;
|
|
let at = self.stack.len().checked_sub(items.len()).ok_or(PsError::Ps("stackunderflow", String::new()))?;
|
|
if !items.write(0, self.stack.get(at..).unwrap_or_default()) {
|
|
return ps_err("rangecheck", "");
|
|
}
|
|
self.stack.truncate(at);
|
|
self.push(Obj::Array { items, exec: false })?;
|
|
}
|
|
Search | AnchorSearch => {
|
|
let seek = self.pop_str()?.to_vec();
|
|
let s = self.pop_str()?;
|
|
let text = s.to_vec();
|
|
let at = if op == Search { super::lex::find(&text, &seek) } else { text.starts_with(&seek).then_some(0) };
|
|
match at {
|
|
Some(i) => {
|
|
let o = Obj::Str(s);
|
|
if op == Search {
|
|
self.push_interval(o.clone(), i + seek.len(), text.len() - i - seek.len())?;
|
|
self.push_interval(o.clone(), i, seek.len())?;
|
|
self.push_interval(o, 0, i)?;
|
|
} else {
|
|
self.push_interval(o.clone(), seek.len(), text.len() - seek.len())?;
|
|
self.push_interval(o, 0, seek.len())?;
|
|
}
|
|
self.push(Obj::Bool(true))?;
|
|
}
|
|
None => {
|
|
self.push(Obj::Str(s))?;
|
|
self.push(Obj::Bool(false))?;
|
|
}
|
|
}
|
|
}
|
|
NewDict => {
|
|
self.pop_count()?;
|
|
self.push(Obj::dict(Default::default()))?;
|
|
}
|
|
Begin => {
|
|
let d = self.pop_dict()?;
|
|
if self.dicts.len() >= MAX_DICTS {
|
|
return Err(PsError::Limit("the dictionary stack overflowed"));
|
|
}
|
|
self.dicts.push(d);
|
|
}
|
|
End => {
|
|
// systemdict, globaldict and userdict stay.
|
|
if self.dicts.len() <= 3 {
|
|
return ps_err("dictstackunderflow", "end");
|
|
}
|
|
self.dicts.pop();
|
|
}
|
|
Def => {
|
|
let v = self.pop()?;
|
|
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
|
insert(&d, k, v)?;
|
|
}
|
|
Load => {
|
|
let k = self.pop()?;
|
|
let name = k.text().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
let v = self.lookup(&name).ok_or_else(|| PsError::Ps("undefined", name.to_string()))?;
|
|
self.push(v)?;
|
|
}
|
|
Store => {
|
|
let v = self.pop()?;
|
|
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
let d = self.dicts.iter().rev().find(|d| d.borrow().contains_key(&k)).or(self.dicts.last()).cloned();
|
|
let d = d.ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
|
insert(&d, k, v)?;
|
|
}
|
|
Known => {
|
|
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
let d = self.pop_dict()?;
|
|
let known = d.borrow().contains_key(&k);
|
|
self.push(Obj::Bool(known))?;
|
|
}
|
|
Where => {
|
|
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
match self.dicts.iter().rev().find(|d| d.borrow().contains_key(&k)).cloned() {
|
|
Some(d) => {
|
|
self.push(Obj::Dict(d))?;
|
|
self.push(Obj::Bool(true))?;
|
|
}
|
|
None => self.push(Obj::Bool(false))?,
|
|
}
|
|
}
|
|
Undef => {
|
|
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
self.pop_dict()?.borrow_mut().remove(&k);
|
|
}
|
|
CurrentDict => {
|
|
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
|
self.push(Obj::Dict(d))?;
|
|
}
|
|
CountDictStack => self.push(Obj::Int(self.dicts.len() as i64))?,
|
|
SystemDict => self.push(Obj::Dict(self.system.clone()))?,
|
|
UserDict => self.push(Obj::Dict(self.user.clone()))?,
|
|
GlobalDict => self.push(Obj::Dict(self.global.clone()))?,
|
|
StatusDict => self.push(Obj::Dict(self.status.clone()))?,
|
|
ErrorDict | DollarError => self.push(Obj::Dict(self.error.clone()))?,
|
|
Bind => {
|
|
let o = self.pop()?;
|
|
if let Obj::Array { items, .. } = &o {
|
|
self.bind(items, 0);
|
|
}
|
|
self.push(o)?;
|
|
}
|
|
Save => {
|
|
self.gsave()?;
|
|
self.push(Obj::Save(self.saved.len()))?;
|
|
}
|
|
Restore => {
|
|
let Obj::Save(level) = self.pop()? else { return ps_err("typecheck", "") };
|
|
while self.saved.len() >= level && !self.saved.is_empty() {
|
|
self.grestore();
|
|
}
|
|
}
|
|
VmStatus => {
|
|
for v in [0, 1 << 20, 1 << 30] {
|
|
self.push(Obj::Int(v))?;
|
|
}
|
|
}
|
|
Version => self.push(Obj::string(b"3010".to_vec()))?,
|
|
Product => self.push(Obj::string(b"VectorCraft".to_vec()))?,
|
|
RealTime | UserTime => self.push(Obj::Int(0))?,
|
|
Print | EqPrint | EqEqPrint => {
|
|
self.pop()?;
|
|
}
|
|
Pstack | Stack | Flush => {}
|
|
_ => self.graphics_op(op)?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The objects above the topmost mark, which is removed.
|
|
fn pop_to_mark(&mut self) -> Res<Vec<Obj>> {
|
|
let at = self.stack.iter().rposition(|o| matches!(o, Obj::Mark)).ok_or(PsError::Ps("unmatchedmark", String::new()))?;
|
|
let items = self.stack.split_off(at + 1);
|
|
self.stack.pop();
|
|
Ok(items)
|
|
}
|
|
|
|
/// `copy`: the top `n` operands again, or a composite into another.
|
|
fn copy(&mut self) -> Res {
|
|
match self.pop()? {
|
|
Obj::Int(n) => {
|
|
let n = usize::try_from(n).map_err(|_| PsError::Ps("rangecheck", String::new()))?;
|
|
let at = self.stack.len().checked_sub(n).ok_or(PsError::Ps("stackunderflow", String::new()))?;
|
|
self.alloc(n * std::mem::size_of::<Obj>())?;
|
|
let top: Vec<Obj> = self.stack.get(at..).unwrap_or_default().to_vec();
|
|
for o in top {
|
|
self.push(o)?;
|
|
}
|
|
}
|
|
Obj::Array { items, exec } => {
|
|
let from = self.pop_array()?.to_vec();
|
|
if !items.write(0, &from) {
|
|
return ps_err("rangecheck", "copy");
|
|
}
|
|
self.push_interval(Obj::Array { items, exec }, 0, from.len())?;
|
|
}
|
|
Obj::Str(s) => {
|
|
let from = self.pop_str()?.to_vec();
|
|
if !s.write(0, &from) {
|
|
return ps_err("rangecheck", "copy");
|
|
}
|
|
self.push_interval(Obj::Str(s), 0, from.len())?;
|
|
}
|
|
Obj::Dict(d) => {
|
|
let from = self.pop_dict()?.borrow().clone();
|
|
d.borrow_mut().extend(from);
|
|
self.push(Obj::Dict(d))?;
|
|
}
|
|
_ => return ps_err("typecheck", "copy"),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// `n` items of an array or string from `at`, sharing its storage.
|
|
pub fn push_interval(&mut self, o: Obj, at: usize, n: usize) -> Res {
|
|
let range = || PsError::Ps("rangecheck", String::new());
|
|
let r = match o {
|
|
Obj::Array { items, exec } => Obj::Array { items: items.sub(at, n).ok_or_else(range)?, exec },
|
|
Obj::Str(s) => Obj::Str(s.sub(at, n).ok_or_else(range)?),
|
|
_ => return ps_err("typecheck", "getinterval"),
|
|
};
|
|
self.push(r)
|
|
}
|
|
|
|
fn forall(&mut self) -> Res {
|
|
let p = self.pop_proc()?;
|
|
match self.pop()? {
|
|
Obj::Array { items, .. } => {
|
|
let mut i = 0;
|
|
while let Some(o) = items.get(i) {
|
|
i += 1;
|
|
self.push(o)?;
|
|
if !self.body(&p)? {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Obj::Str(s) => {
|
|
for b in s.to_vec() {
|
|
self.push(Obj::Int(i64::from(b)))?;
|
|
if !self.body(&p)? {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Obj::Dict(d) => {
|
|
let pairs: Vec<(Key, Obj)> = d.borrow().iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
|
for (k, v) in pairs {
|
|
self.push(k.obj())?;
|
|
self.push(v)?;
|
|
if !self.body(&p)? {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_ => return ps_err("typecheck", "forall"),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Replace the names in a procedure that stand for operators by the operators, down through
|
|
/// nested procedures.
|
|
fn bind(&mut self, items: &Shared<Obj>, depth: u32) {
|
|
if depth > 64 {
|
|
return;
|
|
}
|
|
for i in 0..items.len() {
|
|
match items.get(i) {
|
|
Some(Obj::Exec(name)) => {
|
|
if let Some(o @ Obj::Op(_)) = self.lookup(&name) {
|
|
// `i` is in range and nothing is reading the procedure: the write can't fail.
|
|
items.write(i, &[o]);
|
|
}
|
|
}
|
|
Some(Obj::Array { items: inner, exec: true }) if !inner.shares(items) => self.bind(&inner, depth + 1),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- resources ----------
|
|
|
|
/// The resources of `category`.
|
|
pub fn category(&mut self, category: &Obj) -> Res<DictRef> {
|
|
let k = category.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
|
let mut all = self.resources.borrow_mut();
|
|
Ok(all.entry(k).or_insert_with(|| Obj::Dict(new_dict())).clone()).and_then(|o| match o {
|
|
Obj::Dict(d) => Ok(d),
|
|
_ => ps_err("typecheck", ""),
|
|
})
|
|
}
|
|
|
|
/// The instances of `category`: `FontDirectory` for fonts.
|
|
pub fn instances(&mut self, category: &Obj) -> Res<DictRef> {
|
|
if category.text().as_deref() == Some("Font") { Ok(self.fonts.clone()) } else { self.category(category) }
|
|
}
|
|
|
|
/// `template proc scratch category resourceforall`: `proc` run on each instance name that
|
|
/// matches `template`, copied into `scratch` (in name order).
|
|
pub fn resource_for_all(&mut self) -> Res {
|
|
let category = self.pop()?;
|
|
let scratch = self.pop_str()?;
|
|
let p = self.pop_proc()?;
|
|
let template = self.pop_str()?.to_vec();
|
|
let mut names: Vec<Vec<u8>> = self.instances(&category)?.borrow().keys().map(|k| show_text(&k.obj()).into_bytes()).collect();
|
|
names.sort();
|
|
for name in names {
|
|
if !self.matches(&template, &name)? {
|
|
continue;
|
|
}
|
|
if !scratch.write(0, &name) {
|
|
return ps_err("rangecheck", "resourceforall");
|
|
}
|
|
self.push_interval(Obj::Str(scratch.clone()), 0, name.len())?;
|
|
if !self.body(&p)? {
|
|
break;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Does `name` match a `resourceforall` template: `*` any run of bytes, `?` any one, `\` the
|
|
/// next one as it is? Each step counts as an operation.
|
|
fn matches(&mut self, template: &[u8], name: &[u8]) -> Res<bool> {
|
|
let (mut t, mut n) = (0, 0);
|
|
// Where the last `*` was followed (template, name): a mismatch after it retries from there.
|
|
let mut star: Option<(usize, usize)> = None;
|
|
loop {
|
|
self.tick()?;
|
|
if n == name.len() {
|
|
return Ok(template.get(t..).unwrap_or_default().iter().all(|b| *b == b'*'));
|
|
}
|
|
let (want, width) = match template.get(t) {
|
|
Some(b'*') => {
|
|
t += 1;
|
|
star = Some((t, n));
|
|
continue;
|
|
}
|
|
Some(b'?') => (name.get(n).copied(), 1),
|
|
Some(b'\\') => (template.get(t + 1).copied(), 2),
|
|
c => (c.copied(), 1),
|
|
};
|
|
if want.is_some() && want == name.get(n).copied() {
|
|
(t, n) = (t + width, n + 1);
|
|
continue;
|
|
}
|
|
// The last `*` takes one more byte, else there is no match.
|
|
let Some((st, sn)) = star else { return Ok(false) };
|
|
star = Some((st, sn + 1));
|
|
(t, n) = (st, sn + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Put `v` under `k` in `d`, within [`MAX_ALLOC`] entries.
|
|
fn insert(d: &DictRef, k: Key, v: Obj) -> Res {
|
|
let mut d = d.borrow_mut();
|
|
if d.len() >= MAX_ALLOC && !d.contains_key(&k) {
|
|
return Err(PsError::Limit("a dictionary grew too large"));
|
|
}
|
|
d.insert(k, v);
|
|
Ok(())
|
|
}
|
|
|
|
/// An array or string index.
|
|
fn index(k: &Obj) -> Res<usize> {
|
|
match *k {
|
|
Obj::Int(i) if i >= 0 => Ok(i as usize),
|
|
_ => ps_err("rangecheck", "an index"),
|
|
}
|
|
}
|
|
|
|
/// `eq`: numbers by value, strings and names by text, composites by identity.
|
|
fn equal(a: &Obj, b: &Obj) -> bool {
|
|
match (a, b) {
|
|
(Obj::Null, Obj::Null) | (Obj::Mark, Obj::Mark) => true,
|
|
(Obj::Bool(x), Obj::Bool(y)) => x == y,
|
|
(Obj::Array { items: x, .. }, Obj::Array { items: y, .. }) => x.same(y),
|
|
(Obj::Dict(x), Obj::Dict(y)) => Rc::ptr_eq(x, y),
|
|
(Obj::Op(x), Obj::Op(y)) => x == y,
|
|
_ => match (a.as_num(), b.as_num()) {
|
|
(Some(x), Some(y)) => x == y,
|
|
_ => matches!((a.text(), b.text()), (Some(x), Some(y)) if x == y),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// The text `cvs` writes for `o`.
|
|
fn show_text(o: &Obj) -> String {
|
|
match o {
|
|
Obj::Int(i) => i.to_string(),
|
|
Obj::Real(r) => {
|
|
let s = format!("{r}");
|
|
if s.contains('.') || s.contains('e') { s } else { format!("{s}.0") }
|
|
}
|
|
Obj::Bool(b) => b.to_string(),
|
|
Obj::Name(n) | Obj::Exec(n) => n.to_string(),
|
|
Obj::Str(s) => String::from_utf8_lossy(&s.borrow()).into_owned(),
|
|
Obj::Op(op) => op.name().to_string(),
|
|
_ => "--nostringval--".into(),
|
|
}
|
|
}
|
|
|
|
/// `v` in `radix`, as `cvrs` writes it (negative numbers as their 32-bit two's complement).
|
|
fn radix_text(v: i64, radix: u32) -> String {
|
|
let mut n = if v < 0 { u64::from(v as u32) } else { v as u64 };
|
|
if n == 0 {
|
|
return "0".into();
|
|
}
|
|
let mut digits = vec![];
|
|
while n > 0 {
|
|
digits.push(std::char::from_digit((n % u64::from(radix)) as u32, radix).unwrap_or('0').to_ascii_uppercase());
|
|
n /= u64::from(radix);
|
|
}
|
|
digits.iter().rev().collect()
|
|
}
|
|
|
|
/// A matrix from six numbers.
|
|
pub(crate) fn matrix_of(items: &[Obj]) -> Option<Affine> {
|
|
let [a, b, c, d, e, f] = items else { return None };
|
|
let v = [a, b, c, d, e, f].map(|o| o.as_num().filter(|v| v.is_finite()));
|
|
let [Some(a), Some(b), Some(c), Some(d), Some(e), Some(f)] = v else { return None };
|
|
Some(Affine::new([a, b, c, d, e, f]))
|
|
}
|
|
|
|
/// A matrix as an array.
|
|
pub(crate) fn matrix_obj(m: Affine) -> Obj {
|
|
Obj::array(m.as_coeffs().iter().map(|v| Obj::Real(*v)).collect())
|
|
}
|