@holistic-stack/openscad-parser
v0.1.2
Published
TypeScript parser for OpenSCAD using tree-sitter
Maintainers
Readme
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
