Parsimony
Parsimony is Cruft's JavaScript parser, the front of the engine that turns source text into a syntax tree or a ParseError. It covers modern JavaScript, threads parse-goal and strict-mode context, raises early errors, and caps nesting depth and input size so hostile source cannot crash the runtime.
Parsimony (rusty-js-parser) is Cruft's JavaScript parser. It is the front of the engine: source text comes in, and a syntax tree (rusty_js_ast::Module) or a ParseError comes out. Everything the runtime executes passes through it first, including every dependency in node_modules, most of it code the developer has never read. It replaces the parser V8 uses internally and the standalone parsers other toolchains reach for (swc, oxc, tree-sitter, Babel).
Alpha.rusty-js-parser(Cruft 0.0.10) stands in forswc/oxc/tree-sitter. Its grammar coverage and interop with the wider ecosystem are still narrower than those mature parsers. Trust it for safety on untrusted input before you trust it for full parity with V8.
Only the parse step
Many JavaScript "parsers" are really toolchains: swc transpiles, oxc lints, tree-sitter drives editors. Parsimony is only the parse step. It is a recursive-descent parser with its own lexer and separate expression and statement grammars, exposed through a small set of entry points (parse_module, parse_script, and their goal- and strict-mode variants). It does not transform, emit code, lint, or type-check. TypeScript type annotations are erased earlier in the loader, not here. Bytes in, a tree out, and nothing else.
Grammar coverage
The grammar covers modern JavaScript: arrow functions, object and array literals with cover-grammar disambiguation, classes and private names, regular-expression literals, template strings, destructuring, and the identifier rules from the current Unicode tables. It parses the language the vast majority of packages are written in.
What it does not yet have is the coverage that comes from years of exposure to every corner of npm. swc, oxc, and V8's own parser have that history; this one does not. For the common language it is solid, and the remaining gap is in the unusual and the adversarial, which is where a parser earns full parity.
Beyond building the tree
Three responsibilities matter as much as the tree itself:
- Goal threading. The same token sequence means different things depending on the parse goal:
ScriptversusModule, sloppy versus strict, inside a generator versus not, whereyieldis an identifier in one context and a keyword in another. Parsimony threads the goal context through the whole parse rather than patching it up afterward. Getting this loose is where a large family of conformance bugs would live, so it is settled as the parse proceeds. - Early errors. ECMA-262 defines a class of errors that must be raised before any code runs: a duplicate
letbinding,delete xin strict mode, abreakwithout a target, a redeclaration across a function or lexical boundary. Parsimony owns these, and they carry real meaning. An early error means the whole containing script or module never starts, which is observably different from a runtime throw partway through. - Unicode identity. Identifier validity (
ID_StartandID_Continue) comes from the Unicode tables, and private-name validity has its own check. Lexing is table-driven so it stays faithful to the current Unicode version.
Parsimony is not only a startup phase. eval, new Function, and dynamic import() re-enter the full parse-and-compile pipeline from running code, with the goal context set appropriately: a new Function body is its own parse goal, and direct and indirect eval differ in the scope they can see.
Parsing untrusted input safely
Because Parsimony reads every dependency's source, a hostile or malformed package is untrusted input, and the failure that matters is not a wrong tree but a crash on input shaped to break the parser.
Recursive-descent parsers have a specific version of that risk: each level of nesting, ((((...)))) or [[[[...]]]] or nested blocks, costs a native stack frame, and deep enough nesting overflows the stack. In Rust a stack overflow is an uncatchable abort, so left unbounded it would be the highest-impact denial of service in the runtime, since all code flows through here.
Parsimony bounds it. A scan over the raw bytes measures bracket depth and rejects anything past a nesting limit of 256 before the recursive parser is even constructed, and the recursion carries the same bound as a second guard. Deep nesting therefore surfaces as an ordinary, catchable SyntaxError rather than a crash:
eval("((((1))))"); // 1
eval("(".repeat(5000) + "1" + ")".repeat(5000)); // SyntaxError: parser nesting depth exceeded
Two more bounds sit alongside it. Every entry point rejects a source larger than 32 MiB before any parse work begins. And the regular-expression lexer stays on UTF-8 boundaries on adversarial input, returning a lexer error rather than risking a panic. The crate has no unsafe, and malformed input produces a ParseError, never a panic.
Limitations
- Coverage is narrower than the mature parsers.
swc,oxc, and V8 carry orders of magnitude more real-world exposure, and for full parity on the long tail of npm and on unusual grammar they remain ahead. That gap is the reason for the alpha banner. - The nesting limit is conservative. Mainstream engines allow nesting somewhere in the thousands. Cruft's limit of 256 is safer against stack overflow but will reject some legitimately deep machine-generated code that V8 accepts. This is a deliberate trade of permissiveness for denial-of-service safety, and a real behavioral difference to know about.
- It parses; it does not transform. No transpilation, minification, source maps, or linting. Those belong to other tools; this crate produces a tree and stops.