mod common;
use alice_sdf::interval::{eval_interval, Vec3Interval};
use alice_sdf::prelude::*;
use common::corpus::corpus;
use std::collections::BTreeMap;
const LEDGER: &str = "docs/node-support.md";
const COLUMNS: [&str; NCOL] = [
"eval", "interval", "compile", "jit", "jit_simd", "msl", "glsl", "wgsl", "hlsl", "rust", "ffi",
"python",
];
const NCOL: usize = 12;
fn enum_variants() -> Vec<String> {
enum_variants_in(include_str!("../src/types/mod.rs"))
}
fn enum_variants_in(src: &str) -> Vec<String> {
let src = src.replace("\r\n", "\n");
let src = src.as_str();
let start = src
.find("pub enum SdfNode {")
.expect("pub enum SdfNode in src/types/mod.rs");
let body = &src[start..];
let end = body.find("\n}\n").expect("end of enum SdfNode");
let mut out = Vec::new();
for line in body[..end].lines().skip(1) {
if let Some(rest) = line.strip_prefix(" ") {
if rest.starts_with(' ') {
continue;
}
let name: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric())
.collect();
if name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
out.push(name);
}
}
}
out
}
fn variant_of(node: &SdfNode) -> String {
format!("{node:?}")
.chars()
.take_while(|c| c.is_ascii_alphanumeric())
.collect()
}
fn eval_cell(node: &SdfNode) -> &'static str {
let pts = [
Vec3::new(0.3, -0.2, 0.1),
Vec3::new(1.7, 0.4, -0.9),
Vec3::ZERO,
];
let ok = pts.iter().any(|&p| {
let d = eval(node, p);
d.is_finite() && d != f32::MAX
});
if ok {
"ok"
} else {
"fallback"
}
}
fn interval_cell(node: &SdfNode) -> &'static str {
let b = Vec3Interval::from_bounds(Vec3::splat(-2.0), Vec3::splat(2.0));
let i = eval_interval(node, b);
if i.lo.is_finite() || i.hi.is_finite() {
"ok"
} else {
"everything"
}
}
fn compile_cell(node: &SdfNode) -> &'static str {
if CompiledSdf::try_compile(node).is_ok() {
"ok"
} else {
"err"
}
}
#[cfg(feature = "jit")]
fn jit_cell(node: &SdfNode) -> Option<&'static str> {
Some(
if alice_sdf::compiled::jit::JitCompiledSdf::compile(node).is_ok() {
"ok"
} else {
"err"
},
)
}
#[cfg(not(feature = "jit"))]
const fn jit_cell(_: &SdfNode) -> Option<&'static str> {
None
}
#[cfg(feature = "jit")]
fn jit_simd_cell(node: &SdfNode) -> Option<&'static str> {
let Ok(c) = CompiledSdf::try_compile(node) else {
return Some("-");
};
Some(
if alice_sdf::compiled::jit::JitSimdSdf::compile(&c).is_ok() {
"ok"
} else {
"err"
},
)
}
#[cfg(not(feature = "jit"))]
const fn jit_simd_cell(_: &SdfNode) -> Option<&'static str> {
None
}
#[cfg(feature = "msl")]
fn msl_cell(node: &SdfNode) -> Option<&'static str> {
use alice_sdf::compiled::{msl::MslShader, TranspileMode};
Some(
if MslShader::transpile(node, TranspileMode::Hardcoded).is_ok() {
"ok"
} else {
"err"
},
)
}
#[cfg(not(feature = "msl"))]
const fn msl_cell(_: &SdfNode) -> Option<&'static str> {
None
}
fn distance_grid() -> Vec<Vec3> {
let mut v = Vec::new();
for i in -4..=4 {
for j in -4..=4 {
for k in -4..=4 {
v.push(
Vec3::new(i as f32, j as f32, k as f32) * 0.5 + Vec3::new(0.03, -0.02, 0.01),
);
}
}
}
v
}
fn changes_distance(node: &SdfNode, child: &SdfNode) -> bool {
distance_grid()
.iter()
.any(|&p| eval(node, p).to_bits() != eval(child, p).to_bits())
}
#[cfg(any(feature = "glsl", feature = "gpu", feature = "hlsl"))]
fn transpile_cell(node: &SdfNode, emit: impl Fn(&SdfNode) -> String) -> &'static str {
match common::corpus::single_child(node) {
Some(child) if changes_distance(node, &child) && emit(node) == emit(&child) => {
"pass-through"
}
_ => "ok",
}
}
#[cfg(feature = "glsl")]
fn glsl_emit(node: &SdfNode) -> String {
use alice_sdf::compiled::glsl::{GlslShader, GlslTranspileMode};
GlslShader::transpile(node, GlslTranspileMode::Hardcoded).source
}
#[cfg(feature = "glsl")]
fn glsl_cell(node: &SdfNode) -> Option<&'static str> {
Some(transpile_cell(node, glsl_emit))
}
#[cfg(not(feature = "glsl"))]
const fn glsl_cell(_: &SdfNode) -> Option<&'static str> {
None
}
#[cfg(feature = "gpu")]
fn wgsl_emit(node: &SdfNode) -> String {
use alice_sdf::compiled::{TranspileMode, WgslShader};
WgslShader::transpile(node, TranspileMode::Hardcoded).source
}
#[cfg(feature = "gpu")]
fn wgsl_cell(node: &SdfNode) -> Option<&'static str> {
Some(transpile_cell(node, wgsl_emit))
}
#[cfg(not(feature = "gpu"))]
const fn wgsl_cell(_: &SdfNode) -> Option<&'static str> {
None
}
#[cfg(feature = "hlsl")]
fn hlsl_emit(node: &SdfNode) -> String {
use alice_sdf::compiled::hlsl::{HlslShader, HlslTranspileMode};
HlslShader::transpile(node, HlslTranspileMode::Hardcoded).source
}
#[cfg(feature = "hlsl")]
fn hlsl_cell(node: &SdfNode) -> Option<&'static str> {
Some(transpile_cell(node, hlsl_emit))
}
#[cfg(not(feature = "hlsl"))]
const fn hlsl_cell(_: &SdfNode) -> Option<&'static str> {
None
}
#[cfg(feature = "rust")]
fn rust_cell(node: &SdfNode) -> Option<&'static str> {
Some(
if alice_sdf::compiled::rust::RustSource::transpile(node).is_ok() {
"ok"
} else {
"err"
},
)
}
#[cfg(not(feature = "rust"))]
const fn rust_cell(_: &SdfNode) -> Option<&'static str> {
None
}
fn constructor_name(variant: &str) -> String {
let v = variant
.replace("2D", "_2d")
.replace("Subtraction", "Subtract");
let mut out = String::new();
let mut prev: Option<char> = None;
for c in v.chars() {
if c.is_ascii_uppercase()
&& prev.is_some_and(|p| p.is_ascii_lowercase() || p.is_ascii_digit())
{
out.push('_');
}
out.push(c.to_ascii_lowercase());
prev = Some(c);
}
out
}
fn binding_aliases(variant: &str) -> (Option<&'static str>, Option<&'static str>) {
match variant {
"Box3d" => (Some("box"), None),
"SdfSkinning" => (Some("skinning"), None),
"RepeatInfinite" => (Some("repeat"), Some("repeat")),
"ScaleNonUniform" => (None, Some("scale_xyz")),
"Diamond" => (None, Some("diamond_shape")),
"Stairs" => (None, Some("stairs_shape")),
_ => (None, None),
}
}
fn read_tree(dir: &str) -> String {
fn walk(p: &std::path::Path, out: &mut String) {
let mut entries: Vec<_> = std::fs::read_dir(p)
.expect("read binding dir")
.flatten()
.collect();
entries.sort_by_key(std::fs::DirEntry::path);
for e in entries {
let path = e.path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|x| x == "rs") {
*out += &std::fs::read_to_string(&path).expect("read binding source");
out.push('\n');
}
}
}
let mut s = String::new();
walk(
&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(dir),
&mut s,
);
assert!(s.len() > 1000, "{dir}: no source read");
s
}
fn has_fn(src: &str, name: &str) -> bool {
let pat = format!("fn {name}");
src.match_indices(&pat).any(|(i, _)| {
let before_ok = i == 0 || !src.as_bytes()[i - 1].is_ascii_alphanumeric();
let after = src[i + pat.len()..].trim_start();
before_ok && (after.starts_with('(') || after.starts_with('<'))
})
}
fn binding_cells(variant: &str, ffi: &str, py: &str) -> (&'static str, &'static str) {
let base = constructor_name(variant);
let (fa, pa) = binding_aliases(variant);
let f = has_fn(ffi, &format!("alice_sdf_{base}"))
|| fa.is_some_and(|a| has_fn(ffi, &format!("alice_sdf_{a}")));
let p = has_fn(py, &base) || pa.is_some_and(|a| has_fn(py, a));
(
if f { "ok" } else { "missing" },
if p { "ok" } else { "missing" },
)
}
fn entries() -> Vec<(&'static str, SdfNode)> {
let mut v = corpus();
v.push((
"triangle",
SdfNode::triangle(
Vec3::new(-0.5, 0.0, 0.0),
Vec3::new(0.5, 0.0, 0.0),
Vec3::new(0.0, 0.6, 0.2),
),
));
v.push((
"projective_transform_nonidentity",
common::corpus::projective_nonidentity(),
));
v.push((
"bezier",
SdfNode::bezier(
Vec3::new(-0.6, 0.0, 0.0),
Vec3::new(0.0, 0.8, 0.0),
Vec3::new(0.6, 0.0, 0.0),
0.1,
),
));
v
}
fn measure() -> BTreeMap<String, [Option<String>; NCOL]> {
let mut acc: BTreeMap<String, [Vec<&'static str>; NCOL]> = BTreeMap::new();
let mut seen_feature = [
true, true, true, false, false, false, false, false, false, false, true, true,
];
let ffi = read_tree("src/ffi");
let py = read_tree("src/python");
for (_name, node) in entries() {
let (f, p) = binding_cells(&variant_of(&node), &ffi, &py);
let cells = [
Some(eval_cell(&node)),
Some(interval_cell(&node)),
Some(compile_cell(&node)),
jit_cell(&node),
jit_simd_cell(&node),
msl_cell(&node),
glsl_cell(&node),
wgsl_cell(&node),
hlsl_cell(&node),
rust_cell(&node),
Some(f),
Some(p),
];
let row = acc.entry(variant_of(&node)).or_default();
for (i, c) in cells.iter().enumerate() {
if let Some(c) = c {
seen_feature[i] = true;
if !row[i].contains(c) {
row[i].push(c);
}
}
}
}
acc.into_iter()
.map(|(k, v)| {
let cols = std::array::from_fn(|i| {
seen_feature[i].then(|| {
let mut s = v[i].clone();
s.sort_unstable();
s.join("/")
})
});
(k, cols)
})
.collect()
}
fn render(variants: &[String], m: &BTreeMap<String, [Option<String>; NCOL]>) -> String {
let mut out = String::from(
"# SdfNode backend support\n\n\
_Generated by `tests/test_node_backend_matrix.rs` (`ALICE_SDF_WRITE_NODE_SUPPORT=1 cargo test --features jit,msl,rust,glsl,hlsl --test test_node_backend_matrix`); \
no timestamp, the file changes only when its content does._\n\n\
Every `SdfNode` variant, run through the backends whose outcome can be observed at run time, \
using the shared test corpus (`tests/common/corpus.rs`). CI fails when a cell changes and this file does not.\n\n\
| Column | `ok` | otherwise |\n|--------|------|-----------|\n\
| `eval` | the tree evaluator returns a finite distance other than `f32::MAX` | `fallback`: the unknown-variant arm |\n\
| `interval` | `eval_interval` returns a bounded enclosure over a ±2 box | `everything`: no pruning for this variant |\n\
| `compile` | `CompiledSdf::try_compile` succeeds | `err` |\n\
| `jit` | `JitCompiledSdf::compile` (scalar tree JIT) succeeds (`jit` feature) | `err` |\n\
| `jit_simd` | `JitSimdSdf::compile` of the compiled tree succeeds (`jit` feature) | `err`; `-` when the tree does not compile |\n\
| `msl` | `MslShader::transpile` succeeds (`msl` feature) | `err` |\n\
| `glsl` / `wgsl` / `hlsl` | the emitted source is not the child's source forwarded unchanged, for a node whose tree evaluator changes the child's distance (`glsl` / `gpu` / `hlsl` features; `msl` turns on `gpu`) | `pass-through` |\n\
| `rust` | `RustSource::transpile` succeeds (`rust` feature) | `err` |\n\
| `ffi` | a constructor `alice_sdf_<name>` exists in `src/ffi/` (matched by name) | `missing` |\n\
| `python` | a constructor `fn <name>(` exists in `src/python/` (matched by name) | `missing` |\n\n\
GLSL, WGSL, HLSL and BlinkScript share one dispatch (`transpile_node_inner` in \
`src/compiled/transpiler_common.rs`) whose `match` has no wildcard arm, so rustc rejects a variant without an arm \
(`transpiler_dispatch_has_no_wildcard_arm` keeps it that way); the three shader columns check that the arm does \
more than forward the child (BlinkScript is the HLSL body). `ProjectiveTransform` is measured with the corpus \
identity and a non-identity matrix.\n\n\
| Variant | eval | interval | compile | jit | jit_simd | msl | glsl | wgsl | hlsl | rust | ffi | python |\n\
|---------|------|----------|---------|-----|----------|-----|------|------|------|------|-----|--------|\n",
);
for v in variants {
let row = m.get(v).expect("every variant is measured");
let cells: Vec<String> = row
.iter()
.map(|c| c.clone().unwrap_or_else(|| "?".into()))
.collect();
out += &format!("| `{v}` | {} |\n", cells.join(" | "));
}
out
}
fn parse_ledger(text: &str) -> BTreeMap<String, Vec<String>> {
let mut out = BTreeMap::new();
for line in text.lines() {
let Some(rest) = line.strip_prefix("| `") else {
continue;
};
let Some((name, tail)) = rest.split_once("` |") else {
continue;
};
if !name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
continue; }
let cells: Vec<String> = tail
.split('|')
.map(|c| c.trim().to_string())
.filter(|c| !c.is_empty())
.collect();
out.insert(name.to_string(), cells);
}
out
}
#[test]
fn every_variant_reaches_the_backends_the_ledger_records() {
let variants = enum_variants();
assert!(
variants.len() >= 100,
"read only {} variants from src/types/mod.rs",
variants.len()
);
let m = measure();
let missing: Vec<&String> = variants.iter().filter(|v| !m.contains_key(*v)).collect();
assert!(
missing.is_empty(),
"SdfNode variants with no entry in tests/common/corpus.rs (add one): {missing:?}"
);
let unknown: Vec<&String> = m.keys().filter(|k| !variants.contains(k)).collect();
assert!(
unknown.is_empty(),
"corpus top-level variants not in the enum: {unknown:?}"
);
let path = format!("{}/{LEDGER}", env!("CARGO_MANIFEST_DIR"));
if std::env::var_os("ALICE_SDF_WRITE_NODE_SUPPORT").is_some() {
assert!(
m.values().all(|r| r.iter().all(Option::is_some)),
"write the ledger with every feature column on: --features jit,msl,rust,glsl,hlsl"
);
std::fs::write(&path, render(&variants, &m)).expect("write ledger");
return;
}
let ledger =
parse_ledger(&std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{LEDGER}: {e}")));
let ledger_names: Vec<&String> = ledger.keys().collect();
let mut sorted_variants: Vec<&String> = variants.iter().collect();
sorted_variants.sort();
assert_eq!(
ledger_names, sorted_variants,
"{LEDGER} does not list exactly the SdfNode variants"
);
let mut diffs = Vec::new();
let mut compared = 0usize;
for v in &variants {
let row = &m[v];
let want = &ledger[v];
assert_eq!(
want.len(),
COLUMNS.len(),
"{LEDGER}: row `{v}` has {} cells",
want.len()
);
for (i, col) in COLUMNS.iter().enumerate() {
if let Some(got) = &row[i] {
compared += 1;
if got != &want[i] {
diffs.push(format!("{v}.{col}: measured `{got}`, ledger `{}`", want[i]));
}
}
}
}
assert!(
compared >= variants.len() * 3,
"compared only {compared} cells"
);
assert!(
diffs.is_empty(),
"backend support changed ({} cells); if intended, regenerate {LEDGER} \
(ALICE_SDF_WRITE_NODE_SUPPORT=1 cargo test --features jit,msl,rust,glsl,hlsl --test test_node_backend_matrix):\n {}",
diffs.len(),
diffs.join("\n ")
);
}
#[test]
fn the_enum_reader_finds_the_variants_it_should() {
let v = enum_variants();
for name in [
"Sphere",
"Box3d",
"MetricBall",
"MetricBlend",
"Union",
"Translate",
"Twist",
"WithMaterial",
"Terrain",
] {
assert!(
v.contains(&name.to_string()),
"{name} not read from src/types/mod.rs"
);
}
assert!(!v
.iter()
.any(|n| n == "Radius" || n.chars().next().is_some_and(char::is_lowercase)));
}
#[test]
fn constructor_names_follow_the_binding_convention() {
assert_eq!(constructor_name("Circle2D"), "circle_2d");
assert_eq!(constructor_name("RoundedRect2D"), "rounded_rect_2d");
assert_eq!(
constructor_name("ExpSmoothSubtraction"),
"exp_smooth_subtract"
);
assert_eq!(constructor_name("Box3d"), "box3d");
assert_eq!(constructor_name("IWP"), "iwp");
assert_eq!(constructor_name("FischerKochS"), "fischer_koch_s");
assert!(has_fn(
"pub extern \"C\" fn alice_sdf_box(x: f32)",
"alice_sdf_box"
));
assert!(!has_fn(
"pub extern \"C\" fn alice_sdf_box_frame(x: f32)",
"alice_sdf_box"
));
assert!(!has_fn("fn my_alice_sdf_box(", "alice_sdf_box"));
}
fn match_body(src: &str, fn_name: &str) -> String {
let src = src.replace("\r\n", "\n");
let code: String = src
.lines()
.map(|l| l.split("//").next().unwrap_or(""))
.collect::<Vec<_>>()
.join("\n");
let f = code
.find(&format!("fn {fn_name}"))
.unwrap_or_else(|| panic!("fn {fn_name} not found"));
let m = f + code[f..]
.find("match node {")
.unwrap_or_else(|| panic!("no `match node` in {fn_name}"));
let start = m + "match node {".len();
let mut depth = 1;
for (i, c) in code[start..].char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return code[start..start + i].to_string();
}
}
_ => {}
}
}
panic!("unbalanced match in {fn_name}");
}
fn has_top_level_wildcard(body: &str) -> bool {
let mut depth = 0i32;
let mut top = String::new();
for c in body.chars() {
match c {
'{' | '(' | '[' => depth += 1,
'}' | ')' | ']' => depth -= 1,
_ if depth == 0 => top.push(c),
_ => {}
}
}
top.split([',', '\n']).any(|arm| {
let a = arm.trim_start_matches(|c: char| c.is_whitespace() || c == '#' || c == '|');
a.starts_with("_ =>") || a.starts_with("_ if ") || a == "_"
})
}
#[test]
fn transpiler_dispatch_has_no_wildcard_arm() {
let body = match_body(
include_str!("../src/compiled/transpiler_common.rs"),
"transpile_node_inner",
);
assert!(
body.matches("SdfNode::").count() >= 100,
"transpile_node_inner no longer dispatches on SdfNode"
);
assert!(
!has_top_level_wildcard(&body),
"transpile_node_inner gained a `_ =>` arm: a new SdfNode variant would compile without a shader law"
);
assert!(has_top_level_wildcard(
"SdfNode::A { .. } => 1,\n _ => 0,"
));
assert!(!has_top_level_wildcard(
"SdfNode::A { x } => match x { _ => 0 },"
));
assert!(has_top_level_wildcard(
"SdfNode::A { .. } => 1,\n #[allow(unreachable_patterns)] _ => 0,"
));
assert!(has_top_level_wildcard(
"SdfNode::A { .. } => 1,\n _ if false => 0,"
));
}
#[test]
fn the_source_readers_accept_crlf_line_endings() {
let lf = include_str!("../src/types/mod.rs");
let crlf = lf.replace("\r\n", "\n").replace('\n', "\r\n");
assert_eq!(enum_variants_in(&crlf), enum_variants_in(lf));
let common = include_str!("../src/compiled/transpiler_common.rs");
let common_crlf = common.replace("\r\n", "\n").replace('\n', "\r\n");
assert_eq!(
match_body(&common_crlf, "transpile_node_inner").len(),
match_body(common, "transpile_node_inner").len()
);
}
#[test]
fn category_counts_match_one_node_per_variant() {
let variants = enum_variants();
let mut seen = std::collections::BTreeMap::new();
for (_, node) in entries() {
seen.entry(variant_of(&node))
.or_insert_with(|| node.category());
}
assert_eq!(
seen.len(),
variants.len(),
"corpus does not cover every variant, the counts below would be partial"
);
for cat in [
SdfCategory::Primitive,
SdfCategory::Operation,
SdfCategory::Transform,
SdfCategory::Modifier,
] {
let measured = seen.values().filter(|c| **c == cat).count() as u32;
assert_eq!(cat.count(), measured, "SdfCategory::{cat:?}::count()");
}
assert_eq!(SdfCategory::total() as usize, variants.len());
}
const DISTANCE_TRANSPARENT: [&str; 2] = ["Animated", "WithMaterial"];
#[test]
fn every_one_child_variant_has_an_entry_that_changes_the_distance() {
let mut one_child: BTreeMap<String, bool> = BTreeMap::new();
for (_name, node) in entries() {
if let Some(child) = common::corpus::single_child(&node) {
*one_child.entry(variant_of(&node)).or_default() |= changes_distance(&node, &child);
}
}
let declared =
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/types/mod.rs"))
.expect("read src/types/mod.rs")
.matches("child: Arc<Self>")
.count();
assert!(
declared >= 30,
"read only {declared} one-child variants from src/types/mod.rs"
);
assert_eq!(
one_child.len(),
declared,
"the serde child reader found {} one-child variants, the enum declares {declared}: {:?}",
one_child.len(),
one_child.keys().collect::<Vec<_>>()
);
let trivial: Vec<&String> = one_child
.iter()
.filter(|(v, changes)| !**changes && !DISTANCE_TRANSPARENT.contains(&v.as_str()))
.map(|(v, _)| v)
.collect();
assert!(
trivial.is_empty(),
"one-child variants whose every entry leaves the distance unchanged \
(add a non-trivial entry): {trivial:?}"
);
for v in DISTANCE_TRANSPARENT {
assert_eq!(
one_child.get(v),
Some(&false),
"{v} is not distance-transparent"
);
}
}
#[cfg(all(feature = "glsl", feature = "gpu", feature = "hlsl"))]
#[test]
fn no_transpiler_silently_forwards_a_child() {
use alice_sdf::compiled::shader_unsupported_nodes;
type Emit = fn(&SdfNode) -> String;
let langs: [(&str, Emit); 3] = [
("glsl", glsl_emit),
("wgsl", wgsl_emit),
("hlsl", hlsl_emit),
];
let mut lies = Vec::new();
let mut checked = 0usize;
for (name, node) in entries() {
let Some(child) = common::corpus::single_child(&node) else {
continue;
};
if !changes_distance(&node, &child) {
continue;
}
checked += 1;
for (lang, emit) in langs {
if emit(&node) == emit(&child) && shader_unsupported_nodes(&node).is_empty() {
lies.push(format!("{name}: {lang} forwards the child unchanged"));
}
}
}
assert!(
checked >= 40,
"only {checked} one-child entries change the distance"
);
assert!(
lies.is_empty(),
"transpilers forward the child of a node they report as supported:\n {}",
lies.join("\n ")
);
}