@holistic-stack/openscad-parser v0.1.2
OpenSCAD Parser
A comprehensive TypeScript parser for OpenSCAD code that transforms Tree-sitter Concrete Syntax Trees (CST) into semantic Abstract Syntax Trees (AST) with advanced error handling and IDE support capabilities.
๐ฏ Overview
The OpenSCAD Parser is the core parsing engine that bridges raw OpenSCAD source code and structured, semantic representations. Built with functional programming principles, it leverages the Tree-sitter OpenSCAD grammar to provide robust parsing with comprehensive error recovery and a type-safe AST generation system.
Key Features
- ๐ High-Performance Parsing: Tree-sitter WASM-based parsing with 100% test success rate (611/611 tests)
- ๐ฏ Semantic AST Generation: Rich AST with 80+ node types and comprehensive type information
- ๐งฉ Functional Core Design: Pure functions, immutability, and composable architecture
- ๐ง Advanced Error Handling: Detailed error reporting with recovery strategies and multiple error handler implementations
- ๐๏ธ Visitor Pattern Architecture: Extensible design with specialized visitors for different language constructs
- ๐ IDE Support: Symbol information, hover support, completion context, and position utilities
- ๐ก๏ธ Strong Type Safety: Comprehensive TypeScript type definitions and type guards with zero
anytypes - ๐ Legacy Support: Complete support for deprecated assign statements and assert statements
- ๐ Complete Language Coverage: Full OpenSCAD statement support including modules, functions, transformations, and primitives
- ๐ Debug Support: Complete echo statement parsing with complex expression support
- ๐ List Comprehension: Full support for OpenSCAD's list comprehension expressions
- โก Real-time Updates: Incremental parsing for efficient editor integration
Architecture Overview
flowchart TD
A[OpenSCAD Source] --> B[Tree-sitter Grammar]
B --> C[Concrete Syntax Tree]
C --> D[OpenscadParser]
D --> E[Visitor Chain]
E --> F1[PrimitiveVisitor]
E --> F2[TransformVisitor]
E --> F3[CSGVisitor]
E --> F4[ExpressionVisitor]
E --> F5[ModuleVisitor]
E --> F6[FunctionVisitor]
F1 & F2 & F3 & F4 & F5 & F6 --> G[Typed AST Nodes]
H[IErrorHandler] --> D
I[ErrorHandler] -.-> H
J[SimpleErrorHandler] -.-> H
G --> K[Applications]
K --> L[Symbol Provider]
K --> M[Position Utilities]
K --> N[Hover & Completion]Functional Design Principles
The OpenSCAD Parser is built on solid functional programming principles:
- Pure Functions: Core parsing logic uses pure functions with deterministic outputs
- Immutability: AST nodes are immutable once created, preventing side-effect bugs
- Composition: Visitors compose specialized parsing functionality
- Type Safety: Comprehensive TypeScript types with zero
anytypes - Error Handling: Explicit error handling with Either/Result patterns
- Separation of Concerns: Clear boundaries between parsing, AST generation, and error handling
๐ฆ Installation
# Using npm
npm install @openscad/parser
# Using pnpm
pnpm add @openscad/parser
# Using yarn
yarn add @openscad/parser๐ Quick Start
Basic Usage
import { OpenscadParser } from '@openscad/parser';
// Initialize the parser with async init pattern for better resource management
const parseOpenSCAD = async (source: string) => {
// Create a new parser instance (lightweight object until initialized)
const parser = new OpenscadParser();
try {
// Initialize parser (loads WASM modules)
await parser.init();
// Parse source to AST (pure function call - same input always yields same output)
return parser.parseAST(source);
} finally {
// Always clean up resources
parser.dispose();
}
};
// Example OpenSCAD code with modules, transformations, and primitives
const code = `
module house(width = 10, height = 15) {
cube([width, width, height]);
translate([0, 0, height]) {
rotate([0, 45, 0]) cube([width*1.4, width, 2]);
}
}
house(20, 25);
`;
// Execute parsing
const ast = await parseOpenSCAD(code);
// Typesafe node traversal with type guards
const findModuleNodes = (nodes: ASTNode[]): ModuleDefinitionNode[] => {
return nodes.filter(node => node.type === 'module_definition') as ModuleDefinitionNode[];
};
const moduleNodes = findModuleNodes(ast);
console.log(`Found ${moduleNodes.length} module(s):`,
moduleNodes.map(m => m.name).join(', '));Advanced Usage with Error Handling
import {
OpenscadParser,
SimpleErrorHandler,
type ASTNode,
type ParserError,
type ParserWarning
} from '@openscad/parser';
// Using the Result/Either pattern for functional error handling
type Result<T, E> = Success<T, E> | Failure<T, E>;
interface Success<T, E> {
readonly tag: 'success';
readonly value: T;
readonly errors: ReadonlyArray<E>;
readonly warnings: ReadonlyArray<ParserWarning>;
}
interface Failure<T, E> {
readonly tag: 'failure';
readonly errors: ReadonlyArray<E>;
readonly warnings: ReadonlyArray<ParserWarning>;
}
// Pure factory functions for creating immutable result objects
const success = <T, E>(value: T, errors: ReadonlyArray<E> = [], warnings: ReadonlyArray<ParserWarning> = []): Success<T, E> =>
Object.freeze({ tag: 'success', value, errors: Object.freeze([...errors]), warnings: Object.freeze([...warnings]) });
const failure = <T, E>(errors: ReadonlyArray<E>, warnings: ReadonlyArray<ParserWarning> = []): Failure<T, E> =>
Object.freeze({ tag: 'failure', errors: Object.freeze([...errors]), warnings: Object.freeze([...warnings]) });
// Pure function for parsing with comprehensive functional error handling
const parseOpenSCADWithErrorHandling = async (code: string): Promise<Result<ReadonlyArray<ASTNode>, ParserError>> => {
// Create immutable dependencies
const errorHandler = new SimpleErrorHandler();
const parser = new OpenscadParser(errorHandler);
try {
// Initialize parser (async operation with proper await)
await parser.init();
// Parse with pure function call (same input โ same output)
const ast = Object.freeze(parser.parseAST(code));
const errors = Object.freeze(errorHandler.getErrors());
const warnings = Object.freeze(errorHandler.getWarnings());
// Return properly tagged result using pattern matching-friendly structure
return errors.length > 0
? failure<ReadonlyArray<ASTNode>, ParserError>(errors, warnings) // Error case
: success<ReadonlyArray<ASTNode>, ParserError>(ast, [], warnings); // Success case
} catch (error) {
// Create critical error with factory function
const criticalError: ParserError = Object.freeze({
type: 'critical_error',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
severity: 'error'
});
// Return failure with immutable error collection
return failure<ReadonlyArray<ASTNode>, ParserError>([criticalError]);
} finally {
// Explicit resource management - functional resource lifecycle
parser.dispose();
}
};
// Example usage with pattern matching on the Result type
const processParseResult = async (code: string): Promise<void> => {
const result = await parseOpenSCADWithErrorHandling(code);
// Pure transformation functions for AST processing
const countNodesByType = (nodes: ReadonlyArray<ASTNode>, nodeType: string): number =>
nodes.filter(node => node.type === nodeType).length;
// Apply pattern matching on discriminated union (Result type)
match(result, {
success: ({ value, warnings }) => {
// Process with pure functions
const moduleCount = countNodesByType(value, 'module_definition');
const stats = {
moduleCount,
totalNodes: value.length,
warningCount: warnings.length
};
console.log(`Parse successful: ${stats.moduleCount} modules in ${stats.totalNodes} total nodes`);
if (stats.warningCount > 0) {
console.log(`Found ${stats.warningCount} warnings to address`);
}
},
failure: ({ errors }) => {
// Error classification with pure functions
const errorsByType = classifyErrors(errors);
console.error(`Parse failed with ${errors.length} errors:`);
if (errorsByType.bracketErrors.length > 0) {
console.error(`- ${errorsByType.bracketErrors.length} bracket errors detected`);
}
if (errorsByType.syntaxErrors.length > 0) {
console.error(`- ${errorsByType.syntaxErrors.length} syntax errors detected`);
}
}
});
};
// Helper function to implement pattern matching on Result type
function match<T, E, R>(result: Result<T, E>, patterns: {
success: (result: Success<T, E>) => R;
failure: (result: Failure<T, E>) => R;
}): R {
return result.tag === 'success'
? patterns.success(result)
: patterns.failure(result);
}
// Pure function for error classification
function classifyErrors(errors: ReadonlyArray<ParserError>): {
readonly bracketErrors: ReadonlyArray<ParserError>;
readonly syntaxErrors: ReadonlyArray<ParserError>;
readonly otherErrors: ReadonlyArray<ParserError>;
} {
return {
bracketErrors: errors.filter(e => e.message.includes('bracket')),
syntaxErrors: errors.filter(e => e.message.includes('syntax') || e.message.includes('token')),
otherErrors: errors.filter(e =>
!e.message.includes('bracket') &&
!e.message.includes('syntax') &&
!e.message.includes('token')
)
};
}
// Example usage with real OpenSCAD code
const exampleCode = `
module myModule() {
// Syntax error: missing closing bracket
cube([10, 10, 10);
}
`;
// Process the OpenSCAD code using our functional parsing approach
const processExample = async (): Promise<void> => {
const result = await parseOpenSCADWithErrorHandling(exampleCode);
match(result, {
success: ({ value, warnings }) => {
console.log(`Parse successful: ${value.length} nodes`);
},
failure: ({ errors }) => {
console.error(`Parse failed with ${errors.length} errors:`);
errors.forEach(error => {
const { message, location } = error;
console.error(`- ${message} at ${location.start.line}:${location.start.column}`);
});
}
});
};
// Usage with pattern matching-like structure
const result = await parseWithErrorHandling(`
module invalid_syntax(
cube(10);
}
`);
// Process result with functional approach
const processResult = (result: ParseResult) => {
if (result.success && result.ast) {
return `Parsed successfully: ${result.ast.length} nodes found`;
} else {
return `Parse errors: ${result.errors.join(', ')}`;
}
};
console.log(processResult(result));IDE Support Features with Position Utilities
import {
OpenscadParser,
OpenSCADSymbolProvider,
OpenSCADPositionUtilities,
SimpleErrorHandler,
Position,
ASTNode,
SymbolInfo,
HoverInfo
} from '@openscad/parser';
// Pure function to extract symbols from AST
const extractSymbols = (ast: ASTNode[]): SymbolInfo[] => {
const symbolProvider = new OpenSCADSymbolProvider();
return symbolProvider.getSymbols(ast);
};
// Function to get hover information at a specific position
const getHoverAtPosition = (ast: ASTNode[], position: Position): HoverInfo | null => {
const positionUtils = new OpenSCADPositionUtilities();
return positionUtils.getHoverInfo(ast, position);
};
// Main function composition
const analyzeOpenSCAD = async (code: string) => {
const parser = new OpenscadParser(new SimpleErrorHandler());
try {
await parser.init();
const ast = parser.parseAST(code);
// Get all symbols
const symbols = extractSymbols(ast);
// Extract hover information for position (3, 10) - line 4, column 11
const hoverInfo = getHoverAtPosition(ast, { line: 3, column: 10 });
return { ast, symbols, hoverInfo };
} finally {
parser.dispose();
}
};
// Test with a module definition
const result = await analyzeOpenSCAD(`
module house(width = 10, height = 15) {
cube([width, width, height]);
translate([0, 0, height]) {
sphere(width / 2);
}
}
`);
// Log the analysis results
console.log(`Found ${result.symbols.length} symbols:`,
result.symbols.map(s => `${s.type} ${s.name}`).join(', '));
console.log('Hover info:', result.hoverInfo?.content);๐ Core Components
OpenscadParser
The main parser orchestrates the parsing process using functional programming principles:
class OpenscadParser {
// Constructor with optional error handler - dependency injection pattern
constructor(errorHandler?: IErrorHandler)
// Asynchronous initialization with resource management
async init(wasmPath?: string, treeSitterWasmPath?: string): Promise<void>
// Pure function - same input always yields same output AST nodes
parseAST(source: string): ASTNode[]
// Low-level CST access for advanced usage
parseCST(source: string): Tree | null
// Efficient incremental parsing for editor integration
update(newSource: string, oldTree: Tree, edits?: Edit[]): Tree | null
// Incremental AST generation (efficient composition)
updateAST(newSource: string, oldTree: Tree, edits?: Edit[]): ASTNode[]
// Access error handler through interface (not implementation)
getErrorHandler(): IErrorHandler
// Explicit resource cleanup (no hidden side effects)
dispose(): void
}Visitor Pattern Implementation
// Base visitor with functional composability
interface ASTVisitor {
// Pure transformation function from CST node to AST node
visitNode(node: CSTNode): ASTNode | null;
// Context-aware utility function for visiting children
visitChildren?(node: CSTNode): ASTNode[];
}
// Composite visitor applying the functional composition pattern
class CompositeVisitor implements ASTVisitor {
// Immutable visitor registry
private readonly visitors: ReadonlyMap<string, ASTVisitor>;
// Pure visitor dispatch based on node type
visitNode(node: CSTNode): ASTNode | null;
}Immutable AST Node Types
The parser generates a rich, immutable AST with strong typing:
Common Node Structure
// Base node interface with discriminated union type field
interface BaseNode {
readonly type: string; // Discriminant for pattern matching
readonly location: SourceLocation; // Immutable location data
}
// Source location with immutable range data
interface SourceLocation {
readonly start: Position;
readonly end: Position;
}Module Definitions
interface ModuleDefinitionNode extends BaseNode {
readonly type: 'module_definition'; // Literal type for precise type checking
readonly name: string;
readonly parameters: readonly Parameter[];
readonly body: readonly StatementNode[];
readonly location: SourceLocation;
}
// Type guard for safe pattern matching
function isModuleDefinitionNode(node: unknown): node is ModuleDefinitionNode {
return isBaseNode(node) && node.type === 'module_definition';
}Function Definitions
interface FunctionDefinitionNode extends BaseNode {
readonly type: 'function_definition';
readonly name: string;
readonly parameters: readonly Parameter[];
readonly body: ExpressionNode; // Strongly typed expression
readonly location: SourceLocation;
}
// Example usage with type guard pattern matching
const processFunctions = (nodes: readonly ASTNode[]): string[] => {
return nodes
.filter(isFunctionDefinitionNode)
.map(func => `${func.name}(${func.parameters.length} params)`);
};Expression Hierarchy
// Union type for all expressions
type ExpressionNode =
| BinaryExpressionNode
| UnaryExpressionNode
| LiteralExpressionNode
| VectorExpressionNode
| FunctionCallNode
| IdentifierExpressionNode
| RangeExpressionNode
| ListComprehensionExpressionNode;
interface BinaryExpressionNode extends BaseNode {
readonly type: 'binary_expression';
readonly operator: BinaryOperator; // Union of literal string types
readonly left: ExpressionNode; // Recursive type for expressions
readonly right: ExpressionNode;
readonly location: SourceLocation;
}
// Type-safe binary operators with no magic strings
type BinaryOperator =
| '+' | '-' | '*' | '/' | '%' | '^'
| '&&' | '||'
| '==' | '!=' | '<' | '>' | '<=' | '>=';Functional Error Handling
The parser implements a sophisticated functional error handling approach with Either-like patterns and immutable error collections:
// Core error handler interface - minimal surface area for composability
interface IErrorHandler {
// Pure reporting functions (no side effects beyond internal state)
logInfo(message: string): void
logWarning(message: string, context?: string, node?: BaseNode): void
handleError(error: Error | string, context?: string, node?: BaseNode): void
}
// Simple implementation focused on immutable state management
class SimpleErrorHandler implements IErrorHandler {
// Internal immutable collections
private readonly errors: string[] = [];
private readonly warnings: string[] = [];
private readonly infos: string[] = [];
// Accessor methods return copies to maintain immutability
getErrors(): readonly string[] { return [...this.errors]; }
getWarnings(): readonly string[] { return [...this.warnings]; }
getInfos(): readonly string[] { return [...this.infos]; }
// Pure predicates
hasErrors(): boolean { return this.errors.length > 0; }
hasWarnings(): boolean { return this.warnings.length > 0; }
// Factory method pattern for controlled object creation
static createEmpty(): SimpleErrorHandler { return new SimpleErrorHandler(); }
}
// Advanced error handler with tagged union error types
class ErrorHandler {
// Typed error creation (factory methods)
createParserError(message: string, errorCode: string, severity: Severity, location?: SourceLocation): ParserError
createSyntaxError(message: string, location?: SourceLocation): SyntaxError
createTypeError(message: string, location?: SourceLocation): TypeError
// Recovery strategies using pure functions
attemptRecovery(error: ParserError, source: string): string | null
// Immutable error collection access
getErrors(): readonly ParserError[]
getErrorsBySeverity(minSeverity: Severity): readonly ParserError[]
}
// Tagged union pattern for error types
type ParserErrorType = 'syntax' | 'semantic' | 'type' | 'runtime';
// Discriminated union base with severity enum
interface ParserError {
readonly type: ParserErrorType;
readonly message: string;
readonly errorCode: string;
readonly severity: Severity;
readonly location?: SourceLocation;
}
// Error severity as enumeration (not magic strings)
enum Severity {
Info = 0,
Warning = 1,
Error = 2,
Critical = 3
}Functional IDE Support
Built-in IDE support features implemented with functional principles:
// Pure symbol extraction from AST
class OpenSCADSymbolProvider {
// Pure extraction functions with no side effects
getSymbols(ast: readonly ASTNode[]): readonly SymbolInfo[]
getSymbolsInScope(ast: readonly ASTNode[], position: Position): readonly SymbolInfo[]
// Memoization for performance optimization
private memoizeSymbols(ast: readonly ASTNode[]): Map<string, SymbolInfo>
}
// Position utilities as pure mapping functions
class OpenSCADPositionUtilities {
// Pure node finding without mutation
findNodeAt(ast: readonly ASTNode[], position: Position): ASTNode | null
// Range calculation as pure function
getNodeRange(node: ASTNode): Range
// Information extraction functions
getHoverInfo(ast: readonly ASTNode[], position: Position): HoverInfo | null
getCompletionContext(ast: readonly ASTNode[], position: Position): CompletionContext
}
// Strongly typed result interfaces
interface SymbolInfo {
readonly name: string;
readonly type: 'module' | 'function' | 'variable' | 'parameter';
readonly location: SourceLocation;
readonly documentation?: string;
readonly parameters?: readonly Parameter[];
}
interface HoverInfo {
readonly content: string;
readonly range: Range;
}๐งฉ Functional Visitor Architecture
The parser implements a sophisticated visitor pattern following functional programming principles:
Immutable Visitor Composition
// Base visitor with pure transformation functions
abstract class BaseASTVisitor {
// Source code is read-only input
protected readonly source: string;
// Error handler accessed through interface for dependency injection
protected readonly errorHandler: IErrorHandler;
// Pure transformation functions
abstract visitNode(node: CSTNode): ASTNode | null;
// Template method pattern for specialized visitation
protected visitChildren(node: CSTNode): ASTNode[] {
// Functional transformation via map/filter/reduce
return Array.from({ length: node.childCount })
.map((_, i) => node.child(i))
.filter(Boolean)
.map(child => this.visitNode(child))
.filter(Boolean) as ASTNode[];
}
}
// Composite pattern with functional composition
class CompositeVisitor extends BaseASTVisitor {
// Immutable visitor registry
private readonly visitors: ReadonlyMap<string, ASTVisitor>;
// Pure visitor dispatch based on node type
visitNode(node: CSTNode): ASTNode | null {
const visitor = this.visitors.get(node.type);
return visitor ? visitor.visitNode(node) : this.visitGenericNode(node);
}
// Factory pattern for immutable visitor addition
withVisitor(nodeType: string, visitor: ASTVisitor): CompositeVisitor {
const newVisitors = new Map(this.visitors);
newVisitors.set(nodeType, visitor);
return new CompositeVisitor(this.source, newVisitors, this.errorHandler);
}
}Specialized Functional Visitors
// Example of specialized expression visitor
class ExpressionVisitor extends BaseASTVisitor {
// Pure factory method for expression creation
createExpressionNode(node: CSTNode): ExpressionNode | null {
// Pattern matching using node.type as discriminant
switch (node.type) {
case 'binary_expression':
return this.createBinaryExpression(node);
case 'identifier':
return this.createIdentifierExpression(node);
// Additional cases...
default:
return null;
}
}
// Pure factory functions for specific node types
private createBinaryExpression(node: CSTNode): BinaryExpressionNode | null {
const left = this.getRequiredChildExpression(node, 'left');
const right = this.getRequiredChildExpression(node, 'right');
const operator = this.getOperator(node);
if (!left || !right || !operator) return null;
// Immutable object creation
return Object.freeze({
type: 'binary_expression',
operator,
left,
right,
location: getLocation(node)
});
}
}Extending with Custom Visitors
Extend the parser with custom visitors using functional composition:
import { BaseASTVisitor, EnhancedOpenscadParser } from '@openscad/parser';
// Custom visitor implementation
class ListComprehensionVisitor extends BaseASTVisitor {
visitNode(node: CSTNode): ASTNode | null {
if (node.type !== 'list_comprehension') return null;
// Extract components with pure helper functions
const forClause = this.extractForClause(node.childForFieldName('for_clause'));
const expr = this.visitExpression(node.childForFieldName('expression'));
const ifClause = this.extractIfClause(node.childForFieldName('if_clause'));
// Return immutable node with all required fields
return Object.freeze({
type: 'list_comprehension_expression',
iterator: forClause.iterator,
range: forClause.range,
expression: expr,
condition: ifClause,
location: getLocation(node)
});
}
// Pure extraction functions
private extractForClause(forClauseNode: CSTNode | null): ForClause {
if (!forClauseNode) {
// Handle error case functionally
this.errorHandler.handleError('Missing for clause in list comprehension');
return { iterator: 'i', range: null };
}
// Extract iterator and range expressions
const iterator = forClauseNode.childForFieldName('iterator')?.text || 'i';
const range = this.visitExpression(forClauseNode.childForFieldName('range'));
return { iterator, range };
}
}
// Using the custom visitor with immutable composition
const parser = new EnhancedOpenscadParser();
const enhanced = parser.withVisitor('list_comprehension', new ListComprehensionVisitor());๐งช Testing and Validation
Property-Based and Unit Testing
The parser follows functional testing principles with both property-based tests and unit tests to validate correctness, immutability, and edge case handling:
// Property-based testing example using fast-check
import * as fc from 'fast-check';
describe('Parser immutability properties', () => {
// Test that parsing is a pure function (idempotent)
it('should be idempotent - parsing same input multiple times yields identical results', () => {
fc.assert(
fc.property(fc.string(), (source) => {
const parser = new EnhancedOpenscadParser();
const result1 = parser.parseAST(source);
const result2 = parser.parseAST(source);
// Deep equality check should pass for pure functions
expect(deepEqual(result1, result2)).toBe(true);
})
);
});
// Test that parsing preserves input immutability
it('should not mutate input source', () => {
fc.assert(
fc.property(fc.string(), (source) => {
const originalSource = source.slice();
const parser = new EnhancedOpenscadParser();
parser.parseAST(source);
// Input should remain unchanged
expect(source).toBe(originalSource);
})
);
});
});Running Tests
# Run all parser tests with functional assertions
nx test openscad-parser
# Run tests with coverage to verify full path testing
nx test openscad-parser --coverage
# Run tests in watch mode
nx test openscad-parser --watch
# Run specific test file
nx test openscad-parser --testFile=src/lib/openscad-parser.test.ts
# Run all quality gates
nx test openscad-parser && nx typecheck openscad-parser && nx lint openscad-parser- Real-world Examples: Complex OpenSCAD files and edge cases
Current Test Status: 611/611 tests passing (100% success rate)
Writing Tests
import { OpenscadParser } from '@openscad/parser';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('OpenSCAD Parser', () => {
let parser: EnhancedOpenscadParser;
beforeEach(async () => {
parser = new EnhancedOpenscadParser();
await parser.init('./tree-sitter-openscad.wasm');
});
afterEach(() => {
parser.dispose();
});
it('should parse module definitions', () => {
const code = `
module test_module(size = 10) {
cube(size);
}
`;
const ast = parser.parseAST(code);
expect(ast).toHaveLength(1);
expect(ast[0].type).toBe('module_definition');
expect(ast[0].name).toBe('test_module');
});
it('should handle syntax errors gracefully', () => {
const code = `
module invalid_syntax(
cube(10);
}
`;
const ast = parser.parseAST(code);
const errors = parser.getErrors();
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].type).toBe('SYNTAX_ERROR');
});
});๏ฟฝ Documentation
Auto-Generated API Documentation
The parser includes comprehensive auto-generated API documentation using TypeDoc:
# Generate API documentation
nx docs:generate openscad-parser
# Output location: packages/openscad-parser/docs/typedocs/Documentation Coverage:
- 6 Core Classes: EnhancedOpenscadParser, ErrorHandler, SimpleErrorHandler, etc.
- 80+ Interfaces: All AST node types, utility interfaces, IDE support types
- 20+ Functions: Utility functions, extractors, type guards
- Type Aliases: ASTNode, StatementNode, Vector types, etc.
Features:
- Comprehensive API reference with examples
- Source code linking to GitHub
- Hierarchical organization and navigation
- Always up-to-date with code changes
Manual Documentation
- Architecture Guide:
docs/architecture.md- Functional system design and patterns - Usage Examples:
docs/examples/- Idiomatic functional code examples - API Guides:
docs/api/- Detailed usage guides for major components
๐ Functional Performance
The parser is optimized for performance while maintaining functional programming principles:
Benchmarks with Immutable Data Flow
| File Size | Parse Time | Memory Usage | AST Nodes | Immutable Nodes | Test Success |
|---|---|---|---|---|---|
| 1KB | ~2ms | ~2MB | ~50 | 100% | 100% |
| 10KB | ~15ms | ~8MB | ~500 | 100% | 100% |
| 100KB | ~150ms | ~40MB | ~5000 | 100% | 100% |
| 1MB | ~1.5s | ~200MB | ~50000 | 100% | 100% |
Functional Performance Tips
- Resource Lifecycle Management: Follow the functional initialization pattern
- Leverage Pure Function Memoization: Cache results of expensive pure operations
- Immutable Tree Transformations: Use structural sharing for efficient updates
- Proper Resource Disposal: Explicitly manage resources with
dispose() - Functional Composition: Combine operations with function composition
// Functional approach: Resource management and data transformation pipeline
const parseFiles = async (filePaths: string[]): Promise<ReadonlyArray<ASTNode[]>> => {
// Single resource allocation - functional resource management pattern
const parser = new EnhancedOpenscadParser();
await parser.init('./tree-sitter-openscad.wasm');
try {
// Functional data pipeline with pure transformations
return filePaths
.map(readFileContent) // Pure function: path -> content
.map(content => parser.parseAST(content)) // Pure function: content -> AST
.map(ast => Object.freeze(ast)); // Ensure immutability
} finally {
// Explicit resource cleanup (functional resource management)
parser.dispose();
}
};
// Pure helper function
const readFileContent = (path: string): string => fs.readFileSync(path, 'utf8');
// Usage with immutable result handling
const processASTs = async (filePaths: string[]): Promise<void> => {
const astResults = await parseFiles(filePaths);
// Process immutable ASTs with pure functions
const statistics = astResults.map(analyzeAST); // Pure: AST -> statistics
const optimized = astResults.map(optimizeAST); // Pure: AST -> optimized AST
// No mutations to astResults anywhere in the pipeline
};๐ง Functional Configuration
Immutable Parser Options
// Immutable configuration using readonly properties
interface ParserOptions {
// Feature flags as pure configuration
readonly enableExpressionEvaluation?: boolean;
// Type-safe enumeration instead of magic strings
readonly errorRecovery?: ErrorRecoveryStrategy;
// Recursion safety through explicit constraints
readonly maxDepth?: number;
// Optimization flags
readonly incrementalParsing?: boolean;
// Functional extension point with immutable visitor registry
readonly customVisitors?: ReadonlyMap<string, ASTVisitor>;
}
// Type-safe enumerated values
enum ErrorRecoveryStrategy {
Strict = 'strict', // No recovery, fail fast
Lenient = 'lenient', // Basic recovery for common errors
Aggressive = 'aggressive' // Maximum recovery attempt
}
// Factory function pattern for creating configured parsers
const createParser = (options?: ParserOptions): EnhancedOpenscadParser => {
// Create parser with deep-copied options (preserving immutability)
return new EnhancedOpenscadParser(options ? { ...options } : undefined);
};Functional Configuration Management
// Configuration with immutable updates
const baseConfig: ParserOptions = Object.freeze({
enableExpressionEvaluation: false,
errorRecovery: ErrorRecoveryStrategy.Lenient,
maxDepth: 100
});
// Pure function to derive new configurations
const withCustomVisitors = (base: ParserOptions, visitors: Map<string, ASTVisitor>): ParserOptions => {
return Object.freeze({
...base,
customVisitors: new Map(visitors) // Create copy to ensure immutability
});
};
// Usage with functional composition
const myConfig = withCustomVisitors(
baseConfig,
new Map([['list_comprehension', new ListComprehensionVisitor()]])
);
// Create parser with immutable configuration
const parser = createParser(myConfig);Environment Variables
# Enable functional debugging features
DEBUG=openscad-parser:*
# Set memory limits for WASM (memory management)
OPENSCAD_PARSER_MEMORY_LIMIT=512MB
# Enable pure functional performance profiling
OPENSCAD_PARSER_PROFILE=true
# Toggle strict immutability checks
OPENSCAD_PARSER_STRICT_IMMUTABLE=true๐ค Functional Contribution Guidelines
We welcome contributions that follow functional programming principles! Please see our Contributing Guidelines for details on:
- Pure Functions: Ensure functions have no side effects and don't mutate state
- Immutability: Use readonly types and immutable data structures
- Type Safety: Maintain strict TypeScript typing with no
anytypes - Testing Purity: Write tests that verify functional properties
- Functional Patterns: Properly implement Option/Maybe, Result/Either, and other FP patterns
Functional Development Workflow
# Clone and setup
git clone https://github.com/user/openscad-tree-sitter.git
cd openscad-tree-sitter/packages/openscad-parser
# Install dependencies
pnpm install
# Run tests with property-based testing
pnpm test
# Run immutability tests
pnpm test:immutable
# Build the package
pnpm build
# Lint for functional principles
pnpm lint
# Type checking with strict functional constraints
pnpm typecheck
# Verify pure function properties
pnpm test:functionalFunctional Programming Checklist
Before submitting a PR, ensure your code follows these principles:
- Functions are pure with explicit inputs and outputs
- Data structures are immutable (use readonly, Object.freeze)
- Side effects are isolated and explicitly managed
- Error handling uses functional Result/Either pattern
- Type definitions are strict with no
anytypes - Tests verify functional properties and immutability
๐ Functional API Reference
Detailed API documentation focusing on functional patterns is available in the docs/ directory:
- Parser API - Pure parsing functions and immutable API
- AST Types - Immutable node structures and type guards
- Visitor Pattern - Functional composition with visitor pattern
- Functional Error Handling - Either/Result patterns
- Expression Evaluation - Pure evaluation functions
- Functional Utilities - Composition helpers and functional patterns
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Related Projects
- @openscad/tree-sitter-openscad: Tree-sitter grammar for OpenSCAD
- @openscad/editor: Monaco editor integration
- @openscad/demo: Interactive demo application
Part of the OpenSCAD Tree-sitter Parser monorepo