mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 03:58:45 -06:00
Found while testing crates/js/src/formcalc.rs:2169 (Str) and :2258 (Format number picture) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2591 lines
102 KiB
Rust
2591 lines
102 KiB
Rust
//! FormCalc (XFA 3.3 part 8), the default scripting language of older Designer forms: a lexer,
|
|
//! a parser and a tree-walking evaluator over the same form table and effects as the JavaScript
|
|
//! model ([`crate::xfa`]), so the engine treats both languages alike.
|
|
//!
|
|
//! Covered: expressions with FormCalc's operators and coercions, `var`, assignment to fields
|
|
//! and properties, `if`/`elseif`/`else`, `while`, `for … upto/downto … step`, `foreach`,
|
|
//! `func … endfunc`, `break`/`continue`/`return`/`exit`, accessors (`$`, `$host`, `$form`,
|
|
//! `$record`, `$layout`, `$event`, `xfa.…`, `..name`, `[n]`, `[*]`, `_name` instance managers),
|
|
//! and the arithmetic, logical, string, date/time, financial and unit built-in functions.
|
|
//! `Get`, `Post`, `Put` (network) refuse.
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::rc::Rc;
|
|
|
|
use crate::Limits;
|
|
use crate::xfa::{
|
|
HNode, XHost, XfaDoc, XfaEffect, XfaEvent, XfaKind, XfaNode, XfaOutcome, child_named, clip, clip_result, descendants_named, instances,
|
|
resolve_som, run_within,
|
|
};
|
|
|
|
/// Deepest nesting the parser follows.
|
|
const MAX_PARSE_DEPTH: usize = 64;
|
|
/// Most statements and expressions evaluated in one run (loops included).
|
|
const MAX_STEPS: u64 = 5_000_000;
|
|
/// Longest string a script may build.
|
|
const MAX_STRING: usize = 1 << 20;
|
|
/// Most elements a list may hold.
|
|
const MAX_LIST: usize = 100_000;
|
|
/// Most operators and accessors in one expression (`a + b + …`, `a.b.c…`, brackets included):
|
|
/// the tree is as deep as the chain, and dropping or cloning it recurses.
|
|
const MAX_CHAIN: usize = 1000;
|
|
// The errors a run stops with when it reaches a limit (rather than a fault in the script).
|
|
const RAN_TOO_LONG: &str = "the script ran too long";
|
|
const TOO_DEEP: &str = "the script is nested too deeply";
|
|
const CALLS_TOO_DEEP: &str = "functions are nested too deeply";
|
|
const LOOPED_TOO_MUCH: &str = "the script looped too many times";
|
|
const LIST_TOO_LONG: &str = "a list is too long";
|
|
const STRING_TOO_LONG: &str = "a string is too long";
|
|
|
|
/// Whether `e` is a limit the run reached: those stop the script even where a fault is
|
|
/// otherwise an answer (`Exists`).
|
|
fn is_limit(e: &str) -> bool {
|
|
[RAN_TOO_LONG, TOO_DEEP, CALLS_TOO_DEEP, LOOPED_TOO_MUCH, LIST_TOO_LONG, STRING_TOO_LONG].contains(&e)
|
|
}
|
|
|
|
/// Steps between looks at the clock.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
const CLOCK_EVERY: u64 = 256;
|
|
|
|
// ── lexer ───────────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
enum Tok {
|
|
Num(f64),
|
|
Str(String),
|
|
Ident(String),
|
|
Op(&'static str),
|
|
Newline,
|
|
Eof,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
struct Token {
|
|
tok: Tok,
|
|
line: usize,
|
|
}
|
|
|
|
const OPS: [&str; 20] = ["<=", ">=", "==", "<>", "..", "(", ")", "[", "]", ".", ",", "=", "<", ">", "+", "-", "*", "/", "&", "|"];
|
|
|
|
fn lex(src: &str) -> Result<Vec<Token>, String> {
|
|
let chars: Vec<char> = src.chars().collect();
|
|
let mut out = Vec::new();
|
|
let mut i = 0;
|
|
let mut line = 1;
|
|
let mut depth = 0usize;
|
|
while i < chars.len() {
|
|
let c = chars[i];
|
|
match c {
|
|
'\n' => {
|
|
line += 1;
|
|
i += 1;
|
|
// Newlines inside brackets continue the expression.
|
|
if depth == 0 {
|
|
out.push(Token { tok: Tok::Newline, line });
|
|
}
|
|
}
|
|
' ' | '\t' | '\r' => i += 1,
|
|
';' => {
|
|
while i < chars.len() && chars[i] != '\n' {
|
|
i += 1;
|
|
}
|
|
}
|
|
'/' if chars.get(i + 1) == Some(&'/') => {
|
|
while i < chars.len() && chars[i] != '\n' {
|
|
i += 1;
|
|
}
|
|
}
|
|
'"' => {
|
|
let mut s = String::new();
|
|
i += 1;
|
|
loop {
|
|
match chars.get(i) {
|
|
None => return Err(format!("line {line}: unterminated string")),
|
|
Some('"') if chars.get(i + 1) == Some(&'"') => {
|
|
s.push('"');
|
|
i += 2;
|
|
}
|
|
Some('"') => {
|
|
i += 1;
|
|
break;
|
|
}
|
|
Some(&ch) => {
|
|
if ch == '\n' {
|
|
line += 1;
|
|
}
|
|
s.push(ch);
|
|
i += 1;
|
|
}
|
|
}
|
|
if s.len() > MAX_STRING {
|
|
return Err(format!("line {line}: a string is too long"));
|
|
}
|
|
}
|
|
out.push(Token { tok: Tok::Str(s), line });
|
|
}
|
|
c if c.is_ascii_digit() || (c == '.' && chars.get(i + 1).is_some_and(|d| d.is_ascii_digit())) => {
|
|
let start = i;
|
|
while i < chars.len() && (chars[i].is_ascii_digit() || (chars[i] == '.' && chars.get(i + 1) != Some(&'.'))) {
|
|
i += 1;
|
|
}
|
|
if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
|
|
let mut j = i + 1;
|
|
if j < chars.len() && (chars[j] == '+' || chars[j] == '-') {
|
|
j += 1;
|
|
}
|
|
if j < chars.len() && chars[j].is_ascii_digit() {
|
|
i = j;
|
|
while i < chars.len() && chars[i].is_ascii_digit() {
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
let text: String = chars[start..i].iter().collect();
|
|
let n: f64 = text.parse().map_err(|_| format!("line {line}: bad number {text}"))?;
|
|
out.push(Token { tok: Tok::Num(n), line });
|
|
}
|
|
'!' => {
|
|
out.push(Token { tok: Tok::Ident("!".into()), line });
|
|
i += 1;
|
|
}
|
|
c if c.is_alphabetic() || c == '_' || c == '$' => {
|
|
let start = i;
|
|
i += 1;
|
|
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '$') {
|
|
i += 1;
|
|
}
|
|
let text: String = chars[start..i].iter().collect();
|
|
out.push(Token { tok: Tok::Ident(text), line });
|
|
}
|
|
_ => {
|
|
let rest: String = chars[i..chars.len().min(i + 2)].iter().collect();
|
|
let op = OPS.iter().find(|o| rest.starts_with(*o)).copied().ok_or_else(|| format!("line {line}: unexpected character {c:?}"))?;
|
|
match op {
|
|
"(" | "[" => depth += 1,
|
|
")" | "]" => depth = depth.saturating_sub(1),
|
|
_ => {}
|
|
}
|
|
out.push(Token { tok: Tok::Op(op), line });
|
|
i += op.len();
|
|
}
|
|
}
|
|
}
|
|
out.push(Token { tok: Tok::Newline, line });
|
|
out.push(Token { tok: Tok::Eof, line });
|
|
Ok(out)
|
|
}
|
|
|
|
// ── syntax ──────────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
enum Expr {
|
|
Num(f64),
|
|
Str(String),
|
|
Null,
|
|
Ident(String),
|
|
/// `base.name`
|
|
Member(Box<Expr>, String),
|
|
/// `base[n]`, `base[*]`, `base[-n]`
|
|
Index(Box<Expr>, Idx),
|
|
/// `base..name`
|
|
Descend(Box<Expr>, String),
|
|
Call(Box<Expr>, Vec<Expr>),
|
|
Neg(Box<Expr>),
|
|
Not(Box<Expr>),
|
|
Bin(&'static str, Box<Expr>, Box<Expr>),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
enum Idx {
|
|
/// `[*]`: every instance.
|
|
All,
|
|
/// `[n]`: the n-th instance (from 0).
|
|
Abs(Box<Expr>),
|
|
/// `[-n]` / `[+n]`: n instances before or after the one the script runs in.
|
|
Rel(i64),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
enum Stmt {
|
|
Var(String, Option<Expr>),
|
|
Assign(Expr, Expr),
|
|
Expr(Expr),
|
|
If(Vec<(Expr, Vec<Stmt>)>, Option<Vec<Stmt>>),
|
|
While(Expr, Vec<Stmt>),
|
|
For { var: String, from: Expr, to: Expr, up: bool, step: Option<Expr>, body: Vec<Stmt> },
|
|
Foreach { var: String, list: Vec<Expr>, body: Vec<Stmt> },
|
|
Func { name: String, f: Rc<Func> },
|
|
Return(Option<Expr>),
|
|
Break,
|
|
Continue,
|
|
Exit,
|
|
Throw(Expr),
|
|
}
|
|
|
|
struct Parser {
|
|
toks: Vec<Token>,
|
|
at: usize,
|
|
depth: usize,
|
|
/// Operators and accessors chained so far in the expression being parsed.
|
|
chain: usize,
|
|
}
|
|
|
|
fn kw(t: &Tok, word: &str) -> bool {
|
|
matches!(t, Tok::Ident(s) if s.eq_ignore_ascii_case(word))
|
|
}
|
|
|
|
impl Parser {
|
|
fn peek(&self) -> &Tok {
|
|
self.toks.get(self.at).map_or(&Tok::Eof, |t| &t.tok)
|
|
}
|
|
|
|
fn line(&self) -> usize {
|
|
self.toks.get(self.at).map_or(0, |t| t.line)
|
|
}
|
|
|
|
fn next(&mut self) -> Tok {
|
|
let t = self.peek().clone();
|
|
self.at += 1;
|
|
t
|
|
}
|
|
|
|
fn is_op(&self, op: &str) -> bool {
|
|
matches!(self.peek(), Tok::Op(o) if *o == op)
|
|
}
|
|
|
|
fn eat_op(&mut self, op: &str) -> bool {
|
|
if self.is_op(op) {
|
|
self.at += 1;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn expect_op(&mut self, op: &str) -> Result<(), String> {
|
|
if self.eat_op(op) { Ok(()) } else { Err(format!("line {}: expected {op}", self.line())) }
|
|
}
|
|
|
|
fn eat_kw(&mut self, word: &str) -> bool {
|
|
if kw(self.peek(), word) {
|
|
self.at += 1;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn expect_kw(&mut self, word: &str) -> Result<(), String> {
|
|
if self.eat_kw(word) { Ok(()) } else { Err(format!("line {}: expected {word}", self.line())) }
|
|
}
|
|
|
|
fn skip_newlines(&mut self) {
|
|
while matches!(self.peek(), Tok::Newline) {
|
|
self.at += 1;
|
|
}
|
|
}
|
|
|
|
fn enter(&mut self) -> Result<(), String> {
|
|
self.depth += 1;
|
|
if self.depth > MAX_PARSE_DEPTH {
|
|
return Err("the script is nested too deeply".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn leave(&mut self) {
|
|
self.depth = self.depth.saturating_sub(1);
|
|
}
|
|
|
|
/// One more link in an operator or accessor chain.
|
|
fn link(&mut self) -> Result<(), String> {
|
|
self.chain += 1;
|
|
if self.chain > MAX_CHAIN {
|
|
return Err(format!("line {}: an expression is too long", self.line()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Statements up to one of `enders` (not consumed).
|
|
fn block(&mut self, enders: &[&str]) -> Result<Vec<Stmt>, String> {
|
|
self.enter()?;
|
|
let mut out = Vec::new();
|
|
loop {
|
|
self.skip_newlines();
|
|
match self.peek() {
|
|
Tok::Eof => break,
|
|
Tok::Ident(w) if enders.iter().any(|e| w.eq_ignore_ascii_case(e)) => break,
|
|
_ => out.push(self.statement()?),
|
|
}
|
|
}
|
|
self.leave();
|
|
Ok(out)
|
|
}
|
|
|
|
fn statement(&mut self) -> Result<Stmt, String> {
|
|
let line = self.line();
|
|
match self.peek().clone() {
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("var") => {
|
|
self.at += 1;
|
|
let Tok::Ident(name) = self.next() else { return Err(format!("line {line}: expected a name after var")) };
|
|
let init = if self.eat_op("=") { Some(self.top_expr()?) } else { None };
|
|
Ok(Stmt::Var(name, init))
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("if") => {
|
|
self.at += 1;
|
|
let mut arms = Vec::new();
|
|
let cond = self.top_expr()?;
|
|
self.expect_kw("then")?;
|
|
let body = self.block(&["elseif", "else", "endif"])?;
|
|
arms.push((cond, body));
|
|
let mut otherwise = None;
|
|
loop {
|
|
if self.eat_kw("elseif") {
|
|
let c = self.top_expr()?;
|
|
self.expect_kw("then")?;
|
|
let b = self.block(&["elseif", "else", "endif"])?;
|
|
arms.push((c, b));
|
|
} else if self.eat_kw("else") {
|
|
otherwise = Some(self.block(&["endif"])?);
|
|
} else {
|
|
self.expect_kw("endif")?;
|
|
break;
|
|
}
|
|
}
|
|
Ok(Stmt::If(arms, otherwise))
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("while") => {
|
|
self.at += 1;
|
|
let cond = self.top_expr()?;
|
|
self.expect_kw("do")?;
|
|
let body = self.block(&["endwhile"])?;
|
|
self.expect_kw("endwhile")?;
|
|
Ok(Stmt::While(cond, body))
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("for") => {
|
|
self.at += 1;
|
|
let Tok::Ident(var) = self.next() else { return Err(format!("line {line}: expected a variable after for")) };
|
|
self.expect_op("=")?;
|
|
let from = self.top_expr()?;
|
|
let up = if self.eat_kw("upto") {
|
|
true
|
|
} else if self.eat_kw("downto") {
|
|
false
|
|
} else {
|
|
return Err(format!("line {line}: expected upto or downto"));
|
|
};
|
|
let to = self.top_expr()?;
|
|
let step = if self.eat_kw("step") { Some(self.top_expr()?) } else { None };
|
|
self.expect_kw("do")?;
|
|
let body = self.block(&["endfor"])?;
|
|
self.expect_kw("endfor")?;
|
|
Ok(Stmt::For { var, from, to, up, step, body })
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("foreach") => {
|
|
self.at += 1;
|
|
let Tok::Ident(var) = self.next() else { return Err(format!("line {line}: expected a variable after foreach")) };
|
|
self.expect_kw("in")?;
|
|
self.expect_op("(")?;
|
|
let mut list = Vec::new();
|
|
if !self.is_op(")") {
|
|
loop {
|
|
list.push(self.top_expr()?);
|
|
if !self.eat_op(",") {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
self.expect_op(")")?;
|
|
self.expect_kw("do")?;
|
|
let body = self.block(&["endfor"])?;
|
|
self.expect_kw("endfor")?;
|
|
Ok(Stmt::Foreach { var, list, body })
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("func") => {
|
|
self.at += 1;
|
|
let Tok::Ident(name) = self.next() else { return Err(format!("line {line}: expected a name after func")) };
|
|
self.expect_op("(")?;
|
|
let mut params = Vec::new();
|
|
if !self.is_op(")") {
|
|
loop {
|
|
let Tok::Ident(p) = self.next() else { return Err(format!("line {line}: expected a parameter name")) };
|
|
params.push(p);
|
|
if !self.eat_op(",") {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
self.expect_op(")")?;
|
|
self.expect_kw("do")?;
|
|
let body = self.block(&["endfunc"])?;
|
|
self.expect_kw("endfunc")?;
|
|
Ok(Stmt::Func { name, f: Rc::new(Func { params, body }) })
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("return") => {
|
|
self.at += 1;
|
|
let v = if matches!(self.peek(), Tok::Newline | Tok::Eof) { None } else { Some(self.top_expr()?) };
|
|
Ok(Stmt::Return(v))
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("break") => {
|
|
self.at += 1;
|
|
Ok(Stmt::Break)
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("continue") => {
|
|
self.at += 1;
|
|
Ok(Stmt::Continue)
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("exit") => {
|
|
self.at += 1;
|
|
Ok(Stmt::Exit)
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("throw") => {
|
|
self.at += 1;
|
|
Ok(Stmt::Throw(self.top_expr()?))
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("do") => {
|
|
// `do … end`: a block as one statement.
|
|
self.at += 1;
|
|
let body = self.block(&["end"])?;
|
|
self.expect_kw("end")?;
|
|
Ok(Stmt::If(vec![(Expr::Num(1.0), body)], None))
|
|
}
|
|
_ => {
|
|
let e = self.top_expr()?;
|
|
if self.eat_op("=") {
|
|
let v = self.top_expr()?;
|
|
return Ok(Stmt::Assign(e, v));
|
|
}
|
|
Ok(Stmt::Expr(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An expression that starts a tree of its own (a condition, a bound, a statement's
|
|
/// expression): its operator chain is counted from zero, whatever came before it.
|
|
fn top_expr(&mut self) -> Result<Expr, String> {
|
|
self.chain = 0;
|
|
self.expr()
|
|
}
|
|
|
|
fn expr(&mut self) -> Result<Expr, String> {
|
|
self.enter()?;
|
|
let e = self.or_expr();
|
|
self.leave();
|
|
e
|
|
}
|
|
|
|
fn or_expr(&mut self) -> Result<Expr, String> {
|
|
let mut l = self.and_expr()?;
|
|
loop {
|
|
if self.eat_op("|") || self.eat_kw("or") {
|
|
self.link()?;
|
|
let r = self.and_expr()?;
|
|
l = Expr::Bin("|", Box::new(l), Box::new(r));
|
|
} else {
|
|
return Ok(l);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn and_expr(&mut self) -> Result<Expr, String> {
|
|
let mut l = self.cmp_expr()?;
|
|
loop {
|
|
if self.eat_op("&") || self.eat_kw("and") {
|
|
self.link()?;
|
|
let r = self.cmp_expr()?;
|
|
l = Expr::Bin("&", Box::new(l), Box::new(r));
|
|
} else {
|
|
return Ok(l);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cmp_expr(&mut self) -> Result<Expr, String> {
|
|
let mut l = self.add_expr()?;
|
|
loop {
|
|
let op = match self.peek() {
|
|
Tok::Op("==") => "==",
|
|
Tok::Op("<>") => "<>",
|
|
Tok::Op("<=") => "<=",
|
|
Tok::Op(">=") => ">=",
|
|
Tok::Op("<") => "<",
|
|
Tok::Op(">") => ">",
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("eq") => "==",
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("ne") => "<>",
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("le") => "<=",
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("ge") => ">=",
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("lt") => "<",
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("gt") => ">",
|
|
_ => return Ok(l),
|
|
};
|
|
self.at += 1;
|
|
self.link()?;
|
|
let r = self.add_expr()?;
|
|
l = Expr::Bin(op, Box::new(l), Box::new(r));
|
|
}
|
|
}
|
|
|
|
fn add_expr(&mut self) -> Result<Expr, String> {
|
|
let mut l = self.mul_expr()?;
|
|
loop {
|
|
let op = match self.peek() {
|
|
Tok::Op("+") => "+",
|
|
Tok::Op("-") => "-",
|
|
_ => return Ok(l),
|
|
};
|
|
self.at += 1;
|
|
self.link()?;
|
|
let r = self.mul_expr()?;
|
|
l = Expr::Bin(op, Box::new(l), Box::new(r));
|
|
}
|
|
}
|
|
|
|
fn mul_expr(&mut self) -> Result<Expr, String> {
|
|
let mut l = self.unary()?;
|
|
loop {
|
|
let op = match self.peek() {
|
|
Tok::Op("*") => "*",
|
|
Tok::Op("/") => "/",
|
|
_ => return Ok(l),
|
|
};
|
|
self.at += 1;
|
|
self.link()?;
|
|
let r = self.unary()?;
|
|
l = Expr::Bin(op, Box::new(l), Box::new(r));
|
|
}
|
|
}
|
|
|
|
fn unary(&mut self) -> Result<Expr, String> {
|
|
self.enter()?;
|
|
let e = if self.eat_op("-") {
|
|
Ok(Expr::Neg(Box::new(self.unary()?)))
|
|
} else if self.eat_op("+") {
|
|
self.unary()
|
|
} else if self.eat_kw("not") || matches!(self.peek(), Tok::Ident(w) if w == "!") {
|
|
if matches!(self.peek(), Tok::Ident(w) if w == "!") {
|
|
self.at += 1;
|
|
}
|
|
Ok(Expr::Not(Box::new(self.unary()?)))
|
|
} else {
|
|
self.postfix()
|
|
};
|
|
self.leave();
|
|
e
|
|
}
|
|
|
|
fn postfix(&mut self) -> Result<Expr, String> {
|
|
let mut e = self.primary()?;
|
|
loop {
|
|
if matches!(self.peek(), Tok::Op("." | ".." | "[" | "(")) {
|
|
self.link()?;
|
|
}
|
|
if self.eat_op(".") {
|
|
let Tok::Ident(name) = self.next() else { return Err(format!("line {}: expected a name after .", self.line())) };
|
|
e = Expr::Member(Box::new(e), name);
|
|
} else if self.eat_op("..") {
|
|
let Tok::Ident(name) = self.next() else { return Err(format!("line {}: expected a name after ..", self.line())) };
|
|
e = Expr::Descend(Box::new(e), name);
|
|
} else if self.eat_op("[") {
|
|
let sign = match self.peek() {
|
|
Tok::Op("-") => Some(-1),
|
|
Tok::Op("+") => Some(1),
|
|
_ => None,
|
|
};
|
|
let relative = sign.and_then(|sign| match (self.toks.get(self.at + 1).map(|t| &t.tok), self.toks.get(self.at + 2).map(|t| &t.tok)) {
|
|
(Some(Tok::Num(n)), Some(Tok::Op("]"))) if n.is_finite() && *n >= 0.0 && *n < 1e6 => Some(sign * (*n as i64)),
|
|
_ => None,
|
|
});
|
|
let idx = if let Some(k) = relative {
|
|
self.at += 2;
|
|
Idx::Rel(k)
|
|
} else if self.eat_op("*") {
|
|
Idx::All
|
|
} else {
|
|
Idx::Abs(Box::new(self.expr()?))
|
|
};
|
|
self.expect_op("]")?;
|
|
e = Expr::Index(Box::new(e), idx);
|
|
} else if self.eat_op("(") {
|
|
let mut args = Vec::new();
|
|
self.skip_newlines();
|
|
if !self.is_op(")") {
|
|
loop {
|
|
args.push(self.expr()?);
|
|
self.skip_newlines();
|
|
if !self.eat_op(",") {
|
|
break;
|
|
}
|
|
self.skip_newlines();
|
|
}
|
|
}
|
|
self.expect_op(")")?;
|
|
e = Expr::Call(Box::new(e), args);
|
|
} else {
|
|
return Ok(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn primary(&mut self) -> Result<Expr, String> {
|
|
let line = self.line();
|
|
match self.next() {
|
|
Tok::Num(n) => Ok(Expr::Num(n)),
|
|
Tok::Str(s) => Ok(Expr::Str(s)),
|
|
Tok::Op("(") => {
|
|
self.skip_newlines();
|
|
let e = self.expr()?;
|
|
self.skip_newlines();
|
|
self.expect_op(")")?;
|
|
Ok(e)
|
|
}
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("null") => Ok(Expr::Null),
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("nan") => Ok(Expr::Num(f64::NAN)),
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("infinity") => Ok(Expr::Num(f64::INFINITY)),
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("true") => Ok(Expr::Num(1.0)),
|
|
Tok::Ident(w) if w.eq_ignore_ascii_case("false") => Ok(Expr::Num(0.0)),
|
|
Tok::Ident(w) => Ok(Expr::Ident(w)),
|
|
t => Err(format!("line {line}: unexpected {}", describe(&t))),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn describe(t: &Tok) -> String {
|
|
match t {
|
|
Tok::Num(n) => format!("number {n}"),
|
|
Tok::Str(s) => format!("string {s:?}"),
|
|
Tok::Ident(s) => s.clone(),
|
|
Tok::Op(o) => format!("{o:?}"),
|
|
Tok::Newline => "end of line".into(),
|
|
Tok::Eof => "end of script".into(),
|
|
}
|
|
}
|
|
|
|
fn parse(src: &str) -> Result<Vec<Stmt>, String> {
|
|
let toks = lex(src)?;
|
|
let mut p = Parser { toks, at: 0, depth: 0, chain: 0 };
|
|
let body = p.block(&[])?;
|
|
match p.peek() {
|
|
Tok::Eof => Ok(body),
|
|
t => Err(format!("line {}: unexpected {}", p.line(), describe(t))),
|
|
}
|
|
}
|
|
|
|
// ── values ──────────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
enum Val {
|
|
Null,
|
|
Num(f64),
|
|
Str(String),
|
|
/// A form object, by index in the host.
|
|
Node(usize),
|
|
/// Several objects or values (`Row[*]`, `Row[*].Amount`).
|
|
List(Vec<Val>),
|
|
Host,
|
|
Layout,
|
|
Event,
|
|
/// `xfa` itself (`xfa.form`, `xfa.host`, …).
|
|
Xfa,
|
|
/// The instance manager of `name` under `parent`.
|
|
Im(usize, String),
|
|
/// A property object whose contents are not modelled (`$.border.fill…`): reads give
|
|
/// another sink, writes change nothing.
|
|
Sink,
|
|
}
|
|
|
|
/// A number as FormCalc prints it: integers without a fraction, otherwise up to 15 digits.
|
|
fn num_text(n: f64) -> String {
|
|
if !n.is_finite() {
|
|
return if n.is_nan() {
|
|
"NaN".into()
|
|
} else if n > 0.0 {
|
|
"Infinity".into()
|
|
} else {
|
|
"-Infinity".into()
|
|
};
|
|
}
|
|
if n.fract() == 0.0 && n.abs() < 1e15 {
|
|
return format!("{}", n as i64);
|
|
}
|
|
let s = format!("{n:.12}");
|
|
let s = s.trim_end_matches('0').trim_end_matches('.');
|
|
if s.is_empty() || s == "-0" { "0".into() } else { s.to_string() }
|
|
}
|
|
|
|
/// `v` with `decimals` places, rounding exact halves away from zero (Rust's formatter rounds them to even).
|
|
fn fixed(v: f64, decimals: usize) -> String {
|
|
let k = 10f64.powi(decimals.min(20) as i32);
|
|
let scaled = (v * k).round() / k;
|
|
format!("{:.decimals$}", if scaled.is_finite() { scaled } else { v })
|
|
}
|
|
|
|
/// The number a string holds: its leading numeric part (`12abc` → 12), `None` for none.
|
|
fn parse_num(s: &str) -> Option<f64> {
|
|
let t = s.trim();
|
|
if t.is_empty() {
|
|
return None;
|
|
}
|
|
let mut end = 0;
|
|
let b = t.as_bytes();
|
|
let mut seen_digit = false;
|
|
let mut seen_dot = false;
|
|
let mut seen_e = false;
|
|
let mut i = 0;
|
|
if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
|
|
i += 1;
|
|
}
|
|
while i < b.len() {
|
|
let c = b[i];
|
|
if c.is_ascii_digit() {
|
|
seen_digit = true;
|
|
end = i + 1;
|
|
} else if c == b'.' && !seen_dot && !seen_e {
|
|
seen_dot = true;
|
|
} else if (c == b'e' || c == b'E') && seen_digit && !seen_e {
|
|
seen_e = true;
|
|
if i + 1 < b.len() && (b[i + 1] == b'-' || b[i + 1] == b'+') {
|
|
i += 1;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
i += 1;
|
|
}
|
|
if !seen_digit {
|
|
return None;
|
|
}
|
|
t.get(..end).and_then(|p| p.parse::<f64>().ok()).filter(|v| v.is_finite())
|
|
}
|
|
|
|
fn is_numeric_text(s: &str) -> bool {
|
|
s.trim().parse::<f64>().is_ok_and(|v| v.is_finite())
|
|
}
|
|
|
|
// ── the evaluator ───────────────────────────────────────────────────────────────────────────
|
|
|
|
enum Flow {
|
|
Next,
|
|
Break,
|
|
Continue,
|
|
Return(Val),
|
|
Exit,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
struct Func {
|
|
params: Vec<String>,
|
|
body: Vec<Stmt>,
|
|
}
|
|
|
|
struct Interp<'h> {
|
|
h: &'h mut XHost,
|
|
scopes: Vec<HashMap<String, Val>>,
|
|
/// User functions by lower-case name (FormCalc names are case-insensitive).
|
|
funcs: HashMap<String, Rc<Func>>,
|
|
event: XfaEvent,
|
|
steps: u64,
|
|
loops_left: u64,
|
|
depth: usize,
|
|
max_depth: usize,
|
|
/// `exit` ran: every block ends at once.
|
|
exited: bool,
|
|
/// When the caller stops waiting: the script stops itself soon after (no thread is left
|
|
/// burning a core to the loop limit).
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
deadline: Option<std::time::Instant>,
|
|
}
|
|
|
|
type R<T> = Result<T, String>;
|
|
|
|
impl Interp<'_> {
|
|
fn step(&mut self) -> R<()> {
|
|
self.charge(1)
|
|
}
|
|
|
|
/// `n` units of work done (a step per statement or expression, more for work on long
|
|
/// strings and lists).
|
|
fn charge(&mut self, n: u64) -> R<()> {
|
|
let before = self.steps;
|
|
self.steps = self.steps.saturating_add(n);
|
|
if self.steps > MAX_STEPS {
|
|
return Err(RAN_TOO_LONG.into());
|
|
}
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if self.steps / CLOCK_EVERY != before / CLOCK_EVERY
|
|
&& let Some(d) = self.deadline
|
|
&& std::time::Instant::now() > d
|
|
{
|
|
return Err(RAN_TOO_LONG.into());
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
let _ = before;
|
|
Ok(())
|
|
}
|
|
|
|
/// `more` nodes into `out`, each once, never past [`MAX_LIST`].
|
|
fn gather(&mut self, out: &mut Vec<usize>, seen: &mut HashSet<usize>, more: impl IntoIterator<Item = usize>) -> R<()> {
|
|
for i in more {
|
|
if seen.insert(i) {
|
|
out.push(i);
|
|
if out.len() > MAX_LIST {
|
|
return Err(LIST_TOO_LONG.into());
|
|
}
|
|
}
|
|
}
|
|
self.charge(1)
|
|
}
|
|
|
|
/// The nodes of `v`, each once.
|
|
fn nodes_list(&self, v: &Val) -> Vec<usize> {
|
|
let mut seen = HashSet::new();
|
|
self.nodes_of(v).into_iter().filter(|i| seen.insert(*i)).collect()
|
|
}
|
|
|
|
fn node(&self, i: usize) -> Option<&HNode> {
|
|
self.h.nodes.get(i).filter(|n| !n.gone)
|
|
}
|
|
|
|
// ── coercions
|
|
|
|
/// A node's value as a FormCalc value.
|
|
fn node_value(&self, i: usize) -> Val {
|
|
let Some(n) = self.node(i) else { return Val::Null };
|
|
if n.value.is_empty() {
|
|
return Val::Null;
|
|
}
|
|
if n.numeric
|
|
&& let Some(v) = parse_num(&n.value)
|
|
{
|
|
return Val::Num(v);
|
|
}
|
|
Val::Str(n.value.clone())
|
|
}
|
|
|
|
/// What copying `v` (or the values it stands for) costs, in steps: one per 256 bytes, so
|
|
/// copying long strings over and over runs out of steps even where there is no clock
|
|
/// (wasm). A form object counts its value; a list its items too.
|
|
fn cost(&self, v: &Val) -> u64 {
|
|
let mut bytes = 0usize;
|
|
let mut todo = vec![v];
|
|
while let Some(v) = todo.pop() {
|
|
match v {
|
|
Val::Str(s) => bytes = bytes.saturating_add(s.len()),
|
|
Val::Node(i) => bytes = bytes.saturating_add(self.node(*i).map_or(0, |n| n.value.len())),
|
|
Val::List(items) => {
|
|
bytes = bytes.saturating_add(items.len().saturating_mul(16));
|
|
todo.extend(items.iter());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
(bytes / 256) as u64
|
|
}
|
|
|
|
/// [`Self::deref`], charged for the copy.
|
|
fn take(&mut self, v: Val) -> R<Val> {
|
|
self.charge(self.cost(&v))?;
|
|
Ok(self.deref(v))
|
|
}
|
|
|
|
/// Objects become their values; lists of objects become lists of values.
|
|
fn deref(&self, v: Val) -> Val {
|
|
match v {
|
|
Val::Node(i) => self.node_value(i),
|
|
Val::List(items) => Val::List(items.into_iter().map(|x| self.deref(x)).collect()),
|
|
other => other,
|
|
}
|
|
}
|
|
|
|
fn to_num(&self, v: &Val) -> f64 {
|
|
match v {
|
|
Val::Null => 0.0,
|
|
Val::Num(n) => *n,
|
|
Val::Str(s) => parse_num(s).unwrap_or(0.0),
|
|
Val::Node(i) => self.to_num(&self.node_value(*i)),
|
|
Val::List(items) => items.first().map_or(0.0, |x| self.to_num(x)),
|
|
_ => 0.0,
|
|
}
|
|
}
|
|
|
|
fn to_str(&self, v: &Val) -> String {
|
|
match v {
|
|
Val::Null => String::new(),
|
|
Val::Num(n) => num_text(*n),
|
|
Val::Str(s) => s.clone(),
|
|
Val::Node(i) => self.to_str(&self.node_value(*i)),
|
|
Val::List(items) => items.first().map_or(String::new(), |x| self.to_str(x)),
|
|
Val::Host => "host".into(),
|
|
Val::Layout => "layout".into(),
|
|
Val::Event => "event".into(),
|
|
Val::Xfa => "xfa".into(),
|
|
Val::Im(..) => "instanceManager".into(),
|
|
Val::Sink => String::new(),
|
|
}
|
|
}
|
|
|
|
fn truthy(&self, v: &Val) -> bool {
|
|
match v {
|
|
Val::Null => false,
|
|
Val::Num(n) => *n != 0.0 && !n.is_nan(),
|
|
Val::Str(s) => parse_num(s).is_some_and(|n| n != 0.0),
|
|
Val::Node(i) => {
|
|
let d = self.node_value(*i);
|
|
self.truthy(&d)
|
|
}
|
|
Val::List(items) => !items.is_empty(),
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
fn is_null(&self, v: &Val) -> bool {
|
|
match v {
|
|
Val::Null => true,
|
|
Val::Node(i) => self.node(*i).is_none_or(|n| n.value.is_empty()),
|
|
Val::List(items) => items.is_empty(),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
// ── scopes
|
|
|
|
fn lookup(&self, name: &str) -> Option<Val> {
|
|
self.scopes.iter().rev().find_map(|s| s.get(name).cloned())
|
|
}
|
|
|
|
fn set_var(&mut self, name: &str, v: Val) -> bool {
|
|
for s in self.scopes.iter_mut().rev() {
|
|
if let Some(slot) = s.get_mut(name) {
|
|
*slot = v;
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn declare(&mut self, name: &str, v: Val) {
|
|
if let Some(s) = self.scopes.last_mut() {
|
|
s.insert(name.to_string(), v);
|
|
}
|
|
}
|
|
|
|
// ── accessors
|
|
|
|
/// The objects an identifier names from the current node: `$`, the special objects, an
|
|
/// instance manager (`_name`), a variable, or a form object by name (children first, then
|
|
/// the containing subforms).
|
|
fn ident(&self, name: &str) -> R<Val> {
|
|
if let Some(v) = self.lookup(name) {
|
|
return Ok(v);
|
|
}
|
|
let lower = name.to_ascii_lowercase();
|
|
Ok(match lower.as_str() {
|
|
"$" | "this" => Val::Node(self.h.current),
|
|
"$host" => Val::Host,
|
|
"$layout" => Val::Layout,
|
|
"$event" => Val::Event,
|
|
"$form" | "$data" | "$dataset" | "$datasets" => Val::Node(0),
|
|
"$record" => Val::Node(if self.node(1).is_some() { 1 } else { 0 }),
|
|
"$template" | "$xfa" => Val::Xfa,
|
|
"xfa" => Val::Xfa,
|
|
_ => {
|
|
if let Some(child) = name.strip_prefix('_') {
|
|
// The instance manager of `child`, looked for like a name.
|
|
let found = resolve_som(self.h, self.h.current, child);
|
|
let Some(&first) = found.first() else { return Err(format!("unknown instance manager _{child}")) };
|
|
let parent = self.node(first).and_then(|n| n.parent).ok_or_else(|| format!("unknown instance manager _{child}"))?;
|
|
return Ok(Val::Im(parent, child.to_string()));
|
|
}
|
|
let found = resolve_som(self.h, self.h.current, name);
|
|
match found.as_slice() {
|
|
[] => return Err(format!("unknown name {name}")),
|
|
[one] => Val::Node(*one),
|
|
many => Val::List(many.iter().map(|i| Val::Node(*i)).collect()),
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Every object a value stands for.
|
|
fn nodes_of(&self, v: &Val) -> Vec<usize> {
|
|
match v {
|
|
Val::Node(i) => vec![*i],
|
|
Val::List(items) => items.iter().flat_map(|x| self.nodes_of(x)).collect(),
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn member(&mut self, base: Val, name: &str) -> R<Val> {
|
|
match base {
|
|
Val::Node(i) => self.node_member(i, name),
|
|
Val::List(items) => {
|
|
let mut out = Vec::new();
|
|
let mut seen = HashSet::new();
|
|
for it in items {
|
|
match self.member(it, name)? {
|
|
Val::List(more) => {
|
|
for m in more {
|
|
self.list_push(&mut out, &mut seen, m)?;
|
|
}
|
|
}
|
|
Val::Null => {}
|
|
v => self.list_push(&mut out, &mut seen, v)?,
|
|
}
|
|
}
|
|
Ok(match out.len() {
|
|
0 => Val::Null,
|
|
1 => out.remove(0),
|
|
_ => Val::List(out),
|
|
})
|
|
}
|
|
Val::Host => Ok(match name.to_ascii_lowercase().as_str() {
|
|
"numpages" => Val::Num(self.h.doc.page_count as f64),
|
|
"currentpage" => Val::Num(self.h.doc.page as f64),
|
|
"name" => Val::Str("Acrobat".into()),
|
|
"apptype" => Val::Str("Exchange-Pro".into()),
|
|
"version" => Val::Str("11".into()),
|
|
"variation" => Val::Str("Full".into()),
|
|
"language" => Val::Str("en".into()),
|
|
"platform" => Val::Str(crate::platform().into()),
|
|
"title" => Val::Str(self.h.doc.file_name.clone()),
|
|
"calculationsenabled" | "validationsenabled" => Val::Num(1.0),
|
|
_ => Val::Sink,
|
|
}),
|
|
Val::Layout => Ok(Val::Sink),
|
|
Val::Event => Ok(match name.to_ascii_lowercase().as_str() {
|
|
"name" => Val::Str(self.event.activity.clone()),
|
|
"newtext" | "fulltext" => Val::Str(self.event.new_text.clone()),
|
|
"prevtext" => Val::Str(self.event.prev_text.clone()),
|
|
"change" => Val::Str(String::new()),
|
|
"target" => Val::Node(self.h.current),
|
|
"selstart" | "selend" | "cancelaction" | "reenter" | "shift" | "modifier" | "keydown" | "commitkey" => Val::Num(0.0),
|
|
_ => Val::Sink,
|
|
}),
|
|
Val::Xfa => Ok(match name.to_ascii_lowercase().as_str() {
|
|
"form" | "datasets" | "data" => Val::Node(0),
|
|
"record" => Val::Node(if self.node(1).is_some() { 1 } else { 0 }),
|
|
"host" => Val::Host,
|
|
"layout" => Val::Layout,
|
|
"event" => Val::Event,
|
|
_ => Val::Sink,
|
|
}),
|
|
Val::Im(parent, child) => Ok(match name.to_ascii_lowercase().as_str() {
|
|
"count" => Val::Num(instances(self.h, parent, &child).len() as f64),
|
|
"min" => Val::Num(instances(self.h, parent, &child).first().and_then(|i| self.node(*i)).map_or(0.0, |n| n.occur_min as f64)),
|
|
"max" => Val::Num(
|
|
instances(self.h, parent, &child).first().and_then(|i| self.node(*i)).and_then(|n| n.occur_max).map_or(-1.0, |m| m as f64),
|
|
),
|
|
"classname" => Val::Str("instanceManager".into()),
|
|
_ => Val::Sink,
|
|
}),
|
|
Val::Sink => Ok(Val::Sink),
|
|
Val::Null => Err(format!("no object to take {name} from")),
|
|
Val::Num(_) | Val::Str(_) => Err(format!("{} has no property {name}", self.to_str(&base))),
|
|
}
|
|
}
|
|
|
|
/// `v` onto `out`: a node only once, never past [`MAX_LIST`].
|
|
fn list_push(&mut self, out: &mut Vec<Val>, seen: &mut HashSet<usize>, v: Val) -> R<()> {
|
|
if let Val::Node(i) = v
|
|
&& !seen.insert(i)
|
|
{
|
|
return Ok(());
|
|
}
|
|
out.push(v);
|
|
if out.len() > MAX_LIST {
|
|
return Err(LIST_TOO_LONG.into());
|
|
}
|
|
self.charge(1)
|
|
}
|
|
|
|
fn node_member(&mut self, i: usize, name: &str) -> R<Val> {
|
|
let Some(n) = self.node(i) else { return Ok(Val::Null) };
|
|
Ok(match name {
|
|
"rawValue" | "value" | "formattedValue" => self.node_value(i),
|
|
"name" => Val::Str(n.name.clone()),
|
|
"somExpression" => Val::Str(n.som.clone()),
|
|
"className" => Val::Str(n.kind.class_name().into()),
|
|
"index" | "instanceIndex" => Val::Num(n.index as f64),
|
|
"presence" => Val::Str(n.presence.clone()),
|
|
"access" => Val::Str(n.access.clone()),
|
|
"isNull" => Val::Num(if n.value.is_empty() { 1.0 } else { 0.0 }),
|
|
"mandatory" => Val::Str("disabled".into()),
|
|
"parent" => n.parent.map_or(Val::Null, Val::Node),
|
|
"nodes" => Val::List(n.children.iter().copied().filter(|c| self.node(*c).is_some()).map(Val::Node).collect()),
|
|
"all" => match n.parent {
|
|
Some(p) => Val::List(instances(self.h, p, &n.name).into_iter().map(Val::Node).collect()),
|
|
None => Val::Node(i),
|
|
},
|
|
"instanceManager" => match n.parent {
|
|
Some(p) => Val::Im(p, n.name.clone()),
|
|
None => Val::Null,
|
|
},
|
|
"border" | "ui" | "font" | "caption" | "fillColor" | "borderColor" | "fontColor" | "margin" | "para" | "assist" | "validate"
|
|
| "format" | "items" | "bind" | "keep" | "h" | "w" | "x" | "y" | "minH" | "maxH" | "minW" | "maxW" | "layout" | "model"
|
|
| "oneOfChild" | "locale" | "relevant" | "colSpan" | "dataNode" | "defaultValue" | "editValue" | "selectedIndex" | "event"
|
|
| "calculate" | "traversal" | "extras" | "desc" => Val::Sink,
|
|
other => {
|
|
if let Some(child) = other.strip_prefix('_') {
|
|
return Ok(if child_named(self.h, i, child).is_some() { Val::Im(i, child.to_string()) } else { Val::Null });
|
|
}
|
|
let found = instances(self.h, i, other);
|
|
match found.as_slice() {
|
|
[] => Val::Null,
|
|
[one] => Val::Node(*one),
|
|
many => Val::List(many.iter().map(|k| Val::Node(*k)).collect()),
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// `base[n]` / `base[*]`: all the instances the last step named, then one or all.
|
|
fn index(&mut self, base: &Expr, idx: &Idx) -> R<Val> {
|
|
let all = self.all_of(base)?;
|
|
match idx {
|
|
Idx::All => Ok(Val::List(all.into_iter().map(Val::Node).collect())),
|
|
Idx::Abs(e) => {
|
|
let v = self.eval(e)?;
|
|
let n = self.to_num(&v);
|
|
if !n.is_finite() || n < 0.0 {
|
|
return Ok(Val::Null);
|
|
}
|
|
Ok(all.get(n as usize).map_or(Val::Null, |i| Val::Node(*i)))
|
|
}
|
|
Idx::Rel(k) => {
|
|
// From the instance the script runs in (`row[-1]` in a row is the row above),
|
|
// else from the first.
|
|
let here = all.iter().position(|&i| self.encloses(i, self.h.current)).unwrap_or(0);
|
|
let at = (here as i64).saturating_add(*k);
|
|
Ok(usize::try_from(at).ok().and_then(|at| all.get(at)).map_or(Val::Null, |i| Val::Node(*i)))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether `node` is `inner` or one of its containers.
|
|
fn encloses(&self, node: usize, inner: usize) -> bool {
|
|
let mut at = Some(inner);
|
|
let mut guard = 0;
|
|
while let Some(i) = at
|
|
&& guard < MAX_PARSE_DEPTH
|
|
{
|
|
if i == node {
|
|
return true;
|
|
}
|
|
at = self.h.nodes.get(i).and_then(|n| n.parent);
|
|
guard += 1;
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Every instance an accessor names (`Row` → all rows, not just the first), each once.
|
|
fn all_of(&mut self, e: &Expr) -> R<Vec<usize>> {
|
|
let mut out = Vec::new();
|
|
let mut seen = HashSet::new();
|
|
match e {
|
|
Expr::Ident(name) => {
|
|
if self.lookup(name).is_some() || name.starts_with('$') || name.eq_ignore_ascii_case("xfa") {
|
|
let v = self.eval(e)?;
|
|
return Ok(self.nodes_list(&v));
|
|
}
|
|
// The name resolves to its first instance; `row[*]` means every sibling of it.
|
|
for first in resolve_som(self.h, self.h.current, name) {
|
|
match self.node(first).and_then(|n| n.parent.map(|p| (p, n.name.clone()))) {
|
|
Some((p, n)) => self.gather(&mut out, &mut seen, instances(self.h, p, &n))?,
|
|
None => self.gather(&mut out, &mut seen, [first])?,
|
|
}
|
|
}
|
|
}
|
|
Expr::Member(base, name) => {
|
|
let b = self.eval(base)?;
|
|
for i in self.nodes_list(&b) {
|
|
self.gather(&mut out, &mut seen, instances(self.h, i, name))?;
|
|
}
|
|
if out.is_empty() {
|
|
let v = self.member(b, name)?;
|
|
out = self.nodes_list(&v);
|
|
}
|
|
}
|
|
Expr::Descend(base, name) => {
|
|
let b = self.eval(base)?;
|
|
for i in self.nodes_list(&b) {
|
|
self.gather(&mut out, &mut seen, descendants_named(self.h, i, name, 0))?;
|
|
}
|
|
}
|
|
other => {
|
|
let v = self.eval(other)?;
|
|
out = self.nodes_list(&v);
|
|
}
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
// ── expressions
|
|
|
|
fn eval(&mut self, e: &Expr) -> R<Val> {
|
|
self.step()?;
|
|
self.depth += 1;
|
|
if self.depth > self.max_depth {
|
|
self.depth -= 1;
|
|
return Err(TOO_DEEP.into());
|
|
}
|
|
let r = self.eval_inner(e);
|
|
self.depth -= 1;
|
|
r
|
|
}
|
|
|
|
fn eval_inner(&mut self, e: &Expr) -> R<Val> {
|
|
Ok(match e {
|
|
Expr::Num(n) => Val::Num(*n),
|
|
Expr::Str(s) => {
|
|
self.charge((s.len() / 256) as u64)?;
|
|
Val::Str(s.clone())
|
|
}
|
|
Expr::Null => Val::Null,
|
|
Expr::Ident(name) => {
|
|
let v = self.ident(name)?;
|
|
self.charge(self.cost(&v))?;
|
|
v
|
|
}
|
|
Expr::Member(base, name) => {
|
|
let b = self.eval(base)?;
|
|
self.member(b, name)?
|
|
}
|
|
Expr::Index(base, idx) => self.index(base, idx)?,
|
|
Expr::Descend(base, name) => {
|
|
let b = self.eval(base)?;
|
|
let mut out = Vec::new();
|
|
let mut seen = HashSet::new();
|
|
for i in self.nodes_list(&b) {
|
|
self.gather(&mut out, &mut seen, descendants_named(self.h, i, name, 0))?;
|
|
}
|
|
match out.as_slice() {
|
|
[] => Val::Null,
|
|
[one] => Val::Node(*one),
|
|
many => Val::List(many.iter().map(|k| Val::Node(*k)).collect()),
|
|
}
|
|
}
|
|
Expr::Call(callee, args) => self.call(callee, args)?,
|
|
Expr::Neg(x) => {
|
|
let v = self.eval(x)?;
|
|
if self.is_null(&v) { Val::Null } else { Val::Num(-self.to_num(&v)) }
|
|
}
|
|
Expr::Not(x) => {
|
|
let v = self.eval(x)?;
|
|
Val::Num(if self.truthy(&v) { 0.0 } else { 1.0 })
|
|
}
|
|
Expr::Bin(..) => {
|
|
// The parser leans left (`a + b + c` is `(a + b) + c`), so a chain is walked
|
|
// from its leftmost operand and folded without recursing per operator.
|
|
let mut links = Vec::new();
|
|
let mut leftmost = e;
|
|
while let Expr::Bin(op, l, r) = leftmost {
|
|
links.push((*op, r.as_ref()));
|
|
leftmost = l;
|
|
}
|
|
let mut acc = self.eval(leftmost)?;
|
|
for (op, r) in links.into_iter().rev() {
|
|
self.step()?;
|
|
acc = match op {
|
|
"&" => {
|
|
if !self.truthy(&acc) {
|
|
// Not evaluated: the result is settled.
|
|
Val::Num(0.0)
|
|
} else {
|
|
let b = self.eval(r)?;
|
|
Val::Num(if self.truthy(&b) { 1.0 } else { 0.0 })
|
|
}
|
|
}
|
|
"|" => {
|
|
if self.truthy(&acc) {
|
|
Val::Num(1.0)
|
|
} else {
|
|
let b = self.eval(r)?;
|
|
Val::Num(if self.truthy(&b) { 1.0 } else { 0.0 })
|
|
}
|
|
}
|
|
_ => {
|
|
let b = self.eval(r)?;
|
|
// Comparing (and dereferencing) long strings is work too.
|
|
self.charge(self.cost(&acc).saturating_add(self.cost(&b)))?;
|
|
self.binary(op, acc, b)?
|
|
}
|
|
};
|
|
}
|
|
acc
|
|
}
|
|
})
|
|
}
|
|
|
|
fn binary(&self, op: &str, a: Val, b: Val) -> R<Val> {
|
|
let (a, b) = (self.deref(a), self.deref(b));
|
|
// Null with an arithmetic operator is null only when both sides are null.
|
|
match op {
|
|
"+" | "-" | "*" | "/" => {
|
|
if self.is_null(&a) && self.is_null(&b) {
|
|
return Ok(Val::Null);
|
|
}
|
|
let (x, y) = (self.to_num(&a), self.to_num(&b));
|
|
let v = match op {
|
|
"+" => x + y,
|
|
"-" => x - y,
|
|
"*" => x * y,
|
|
_ => {
|
|
if y == 0.0 {
|
|
return Err("division by zero".into());
|
|
}
|
|
x / y
|
|
}
|
|
};
|
|
Ok(Val::Num(v))
|
|
}
|
|
_ => {
|
|
let ord = match (&a, &b) {
|
|
(Val::Null, Val::Null) => Some(std::cmp::Ordering::Equal),
|
|
(Val::Null, _) | (_, Val::Null) => None,
|
|
(Val::Str(s), Val::Str(t)) if !(is_numeric_text(s) && is_numeric_text(t)) => Some(s.cmp(t)),
|
|
(Val::Str(s), Val::Num(_)) if !is_numeric_text(s) => Some(s.as_str().cmp(num_text(self.to_num(&b)).as_str())),
|
|
(Val::Num(_), Val::Str(t)) if !is_numeric_text(t) => Some(num_text(self.to_num(&a)).as_str().cmp(t.as_str())),
|
|
_ => self.to_num(&a).partial_cmp(&self.to_num(&b)),
|
|
};
|
|
let r = match (op, ord) {
|
|
("==", Some(o)) => o.is_eq(),
|
|
("<>", Some(o)) => !o.is_eq(),
|
|
("<>", None) => true,
|
|
("<", Some(o)) => o.is_lt(),
|
|
("<=", Some(o)) => o.is_le(),
|
|
(">", Some(o)) => o.is_gt(),
|
|
(">=", Some(o)) => o.is_ge(),
|
|
_ => false,
|
|
};
|
|
Ok(Val::Num(if r { 1.0 } else { 0.0 }))
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── calls
|
|
|
|
fn call(&mut self, callee: &Expr, args: &[Expr]) -> R<Val> {
|
|
match callee {
|
|
Expr::Ident(name) => {
|
|
if let Some(f) = self.funcs.get(&name.to_ascii_lowercase()).cloned() {
|
|
let mut vals = Vec::new();
|
|
for a in args {
|
|
let v = self.eval(a)?;
|
|
vals.push(self.take(v)?);
|
|
}
|
|
return self.call_user(&f, vals);
|
|
}
|
|
self.builtin(name, args)
|
|
}
|
|
Expr::Member(base, method) => {
|
|
let b = self.eval(base)?;
|
|
self.method(b, method, args)
|
|
}
|
|
_ => Err("not a function".into()),
|
|
}
|
|
}
|
|
|
|
fn call_user(&mut self, f: &Func, vals: Vec<Val>) -> R<Val> {
|
|
if self.scopes.len() > self.max_depth {
|
|
return Err(CALLS_TOO_DEEP.into());
|
|
}
|
|
let mut scope = HashMap::new();
|
|
for (i, p) in f.params.iter().enumerate() {
|
|
scope.insert(p.clone(), vals.get(i).cloned().unwrap_or(Val::Null));
|
|
}
|
|
self.scopes.push(scope);
|
|
let r = self.block(&f.body);
|
|
self.scopes.pop();
|
|
match r? {
|
|
(Flow::Return(v), _) => Ok(v),
|
|
(Flow::Exit, last) => {
|
|
self.exited = true;
|
|
Ok(last)
|
|
}
|
|
(_, last) => Ok(last),
|
|
}
|
|
}
|
|
|
|
fn arg(&mut self, args: &[Expr], i: usize) -> R<Val> {
|
|
match args.get(i) {
|
|
Some(e) => {
|
|
let v = self.eval(e)?;
|
|
self.take(v)
|
|
}
|
|
None => Ok(Val::Null),
|
|
}
|
|
}
|
|
|
|
fn arg_num(&mut self, args: &[Expr], i: usize) -> R<f64> {
|
|
let v = self.arg(args, i)?;
|
|
Ok(self.to_num(&v))
|
|
}
|
|
|
|
fn arg_str(&mut self, args: &[Expr], i: usize) -> R<String> {
|
|
let v = self.arg(args, i)?;
|
|
Ok(self.to_str(&v))
|
|
}
|
|
|
|
/// Every value the arguments hold, lists flattened, nulls left out.
|
|
fn arg_values(&mut self, args: &[Expr]) -> R<Vec<Val>> {
|
|
self.arg_values_keeping(args, false)
|
|
}
|
|
|
|
/// [`Self::arg_values`]; `objects` keeps form objects (fields, subforms) as themselves, so
|
|
/// `Count(row[*])` counts rows rather than filled-in values.
|
|
fn arg_values_keeping(&mut self, args: &[Expr], objects: bool) -> R<Vec<Val>> {
|
|
let mut out = Vec::new();
|
|
for a in args {
|
|
let v = self.eval(a)?;
|
|
let v = if objects { v } else { self.take(v)? };
|
|
match v {
|
|
Val::List(items) => out.extend(items.into_iter().filter(|x| !matches!(x, Val::Null))),
|
|
Val::Null => {}
|
|
other => out.push(other),
|
|
}
|
|
if out.len() > MAX_LIST {
|
|
return Err(LIST_TOO_LONG.into());
|
|
}
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
fn method(&mut self, base: Val, method: &str, args: &[Expr]) -> R<Val> {
|
|
let m = method.to_ascii_lowercase();
|
|
match base {
|
|
Val::Host => match m.as_str() {
|
|
"messagebox" => {
|
|
let t = self.arg_str(args, 0)?;
|
|
self.h.push(XfaEffect::MessageBox(t));
|
|
Ok(Val::Num(1.0))
|
|
}
|
|
"resetdata" => {
|
|
let list = self.arg_str(args, 0)?;
|
|
let names: Vec<String> = list.split(',').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string).collect();
|
|
self.h.push(XfaEffect::ResetData(names));
|
|
Ok(Val::Null)
|
|
}
|
|
"print" => {
|
|
self.h.push(XfaEffect::Print);
|
|
Ok(Val::Null)
|
|
}
|
|
"beep" => {
|
|
self.h.push(XfaEffect::Beep);
|
|
Ok(Val::Null)
|
|
}
|
|
"gotourl" => {
|
|
let u = self.arg_str(args, 0)?;
|
|
self.h.push(XfaEffect::LaunchUrl(u));
|
|
Ok(Val::Null)
|
|
}
|
|
"setfocus" => {
|
|
let target = match args.first() {
|
|
Some(e) => {
|
|
let raw = self.eval(e)?;
|
|
match self.nodes_of(&raw).first() {
|
|
Some(i) => self.node(*i).map(|n| n.som.clone()).unwrap_or_default(),
|
|
None => {
|
|
let s = self.to_str(&raw);
|
|
let found = resolve_som(self.h, self.h.current, &s);
|
|
found.first().and_then(|i| self.node(*i)).map(|n| n.som.clone()).unwrap_or(s)
|
|
}
|
|
}
|
|
}
|
|
None => String::new(),
|
|
};
|
|
self.h.push(XfaEffect::SetFocus(target));
|
|
Ok(Val::Null)
|
|
}
|
|
_ => Ok(Val::Null),
|
|
},
|
|
Val::Layout => match m.as_str() {
|
|
"page" | "abspage" => Ok(Val::Num(self.h.doc.page as f64 + if m == "page" { 1.0 } else { 0.0 })),
|
|
"pagecount" | "abspagecount" => Ok(Val::Num(self.h.doc.page_count as f64)),
|
|
"relayout" | "relayoutpagearea" => {
|
|
self.h.push(XfaEffect::Relayout);
|
|
Ok(Val::Null)
|
|
}
|
|
_ => Ok(Val::Num(0.0)),
|
|
},
|
|
Val::Xfa => match m.as_str() {
|
|
"resolvenode" | "resolvenodes" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let found = resolve_som(self.h, self.h.current, &s);
|
|
Ok(self.set_val(found, m == "resolvenodes"))
|
|
}
|
|
"recalculate" => {
|
|
self.h.push(XfaEffect::Recalculate);
|
|
Ok(Val::Null)
|
|
}
|
|
"remerge" => {
|
|
self.h.push(XfaEffect::Relayout);
|
|
Ok(Val::Null)
|
|
}
|
|
_ => Ok(Val::Null),
|
|
},
|
|
Val::Node(i) => match m.as_str() {
|
|
"resolvenode" | "resolvenodes" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let found = resolve_som(self.h, i, &s);
|
|
Ok(self.set_val(found, m == "resolvenodes"))
|
|
}
|
|
"recalculate" => {
|
|
self.h.push(XfaEffect::Recalculate);
|
|
Ok(Val::Null)
|
|
}
|
|
"remerge" => {
|
|
self.h.push(XfaEffect::Relayout);
|
|
Ok(Val::Null)
|
|
}
|
|
_ => Ok(Val::Null),
|
|
},
|
|
Val::Im(parent, name) => match m.as_str() {
|
|
"addinstance" | "insertinstance" => Ok(self.h.add_instance(parent, &name).map_or(Val::Null, Val::Node)),
|
|
"removeinstance" => {
|
|
let i = self.arg_num(args, 0)?;
|
|
if i.is_finite() && i >= 0.0 {
|
|
self.h.remove_instance(parent, &name, i as usize);
|
|
}
|
|
Ok(Val::Null)
|
|
}
|
|
"setinstances" => {
|
|
let want = self.arg_num(args, 0)?;
|
|
if want.is_finite() && want >= 0.0 {
|
|
self.h.set_instances(parent, &name, want as usize);
|
|
}
|
|
Ok(Val::Null)
|
|
}
|
|
_ => Ok(Val::Null),
|
|
},
|
|
Val::Event | Val::Sink | Val::List(_) => Ok(Val::Null),
|
|
Val::Null => Err(format!("no object to call {method} on")),
|
|
Val::Num(_) | Val::Str(_) => Err(format!("{} has no method {method}", self.to_str(&base))),
|
|
}
|
|
}
|
|
|
|
fn set_val(&self, found: Vec<usize>, many: bool) -> Val {
|
|
if many {
|
|
return Val::List(found.into_iter().map(Val::Node).collect());
|
|
}
|
|
match found.as_slice() {
|
|
[] => Val::Null,
|
|
[one] => Val::Node(*one),
|
|
all => Val::List(all.iter().map(|i| Val::Node(*i)).collect()),
|
|
}
|
|
}
|
|
|
|
// ── assignment
|
|
|
|
fn assign(&mut self, target: &Expr, v: Val) -> R<()> {
|
|
let v = self.deref(v);
|
|
match target {
|
|
Expr::Ident(name) => {
|
|
if self.set_var(name, v.clone()) {
|
|
return Ok(());
|
|
}
|
|
let t = self.ident(name)?;
|
|
self.assign_to(t, "rawValue", v)
|
|
}
|
|
Expr::Member(base, prop) => {
|
|
let b = self.eval(base)?;
|
|
self.assign_to(b, prop, v)
|
|
}
|
|
Expr::Index(..) | Expr::Descend(..) => {
|
|
let t = self.eval(target)?;
|
|
self.assign_to(t, "rawValue", v)
|
|
}
|
|
_ => Err("cannot assign to that".into()),
|
|
}
|
|
}
|
|
|
|
fn assign_to(&mut self, target: Val, prop: &str, v: Val) -> R<()> {
|
|
match target {
|
|
Val::Node(i) => {
|
|
let Some(n) = self.node(i) else { return Ok(()) };
|
|
let som = n.som.clone();
|
|
match prop {
|
|
"rawValue" | "value" | "formattedValue" => {
|
|
let text = self.to_str(&v);
|
|
if !self.h.set_value(i, text) {
|
|
return Err(format!("{} is not a field", if som.is_empty() { "the form" } else { som.as_str() }));
|
|
}
|
|
}
|
|
"presence" => {
|
|
let p = self.to_str(&v);
|
|
if matches!(p.as_str(), "visible" | "invisible" | "hidden" | "inactive") {
|
|
if let Some(n) = self.h.nodes.get_mut(i) {
|
|
n.presence = p.clone();
|
|
}
|
|
self.h.push(XfaEffect::SetPresence { som, presence: p });
|
|
}
|
|
}
|
|
"access" => {
|
|
let a = self.to_str(&v);
|
|
if matches!(a.as_str(), "open" | "readOnly" | "protected" | "nonInteractive") {
|
|
if let Some(n) = self.h.nodes.get_mut(i) {
|
|
n.access = a.clone();
|
|
}
|
|
self.h.push(XfaEffect::SetAccess { som, access: a });
|
|
}
|
|
}
|
|
other => {
|
|
// `page1.qty = 7`: the child object named by the last step takes the value.
|
|
if let Some(child) = other.strip_prefix('_') {
|
|
if child_named(self.h, i, child).is_some() {
|
|
return self.assign_to(Val::Im(i, child.to_string()), "count", v);
|
|
}
|
|
return Ok(());
|
|
}
|
|
let kids = instances(self.h, i, other);
|
|
if kids.is_empty() {
|
|
return Err(format!("{som} has no {other} to assign to"));
|
|
}
|
|
for k in kids {
|
|
self.assign_to(Val::Node(k), "rawValue", v.clone())?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
Val::List(items) => {
|
|
for it in items {
|
|
self.assign_to(it, prop, v.clone())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Val::Im(parent, name) if prop.eq_ignore_ascii_case("count") => {
|
|
let want = self.to_num(&v);
|
|
if want.is_finite() && want >= 0.0 {
|
|
self.h.set_instances(parent, &name, want as usize);
|
|
}
|
|
Ok(())
|
|
}
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
// ── statements
|
|
|
|
/// Run a block; returns how it ended and the value of its last expression statement.
|
|
fn block(&mut self, body: &[Stmt]) -> R<(Flow, Val)> {
|
|
// Nesting of statements counts like nesting of expressions: both are stack.
|
|
self.depth += 1;
|
|
if self.depth > self.max_depth {
|
|
self.depth -= 1;
|
|
return Err(TOO_DEEP.into());
|
|
}
|
|
let r = self.block_inner(body);
|
|
self.depth -= 1;
|
|
r
|
|
}
|
|
|
|
fn block_inner(&mut self, body: &[Stmt]) -> R<(Flow, Val)> {
|
|
let mut last = Val::Null;
|
|
for s in body {
|
|
self.step()?;
|
|
match self.exec(s)? {
|
|
(Flow::Next, v) => {
|
|
if let Some(v) = v {
|
|
last = v;
|
|
}
|
|
if self.exited {
|
|
return Ok((Flow::Exit, last));
|
|
}
|
|
}
|
|
(flow, _) => return Ok((flow, last)),
|
|
}
|
|
}
|
|
Ok((Flow::Next, last))
|
|
}
|
|
|
|
fn exec(&mut self, s: &Stmt) -> R<(Flow, Option<Val>)> {
|
|
match s {
|
|
Stmt::Var(name, init) => {
|
|
let v = match init {
|
|
Some(e) => {
|
|
let v = self.eval(e)?;
|
|
self.take(v)?
|
|
}
|
|
None => Val::Null,
|
|
};
|
|
self.declare(name, v.clone());
|
|
Ok((Flow::Next, Some(v)))
|
|
}
|
|
Stmt::Assign(target, e) => {
|
|
let v = self.eval(e)?;
|
|
let v = self.take(v)?;
|
|
self.assign(target, v.clone())?;
|
|
Ok((Flow::Next, Some(v)))
|
|
}
|
|
Stmt::Expr(e) => {
|
|
let v = self.eval(e)?;
|
|
let v = self.take(v)?;
|
|
Ok((Flow::Next, Some(v)))
|
|
}
|
|
Stmt::If(arms, otherwise) => {
|
|
for (cond, body) in arms {
|
|
let c = self.eval(cond)?;
|
|
if self.truthy(&c) {
|
|
let (f, v) = self.block(body)?;
|
|
return Ok((f, Some(v)));
|
|
}
|
|
}
|
|
if let Some(body) = otherwise {
|
|
let (f, v) = self.block(body)?;
|
|
return Ok((f, Some(v)));
|
|
}
|
|
Ok((Flow::Next, None))
|
|
}
|
|
Stmt::While(cond, body) => {
|
|
loop {
|
|
let c = self.eval(cond)?;
|
|
if !self.truthy(&c) {
|
|
break;
|
|
}
|
|
self.iterate()?;
|
|
match self.block(body)? {
|
|
(Flow::Break, _) => break,
|
|
(Flow::Return(v), _) => return Ok((Flow::Return(v), None)),
|
|
(Flow::Exit, _) => return Ok((Flow::Exit, None)),
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok((Flow::Next, None))
|
|
}
|
|
Stmt::For { var, from, to, up, step, body } => {
|
|
let start = {
|
|
let v = self.eval(from)?;
|
|
self.to_num(&v)
|
|
};
|
|
let end = {
|
|
let v = self.eval(to)?;
|
|
self.to_num(&v)
|
|
};
|
|
let by = match step {
|
|
Some(e) => {
|
|
let v = self.eval(e)?;
|
|
self.to_num(&v).abs()
|
|
}
|
|
None => 1.0,
|
|
};
|
|
if by.is_nan() || by <= 0.0 || !start.is_finite() || !end.is_finite() {
|
|
return Err("bad for loop bounds".into());
|
|
}
|
|
self.declare(var, Val::Num(start));
|
|
let mut i = start;
|
|
while if *up { i <= end } else { i >= end } {
|
|
self.iterate()?;
|
|
self.set_var(var, Val::Num(i));
|
|
match self.block(body)? {
|
|
(Flow::Break, _) => break,
|
|
(Flow::Return(v), _) => return Ok((Flow::Return(v), None)),
|
|
(Flow::Exit, _) => return Ok((Flow::Exit, None)),
|
|
_ => {}
|
|
}
|
|
i = if *up { i + by } else { i - by };
|
|
}
|
|
Ok((Flow::Next, None))
|
|
}
|
|
Stmt::Foreach { var, list, body } => {
|
|
let items = self.arg_values_keeping(list, true)?;
|
|
self.declare(var, Val::Null);
|
|
for it in items {
|
|
self.iterate()?;
|
|
self.set_var(var, it);
|
|
match self.block(body)? {
|
|
(Flow::Break, _) => break,
|
|
(Flow::Return(v), _) => return Ok((Flow::Return(v), None)),
|
|
(Flow::Exit, _) => return Ok((Flow::Exit, None)),
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok((Flow::Next, None))
|
|
}
|
|
Stmt::Func { name, f } => {
|
|
self.funcs.insert(name.to_ascii_lowercase(), Rc::clone(f));
|
|
Ok((Flow::Next, None))
|
|
}
|
|
Stmt::Return(e) => {
|
|
let v = match e {
|
|
Some(e) => {
|
|
let v = self.eval(e)?;
|
|
self.take(v)?
|
|
}
|
|
None => Val::Null,
|
|
};
|
|
Ok((Flow::Return(v), None))
|
|
}
|
|
Stmt::Break => Ok((Flow::Break, None)),
|
|
Stmt::Continue => Ok((Flow::Continue, None)),
|
|
Stmt::Exit => Ok((Flow::Exit, None)),
|
|
Stmt::Throw(e) => {
|
|
let v = self.eval(e)?;
|
|
self.charge(self.cost(&v))?;
|
|
// The thrown value becomes the error message the caller keeps: cut it as
|
|
// messages are.
|
|
Err(clip(self.to_str(&v)))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn iterate(&mut self) -> R<()> {
|
|
if self.loops_left == 0 {
|
|
return Err(LOOPED_TOO_MUCH.into());
|
|
}
|
|
self.loops_left -= 1;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// ── built-in functions ──────────────────────────────────────────────────────────────────────
|
|
|
|
/// Days from 1900-01-01 (day 1, as FormCalc counts) to a civil date.
|
|
fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
|
|
// Howard Hinnant's algorithm, epoch 1970-01-01, shifted to FormCalc's 1900-01-01 = 1.
|
|
let y = if m <= 2 { y - 1 } else { y };
|
|
let era = if y >= 0 { y } else { y - 399 } / 400;
|
|
let yoe = y - era * 400;
|
|
let mp = (i64::from(m) + 9) % 12;
|
|
let doy = (153 * mp + 2) / 5 + i64::from(d) - 1;
|
|
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
let unix = era * 146_097 + doe - 719_468;
|
|
unix + 25_568
|
|
}
|
|
|
|
fn civil_from_days(n: i64) -> (i64, u32, u32) {
|
|
let z = n - 25_568 + 719_468;
|
|
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
|
let doe = z - era * 146_097;
|
|
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
|
let y = yoe + era * 400;
|
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
let mp = (5 * doy + 2) / 153;
|
|
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
|
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
|
|
(if m <= 2 { y + 1 } else { y }, m, d)
|
|
}
|
|
|
|
/// Day numbers (and years) beyond this are refused rather than computed.
|
|
const MAX_DAYS: f64 = 1e8;
|
|
|
|
const MONTHS: [&str; 12] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
|
|
/// Parse `text` as a date by `picture` (`YYYY-MM-DD`, `MM/DD/YYYY`, `MMM D, YYYY`, …).
|
|
fn parse_date(text: &str, picture: &str) -> Option<(i64, u32, u32)> {
|
|
let pic: Vec<char> = picture.chars().collect();
|
|
let txt: Vec<char> = text.trim().chars().collect();
|
|
let (mut y, mut m, mut d) = (None, None, None);
|
|
let (mut i, mut j) = (0, 0);
|
|
while i < pic.len() {
|
|
let c = pic[i].to_ascii_uppercase();
|
|
if c == 'Y' || c == 'M' || c == 'D' {
|
|
let mut run = 1;
|
|
while i + run < pic.len() && pic[i + run].to_ascii_uppercase() == c {
|
|
run += 1;
|
|
}
|
|
i += run;
|
|
if c == 'M' && run >= 3 {
|
|
// Month name.
|
|
let rest: String = txt.get(j..).unwrap_or(&[]).iter().collect();
|
|
let lower = rest.to_ascii_lowercase();
|
|
let found = MONTHS
|
|
.iter()
|
|
.position(|name| lower.starts_with(&name.to_ascii_lowercase()) || lower.starts_with(&name[..3].to_ascii_lowercase()));
|
|
let k = found?;
|
|
m = Some(k as u32 + 1);
|
|
let full = MONTHS[k].to_ascii_lowercase();
|
|
j += if lower.starts_with(&full) { full.chars().count() } else { 3 };
|
|
continue;
|
|
}
|
|
let width = if run == 1 { 2 } else { run };
|
|
let mut digits = String::new();
|
|
while j < txt.len() && txt[j].is_ascii_digit() && digits.len() < width {
|
|
digits.push(txt[j]);
|
|
j += 1;
|
|
}
|
|
if digits.is_empty() {
|
|
return None;
|
|
}
|
|
let v: i64 = digits.parse().ok()?;
|
|
match c {
|
|
'Y' => y = Some(if run <= 2 { if v < 50 { 2000 + v } else { 1900 + v } } else { v }),
|
|
'M' => m = Some(v as u32),
|
|
_ => d = Some(v as u32),
|
|
}
|
|
} else {
|
|
if txt.get(j) != Some(&pic[i]) {
|
|
return None;
|
|
}
|
|
i += 1;
|
|
j += 1;
|
|
}
|
|
}
|
|
let (y, m, d) = (y?, m.unwrap_or(1), d.unwrap_or(1));
|
|
((-200_000..=200_000).contains(&y) && (1..=12).contains(&m) && (1..=days_in(y, m)).contains(&d)).then_some((y, m, d))
|
|
}
|
|
|
|
/// Number of days in month `m` (1-12) of year `y`, leap years included.
|
|
fn days_in(y: i64, m: u32) -> u32 {
|
|
match m {
|
|
2 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 => 29,
|
|
2 => 28,
|
|
4 | 6 | 9 | 11 => 30,
|
|
_ => 31,
|
|
}
|
|
}
|
|
|
|
fn format_date(y: i64, m: u32, d: u32, picture: &str) -> String {
|
|
let pic: Vec<char> = picture.chars().collect();
|
|
let mut out = String::new();
|
|
let mut i = 0;
|
|
while i < pic.len() {
|
|
let c = pic[i].to_ascii_uppercase();
|
|
if c == 'Y' || c == 'M' || c == 'D' {
|
|
let mut run = 1;
|
|
while i + run < pic.len() && pic[i + run].to_ascii_uppercase() == c {
|
|
run += 1;
|
|
}
|
|
i += run;
|
|
match (c, run) {
|
|
('Y', 1 | 2) => out.push_str(&format!("{:02}", y.rem_euclid(100))),
|
|
('Y', _) => out.push_str(&format!("{y:04}")),
|
|
('M', 1) => out.push_str(&m.to_string()),
|
|
('M', 2) => out.push_str(&format!("{m:02}")),
|
|
('M', 3) => out.push_str(MONTHS.get((m as usize).saturating_sub(1)).map_or("", |n| &n[..3])),
|
|
('M', _) => out.push_str(MONTHS.get((m as usize).saturating_sub(1)).copied().unwrap_or("")),
|
|
('D', 1) => out.push_str(&d.to_string()),
|
|
(_, _) => out.push_str(&format!("{d:02}")),
|
|
}
|
|
} else {
|
|
out.push(pic[i]);
|
|
i += 1;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
const ONES: [&str; 20] = [
|
|
"Zero",
|
|
"One",
|
|
"Two",
|
|
"Three",
|
|
"Four",
|
|
"Five",
|
|
"Six",
|
|
"Seven",
|
|
"Eight",
|
|
"Nine",
|
|
"Ten",
|
|
"Eleven",
|
|
"Twelve",
|
|
"Thirteen",
|
|
"Fourteen",
|
|
"Fifteen",
|
|
"Sixteen",
|
|
"Seventeen",
|
|
"Eighteen",
|
|
"Nineteen",
|
|
];
|
|
const TENS: [&str; 10] = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
|
|
|
|
fn words_below_1000(n: u64) -> String {
|
|
let mut parts = Vec::new();
|
|
if n >= 100 {
|
|
parts.push(format!("{} Hundred", ONES[(n / 100) as usize]));
|
|
}
|
|
let r = n % 100;
|
|
if r >= 20 {
|
|
parts.push(if r.is_multiple_of(10) {
|
|
TENS[(r / 10) as usize].to_string()
|
|
} else {
|
|
format!("{}-{}", TENS[(r / 10) as usize], ONES[(r % 10) as usize])
|
|
});
|
|
} else if r > 0 || n == 0 {
|
|
parts.push(ONES[r as usize].to_string());
|
|
}
|
|
parts.join(" ")
|
|
}
|
|
|
|
/// `WordNum`: an integer in English words (up to the billions).
|
|
fn word_num(n: f64, cents: bool) -> String {
|
|
if !n.is_finite() || !(0.0..1e12).contains(&n) {
|
|
return String::new();
|
|
}
|
|
let cents_total = (n * 100.0).round() as u64;
|
|
let whole = if cents { cents_total / 100 } else { n.trunc() as u64 };
|
|
let mut parts = Vec::new();
|
|
let groups = [(1_000_000_000u64, "Billion"), (1_000_000, "Million"), (1_000, "Thousand")];
|
|
let mut rest = whole;
|
|
for (unit, name) in groups {
|
|
if rest >= unit {
|
|
parts.push(format!("{} {name}", words_below_1000(rest / unit)));
|
|
rest %= unit;
|
|
}
|
|
}
|
|
if rest > 0 || parts.is_empty() {
|
|
parts.push(words_below_1000(rest));
|
|
}
|
|
let mut out = parts.join(" ");
|
|
if cents {
|
|
let c = cents_total % 100;
|
|
let dollars = if whole == 1 { "Dollar" } else { "Dollars" };
|
|
let cent = if c == 1 { "Cent" } else { "Cents" };
|
|
out.push_str(&format!(" {dollars} And {c:02} {cent}"));
|
|
}
|
|
out
|
|
}
|
|
|
|
impl Interp<'_> {
|
|
/// A built-in function: its result is never a string past [`MAX_STRING`], and work on
|
|
/// long strings is charged to the step budget.
|
|
fn builtin(&mut self, name: &str, args: &[Expr]) -> R<Val> {
|
|
let v = self.builtin_inner(name, args)?;
|
|
if let Val::Str(s) = &v {
|
|
if s.len() > MAX_STRING {
|
|
return Err(STRING_TOO_LONG.into());
|
|
}
|
|
self.charge((s.len() / 256) as u64)?;
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
fn builtin_inner(&mut self, name: &str, args: &[Expr]) -> R<Val> {
|
|
let lower = name.to_ascii_lowercase();
|
|
let n = |v: f64| Ok(Val::Num(v));
|
|
Ok(match lower.as_str() {
|
|
// ── arithmetic
|
|
"abs" => return n(self.arg_num(args, 0)?.abs()),
|
|
"ceil" => return n(self.arg_num(args, 0)?.ceil()),
|
|
"floor" => return n(self.arg_num(args, 0)?.floor()),
|
|
"mod" => {
|
|
let (a, b) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?);
|
|
if b == 0.0 {
|
|
return Err("division by zero".into());
|
|
}
|
|
Val::Num(a - b * (a / b).trunc())
|
|
}
|
|
"round" => {
|
|
let a = self.arg_num(args, 0)?;
|
|
let places = if args.len() > 1 { self.arg_num(args, 1)?.clamp(0.0, 12.0) as i32 } else { 0 };
|
|
let k = 10f64.powi(places);
|
|
Val::Num((a * k).round() / k)
|
|
}
|
|
"count" => {
|
|
// Non-null values and objects: an empty field doesn't count, a subform does.
|
|
let vals = self.arg_values_keeping(args, true)?;
|
|
let counts = |v: &Val| match v {
|
|
Val::Node(i) => self.node(*i).is_some_and(|n| !matches!(n.kind, XfaKind::Field | XfaKind::ExclGroup) || !n.value.is_empty()),
|
|
other => !self.is_null(other),
|
|
};
|
|
Val::Num(vals.iter().filter(|v| counts(v)).count() as f64)
|
|
}
|
|
"sum" | "avg" | "max" | "min" => {
|
|
let vals = self.arg_values(args)?;
|
|
let nums: Vec<f64> = vals.iter().map(|v| self.to_num(v)).collect();
|
|
match lower.as_str() {
|
|
"sum" => Val::Num(nums.iter().sum()),
|
|
"avg" => {
|
|
if nums.is_empty() {
|
|
Val::Null
|
|
} else {
|
|
Val::Num(nums.iter().sum::<f64>() / nums.len() as f64)
|
|
}
|
|
}
|
|
"max" => nums.iter().copied().fold(None, |m: Option<f64>, x| Some(m.map_or(x, |m| m.max(x)))).map_or(Val::Null, Val::Num),
|
|
_ => nums.iter().copied().fold(None, |m: Option<f64>, x| Some(m.map_or(x, |m| m.min(x)))).map_or(Val::Null, Val::Num),
|
|
}
|
|
}
|
|
// ── logical
|
|
"choose" => {
|
|
let i = self.arg_num(args, 0)?;
|
|
if !i.is_finite() || i < 1.0 {
|
|
return Ok(Val::Null);
|
|
}
|
|
self.arg(args, i as usize)?
|
|
}
|
|
"exists" => {
|
|
let Some(e) = args.first() else { return Ok(Val::Num(0.0)) };
|
|
// A name that resolves to nothing is the answer "no"; a limit the lookup ran
|
|
// into (time, steps, depth, size) still stops the script.
|
|
match self.all_of(e) {
|
|
Ok(v) => Val::Num(if v.is_empty() { 0.0 } else { 1.0 }),
|
|
Err(e) if is_limit(&e) => return Err(e),
|
|
Err(_) => Val::Num(0.0),
|
|
}
|
|
}
|
|
"hasvalue" => {
|
|
let v = self.arg(args, 0)?;
|
|
Val::Num(if self.is_null(&v) || matches!(&v, Val::Str(s) if s.trim().is_empty()) { 0.0 } else { 1.0 })
|
|
}
|
|
"oneof" => {
|
|
let v = self.arg(args, 0)?;
|
|
let rest = self.arg_values(args.get(1..).unwrap_or_default())?;
|
|
let mut hit = false;
|
|
for x in &rest {
|
|
// Each comparison copies and compares `v`: charged per item.
|
|
self.charge(self.cost(&v).saturating_add(self.cost(x)).saturating_add(1))?;
|
|
if self.binary("==", v.clone(), x.clone()).is_ok_and(|r| self.truthy(&r)) {
|
|
hit = true;
|
|
break;
|
|
}
|
|
}
|
|
Val::Num(if hit { 1.0 } else { 0.0 })
|
|
}
|
|
"within" => {
|
|
let v = self.arg(args, 0)?;
|
|
let (lo, hi) = (self.arg(args, 1)?, self.arg(args, 2)?);
|
|
let ok = self.binary(">=", v.clone(), lo).is_ok_and(|r| self.truthy(&r)) && self.binary("<=", v, hi).is_ok_and(|r| self.truthy(&r));
|
|
Val::Num(if ok { 1.0 } else { 0.0 })
|
|
}
|
|
// ── strings
|
|
"at" => {
|
|
let (s, sub) = (self.arg_str(args, 0)?, self.arg_str(args, 1)?);
|
|
Val::Num(if sub.is_empty() { 0.0 } else { s.find(&sub).map_or(0.0, |b| s[..b].chars().count() as f64 + 1.0) })
|
|
}
|
|
"concat" => {
|
|
let mut out = String::new();
|
|
for i in 0..args.len() {
|
|
out.push_str(&self.arg_str(args, i)?);
|
|
if out.len() > MAX_STRING {
|
|
return Err(STRING_TOO_LONG.into());
|
|
}
|
|
}
|
|
Val::Str(out)
|
|
}
|
|
"left" => {
|
|
let (s, k) = (self.arg_str(args, 0)?, self.arg_num(args, 1)?.max(0.0) as usize);
|
|
Val::Str(s.chars().take(k).collect())
|
|
}
|
|
"right" => {
|
|
let (s, k) = (self.arg_str(args, 0)?, self.arg_num(args, 1)?.max(0.0) as usize);
|
|
let len = s.chars().count();
|
|
Val::Str(s.chars().skip(len.saturating_sub(k)).collect())
|
|
}
|
|
"len" => Val::Num(self.arg_str(args, 0)?.chars().count() as f64),
|
|
"lower" => Val::Str(self.arg_str(args, 0)?.to_lowercase()),
|
|
"upper" => Val::Str(self.arg_str(args, 0)?.to_uppercase()),
|
|
"ltrim" => Val::Str(self.arg_str(args, 0)?.trim_start().to_string()),
|
|
"rtrim" => Val::Str(self.arg_str(args, 0)?.trim_end().to_string()),
|
|
"space" => {
|
|
let k = self.arg_num(args, 0)?.clamp(0.0, MAX_STRING as f64) as usize;
|
|
Val::Str(" ".repeat(k))
|
|
}
|
|
"str" => {
|
|
let v = self.arg_num(args, 0)?;
|
|
let width = if args.len() > 1 { self.arg_num(args, 1)?.clamp(0.0, 1000.0) as usize } else { 10 };
|
|
let prec = if args.len() > 2 { self.arg_num(args, 2)?.clamp(0.0, 20.0) as usize } else { 0 };
|
|
let text = fixed(v, prec);
|
|
Val::Str(if text.len() > width { "*".repeat(width) } else { format!("{text:>width$}") })
|
|
}
|
|
"substr" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let start = self.arg_num(args, 1)?.max(1.0) as usize;
|
|
let len = self.arg_num(args, 2)?.max(0.0) as usize;
|
|
Val::Str(s.chars().skip(start - 1).take(len).collect())
|
|
}
|
|
"stuff" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let start = self.arg_num(args, 1)?.max(1.0) as usize;
|
|
let del = self.arg_num(args, 2)?.max(0.0) as usize;
|
|
let ins = if args.len() > 3 { self.arg_str(args, 3)? } else { String::new() };
|
|
let chars: Vec<char> = s.chars().collect();
|
|
let a = start.saturating_sub(1).min(chars.len());
|
|
let b = a.saturating_add(del).min(chars.len());
|
|
let out: String = chars[..a].iter().chain(ins.chars().collect::<Vec<_>>().iter()).chain(chars[b..].iter()).collect();
|
|
if out.len() > MAX_STRING {
|
|
return Err(STRING_TOO_LONG.into());
|
|
}
|
|
Val::Str(out)
|
|
}
|
|
"replace" => {
|
|
let (s, old) = (self.arg_str(args, 0)?, self.arg_str(args, 1)?);
|
|
let new = if args.len() > 2 { self.arg_str(args, 2)? } else { String::new() };
|
|
if old.is_empty() {
|
|
return Ok(Val::Str(s));
|
|
}
|
|
let grown = s.matches(old.as_str()).count().saturating_mul(new.len()).saturating_add(s.len());
|
|
if grown > MAX_STRING {
|
|
return Err(STRING_TOO_LONG.into());
|
|
}
|
|
Val::Str(s.replace(&old, &new))
|
|
}
|
|
"wordnum" => {
|
|
let v = self.arg_num(args, 0)?;
|
|
let kind = if args.len() > 1 { self.arg_num(args, 1)? } else { 0.0 };
|
|
Val::Str(word_num(v, kind == 2.0))
|
|
}
|
|
"uuid" => Val::Str(format!("{:032x}", (self.steps as u128).wrapping_mul(0x9E37_79B9_7F4A_7C15_F39C_C060_5CED_C835))),
|
|
"encode" | "decode" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let kind = if args.len() > 1 { self.arg_str(args, 1)?.to_ascii_lowercase() } else { "url".into() };
|
|
Val::Str(match (lower.as_str(), kind.as_str()) {
|
|
("encode", "html" | "xml") => s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """),
|
|
("decode", "html" | "xml") => s.replace("<", "<").replace(">", ">").replace(""", "\"").replace("&", "&"),
|
|
("encode", _) => s
|
|
.bytes()
|
|
.map(|b| if b.is_ascii_alphanumeric() || b"-_.~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") })
|
|
.collect(),
|
|
(_, _) => {
|
|
let mut out = Vec::new();
|
|
let b = s.as_bytes();
|
|
let mut i = 0;
|
|
while i < b.len() {
|
|
if b[i] == b'%'
|
|
&& let Some(h) =
|
|
b.get(i + 1..i + 3).and_then(|h| std::str::from_utf8(h).ok()).and_then(|h| u8::from_str_radix(h, 16).ok())
|
|
{
|
|
out.push(h);
|
|
i += 3;
|
|
} else {
|
|
out.push(b[i]);
|
|
i += 1;
|
|
}
|
|
}
|
|
String::from_utf8_lossy(&out).into_owned()
|
|
}
|
|
})
|
|
}
|
|
"format" => {
|
|
// Number pictures: `9` digit, `z` optional digit, `,` separator, `.` point, `$`.
|
|
let pic = self.arg_str(args, 0)?;
|
|
let v = self.arg(args, 1)?;
|
|
if pic.to_ascii_uppercase().contains("YY") || pic.to_ascii_uppercase().contains("DD") {
|
|
let text = self.to_str(&v);
|
|
let days = parse_num(&text).filter(|d| is_numeric_text(&text) && d.abs() < MAX_DAYS).map(|d| d as i64);
|
|
return Ok(match days {
|
|
Some(d) => {
|
|
let (y, m, dd) = civil_from_days(d);
|
|
Val::Str(format_date(y, m, dd, &pic))
|
|
}
|
|
None => parse_date(&text, "YYYY-MM-DD").map_or(Val::Str(text), |(y, m, d)| Val::Str(format_date(y, m, d, &pic))),
|
|
});
|
|
}
|
|
let num = self.to_num(&v);
|
|
let decimals = pic.split_once('.').map_or(0, |(_, f)| f.chars().filter(|c| matches!(c, '9' | 'z' | 'Z')).count()).min(20);
|
|
let grouped = pic.contains(',');
|
|
let mut text = fixed(num.abs(), decimals);
|
|
if grouped {
|
|
let (int, frac) = text.split_once('.').map_or((text.as_str(), ""), |(a, b)| (a, b));
|
|
let mut g = String::new();
|
|
for (k, c) in int.chars().enumerate() {
|
|
if k > 0 && (int.len() - k) % 3 == 0 {
|
|
g.push(',');
|
|
}
|
|
g.push(c);
|
|
}
|
|
text = if frac.is_empty() { g } else { format!("{g}.{frac}") };
|
|
}
|
|
if pic.contains('$') {
|
|
text = format!("${text}");
|
|
}
|
|
if num < 0.0 {
|
|
text = format!("-{text}");
|
|
}
|
|
Val::Str(text)
|
|
}
|
|
"parse" => {
|
|
let pic = self.arg_str(args, 0)?;
|
|
let s = self.arg_str(args, 1)?;
|
|
if pic.to_ascii_uppercase().contains("YY") {
|
|
// The canonical form of a date is ISO.
|
|
return Ok(parse_date(&s, &pic).map_or(Val::Null, |(y, m, d)| Val::Str(format_date(y, m, d, "YYYY-MM-DD"))));
|
|
}
|
|
let cleaned: String = s.chars().filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-').collect();
|
|
parse_num(&cleaned).map_or(Val::Null, Val::Num)
|
|
}
|
|
// ── dates and times (days since 1900-01-01 = 1; milliseconds since midnight)
|
|
"date" => {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0);
|
|
Val::Num((secs.div_euclid(86_400) + 25_568) as f64)
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
Val::Num(25_568.0)
|
|
}
|
|
}
|
|
"time" => {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
let ms = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0);
|
|
Val::Num(ms.rem_euclid(86_400_000) as f64)
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
Val::Num(0.0)
|
|
}
|
|
}
|
|
"date2num" | "isodate2num" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let pic = if lower == "isodate2num" || args.len() < 2 { "YYYY-MM-DD".to_string() } else { self.arg_str(args, 1)? };
|
|
let parsed = parse_date(&s, &pic).or_else(|| parse_date(&s, "YYYYMMDD")).or_else(|| parse_date(&s, "YYYY-MM-DD"));
|
|
parsed.map_or(Val::Num(0.0), |(y, m, d)| Val::Num(days_from_civil(y, m, d) as f64))
|
|
}
|
|
"num2date" => {
|
|
let days = self.arg_num(args, 0)?;
|
|
let pic = if args.len() > 1 { self.arg_str(args, 1)? } else { "MMM D, YYYY".into() };
|
|
if !days.is_finite() || days.abs() >= MAX_DAYS {
|
|
return Ok(Val::Null);
|
|
}
|
|
let (y, m, d) = civil_from_days(days as i64);
|
|
Val::Str(format_date(y, m, d, &pic))
|
|
}
|
|
"datefmt" | "localdatefmt" => {
|
|
let style = if args.is_empty() { 0.0 } else { self.arg_num(args, 0)? };
|
|
Val::Str(
|
|
match style as i32 {
|
|
1 => "M/D/YY",
|
|
2 => "MMM D, YYYY",
|
|
3 => "MMMM D, YYYY",
|
|
4 => "EEEE, MMMM D, YYYY",
|
|
_ => "MMM D, YYYY",
|
|
}
|
|
.into(),
|
|
)
|
|
}
|
|
"timefmt" | "localtimefmt" => Val::Str("HH:MM:SS".into()),
|
|
"num2time" | "num2gmtime" => {
|
|
let ms = self.arg_num(args, 0)?;
|
|
if !ms.is_finite() || ms < 0.0 {
|
|
return Ok(Val::Null);
|
|
}
|
|
let s = (ms / 1000.0) as u64;
|
|
Val::Str(format!("{:02}:{:02}:{:02}", (s / 3600) % 24, (s / 60) % 60, s % 60))
|
|
}
|
|
"time2num" | "isotime2num" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let parts: Vec<u64> = s.split(':').filter_map(|p| p.trim().parse::<u64>().ok()).map(|v| v.min(1_000_000)).collect();
|
|
let (h, m, sec) = (parts.first().copied().unwrap_or(0), parts.get(1).copied().unwrap_or(0), parts.get(2).copied().unwrap_or(0));
|
|
Val::Num(((h * 3600 + m * 60 + sec) * 1000) as f64)
|
|
}
|
|
// ── financial
|
|
"fv" => {
|
|
let (p, r, k) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?, self.arg_num(args, 2)?);
|
|
Val::Num(if r == 0.0 { p * k } else { p * (((1.0 + r).powf(k) - 1.0) / r) })
|
|
}
|
|
"pv" => {
|
|
let (p, r, k) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?, self.arg_num(args, 2)?);
|
|
Val::Num(if r == 0.0 { p * k } else { p * ((1.0 - (1.0 + r).powf(-k)) / r) })
|
|
}
|
|
"pmt" => {
|
|
let (pv, r, k) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?, self.arg_num(args, 2)?);
|
|
if k <= 0.0 {
|
|
return Err("Pmt needs a positive number of periods".into());
|
|
}
|
|
Val::Num(if r == 0.0 { pv / k } else { pv * r / (1.0 - (1.0 + r).powf(-k)) })
|
|
}
|
|
"term" => {
|
|
let (p, r, fv) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?, self.arg_num(args, 2)?);
|
|
if p <= 0.0 || r <= 0.0 || fv <= 0.0 {
|
|
return Err("Term needs positive arguments".into());
|
|
}
|
|
Val::Num(((fv * r / p) + 1.0).ln() / (1.0 + r).ln())
|
|
}
|
|
"cterm" => {
|
|
let (r, fv, pv) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?, self.arg_num(args, 2)?);
|
|
if r <= 0.0 || fv <= 0.0 || pv <= 0.0 {
|
|
return Err("CTerm needs positive arguments".into());
|
|
}
|
|
Val::Num((fv / pv).ln() / (1.0 + r).ln())
|
|
}
|
|
"rate" => {
|
|
let (fv, pv, k) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?, self.arg_num(args, 2)?);
|
|
if fv <= 0.0 || pv <= 0.0 || k <= 0.0 {
|
|
return Err("Rate needs positive arguments".into());
|
|
}
|
|
Val::Num((fv / pv).powf(1.0 / k) - 1.0)
|
|
}
|
|
"npv" => {
|
|
let r = self.arg_num(args, 0)?;
|
|
let flows = self.arg_values(args.get(1..).unwrap_or_default())?;
|
|
Val::Num(flows.iter().enumerate().map(|(i, v)| self.to_num(v) / (1.0 + r).powi(i as i32 + 1)).sum())
|
|
}
|
|
"ipmt" | "ppmt" => {
|
|
let (pv, r, pmt, first, count) = (
|
|
self.arg_num(args, 0)?,
|
|
self.arg_num(args, 1)?,
|
|
self.arg_num(args, 2)?,
|
|
self.arg_num(args, 3)?.max(1.0),
|
|
self.arg_num(args, 4)?.max(1.0),
|
|
);
|
|
let (mut balance, mut interest, mut principal) = (pv, 0.0, 0.0);
|
|
for period in 1..=((first + count - 1.0).min(10_000.0) as u32) {
|
|
self.step()?;
|
|
let i = balance * r;
|
|
let p = (pmt - i).min(balance);
|
|
if period >= first as u32 {
|
|
interest += i;
|
|
principal += p;
|
|
}
|
|
balance -= p;
|
|
if balance <= 0.0 {
|
|
break;
|
|
}
|
|
}
|
|
Val::Num(if lower == "ipmt" { interest } else { principal })
|
|
}
|
|
"apr" => {
|
|
let (principal, payment, periods) = (self.arg_num(args, 0)?, self.arg_num(args, 1)?, self.arg_num(args, 2)?);
|
|
if principal <= 0.0 || payment <= 0.0 || periods <= 0.0 {
|
|
return Err("Apr needs positive arguments".into());
|
|
}
|
|
// Bisection on the monthly rate.
|
|
let (mut lo, mut hi) = (0.0f64, 1.0f64);
|
|
for _ in 0..200 {
|
|
let mid = (lo + hi) / 2.0;
|
|
let pmt = if mid == 0.0 { principal / periods } else { principal * mid / (1.0 - (1.0 + mid).powf(-periods)) };
|
|
if pmt > payment {
|
|
hi = mid;
|
|
} else {
|
|
lo = mid;
|
|
}
|
|
}
|
|
Val::Num((lo + hi) / 2.0 * 12.0)
|
|
}
|
|
// ── units and references
|
|
"unittype" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let unit: String = s.trim().chars().skip_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-' || *c == '+').collect();
|
|
Val::Str(
|
|
match unit.trim().to_ascii_lowercase().as_str() {
|
|
"in" | "inch" | "inches" => "in",
|
|
"mm" | "millimeter" | "millimeters" => "mm",
|
|
"cm" | "centimeter" | "centimeters" => "cm",
|
|
"pt" | "point" | "points" => "pt",
|
|
"pc" | "pica" => "pc",
|
|
_ => "in",
|
|
}
|
|
.into(),
|
|
)
|
|
}
|
|
"unitvalue" => {
|
|
let s = self.arg_str(args, 0)?;
|
|
let to = if args.len() > 1 { self.arg_str(args, 1)? } else { String::new() };
|
|
let v = parse_num(&s).unwrap_or(0.0);
|
|
let unit: String = s.trim().chars().skip_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-' || *c == '+').collect();
|
|
let per_in = |u: &str| match u.trim().to_ascii_lowercase().as_str() {
|
|
"mm" => 25.4,
|
|
"cm" => 2.54,
|
|
"pt" => 72.0,
|
|
"pc" => 6.0,
|
|
_ => 1.0,
|
|
};
|
|
let target = if to.trim().is_empty() { unit.clone() } else { to };
|
|
Val::Num(v / per_in(&unit) * per_in(&target))
|
|
}
|
|
"ref" => {
|
|
let Some(e) = args.first() else { return Ok(Val::Null) };
|
|
self.eval(e)?
|
|
}
|
|
"eval" => {
|
|
let src = self.arg_str(args, 0)?;
|
|
self.charge((src.len() / 16) as u64)?;
|
|
let body = parse(&src)?;
|
|
let (_, v) = self.block(&body)?;
|
|
v
|
|
}
|
|
"get" | "post" | "put" => return Err(format!("{name}: network access is not available to form scripts")),
|
|
_ => return Err(format!("unknown function {name}")),
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── entry points ────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Run a FormCalc `script` for `event` on the form `root`, as [`crate::xfa::run_xfa`] runs a
|
|
/// JavaScript one.
|
|
pub fn run_formcalc(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, limits: Limits) -> XfaOutcome {
|
|
run_formcalc_at(script, event, doc, root.clone(), limits, None)
|
|
}
|
|
|
|
/// [`run_formcalc`] on an owned form, giving up at `deadline` (the clock is read every
|
|
/// [`CLOCK_EVERY`] steps; unused on wasm, which has no clock).
|
|
pub(crate) fn run_formcalc_at(
|
|
script: &str,
|
|
event: &XfaEvent,
|
|
doc: &XfaDoc,
|
|
root: XfaNode,
|
|
limits: Limits,
|
|
deadline: Option<std::time::Duration>,
|
|
) -> XfaOutcome {
|
|
// FormCalc has its own nesting limits (the shared `refuse` reads JavaScript syntax).
|
|
if script.len() > crate::MAX_SCRIPT_BYTES {
|
|
return XfaOutcome {
|
|
error: Some(format!("the script is too long ({} KiB; the limit is {} KiB)", script.len() / 1024, crate::MAX_SCRIPT_BYTES / 1024)),
|
|
..Default::default()
|
|
};
|
|
}
|
|
let body = match parse(script) {
|
|
Ok(b) => b,
|
|
Err(e) => return XfaOutcome { error: Some(e), ..Default::default() },
|
|
};
|
|
let mut host = XHost::new(root, &event.target, doc);
|
|
#[cfg(target_arch = "wasm32")]
|
|
let _ = deadline;
|
|
let mut interp = Interp {
|
|
h: &mut host,
|
|
scopes: vec![HashMap::new()],
|
|
funcs: HashMap::new(),
|
|
event: event.clone(),
|
|
steps: 0,
|
|
loops_left: limits.loop_iterations,
|
|
depth: 0,
|
|
// The JavaScript recursion limit, kept to 8..=128: statement nesting counts against it
|
|
// too, and the interpreter's frames are large.
|
|
max_depth: limits.recursion.clamp(8, 128),
|
|
exited: false,
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
deadline: deadline.and_then(|d| std::time::Instant::now().checked_add(d)),
|
|
};
|
|
let mut out = XfaOutcome::default();
|
|
match interp.block(&body) {
|
|
Ok((_, last)) => {
|
|
// As the JavaScript path reports: null is an empty result (a calculate clears the
|
|
// field) with no truth value.
|
|
let last = interp.deref(last);
|
|
out.result = Some(interp.to_str(&last));
|
|
if !matches!(last, Val::Null) {
|
|
out.result_bool = Some(interp.truthy(&last));
|
|
}
|
|
}
|
|
Err(e) => out.error = Some(e),
|
|
}
|
|
out.notes = host.notes();
|
|
out.effects = host.effects;
|
|
out.console = host.console;
|
|
clip_result(&mut out);
|
|
out
|
|
}
|
|
|
|
/// [`run_formcalc`] on its own thread, abandoned after `timeout` (and told to stop itself then).
|
|
pub fn run_formcalc_within(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNode, limits: Limits, timeout: std::time::Duration) -> XfaOutcome {
|
|
let (script, event, doc) = (script.to_string(), event.clone(), doc.clone());
|
|
// FormCalc has no engine to start: its time (and its own deadline) begins at once.
|
|
run_within("pdfcraft-formcalc", timeout, move |started| {
|
|
started();
|
|
run_formcalc_at(&script, &event, &doc, root, limits, Some(timeout))
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn dates_round_trip_through_formcalc_day_numbers() {
|
|
assert_eq!(days_from_civil(1900, 1, 1), 1);
|
|
assert_eq!(days_from_civil(1970, 1, 1), 25_568);
|
|
assert_eq!(civil_from_days(days_from_civil(2024, 2, 29)), (2024, 2, 29));
|
|
assert_eq!(parse_date("2001-02-03", "YYYY-MM-DD"), Some((2001, 2, 3)));
|
|
assert_eq!(parse_date("Feb 3, 2001", "MMM D, YYYY"), Some((2001, 2, 3)));
|
|
assert_eq!(parse_date("2/3/01", "M/D/YY"), Some((2001, 2, 3)));
|
|
assert_eq!(format_date(2001, 2, 3, "MMMM D, YYYY"), "February 3, 2001");
|
|
assert_eq!(word_num(1234.5, true), "One Thousand Two Hundred Thirty-Four Dollars And 50 Cents");
|
|
assert_eq!(num_text(3.0), "3");
|
|
assert_eq!(num_text(2.5), "2.5");
|
|
assert_eq!(num_text(0.1 + 0.2), "0.3");
|
|
assert_eq!(parse_num("12abc"), Some(12.0));
|
|
assert_eq!(parse_num("abc"), None);
|
|
}
|
|
}
|