solar-parser
v1.5.0
Published
Fast SLR(1) parser generator with s-expression mode (~215x faster than Jison)
Maintainers
Readme
Solar
Fast SLR(1) Parser Generator with S-Expression Mode
Solar is a standalone parser generator (like Yacc/Bison/Jison) that generates parsers ~215× faster than Jison while producing cleaner, simpler output. Instead of complex AST class hierarchies, Solar offers s-expression mode - outputting simple nested arrays that are trivial to transform and debug.
# Jison: 12,500ms to generate parser 😴
# Solar: 58ms to generate parser ⚡
bun add solar-parser # Recommended (fastest)
npm install solar-parser # Also worksOne self-contained JavaScript file. Zero dependencies. Maximum simplicity.
Why Solar?
If you've ever wished Jison was:
- ⚡ ~215× faster at generating parsers
- 🎯 Simpler - arrays instead of AST classes
- 📦 Smaller - 45% less code (1,273 LOC vs Jison's 2,285)
- 🚀 Zero dependencies - completely standalone
- 🎨 More flexible - output s-expressions OR traditional AST nodes
Then Solar is for you.
Quick Start
Installation
# With Bun (recommended - fastest):
bun add -g solar-parser
# With npm:
npm install -g solar-parser
# Now use the 'solar' command:
solar grammar.js -o parser.js
solar --helpYour First Grammar
// calculator.js
export default {
grammar: {
Expression: [
['NUMBER'],
['Expression + Expression', '["+", 1, 3]'],
['Expression * Expression', '["*", 1, 3]'],
['( Expression )', '2']
]
},
operators: [
['left', '+', '-'],
['left', '*', '/']
]
};Generate parser:
solar calculator.js -o parser.jsInput: 2 + 3 * 4
Output: ['+', '2', ['*', '3', '4']]
The S-Expression Advantage
Traditional AST (Complex)
class BinaryOp {
constructor(operator, left, right) {
this.operator = operator;
this.left = left;
this.right = right;
}
compile(options) {
// 50+ lines of compilation logic
// Complex inheritance hierarchies
// Tight coupling everywhere
}
}Problems: Hundreds of node classes, complex inheritance, hard to extend
Solar's S-Expressions (Simple)
// Grammar action:
['Expression + Expression', '["+", 1, 3]']
// Output:
['+', left, right]
// Your compiler (simple pattern matching):
switch (op) {
case '+': return `(${gen(left)} + ${gen(right)})`;
case '*': return `(${gen(left)} * ${gen(right)})`;
}Benefits:
- ✅ Simple pattern matching (switch on first element)
- ✅ Easy to inspect (
console.log()shows everything) - ✅ Easy to transform (tree transformations are trivial)
- ✅ 64% less code (proven: Rip 9,450 LOC vs CoffeeScript 17,760 LOC)
Performance
Solar generates parsers in ~58ms. Jison takes ~12,500ms.
Benchmark Results
Real-world test: Rip's CoffeeScript-compatible grammar (91 types, 406 rules, 802 lines)
| Metric | Jison | Solar (Bun) | Solar (Node) | Winner | |--------|-------|-------------|--------------|--------| | Generation time | 12,500ms | 58ms | 180ms | Solar ~215× | | Dependencies | Many | Zero | Zero | Solar | | Code size | 2,285 LOC | 1,273 LOC | 1,273 LOC | Solar 45% | | Output | AST classes | S-expressions | S-expressions | Solar (simpler) |
Performance breakdown (Solar on Bun):
processGrammar: ~3ms (5%)
buildLRAutomaton: ~40ms (69%)
processLookaheads: ~11ms (19%)
buildParseTable: ~10ms (17%)
──────────────────────────
Total: ~58msWhy speed matters: 58ms feels instant. Edit grammar → test → iterate. Jison's 12.5 seconds kills your flow.
CLI Usage
# Generate parser
solar grammar.js -o parser.js
# Show grammar information
solar --info grammar.js
# Show grammar as s-expression
solar --sexpr grammar.js
# Show version
solar --version
# Help
solar --helpGrammar Syntax
Basic Structure
// grammar.js
export default {
// S-expression mode is default (no mode field needed!)
grammar: {
RuleName: [
['pattern', 'action'],
['another pattern', 'action', { prec: 'OPERATOR' }]
]
},
operators: [
['left', '+', '-'],
['left', '*', '/']
]
};Each rule: [pattern, action?, options?]
Three Action Styles
Style 1: Pass-Through (Default)
Omit action or use 1 to return first token:
Expression: [
['Value'], // Omit action (defaults to 1)
['Operation', 1] // Explicit 1
]Generated: return $$[$0];
Style 2: Simple S-Expression (Most Common)
Bare numbers become token references:
// Pattern positions: 1 2 3 4 5
If: [
['IF Expression Block', '["if", 2, 3]'],
['IF Expression Block ELSE Block', '["if", 2, 3, 5]']
]How it works:
2→$$[$0-3](Expression)3→$$[$0-2](Block)5→$$[$0](Block after ELSE)
Output: ["if", condition, thenBlock, elseBlock?]
Use this for 90% of your rules!
Style 3: Advanced ($n References)
Use $n syntax when you need conditional logic or literal numbers:
Parenthetical: [
['( Body )', '$2.length === 1 ? $2[0] : $2']
]Key: The 1 in .length === 1 and 0 in [0] are NOT replaced because you used $n.
Spread Operator
Build arrays incrementally:
Body: [
['Line', '[1]'], // Wrap: [Line]
['Body TERMINATOR Line', '[...1, 3]'] // Spread: [...Body, Line]
]Precedence & Associativity
operators: [
['right', '=', ':'], // Lowest precedence
['left', '+', '-'],
['left', '*', '/', '%'],
['right', '**'],
['left', '&&'],
['left', '||'],
['nonassoc', '++', '--'], // Highest precedence
]Listed from lowest to highest precedence.
API Reference
Programmatic Usage
import { Generator } from 'solar-parser';
const grammar = {
grammar: {
Expression: [
['NUMBER'],
['Expression + Expression', '["+", 1, 3]']
]
},
operators: [['left', '+']]
};
// Generate parser code
const generator = new Generator(grammar);
const parserCode = generator.generate();
// Write to file
import fs from 'fs';
fs.writeFileSync('parser.js', parserCode);Using Generated Parser
import { Parser } from './parser.js';
const parser = new Parser();
parser.lexer = myLexer; // Attach your lexer
const result = parser.parse('2 + 3 * 4');
console.log(result); // ['+', '2', ['*', '3', '4']]Lexer Interface
Solar works with any lexer implementing this interface:
class MyLexer {
setInput(input, yy) {
this.input = input;
// Initialize lexer state
}
lex() {
// Return token name (string) or EOF (1)
return 'NUMBER';
}
// Required properties:
// - yytext: matched text
// - yyleng: match length
// - yylineno: line number (0-based)
// - yylloc: { first_line, last_line, first_column, last_column }
}Compatible with:
- CoffeeScript's lexer
- Jison's lexer
- Your custom lexer
S-Expression Reference
Common Node Types
// Variables & Assignment
['=', target, value]
['+=', target, value]
// Functions
['def', name, params, body]
['->', params, body] // Arrow function
['=>', params, body] // Fat arrow (bound this)
// Calls & Access
[callee, ...args] // Function call
['.', obj, 'prop'] // Property access
['[]', arr, index] // Array index
// Operators
['+', left, right] // Binary
['!', expr] // Unary
['?:', cond, then, else] // Ternary
// Control Flow
['if', condition, thenBlock, elseBlock?]
['while', condition, body]
['for-in', vars, iterable, guard?, body]
// Data Structures
['array', ...elements]
['object', ...pairs] // pairs: [key, value]
['...', expr] // Spread
// Other
['block', ...statements]
['return', expr?]Disambiguation by Arity
['...', expr] // Unary spread (1 operand)
['...', from, to] // Exclusive range (2 operands)
['..', from, to] // Inclusive range (2 operands)Your codegen checks operand count to determine meaning.
Grammar Development
Tips for Writing Grammars
- Start simple - Build incrementally
- Test often - 58ms makes testing pleasant!
- Use Style 2 actions -
'["+", 1, 3]'for most rules - Document positions - Add comments for clarity:
['IF Expression Block ELSE Block', '["if", 2, 3, 5]'] // 1 2 3 4 5 cond then else
Common Patterns
// Assignment
['Assignable = Expression', '["=", 1, 3]']
// Binary operator
['Expression + Expression', '["+", 1, 3]']
// Unary operator
['! Expression', '["!", 2]']
// Function definition
['FUNCTION Identifier ( ParamList ) Block', '["function", 2, 4, 6]']
// Unwrap parentheses
['( Expression )', '2']
// Build array from single element
['Line', '[1]']
// Build array incrementally
['Lines Line', '[...1, 2]']Debugging
# View generated code
cat parser.js | head -50
# Find specific case
grep -A 2 "case 123:" parser.js
# Check grammar structure
solar --sexpr grammar.jsJison Compatibility Mode
Solar also supports traditional Jison grammars:
const grammar = {
bnf: { // Use 'bnf' instead of 'grammar' for Jison mode
Expression: [
['NUMBER', 'return new NumberNode($1)'],
['Expression + Expression', 'return new BinaryOp("+", $1, $3)']
]
},
operators: [['left', '+']]
};Named symbol references (Jison feature):
Rule: [
['Var[name] = Expr[value]', 'return assign($name, $value)']
// Clearer than: 'return assign($1, $3)'
]Installation & Runtime
Choose Your Runtime
Solar works with Bun, Node.js, and Deno. The shebang is #!/usr/bin/env node which all three can execute.
Install with Bun (recommended):
bun add -g solar-parser
solar grammar.js # Runs on Bun (~58ms) ⚡Install with npm:
npm install -g solar-parser
solar grammar.js # Runs on Node.js (~180ms) ✅Force Bun if you have both:
# One-time:
bun $(which solar) grammar.js -o parser.js
# Or create alias:
alias solar-bun='bun $(which solar)'
solar-bun grammar.js # Always uses BunAs a Library
bun add solar-parser # For projects
npm install solar-parserimport { Generator } from 'solar-parser';Real-World Example
Complete calculator with s-expression output:
// calculator.js
export default {
grammar: {
Program: [
['Expression', '[1]']
],
Expression: [
['NUMBER'],
['Expression + Expression', '["+", 1, 3]'],
['Expression - Expression', '["-", 1, 3]'],
['Expression * Expression', '["*", 1, 3]'],
['Expression / Expression', '["/", 1, 3]'],
['( Expression )', '2'],
['- Expression', '["-", 2]', { prec: 'UMINUS' }]
]
},
operators: [
['left', '+', '-'],
['left', '*', '/'],
['right', 'UMINUS']
]
};Generate and test:
solar calculator.js -o calc-parser.js
# Show statistics
solar --info calculator.js
# Output:
# • Tokens: 8
# • Types: 3
# • Rules: 9
# • States: 17
# • Conflicts: 0Simple evaluator:
function evaluate(sexpr) {
if (typeof sexpr === 'string') return parseFloat(sexpr);
const [op, ...args] = sexpr;
const values = args.map(evaluate);
switch (op) {
case '+': return values[0] + values[1];
case '-': return values[0] - values[1];
case '*': return values[0] * values[1];
case '/': return values[0] / values[1];
}
}
evaluate(['+', '2', ['*', '3', '4']]); // → 14Notice: No AST classes, no visitor pattern, no complex traversal. Just pattern matching.
Grammar Modes
S-Expression Mode (Default)
export default {
grammar: {
Expression: [
['NUMBER'],
['Expression + Expression', '["+", 1, 3]']
]
}
};Output: Simple nested arrays
Jison Mode (Compatible)
export default {
bnf: { // 'bnf' triggers Jison mode
Expression: [
['NUMBER', 'return new NumberNode($1)'],
['Expression + Expression', 'return new BinaryOp("+", $1, $3)']
]
}
};Output: Whatever your actions return (AST nodes, objects, etc.)
Architecture
How Solar Works
Grammar Spec → processGrammar → buildLRAutomaton → processLookaheads → buildParseTable → Generate Code
↓ ↓ ↓ ↓ ↓ ↓
Parse rules Build IR Build states Compute FIRST/ Create action Output
& operators structures FOLLOW sets table parser.jsCore Algorithm (SLR(1))
Process Grammar (~3ms)
- Parse rules and operators
- Build symbol tables
- Assign precedences
Build LR Automaton (~40ms)
- Compute closures
- Build state transitions
- Group items by nextSymbol
Process Lookaheads (~11ms)
- Compute NULLABLE sets
- Compute FIRST sets
- Compute FOLLOW sets
- Assign item lookaheads
Build Parse Table (~10ms)
- Generate shift/reduce/accept actions
- Resolve conflicts with precedence
- Compute default actions
Result: Efficient SLR(1) parse table ready for code generation
Advanced Features
Token Metadata
Your lexer can attach metadata to tokens, which Solar preserves:
// String tokens
token.quote = '"'; // Preserve quote style
token.double = true;
// Number tokens
token.parsedValue = 42; // Pre-parsed value
// All tokens
token.range = [start, end]; // For source mapsThese properties are available in your grammar actions via $n.
Named Symbol References
For complex patterns:
Rule: [
['Var[name] = Expr[value]', '$name = $value']
// Instead of: '$1 = $3'
]Solar strips [name] from patterns and maps them in actions.
Error Handling
// Override parseError for custom handling
parser.yy.parseError = (str, hash) => {
console.error(`Syntax error at line ${hash.line}: ${str}`);
console.error(`Expected: ${hash.expected.join(' or ')}`);
};Debug Mode
const generator = new Generator(grammar, { debug: true });Enables trace output during parsing.
Comparison with Other Tools
vs Jison
| Feature | Jison | Solar | |---------|-------|-------| | Generation speed | 12,500ms | 58ms (~215× faster) | | Output | AST classes | S-expressions or AST | | Dependencies | Many | Zero | | Code size | 2,285 LOC | 1,273 LOC | | Self-hosting | No | Yes | | Learning curve | Steep | Gentle |
vs PEG.js
| Feature | PEG.js | Solar | |---------|--------|-------| | Algorithm | PEG | SLR(1) | | Left recursion | No (manual workaround) | Yes (native) | | Precedence | Manual rewriting | Built-in | | Output | Custom | S-expressions or AST |
vs Hand-Written
| Feature | Hand-Written | Solar | |---------|--------------|-------| | Development time | Weeks | Hours | | Maintenance | Hard | Easy (just edit grammar) | | Correctness | Error-prone | Proven algorithm | | Flexibility | Ultimate | Very high |
Design Philosophy
Simplicity Over Complexity
Three core principles:
Plain Data - S-expressions are just arrays. Easy to inspect, transform, serialize.
Separation of Concerns - Structure (grammar) separate from behavior (your codegen).
Fast Feedback - 58ms generation enables rapid experimentation.
When to Use S-Expressions
Use s-expression mode when:
- Building a compiler or transpiler
- You want clean intermediate representation
- You'll traverse the tree multiple times
- You value simplicity and debuggability
Use Jison mode when:
- Integrating with existing Jison grammars
- You need complex node behaviors
- You're already invested in AST classes
Source Code
Solar is one self-contained JavaScript file:
git clone https://github.com/shreeve/solar
cd solar/lib/
ls -lh solar.js # 47KB, 1,273 linesArchitecture:
- Classes: Token, Type, Rule, Item, State, Generator
- Core algorithms: Closure, FIRST/FOLLOW, conflict resolution
- Code generation: Creates standalone parser modules
- CLI interface: Argument parsing, file I/O
No build step required! Edit lib/solar.js directly and test immediately.
Contributing
Development Workflow
# Clone
git clone https://github.com/shreeve/solar
cd solar
# Make changes
vi lib/solar.js # Edit directly - no build!
# Test
bun lib/solar.js docs/calculator.js -o test.js
# Publish
# 1. Update version in package.json
# 2. Update VERSION in lib/solar.js (line 24)
# 3. Update @version in JSDoc (line 17)
npm publishPhilosophy
- Keep it simple
- Zero runtime dependencies
- Fast feedback (don't sacrifice generation speed)
- S-expressions first
- Pure JavaScript (ES2022)
Real-World Usage
Rip Language Compiler - Production usage of Solar
- Complexity: 91 types, 406 production rules
- Result: Complete self-hosting compiler in 9,450 LOC
- Comparison: CoffeeScript (similar language) is 17,760 LOC
- Reduction: 46% smaller codebase using s-expressions
Learn more: https://github.com/shreeve/rip-lang
FAQ
Q: Is Solar production-ready? A: Yes. Battle-tested in the Rip compiler (864/864 tests passing).
Q: Can I use my existing Jison grammar?
A: Yes! Use bnf instead of grammar in your spec.
Q: What about LR(1) or LALR(1)? A: Solar is SLR(1). Sufficient for most languages. Need stronger? Use Jison.
Q: Why not PEG? A: PEG doesn't handle left recursion naturally. SLR(1) does.
Q: Can I output JSON/XML/etc instead of s-expressions? A: Yes! Your actions can return anything. S-expressions are just recommended.
Q: How do I handle errors?
A: Override parser.yy.parseError() for custom error handling.
Q: Does it work with TypeScript? A: Solar is pure JavaScript, but you can write grammars in .ts files (they're imported dynamically).
Q: Why one file instead of modules? A: Simplicity. One file is easier to understand, debug, and distribute.
Examples
Calculator (see docs/calculator.js)
Basic arithmetic with precedence and parentheses.
Your Grammar Here!
Solar shines when building:
- Programming languages
- DSLs (Domain Specific Languages)
- Config file parsers
- Template languages
- Query languages
License
MIT
Credits
Inspired by:
- Yacc/Bison (algorithm)
- Jison (API design)
- CoffeeScript (lexer integration)
- Lisp/Scheme (s-expressions)
Built by: Steve Shreeve
Performance enabled by: Clean algorithms + modern JavaScript + simple data structures
Start simple. Build incrementally. Ship elegantly. ✨
Try Solar today and rediscover the joy of fast iteration.
