A OpenSCAD parser library for Rust.
Parses .scad source files into a well-typed AST suitable for building compilers, formatters, linters, and language servers.
- Fast — logos-based zero-copy lexer compiles to jump tables
- Complete — Covers the full OpenSCAD language grammar (98.5% pass rate on OpenSCAD's own test suite)
- Typed AST — Every node carries source spans for precise error reporting and tooling
- Compiler-ready — Designed as a foundation for downstream compiler applications
- Safe —
#[forbid(unsafe_code)], pedantic clippy, comprehensive tests - Zero dependencies at runtime — Only
logos,miette, andthiserror
Add to your Cargo.toml:
[dependencies]
openscad-rs = "0.1.0"Parse a source file:
use openscad_rs::{parse, Statement, ExprKind};
let source = r#"
module rounded_box(size = [10, 10, 10], r = 1) {
if (r > 0) {
minkowski() {
cube(size - [2*r, 2*r, 2*r]);
sphere(r = r, $fn = 20);
}
} else {
cube(size);
}
}
rounded_box(size = [30, 20, 10], r = 2);
"#;
let ast = parse(source).expect("parse error");
for stmt in &ast.statements {
match stmt {
Statement::ModuleDefinition { name, params, .. } => {
println!("module {name}({} params)", params.len());
}
Statement::ModuleInstantiation { name, args, .. } => {
println!("call {name}({} args)", args.len());
}
_ => {}
}
}
// Output:
// module rounded_box(2 params)
// call rounded_box(2 args)src/
├── lib.rs # Public API: parse(), re-exports
├── token.rs # Token enum (logos-generated)
├── lexer.rs # Tokenizer: source → tokens with spans
├── ast.rs # AST node types: Expr, Statement, etc.
├── parser.rs # Recursive-descent parser
├── span.rs # Source location tracking
├── error.rs # ParseError with miette diagnostics
└── visit.rs # AST visitor trait
| Feature | Status |
|---|---|
| Literals (numbers, hex, strings, booleans, undef) | ✅ |
String escape sequences (\n, \t, \xHH, \uHHHH, \UHHHHHH) |
✅ |
| Variables & assignments | ✅ |
| Full operator precedence (17 levels) | ✅ |
| Ternary expressions | ✅ |
| Vectors & ranges | ✅ |
| Module definitions & instantiation | ✅ |
| Function definitions | ✅ |
| Anonymous functions | ✅ |
if/else (statement & expression) |
✅ |
for, let, each list comprehensions |
✅ |
echo(), assert() |
✅ |
include <file>, use <file> |
✅ |
Modifier prefixes (!, #, %, *) |
✅ |
Comments (//, /* */) |
✅ |
Member access (obj.x) & indexing (v[i]) |
✅ |
Bitwise operators (&, |, <<, >>, ~) |
✅ |
Exponentiation (^) |
✅ |
| Named & positional arguments | ✅ |
| Trailing commas | ✅ |
Traverse the AST with the built-in visitor trait:
use openscad_rs::{parse, Visitor, Expr, ExprKind, Statement};
struct ModuleCounter(usize);
impl Visitor for ModuleCounter {
fn visit_statement(&mut self, stmt: &Statement) {
if matches!(stmt, Statement::ModuleInstantiation { .. }) {
self.0 += 1;
}
// Call default implementation to recurse into children
openscad_rs::visit::Visitor::visit_statement(self, stmt);
}
}
let ast = parse("union() { cube(5); sphere(3); }").unwrap();
let mut counter = ModuleCounter(0);
counter.visit_file(&ast);
assert_eq!(counter.0, 3); // union, cube, sphereThe parser is validated against the OpenSCAD test suite (523 .scad files), achieving a 98.5% pass rate.
To run the compatibility tests:
git submodule update --init
cargo test --test openscad_compat -- --nocapturecargo bench- Parser only — No semantic analysis, type checking, or evaluation. This is purely syntactic parsing. Downstream crates handle the rest.
include/useare AST nodes — We parse the directive but don't resolve or load files. That's the compiler's responsibility.- Owned AST — Uses
StringandBox<Expr>for simplicity. Arena allocation can be added later for zero-copy parsing. - Lossless source locations — Every AST node carries a
Spanwith byte offsets for precise source mapping. - Recursion depth guard — Prevents stack overflow on adversarial/deeply nested inputs.
GPL v3