use std::collections::HashMap;
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
pub const MAX_CALCS: usize = 1 << 16;
pub const FULL: &str = "too many distinct size expressions";
pub fn is_full(err: &str) -> bool {
err.contains(FULL)
}
static REFUSED: AtomicU64 = AtomicU64::new(0);
static LAST_REFUSED: Mutex<String> = Mutex::new(String::new());
pub fn refused() -> (u64, String) {
let n = REFUSED.load(Ordering::Relaxed);
if n == 0 {
return (0, String::new());
}
let last = LAST_REFUSED.lock().map(|s| s.clone()).unwrap_or_default();
(n, last)
}
pub const MAX_DEPTH: u32 = 32;
fn too_deep(what: &str) -> String {
format!("bad size{what}: nested past {MAX_DEPTH}")
}
fn finite(v: f32) -> Result<f32, String> {
if v.is_finite() {
Ok(if v == 0.0 { 0.0 } else { v })
} else {
Err(format!("bad size: {v} is not a finite number"))
}
}
fn canon(e: &mut Expr, depth: u32) -> Result<(), String> {
if depth > MAX_DEPTH {
return Err(too_deep(""));
}
match e {
Expr::Px(v) | Expr::Pct(v) => {
*v = finite(*v)?;
Ok(())
}
Expr::Min(xs) | Expr::Max(xs) => xs.iter_mut().try_for_each(|x| canon(x, depth + 1)),
Expr::Clamp(a, b, c) => {
canon(a, depth + 1)?;
canon(b, depth + 1)?;
canon(c, depth + 1)
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Expr {
Px(f32),
Pct(f32),
Min(Vec<Expr>),
Max(Vec<Expr>),
Clamp(Box<Expr>, Box<Expr>, Box<Expr>),
}
impl Expr {
pub fn resolve(&self, room: f32) -> f32 {
self.eval(room).max(0.0)
}
fn eval(&self, room: f32) -> f32 {
match self {
Expr::Px(px) => *px,
Expr::Pct(f) => room * f,
Expr::Min(xs) => xs
.iter()
.map(|x| x.eval(room))
.fold(f32::INFINITY, f32::min),
Expr::Max(xs) => xs
.iter()
.map(|x| x.eval(room))
.fold(f32::NEG_INFINITY, f32::max),
Expr::Clamp(lo, target, hi) => target.eval(room).min(hi.eval(room)).max(lo.eval(room)),
}
}
pub fn relative(&self) -> bool {
match self {
Expr::Px(_) => false,
Expr::Pct(_) => true,
Expr::Min(xs) | Expr::Max(xs) => xs.iter().any(Expr::relative),
Expr::Clamp(a, b, c) => a.relative() || b.relative() || c.relative(),
}
}
}
impl Eq for Expr {}
impl std::hash::Hash for Expr {
fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
match self {
Expr::Px(v) => (0u8, v.to_bits()).hash(h),
Expr::Pct(v) => (1u8, v.to_bits()).hash(h),
Expr::Min(xs) => (2u8, xs).hash(h),
Expr::Max(xs) => (3u8, xs).hash(h),
Expr::Clamp(a, b, c) => (4u8, a, b, c).hash(h),
}
}
}
impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn list(f: &mut fmt::Formatter<'_>, name: &str, xs: &[&Expr]) -> fmt::Result {
write!(f, "{name}(")?;
for (i, x) in xs.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{x}")?;
}
write!(f, ")")
}
match self {
Expr::Px(px) => write!(f, "{px}px"),
Expr::Pct(p) => write!(f, "{}%", p * 100.0),
Expr::Min(xs) => list(f, "min", &xs.iter().collect::<Vec<_>>()),
Expr::Max(xs) => list(f, "max", &xs.iter().collect::<Vec<_>>()),
Expr::Clamp(a, b, c) => list(f, "clamp", &[a, b, c]),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Calc(u32);
impl Calc {
pub fn resolve(self, room: f32) -> f32 {
table()
.read()
.ok()
.and_then(|t| t.exprs.get(self.0 as usize).map(|e| e.resolve(room)))
.unwrap_or(0.0)
}
pub fn expr(self) -> Option<Arc<Expr>> {
table().read().ok()?.exprs.get(self.0 as usize).cloned()
}
pub fn id(self) -> u32 {
self.0
}
pub fn from_id(id: u32) -> Option<Calc> {
let t = table().read().ok()?;
((id as usize) < t.exprs.len()).then_some(Calc(id))
}
pub fn describe(self) -> String {
self.expr()
.map_or_else(|| "calc(?)".into(), |e| e.to_string())
}
}
#[derive(Default)]
struct Table {
exprs: Vec<Arc<Expr>>,
by_expr: HashMap<Expr, u32>,
by_input: HashMap<String, Norm>,
by_code: HashMap<Box<[u8]>, Norm>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Norm {
Px(f32),
Pct(f32),
Calc(Calc),
}
fn norm(mut e: Expr) -> Result<Norm, String> {
canon(&mut e, 0)?;
Ok(match e {
Expr::Pct(f) => Norm::Pct(f),
e if !e.relative() => Norm::Px(e.resolve(0.0)),
e => Norm::Calc(intern_checked(e)?),
})
}
fn norm_str(s: &str) -> Result<Norm, String> {
if let Some(n) = table().read().ok().and_then(|t| t.by_input.get(s).copied()) {
return Ok(n);
}
let n = norm(parse(s)?)?;
if let Ok(mut t) = table().write()
&& t.by_input.len() < MAX_CALCS
{
t.by_input.insert(s.to_string(), n);
}
Ok(n)
}
fn norm_sizing(n: Norm) -> crate::spec::Sizing {
use crate::spec::Sizing;
match n {
Norm::Px(px) => Sizing::Fixed(px),
Norm::Pct(f) => Sizing::Percent(f),
Norm::Calc(c) => Sizing::Calc(c),
}
}
fn norm_bound(n: Norm) -> Result<crate::spec::Bound, String> {
use crate::spec::Bound;
Ok(match n {
Norm::Px(px) => Bound::Px(px),
Norm::Pct(f) => Bound::Calc(intern(Expr::Pct(f))?),
Norm::Calc(c) => Bound::Calc(c),
})
}
pub fn from_value(v: &crate::value::Value) -> Result<Expr, String> {
value_at(v, 0)
}
fn value_at(v: &crate::value::Value, depth: u32) -> Result<Expr, String> {
use crate::value::Value;
if depth > MAX_DEPTH {
return Err(too_deep(""));
}
match v {
Value::Int(_) | Value::Float(_) => {
Ok(Expr::Px(finite(v.as_float().unwrap_or(0.0) as f32)?))
}
Value::Str(s) => parse(s),
Value::Map(m) => {
let mut it = m.iter();
let (Some((k, arg)), None) = (it.next(), it.next()) else {
return Err(
"bad size: a table names one of pct, percent, px, min, max, clamp".into(),
);
};
let num = || {
arg.as_float()
.map(|n| n as f32)
.ok_or_else(|| format!("bad size: {k} takes a number"))
.and_then(finite)
};
let args = || -> Result<Vec<Expr>, String> {
let Value::List(xs) = arg else {
return Err(format!("bad size: {k} takes a list"));
};
if xs.is_empty() {
return Err(format!("bad size: {k} takes at least one"));
}
xs.iter().map(|x| value_at(x, depth + 1)).collect()
};
match k.as_str() {
"pct" | "percent" => Ok(Expr::Pct(finite(num()? / 100.0)?)),
"px" => Ok(Expr::Px(num()?)),
"min" => Ok(Expr::Min(args()?)),
"max" => Ok(Expr::Max(args()?)),
"clamp" => match <[Expr; 3]>::try_from(args()?) {
Ok([a, b, c]) => Ok(Expr::Clamp(Box::new(a), Box::new(b), Box::new(c))),
Err(_) => Err("bad size: clamp takes three: clamp(MIN, TARGET, MAX)".into()),
},
_ => Err(format!(
"bad size: no {k:?} (pct, percent, px, min, max, clamp)"
)),
}
}
_ => Err("bad size: a number, a string or a table".into()),
}
}
pub fn from_code(code: &[f64]) -> Result<Expr, String> {
fn one(code: &[f64], at: &mut usize, depth: u32) -> Result<Expr, String> {
let mut next = || -> Result<f64, String> {
let v = code.get(*at).copied().ok_or("bad size code: truncated")?;
*at += 1;
Ok(v)
};
if depth > MAX_DEPTH {
return Err(too_deep(" code"));
}
Ok(match next()? as u32 {
1 => Expr::Px(finite(next()? as f32)?),
2 => Expr::Pct(finite(next()? as f32)?),
op @ (3 | 4) => {
let n = next()? as usize;
if n == 0 || n > code.len() {
return Err("bad size code: an argument count out of range".into());
}
let args = (0..n)
.map(|_| one(code, at, depth + 1))
.collect::<Result<Vec<_>, _>>()?;
if op == 3 {
Expr::Min(args)
} else {
Expr::Max(args)
}
}
5 => {
let a = one(code, at, depth + 1)?;
let b = one(code, at, depth + 1)?;
let c = one(code, at, depth + 1)?;
Expr::Clamp(Box::new(a), Box::new(b), Box::new(c))
}
op => return Err(format!("bad size code: no op {op}")),
})
}
let mut at = 0;
let e = one(code, &mut at, 0)?;
if at != code.len() {
return Err("bad size code: slots left over".into());
}
Ok(e)
}
fn norm_code(code: &[f64]) -> Result<Norm, String> {
let bytes =
unsafe { std::slice::from_raw_parts(code.as_ptr().cast::<u8>(), size_of_val(code)) };
if let Some(n) = table()
.read()
.ok()
.and_then(|t| t.by_code.get(bytes).copied())
{
return Ok(n);
}
let n = norm(from_code(code)?)?;
if let Ok(mut t) = table().write()
&& t.by_code.len() < MAX_CALCS
{
t.by_code.insert(bytes.into(), n);
}
Ok(n)
}
pub fn sizing_code(code: &[f64]) -> Result<crate::spec::Sizing, String> {
norm_code(code).map(norm_sizing)
}
pub fn bound_code(code: &[f64]) -> Result<crate::spec::Bound, String> {
norm_bound(norm_code(code)?)
}
pub fn sizing_value(v: &crate::value::Value) -> Result<crate::spec::Sizing, String> {
match v {
crate::value::Value::Str(s) => sizing(s),
v => Ok(norm_sizing(norm(from_value(v)?)?)),
}
}
pub fn bound_value(v: &crate::value::Value) -> Result<crate::spec::Bound, String> {
match v {
crate::value::Value::Str(s) => bound(s),
v => norm_bound(norm(from_value(v)?)?),
}
}
pub fn sizing_of(e: Expr) -> Result<crate::spec::Sizing, String> {
Ok(norm_sizing(norm(e)?))
}
fn table() -> &'static RwLock<Table> {
static TABLE: OnceLock<RwLock<Table>> = OnceLock::new();
TABLE.get_or_init(Default::default)
}
pub fn intern(mut expr: Expr) -> Result<Calc, String> {
canon(&mut expr, 0)?;
intern_checked(expr)
}
fn intern_checked(expr: Expr) -> Result<Calc, String> {
if let Some(&id) = table()
.read()
.map_err(|e| e.to_string())?
.by_expr
.get(&expr)
{
return Ok(Calc(id));
}
let mut t = table().write().map_err(|e| e.to_string())?;
if let Some(&id) = t.by_expr.get(&expr) {
return Ok(Calc(id));
}
if t.exprs.len() >= MAX_CALCS {
drop(t);
return Err(refuse(&expr));
}
let id = t.exprs.len() as u32;
t.exprs.push(Arc::new(expr.clone()));
t.by_expr.insert(expr, id);
Ok(Calc(id))
}
#[cold]
#[inline(never)]
fn refuse(expr: &Expr) -> String {
let spelled = expr.to_string();
REFUSED.fetch_add(1, Ordering::Relaxed);
if let Ok(mut last) = LAST_REFUSED.lock() {
last.clone_from(&spelled);
}
format!(
"{FULL} ({MAX_CALCS}): \"{spelled}\" is not kept — declare one per layout, not one per \
frame"
)
}
pub fn parse(s: &str) -> Result<Expr, String> {
let mut p = Parser {
s: s.as_bytes(),
at: 0,
};
let e = p.expr(0)?;
p.skip_ws();
if p.at < p.s.len() {
return Err(p.error("the end"));
}
Ok(e)
}
pub fn sizing(s: &str) -> Result<crate::spec::Sizing, String> {
norm_str(s).map(norm_sizing)
}
pub fn bound(s: &str) -> Result<crate::spec::Bound, String> {
norm_bound(norm_str(s)?)
}
struct Parser<'a> {
s: &'a [u8],
at: usize,
}
impl Parser<'_> {
fn skip_ws(&mut self) {
while self.s.get(self.at).is_some_and(|c| c.is_ascii_whitespace()) {
self.at += 1;
}
}
fn error(&self, wanted: &str) -> String {
let rest = String::from_utf8_lossy(&self.s[self.at.min(self.s.len())..]);
if rest.is_empty() {
format!("bad size: {wanted} expected at the end")
} else {
format!("bad size: {wanted} expected at {rest:?}")
}
}
fn eat(&mut self, word: &str) -> bool {
self.skip_ws();
if self.s[self.at..].starts_with(word.as_bytes()) {
self.at += word.len();
true
} else {
false
}
}
fn expr(&mut self, depth: u32) -> Result<Expr, String> {
if depth > MAX_DEPTH {
return Err(too_deep(""));
}
self.skip_ws();
for name in ["clamp", "min", "max"] {
let rest = &self.s[self.at..];
if rest.len() > name.len()
&& rest[..name.len()].eq_ignore_ascii_case(name.as_bytes())
&& rest[name.len()] == b'('
{
self.at += name.len() + 1;
let mut args = vec![self.expr(depth + 1)?];
while self.eat(",") {
args.push(self.expr(depth + 1)?);
}
if !self.eat(")") {
return Err(self.error("\",\" or \")\""));
}
return match name {
"clamp" => match <[Expr; 3]>::try_from(args) {
Ok([a, b, c]) => Ok(Expr::Clamp(Box::new(a), Box::new(b), Box::new(c))),
Err(_) => {
Err("bad size: clamp takes three: clamp(MIN, TARGET, MAX)".into())
}
},
"min" => Ok(Expr::Min(args)),
_ => Ok(Expr::Max(args)),
};
}
}
let start = self.at;
while self
.s
.get(self.at)
.is_some_and(|c| c.is_ascii_digit() || *c == b'.')
{
self.at += 1;
}
let n: f32 = std::str::from_utf8(&self.s[start..self.at])
.ok()
.and_then(|t| t.parse().ok())
.ok_or_else(|| {
self.at = start;
self.error("a number, \"N%\", \"Npx\", min(…), max(…) or clamp(…)")
})?;
let n = finite(n)?;
let rest = &self.s[self.at..];
if rest.first() == Some(&b'%') {
self.at += 1;
Ok(Expr::Pct(finite(n / 100.0)?))
} else {
if rest.len() >= 2 && rest[..2].eq_ignore_ascii_case(b"px") {
self.at += 2;
}
Ok(Expr::Px(n))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{Bound, Sizing};
fn px(s: &str, room: f32) -> f32 {
parse(s).unwrap().resolve(room)
}
#[test]
fn expressions_resolve_against_the_room() {
let c = "clamp(400px, 80%, 1000px)";
assert_eq!(px(c, 300.0), 400.0, "the minimum");
assert_eq!(px(c, 1000.0), 800.0, "the target");
assert_eq!(px(c, 2000.0), 1000.0, "the maximum");
assert_eq!(
px("clamp(500, 10%, 200)", 1000.0),
500.0,
"the minimum over the maximum"
);
assert_eq!(px("min(720px, 100%)", 500.0), 500.0);
assert_eq!(px("max(50%, 300)", 400.0), 300.0);
assert_eq!(px("min(clamp(1, 50%, 900), 30%)", 1000.0), 300.0, "nested");
}
#[test]
fn only_what_depends_on_the_room_is_a_calc() {
assert_eq!(sizing("720px").unwrap(), Sizing::Fixed(720.0));
assert_eq!(sizing("min(300px, 400)").unwrap(), Sizing::Fixed(300.0));
assert_eq!(sizing("50%").unwrap(), Sizing::Percent(0.5));
let Sizing::Calc(a) = sizing("clamp(400px,80%,1000px)").unwrap() else {
panic!("a calc");
};
let Sizing::Calc(b) = sizing(" clamp( 400 , 80% , 1000px ) ").unwrap() else {
panic!("a calc");
};
assert_eq!(a, b, "one entry per expression, however spelled");
assert_eq!(a.describe(), "clamp(400px, 80%, 1000px)");
assert_eq!(bound("300").unwrap(), Bound::Px(300.0));
assert!(matches!(bound("50%").unwrap(), Bound::Calc(_)));
}
#[test]
fn the_same_expression_as_data() {
use crate::value::Value;
let list = |xs: Vec<Value>| Value::List(xs);
let map = |k: &str, v: Value| Value::Map([(k.to_string(), v)].into_iter().collect());
let v = map(
"clamp",
list(vec![
Value::Int(400),
map("pct", Value::Int(80)),
Value::Str("1000px".into()),
]),
);
let Sizing::Calc(a) = sizing_value(&v).unwrap() else {
panic!("a calc");
};
assert_eq!(
Some(a),
match sizing("clamp(400px, 80%, 1000px)").unwrap() {
Sizing::Calc(c) => Some(c),
_ => None,
},
"one entry, spelled or built"
);
assert_eq!(
sizing_value(&map("percent", Value::Int(50))).unwrap(),
Sizing::Percent(0.5)
);
assert_eq!(
sizing_value(&map("min", list(vec![Value::Int(300), Value::Int(400)]))).unwrap(),
Sizing::Fixed(300.0)
);
assert!(
sizing_value(&map("clamp", list(vec![Value::Int(1)])))
.unwrap_err()
.contains("three")
);
assert!(
sizing_value(&map("wide", Value::Int(1)))
.unwrap_err()
.contains("no \"wide\"")
);
}
#[test]
fn the_same_expression_in_prefix_code() {
let code = [5.0, 1.0, 400.0, 2.0, 0.8, 3.0, 2.0, 1.0, 1000.0, 2.0, 1.0];
assert_eq!(
from_code(&code).unwrap().to_string(),
"clamp(400px, 80%, min(1000px, 100%))"
);
assert!(from_code(&code[..4]).unwrap_err().contains("truncated"));
assert!(
from_code(&[1.0, 3.0, 9.0])
.unwrap_err()
.contains("left over")
);
assert!(from_code(&[9.0]).unwrap_err().contains("no op 9"));
}
#[test]
fn a_bad_one_says_where() {
assert_eq!(
parse("80%x").unwrap_err(),
"bad size: the end expected at \"x\""
);
assert_eq!(
parse("80 %").unwrap_err(),
"bad size: the end expected at \"%\""
);
assert_eq!(
parse("100 px").unwrap_err(),
"bad size: the end expected at \"px\""
);
assert!(
parse("min (1, 2)")
.unwrap_err()
.contains("at \"min (1, 2)\"")
);
assert!(parse("1.2.3%").is_err());
assert!(parse("50px%").unwrap_err().contains("at \"%\""));
assert_eq!(
parse("MIN(10PX, 50%)").unwrap(),
parse("min(10px, 50%)").unwrap()
);
assert_eq!(
parse(" max( 1px ,2% ) ").unwrap(),
parse("max(1px, 2%)").unwrap()
);
assert!(parse("clamp(1, 2)").unwrap_err().contains("three"));
assert!(parse("wide").unwrap_err().contains("at \"wide\""));
assert!(parse("min(1, 2").unwrap_err().contains("at the end"));
}
fn nested(n: usize) -> (String, crate::value::Value, Vec<f64>, Expr) {
use crate::value::Value;
let spelled = format!("{}50%{}", "min(".repeat(n), ")".repeat(n));
let mut data = Value::Map(vec![("pct".into(), Value::Int(50))]);
let mut code = [3.0, 1.0].repeat(n);
code.extend([2.0, 0.5]);
let mut built = Expr::Pct(0.5);
for _ in 0..n {
data = Value::Map(vec![("min".into(), Value::List(vec![data]))]);
built = Expr::Min(vec![built]);
}
(spelled, data, code, built)
}
#[test]
fn nesting_stops_at_the_cap() {
let deep = parse(&"min(".repeat(100_000)).unwrap_err();
assert_eq!(deep, "bad size: nested past 32");
let (s, v, code, e) = nested(MAX_DEPTH as usize);
assert!(parse(&s).is_ok(), "32 is allowed");
assert!(from_value(&v).is_ok());
assert!(from_code(&code).is_ok());
assert!(intern(e).is_ok());
let (s, v, code, e) = nested(MAX_DEPTH as usize + 1);
assert_eq!(parse(&s).unwrap_err(), "bad size: nested past 32");
assert_eq!(from_value(&v).unwrap_err(), "bad size: nested past 32");
assert_eq!(
from_code(&code).unwrap_err(),
"bad size code: nested past 32"
);
assert_eq!(intern(e.clone()).unwrap_err(), "bad size: nested past 32");
assert!(sizing_of(e).is_err(), "a tree built by hand is held to it");
assert!(sizing(&s).is_err());
}
#[test]
fn only_finite_numbers_and_one_zero() {
use crate::value::Value;
let map = |k: &str, v: Value| Value::Map(vec![(k.to_string(), v)]);
let with = |n: f64| {
map(
"min",
Value::List(vec![Value::Float(n), map("pct", Value::Int(50))]),
)
};
for n in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e300] {
let e = sizing_value(&with(n)).unwrap_err();
assert!(e.contains("not a finite number"), "{n}: {e}");
assert!(sizing_value(&map("px", Value::Float(n))).is_err());
assert!(sizing_value(&map("pct", Value::Float(n))).is_err());
assert!(from_code(&[3.0, 2.0, 1.0, n, 2.0, 0.5]).is_err());
assert!(from_code(&[2.0, n]).is_err());
}
assert!(bound_value(&with(f64::NAN)).is_err());
assert!(intern(Expr::Min(vec![Expr::Px(f32::NAN), Expr::Pct(0.5)])).is_err());
assert!(sizing_of(Expr::Pct(f32::INFINITY)).is_err());
let digits = format!("min(1{}px, 50%)", "0".repeat(40));
assert!(parse(&digits).unwrap_err().contains("not a finite number"));
assert!(parse(&format!("1{}%", "0".repeat(40))).is_err());
let zero = sizing_value(&with(0.0)).unwrap();
assert_eq!(sizing_value(&with(-0.0)).unwrap(), zero, "-0 is 0");
assert_eq!(
sizing_of(Expr::Min(vec![Expr::Px(-0.0), Expr::Pct(0.5)])).unwrap(),
zero
);
assert_eq!(sizing_code(&[3.0, 2.0, 1.0, -0.0, 2.0, 0.5]).unwrap(), zero);
assert_eq!(zero.describe(), "min(0px, 50%)");
}
}