A PEG parser for the OpenSCAD language that can parse OpenSCAD source code and optionally generate an Abstract Syntax Tree (AST) for programmatic analysis and manipulation.
- Full OpenSCAD language support including:
- Module and function definitions
- Expressions with proper operator precedence
- Control structures (if/else, for loops, let, assert, echo)
- List comprehensions
- Module modifiers (
!,#,%,*) - Use and include statements - Parse tree generation using Arpeggio PEG parser
- AST generation with comprehensive node types
- Source position tracking for all AST nodes
- AST tree can contain comment nodes (single-line and multi-line)
- AST tree uses dataclasses and can be pickled/unpickled for caching/serialization
Install from PyPI:
pip install openscad-parser
Or install from source:
git clone https://github.com/belfryscad/openscad_parser.git cd openscad_parser pip install -e .
To parse OpenSCAD code, first create a parser instance, then parse your code:
from openscad_parser import getOpenSCADParser
# Create a parser instance
parser = getOpenSCADParser(reduce_tree=False)
# Parse OpenSCAD code
code = """
module test(x, y=10) {
cube([x, y, 5]);
translate([0, 0, y]) sphere(5);
}
"""
parse_tree = parser.parse(code)
The parser returns an Arpeggio parse tree that represents the structure of your OpenSCAD code.
The getOpenSCADParser() function accepts several options:
parser = getOpenSCADParser(
reduce_tree=False, # Keep full parse tree (default: False)
debug=False # Enable debug output (default: False)
)
reduce_tree: If True, reduces the parse tree by removing single-child nodes. Set to False when generating ASTs.debug: If True, enables verbose debug output during parsing.
The parser can convert the parse tree into an Abstract Syntax Tree (AST) with typed nodes for easier programmatic manipulation.
The easiest way to generate ASTs is using the convenience functions that handle parser creation automatically:
Use getASTfromString() to parse OpenSCAD code from a string:
from openscad_parser.ast import getASTfromString code = "x = 10 + 5;" ast = getASTfromString(code) # ast is a list of top-level statements assignment = ast[0] print(assignment.name.name) # "x" print(assignment.expr) # AdditionOp(left=NumberLiteral(10), right=NumberLiteral(5))
Use getASTfromFile() to parse an OpenSCAD file. This function includes automatic caching - files are only re-parsed if their modification timestamp changes:
from openscad_parser.ast import getASTfromFile
# Parse a file (cached automatically)
ast = getASTfromFile("my_model.scad")
# Subsequent calls return cached AST if file hasn't changed
ast2 = getASTfromFile("my_model.scad") # Returns cached version
The cache is automatically invalidated when the file is modified, ensuring you always get up-to-date results.
Use getASTfromLibraryFile() to find and parse library files using OpenSCAD's search path rules. This is useful for resolving use and include statements:
from openscad_parser.ast import getASTfromLibraryFile
# From a file that includes a library
# Searches: current file directory, OPENSCADPATH, platform defaults
# Returns: (AST, absolute_path) tuple
ast, path = getASTfromLibraryFile("/path/to/main.scad", "utils/math.scad")
# Or without current file context
ast, path = getASTfromLibraryFile("", "MCAD/boxes.scad")
The function searches for library files in this order:
- Directory of the current file (if provided)
- Directories in the
OPENSCADPATHenvironment variable - Platform-specific default library directories:
- Windows:
~/Documents/OpenSCAD/libraries- macOS:~/Documents/OpenSCAD/libraries- Linux:~/.local/share/OpenSCAD/libraries
For more control, you can use parse_ast() directly with a custom parser instance:
from openscad_parser import getOpenSCADParser from openscad_parser.ast import parse_ast # Create a parser instance parser = getOpenSCADParser(reduce_tree=False) # Parse and generate AST code = "x = 10 + 5;" ast = parse_ast(parser, code) # ast is a list of top-level statements assignment = ast[0] print(assignment.name.name) # "x" print(assignment.expr) # AdditionOp(left=NumberLiteral(10), right=NumberLiteral(5))
The parse_ast() function is the lower-level API for AST generation. It takes:
parser: An Arpeggio parser instance (fromgetOpenSCADParser())code: The OpenSCAD code string to parsefile: Optional file path for source location tracking
All AST nodes inherit from ASTNode and have a position attribute for source location tracking:
from openscad_parser.ast import (
getASTfromString, Assignment, Identifier, NumberLiteral, AdditionOp
)
code = "result = 10 + 20;"
ast = getASTfromString(code)
assignment = ast[0]
# Check node types
assert isinstance(assignment, Assignment)
assert isinstance(assignment.name, Identifier)
assert isinstance(assignment.expr, AdditionOp)
# Access node properties
print(assignment.name.name) # "result"
print(assignment.expr.left.val) # 10
print(assignment.expr.right.val) # 20
# Access source position
print(assignment.position.line) # Line number (1-indexed)
print(assignment.position.char) # Column number (1-indexed)
from openscad_parser import getOpenSCADParser from openscad_parser.ast import parse_ast, Assignment, Identifier parser = getOpenSCADParser(reduce_tree=False) code = "x = 42;" ast = parse_ast(parser, code) assignment = ast[0] assert isinstance(assignment, Assignment) assert assignment.name.name == "x" assert assignment.expr.val == 42
From a file:
from openscad_parser.ast import getASTfromFile, ModuleDeclaration, ModularCall
ast = getASTfromFile("box.scad")
module = ast[0]
assert isinstance(module, ModuleDeclaration)
assert module.name.name == "box"
assert len(module.parameters) == 1
assert len(module.children) == 1
assert isinstance(module.children[0], ModularCall)
assert module.children[0].name.name == "cube"
Or from a string:
from openscad_parser.ast import getASTfromString, ModuleDeclaration, ModularCall
code = """
module box(size) {
cube(size);
}
"""
ast = getASTfromString(code)
module = ast[0]
assert isinstance(module, ModuleDeclaration)
assert module.name.name == "box"
assert len(module.parameters) == 1
assert len(module.children) == 1
assert isinstance(module.children[0], ModularCall)
assert module.children[0].name.name == "cube"
from openscad_parser.ast import (
getASTfromString, Assignment, AdditionOp, MultiplicationOp, NumberLiteral
)
code = "result = (10 + 5) * 2;"
ast = getASTfromString(code)
assignment = ast[0]
# The expression tree preserves operator precedence
mult_op = assignment.expr
assert isinstance(mult_op, MultiplicationOp)
assert isinstance(mult_op.left, AdditionOp)
assert mult_op.left.left.val == 10
assert mult_op.left.right.val == 5
assert mult_op.right.val == 2
from openscad_parser.ast import (
getASTfromString, PrimaryCall, PositionalArgument, NamedArgument
)
code = "x = foo(1, b=2);"
ast = getASTfromString(code)
assignment = ast[0]
call = assignment.expr
assert isinstance(call, PrimaryCall)
assert call.left.name == "foo"
assert len(call.arguments) == 2
assert isinstance(call.arguments[0], PositionalArgument)
assert isinstance(call.arguments[1], NamedArgument)
assert call.arguments[1].name.name == "b"
from openscad_parser.ast import getASTfromLibraryFile, ModuleDeclaration
# Parse a library file using OpenSCAD's search path
# Searches: current file dir, OPENSCADPATH, platform defaults
# Returns: (AST, absolute_path) tuple
ast, path = getASTfromLibraryFile("/path/to/main.scad", "utils/math.scad")
# Or without current file context
ast, path = getASTfromLibraryFile("", "MCAD/boxes.scad")
The AST includes comprehensive node types for all OpenSCAD language constructs:
ASTNode: Base class for all AST nodes (includespositionattribute)Expression: Base class for all expression nodesPrimary: Base class for atomic value typesModuleInstantiation: Base class for module-related statements
Identifier: Variable, function, or module namesStringLiteral: String valuesNumberLiteral: Numeric valuesBooleanLiteral: true/false valuesUndefinedLiteral: undef valueRangeLiteral: Range expressions [start:end:step]
Arithmetic:
- AdditionOp, SubtractionOp, MultiplicationOp, DivisionOp
- ModuloOp, ExponentOp, UnaryMinusOp
Logical:
- LogicalAndOp, LogicalOrOp, LogicalNotOp
Comparison:
- EqualityOp, InequalityOp
- GreaterThanOp, GreaterThanOrEqualOp
- LessThanOp, LessThanOrEqualOp
Bitwise:
- BitwiseAndOp, BitwiseOrOp, BitwiseNotOp
- BitwiseShiftLeftOp, BitwiseShiftRightOp
Other:
- TernaryOp: condition ? true_expr : false_expr
LetOp: let(assignments) bodyEchoOp: echo(arguments) bodyAssertOp: assert(arguments) bodyFunctionLiteral: function(parameters) bodyPrimaryCall: function callsPrimaryIndex: array indexing [index]PrimaryMember: member access .member
ListComprehension: Vector/list literalsListCompFor: for loops in list comprehensionsListCompCStyleFor: C-style for loopsListCompIf,ListCompIfElse: ConditionalsListCompLet: let expressionsListCompEach: each expressions
ModularCall: Module calls with arguments and childrenModularFor: for loopsModularCLikeFor: C-style for loopsModularIntersectionFor: intersection_for loopsModularLet: let statementsModularEcho: echo statementsModularAssert: assert statementsModularIf,ModularIfElse: if/else statementsModularModifierShowOnly:!modifierModularModifierHighlight:#modifierModularModifierBackground:%modifierModularModifierDisable:*modifier
ModuleDeclaration: module definitionsFunctionDeclaration: function definitionsParameterDeclaration: function/module parametersAssignment: variable assignments
UseStatement: use <filepath>IncludeStatement: include <filepath>PositionalArgument: Function call positional argumentsNamedArgument: Function call named arguments (name=value)
CommentLine: Single-line comments //CommentSpan: Multi-line comments/* */
All AST node classes are fully documented with docstrings that include: - Description of what the node represents - OpenSCAD code examples - Field/attribute descriptions - Usage notes
getOpenSCADParser(reduce_tree=False, debug=False)Create an Arpeggio parser instance for OpenSCAD code.
param reduce_tree: If True, reduces single-child nodes in parse tree param debug: If True, enables debug output returns: ParserPython instance getASTfromString(code: str)Parse OpenSCAD code from a string and return its AST.
param code: The OpenSCAD source code to be parsed returns: AST node or list of AST nodes (for top-level statements) rtype: ASTNode | list[ASTNode] | None getASTfromFile(file: str)Parse an OpenSCAD source file and return its AST. Includes automatic caching that invalidates when the file's modification timestamp changes.
param file: The OpenSCAD source file to be parsed returns: List of AST nodes (for top-level statements) rtype: list[ASTNode] | None raises FileNotFoundError: If the specified file does not exist raises Exception: If there is an error while reading the file getASTfromLibraryFile(currfile: str, libfile: str)Find and parse an OpenSCAD library file using OpenSCAD's search path rules. Searches in: current file directory, OPENSCADPATH, and platform default paths.
param currfile: Full path to the current OpenSCAD file (can be empty string) param libfile: Partial or full path to the library file to find returns: Tuple of (AST nodes list, absolute file path). The AST list is None if empty or not valid. rtype: tuple[list[ASTNode] | None, str] raises FileNotFoundError: If the library file cannot be found raises Exception: If there is an error while reading or parsing the file parse_ast(parser, code, file="")Parse OpenSCAD code and generate an AST (lower-level API).
param parser: Arpeggio parser instance from getOpenSCADParser() param code: OpenSCAD code string to parse param file: Optional file path for source location tracking returns: AST node or list of AST nodes (for top-level statements) clear_ast_cache()Clear the in-memory AST cache, forcing all subsequent calls to
getASTfromFile()to re-parse files.This function removes all cached AST trees from memory.
All AST node classes are located in openscad_parser.ast. Each node class:
- Inherits from
ASTNode(or a subclass likeExpression) - Has a
positionattribute of typePositionfor source location - Implements
__str__()for string representation - Is a dataclass with typed fields
Import commonly used classes:
from openscad_parser.ast import (
# Base classes
ASTNode, Expression, Primary,
# Literals
Identifier, StringLiteral, NumberLiteral, BooleanLiteral,
# Operators
AdditionOp, SubtractionOp, MultiplicationOp, DivisionOp,
LogicalAndOp, LogicalOrOp, EqualityOp, InequalityOp,
# Expressions
PrimaryCall, PrimaryIndex, PrimaryMember,
LetOp, EchoOp, AssertOp, TernaryOp,
# Modules
ModuleDeclaration, ModularCall, ModularFor,
# Functions
FunctionDeclaration,
# Statements
Assignment, UseStatement, IncludeStatement,
PositionalArgument, NamedArgument, ParameterDeclaration
)
All AST nodes include source position information:
from openscad_parser.ast import getASTfromFile, Position
ast = getASTfromFile("example.scad")
assignment = ast[0]
position = assignment.position
print(position.file) # "example.scad"
print(position.line) # 1 (1-indexed)
print(position.char) # 1 (1-indexed, column number)
print(position.position) # 0 (0-indexed character position)
The Position class provides lazy evaluation of line/column numbers from character positions.
The parser will raise SyntaxError exceptions for invalid OpenSCAD syntax:
from openscad_parser.ast import getASTfromString
try:
code = "x = ;" # Invalid syntax
ast = getASTfromString(code)
except SyntaxError as e:
print(f"Parse error: {e}")
File operations will raise FileNotFoundError for missing files:
from openscad_parser.ast import getASTfromFile, getASTfromLibraryFile
try:
ast = getASTfromFile("nonexistent.scad")
except FileNotFoundError as e:
print(f"File not found: {e}")
try:
ast, path = getASTfromLibraryFile("main.scad", "missing_lib.scad")
except FileNotFoundError as e:
print(f"Library file not found: {e}")
The getASTfromFile() function automatically caches parsed ASTs in memory:
from openscad_parser.ast import getASTfromFile
# First call parses and caches
ast1 = getASTfromFile("model.scad")
# Second call returns cached AST (same object)
ast2 = getASTfromFile("model.scad")
assert ast1 is ast2 # True - same cached object
# After file modification, cache is invalidated and file is re-parsed
# (modify model.scad here)
ast3 = getASTfromFile("model.scad")
assert ast1 is not ast3 # True - new parse after modification
Cache entries are automatically invalidated when a file's modification timestamp changes. To manually clear the cache:
from openscad_parser.ast import clear_ast_cache clear_ast_cache() # Clear all cached ASTs
Parser instances can be reused for parsing multiple code snippets:
parser = getOpenSCADParser(reduce_tree=False) # Parse multiple files ast1 = parse_ast(parser, code1, file="file1.scad") ast2 = parse_ast(parser, code2, file="file2.scad")
Note: For some use cases (like testing), you may need to create fresh parser instances to avoid memoization issues.
The AST is a tree structure that can be traversed recursively:
def visit_node(node):
"""Recursively visit AST nodes."""
if isinstance(node, Assignment):
print(f"Assignment: {node.name.name}")
visit_node(node.expr)
elif isinstance(node, AdditionOp):
print("Addition operation")
visit_node(node.left)
visit_node(node.right)
elif isinstance(node, NumberLiteral):
print(f"Number: {node.val}")
# ... handle other node types
from openscad_parser.ast import getASTfromString
code = "x = 10; y = 20;"
ast = getASTfromString(code)
for node in ast:
visit_node(node)
The project includes a comprehensive test suite. Run tests with:
pytest tests/
Contributions are welcome! The project uses:
MIT License - see LICENSE file for details.