Skip to content

Latest commit

 

History

History
163 lines (124 loc) · 5.57 KB

File metadata and controls

163 lines (124 loc) · 5.57 KB

openscad-rs

Crates.io docs.rs CI

openscad-rs parses OpenSCAD source into a typed Rust syntax tree. It is a parser rather than a geometry evaluator: downstream compilers, formatters, linters, and language servers can interpret the resulting AST for their own purposes.

The crate provides a logos-based lexer, byte spans on every AST node, structured parse errors, a recursion-depth guard, and reusable read-only AST traversal. It forbids unsafe Rust.

Quick start

Add the crate to a project:

[dependencies]
openscad-rs = "0.1"

Parse source and inspect its statements:

use openscad_rs::{Statement, parse};

let source = r#"
    module rounded_box(size = [10, 10, 10], r = 1) {
        minkowski() {
            cube(size - [2*r, 2*r, 2*r]);
            sphere(r = r, $fn = 20);
        }
    }

    rounded_box(size = [30, 20, 10], r = 2);
"#;

let file = parse(source)?;

for statement in &file.statements {
    match statement {
        Statement::ModuleDefinition { name, params, .. } => {
            println!("module {name} has {} parameters", params.len());
        }
        Statement::ModuleInstantiation { name, args, .. } => {
            println!("call to {name} has {} arguments", args.len());
        }
        _ => {}
    }
}

# Ok::<(), openscad_rs::ParseError>(())

Core API

  • parse lexes and parses one source string into a SourceFile.
  • Statement represents assignments, definitions, module calls, conditionals, blocks, and include/use directives.
  • Expr and ExprKind represent literals, operators, calls, indexing, ranges, anonymous functions, and list comprehensions.
  • Span is a half-open byte range into the original UTF-8 source.
  • ParseError reports invalid tokens, unexpected syntax, incomplete input, and excessive nesting.
  • Visitor provides read-only recursive traversal. Its walk_* helpers let an override inspect a node and then continue through that node's children.

For example, count nested module calls while preserving default traversal:

use openscad_rs::{Statement, Visitor, parse, walk_statement};

struct ModuleCounter(usize);

impl Visitor for ModuleCounter {
    fn visit_statement(&mut self, statement: &Statement) {
        if matches!(statement, Statement::ModuleInstantiation { .. }) {
            self.0 += 1;
        }
        walk_statement(self, statement);
    }
}

let file = parse("union() { cube(5); sphere(3); }")?;
let mut counter = ModuleCounter(0);
counter.visit_file(&file);
assert_eq!(counter.0, 3);

# Ok::<(), openscad_rs::ParseError>(())

Language coverage and limits

The parser recognizes OpenSCAD literals, expressions and precedence, vectors, ranges, list comprehensions, assignments, user-defined functions and modules, child statements, modifiers, and include/use syntax. String escape handling and source locations are retained in the AST.

Numeric tokens are parsed directly into hyperreal::Real. Integer, decimal, scientific-notation, and hexadecimal literals therefore retain exact rational meaning in the AST instead of first passing through f64.

Parsing is intentionally syntactic. The crate does not resolve included files, evaluate expressions, type-check programs, or construct geometry. AST strings and expression boxes are owned; this favors a straightforward downstream API over arena allocation or a fully zero-copy tree.

An optional compatibility test runs against the vendored upstream OpenSCAD fixture corpus. That corpus also contains experimental and deliberately invalid inputs, so the test enforces a regression floor rather than claiming universal language acceptance.

Development

cargo fmt --all --check
cargo test --all-targets
cargo clippy --all-targets -- -D warnings
cargo bench
cargo check --manifest-path fuzz/Cargo.toml --bins --locked

The lexer, exact numeric literals, parser diagnostics, AST invariants, and generated valid grammar have dedicated cargo-fuzz campaigns. See fuzz/README.md for the target matrix and bounded nightly commands.

To exercise the upstream fixtures and the command-line comparison benchmark:

git submodule update --init
cargo test --test openscad_compat -- --nocapture
./benches/compare_openscad.sh

The comparison script additionally requires the openscad executable and Python 3. Its numbers are local measurements, not a stable performance claim.

References

Related geometry work: csgrs turns programmatic inputs into constructive-solid-geometry meshes, while synaps-cad builds an interactive CAD application around OpenSCAD-like source and csgrs.

License

Licensed under either of:

at your option.