mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
kernel: one switch, SOLIDCRAFT_KERNEL, picks the kernel for every operation
All operations move to SMLib at once, so the selector is no longer the booleans' own: `SOLIDCRAFT_KERNEL=smlib|truck` (read once, unset is truck, the same refusals for an unknown value or SMLib in a build without it) is answered by `solidcraft_kernel::backend()` (crates/kernel/src/backend.rs), which every operation's dispatch calls. The boolean's SMLib path moves to smlib_boolean.rs. The regression rig's kernel check (`boolean_kernel`) now answers from the switch, and the rig and CLI set SOLIDCRAFT_KERNEL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
@@ -9,9 +9,9 @@ use serde_json::{Value, json};
|
||||
use crate::oracle::{ABS_FRAC, Checks, REL_TOL, totals, v3};
|
||||
use crate::{read_json, recipe};
|
||||
|
||||
/// The kernel this process runs booleans with (`SOLIDCRAFT_BOOLEANS`, set by the caller).
|
||||
/// The kernel this process runs on (`SOLIDCRAFT_KERNEL`, set by the caller).
|
||||
fn kernel() -> String {
|
||||
std::env::var("SOLIDCRAFT_BOOLEANS").ok().filter(|k| !k.is_empty()).unwrap_or_else(|| "default".into())
|
||||
std::env::var("SOLIDCRAFT_KERNEL").ok().filter(|k| !k.is_empty()).unwrap_or_else(|| "default".into())
|
||||
}
|
||||
|
||||
/// How many cases a chain of `base` recipes may go through (a ladder case builds on the one
|
||||
|
||||
@@ -82,7 +82,7 @@ const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
|
||||
/// `oracle`'s lines in the usage.
|
||||
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
|
||||
/// `kernel-cases`'s lines in the usage.
|
||||
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json]\n (the kernel regression rig, tests/kernel-cases; SOLIDCRAFT_BOOLEANS picks the kernel)\n";
|
||||
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json]\n (the kernel regression rig, tests/kernel-cases; SOLIDCRAFT_KERNEL picks the kernel)\n";
|
||||
/// `fuzz`'s lines in the usage.
|
||||
const USAGE_FUZZ: &str =
|
||||
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! Which kernel this process runs its operations on: truck (the default) or SMLib.
|
||||
//!
|
||||
//! The setting is the environment variable `SOLIDCRAFT_KERNEL`, read once per process: `truck`
|
||||
//! (or unset) or `smlib`. Every operation that has an SMLib path asks [`backend`]. SMLib needs a
|
||||
//! build with the kernel's `smlib` feature; set without it, or set to anything else, every such
|
||||
//! operation fails with an error that says so rather than quietly running truck, so a comparison
|
||||
//! run can never compare truck with itself.
|
||||
|
||||
use crate::{KernelError, Result};
|
||||
|
||||
/// The environment variable that picks the kernel.
|
||||
pub const KERNEL_ENV: &str = "SOLIDCRAFT_KERNEL";
|
||||
|
||||
/// A kernel operations run on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Backend {
|
||||
Truck,
|
||||
Smlib,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// The backend a setting names; `None` (unset) is truck.
|
||||
pub fn parse(setting: Option<&str>) -> std::result::Result<Backend, String> {
|
||||
match setting.map(str::trim) {
|
||||
None | Some("") | Some("truck") => Ok(Backend::Truck),
|
||||
Some("smlib") if cfg!(feature = "smlib") => Ok(Backend::Smlib),
|
||||
Some("smlib") => Err(format!("{KERNEL_ENV}=smlib: not supported yet: this build has no SMLib (feature `smlib`)")),
|
||||
Some(other) => Err(format!("{KERNEL_ENV}={other}: expected `truck` or `smlib`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel this process runs on ([`KERNEL_ENV`], read on first use).
|
||||
pub fn backend() -> Result<Backend> {
|
||||
static SETTING: std::sync::OnceLock<std::result::Result<Backend, String>> = std::sync::OnceLock::new();
|
||||
SETTING.get_or_init(|| Backend::parse(std::env::var(KERNEL_ENV).ok().as_deref())).clone().map_err(KernelError::Invalid)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_setting_names_a_backend_or_is_refused() {
|
||||
assert_eq!(Backend::parse(None), Ok(Backend::Truck));
|
||||
assert_eq!(Backend::parse(Some("truck")), Ok(Backend::Truck));
|
||||
assert_eq!(Backend::parse(Some(" ")), Ok(Backend::Truck));
|
||||
assert_eq!(Backend::parse(Some("smlib")).is_ok(), cfg!(feature = "smlib"));
|
||||
assert!(Backend::parse(Some("occt")).is_err());
|
||||
}
|
||||
}
|
||||
@@ -531,7 +531,7 @@ fn stacked_pieces_of_one_cylinder_keep_their_walls() {
|
||||
#[cfg(feature = "smlib")]
|
||||
mod smlib {
|
||||
use super::*;
|
||||
use crate::boolean_backend::smlib_boolean;
|
||||
use crate::smlib_boolean::smlib_boolean;
|
||||
|
||||
fn through_smlib(a: &Body, b: &Body, op: BoolOp) -> Body {
|
||||
smlib_boolean(a, b, op).unwrap().expect("a result")
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod backend;
|
||||
mod blend;
|
||||
mod blend_corner;
|
||||
mod blend_fit;
|
||||
@@ -20,7 +21,6 @@ mod blend_vertex;
|
||||
#[cfg(test)]
|
||||
mod blend_wrong_way_tests;
|
||||
mod body;
|
||||
mod boolean_backend;
|
||||
mod boolean_diagnostics;
|
||||
mod boolean_guard;
|
||||
mod boolean_record;
|
||||
@@ -121,6 +121,8 @@ pub mod provenance;
|
||||
mod record;
|
||||
mod seam_faces;
|
||||
mod sew;
|
||||
#[cfg(feature = "smlib")]
|
||||
mod smlib_boolean;
|
||||
mod smooth_edges;
|
||||
mod sphere_lens;
|
||||
mod spline_conv;
|
||||
@@ -157,11 +159,11 @@ mod validity;
|
||||
pub mod watch;
|
||||
mod whole_face;
|
||||
|
||||
pub use backend::{Backend, KERNEL_ENV, backend};
|
||||
pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_max, fillet_max_each};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
|
||||
pub use body::{Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, Paint};
|
||||
pub use boolean_backend::{BOOLEANS_ENV, BooleanBackend, boolean_backend};
|
||||
pub use boolean_diagnostics::{BooleanDiagnostics, BooleanFallback, BooleanStrategy, capture_boolean_diagnostics};
|
||||
pub use boundary::{Boundary, boundary_patch};
|
||||
pub use boundary_network::{BoundaryCurve, CurveSource, MAX_CHAIN_CURVES, MAX_EDGE_PICKS, boundary_join, boundary_surface};
|
||||
|
||||
@@ -918,18 +918,18 @@ fn take_attempt() -> bool {
|
||||
/// The result carries its exact history ([`Body::history`]) over the operands `[a, b]`,
|
||||
/// whichever way it was made, or none where a step it took does not report one.
|
||||
///
|
||||
/// Truck runs it unless [`crate::BOOLEANS_ENV`] picks SMLib ([`crate::boolean_backend`]).
|
||||
/// Truck runs it unless [`crate::KERNEL_ENV`] picks SMLib ([`crate::backend`]).
|
||||
pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || match crate::boolean_backend()? {
|
||||
crate::BooleanBackend::Truck => boolean_untimed(a, b, op),
|
||||
crate::BooleanBackend::Smlib => smlib_boolean(a, b, op),
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || match crate::backend()? {
|
||||
crate::Backend::Truck => boolean_untimed(a, b, op),
|
||||
crate::Backend::Smlib => smlib_boolean(a, b, op),
|
||||
})
|
||||
}
|
||||
|
||||
/// [`boolean`] through SMLib, its result given the operands' roles as truck's is.
|
||||
#[cfg(feature = "smlib")]
|
||||
fn smlib_boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
let r = guard("SMLib boolean", || crate::boolean_backend::smlib_boolean(a, b, op));
|
||||
let r = guard("SMLib boolean", || crate::smlib_boolean::smlib_boolean(a, b, op));
|
||||
with_operand_roles(a, b, op, r)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,12 @@
|
||||
//! Which kernel runs [`crate::boolean`]: truck (the default) or SMLib.
|
||||
//!
|
||||
//! The setting is the environment variable `SOLIDCRAFT_BOOLEANS`, read once per process:
|
||||
//! `truck` (or unset) or `smlib`. SMLib needs a build with the kernel's `smlib` feature; set
|
||||
//! without it, or set to anything else, every boolean fails with an error that says so rather
|
||||
//! than quietly running truck, so a comparison run can never compare truck with itself.
|
||||
//!
|
||||
//! Through SMLib a boolean is: each operand's schema 4 record built in SMLib and tagged as
|
||||
//! operand 0 or 1, `SmBrep` boolean, the result exported to schema 4 and decoded as a kernel
|
||||
//! body. The result's history is SMLib's own (its tags carried through the operation),
|
||||
//! renumbered from SMLib's face and edge order into the operands' and the result's ([`History`]'s
|
||||
//! contract). A result SMLib's own check or the record's geometry check refuses is an error,
|
||||
//! never a body.
|
||||
//! Booleans through SMLib ([`crate::backend`] picks them): each operand's schema 4 record built
|
||||
//! in SMLib and tagged as operand 0 or 1, `SmBrep` boolean, the result exported to schema 4 and
|
||||
//! decoded as a kernel body. The result's history is SMLib's own (its tags carried through the
|
||||
//! operation), renumbered from SMLib's face and edge order into the operands' and the result's
|
||||
//! ([`crate::history::History`]'s contract). A result SMLib's own check or the record's geometry
|
||||
//! check refuses is an error, never a body.
|
||||
|
||||
use crate::{KernelError, Result};
|
||||
|
||||
/// The environment variable that picks the boolean kernel.
|
||||
pub const BOOLEANS_ENV: &str = "SOLIDCRAFT_BOOLEANS";
|
||||
|
||||
/// A kernel that runs booleans.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BooleanBackend {
|
||||
Truck,
|
||||
Smlib,
|
||||
}
|
||||
|
||||
impl BooleanBackend {
|
||||
/// The backend a setting names; `None` (unset) is truck.
|
||||
pub fn parse(setting: Option<&str>) -> std::result::Result<BooleanBackend, String> {
|
||||
match setting.map(str::trim) {
|
||||
None | Some("") | Some("truck") => Ok(BooleanBackend::Truck),
|
||||
Some("smlib") if cfg!(feature = "smlib") => Ok(BooleanBackend::Smlib),
|
||||
Some("smlib") => Err(format!("{BOOLEANS_ENV}=smlib: not supported yet: this build has no SMLib (feature `smlib`)")),
|
||||
Some(other) => Err(format!("{BOOLEANS_ENV}={other}: expected `truck` or `smlib`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The boolean kernel this process uses ([`BOOLEANS_ENV`], read on first use).
|
||||
pub fn boolean_backend() -> Result<BooleanBackend> {
|
||||
static SETTING: std::sync::OnceLock<std::result::Result<BooleanBackend, String>> = std::sync::OnceLock::new();
|
||||
SETTING.get_or_init(|| BooleanBackend::parse(std::env::var(BOOLEANS_ENV).ok().as_deref())).clone().map_err(KernelError::Invalid)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smlib")]
|
||||
pub(crate) use smlib::boolean as smlib_boolean;
|
||||
|
||||
#[cfg(feature = "smlib")]
|
||||
mod smlib {
|
||||
use solidcraft_smlib::{BooleanOp, CheckLevel};
|
||||
|
||||
@@ -105,17 +66,3 @@ mod smlib {
|
||||
body.with_checked_history(history, &record::counts(&[a, b]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_setting_names_a_backend_or_is_refused() {
|
||||
assert_eq!(BooleanBackend::parse(None), Ok(BooleanBackend::Truck));
|
||||
assert_eq!(BooleanBackend::parse(Some("truck")), Ok(BooleanBackend::Truck));
|
||||
assert_eq!(BooleanBackend::parse(Some(" ")), Ok(BooleanBackend::Truck));
|
||||
assert_eq!(BooleanBackend::parse(Some("smlib")).is_ok(), cfg!(feature = "smlib"));
|
||||
assert!(BooleanBackend::parse(Some("occt")).is_err());
|
||||
}
|
||||
}
|
||||
@@ -65,10 +65,13 @@ pub fn smlib_built() -> bool {
|
||||
cfg!(feature = "smlib")
|
||||
}
|
||||
|
||||
/// The kernel booleans run on in this process: `"default"` or `"smlib"`. The regression rig
|
||||
/// refuses to grade a kernel the process isn't running. The SMLib boolean switch
|
||||
/// (`SOLIDCRAFT_BOOLEANS=smlib`) answers here once it lands; until then this is always
|
||||
/// `"default"`.
|
||||
/// The kernel this process runs on ([`crate::backend`]): `"default"` (truck) or `"smlib"`, and
|
||||
/// `"invalid"` where [`crate::KERNEL_ENV`] names neither or names SMLib in a build without it.
|
||||
/// The regression rig refuses to grade a kernel the process isn't running.
|
||||
pub fn boolean_kernel() -> &'static str {
|
||||
"default"
|
||||
match crate::backend() {
|
||||
Ok(crate::Backend::Truck) => "default",
|
||||
Ok(crate::Backend::Smlib) => "smlib",
|
||||
Err(_) => "invalid",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ cargo run -p solidcraft-cli -- kernel-cases tests/kernel-cases/booleans/<id>
|
||||
```
|
||||
|
||||
For the `smlib` column, build `solidcraft-cli` with `--features smlib` (after
|
||||
`scripts/fetch-smlib.sh`). The rig sets `SOLIDCRAFT_BOOLEANS=smlib` for those runs.
|
||||
`scripts/fetch-smlib.sh`). The rig sets `SOLIDCRAFT_KERNEL=smlib` for those runs.
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -87,9 +87,9 @@ fn run_case(o: &Options, kernel: &str, case: &Path) -> Value {
|
||||
};
|
||||
cmd.current_dir(root()).args(["kernel-cases", "--json"]).arg(&rel).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
if kernel == "default" {
|
||||
cmd.env_remove("SOLIDCRAFT_BOOLEANS");
|
||||
cmd.env_remove("SOLIDCRAFT_KERNEL");
|
||||
} else {
|
||||
cmd.env("SOLIDCRAFT_BOOLEANS", kernel);
|
||||
cmd.env("SOLIDCRAFT_KERNEL", kernel);
|
||||
}
|
||||
let base = |status: &str, error: String| {
|
||||
let area = case.parent().and_then(|p| p.file_name()).map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||||
@@ -278,7 +278,7 @@ fn write_report(o: &Options, dir: &Path, list: &[PathBuf], results: &[Value]) ->
|
||||
);
|
||||
}
|
||||
let mut md = String::from(
|
||||
"# Kernel regression rig\n\nGenerated by `cargo xtask kernel-cases`. Every geometry case the old kernel (truck) got wrong, as an issue, a PR or a crash report recorded it, is a use case in [`tests/kernel-cases/`](../tests/kernel-cases/) with its own right answer: numbers from analysis or a hand calculation (**analytic**) where the geometry has them, and otherwise the invariants (**invariants**): the operation succeeds, or refuses cleanly where that is the right answer; each body is one closed shell that passes the record's structure and geometry checks and, in builds with SMLib, SMLib's own check; the expected face, edge and vertex counts. Never a kernel's output, and never a commercial kernel's.\n\nEach kernel is graded against the case, never against the other kernel. **default** is the kernel SolidCraft ships today; **smlib** runs with `SOLIDCRAFT_BOOLEANS=smlib`. A failing case stays failing until its kernel is fixed; there is no workaround on SolidCraft's side. See [`tests/kernel-cases/README.md`](../tests/kernel-cases/README.md) for the format.\n\n",
|
||||
"# Kernel regression rig\n\nGenerated by `cargo xtask kernel-cases`. Every geometry case the old kernel (truck) got wrong, as an issue, a PR or a crash report recorded it, is a use case in [`tests/kernel-cases/`](../tests/kernel-cases/) with its own right answer: numbers from analysis or a hand calculation (**analytic**) where the geometry has them, and otherwise the invariants (**invariants**): the operation succeeds, or refuses cleanly where that is the right answer; each body is one closed shell that passes the record's structure and geometry checks and, in builds with SMLib, SMLib's own check; the expected face, edge and vertex counts. Never a kernel's output, and never a commercial kernel's.\n\nEach kernel is graded against the case, never against the other kernel. **default** is the kernel SolidCraft ships today; **smlib** runs with `SOLIDCRAFT_KERNEL=smlib`. A failing case stays failing until its kernel is fixed; there is no workaround on SolidCraft's side. See [`tests/kernel-cases/README.md`](../tests/kernel-cases/README.md) for the format.\n\n",
|
||||
);
|
||||
md += "## Coverage\n\n| Area | Cases |";
|
||||
for k in &o.kernels {
|
||||
|
||||
Reference in New Issue
Block a user