whynot.js Formal Language Matching Framework
whynot.js is a generic, VM-based framework for matching formal languages, drawing inspiration from systems like Russ Cox's regular expression engine. It operates by considering all possible branches of a program in parallel, enabling efficient implementation of various language matching tasks, including regular expressions and XML schemas. A key differentiator is its ability to record program progress through input and grammar, providing detailed feedback on *why* an input might not match a given grammar. The current stable version is 5.0.0, with releases focusing on performance, memory optimization, and module compatibility (ESM/CJS). While there isn't a strict release cadence, updates address bug fixes, dependency bumps, and significant architectural improvements.
Common errors
-
Error: Cannot find module 'whynot'
cause This typically occurs after upgrading to v4.x or v5.x when the module resolution system (e.g., Node.js or a bundler) cannot locate the new ESM or UMD bundles, especially if using CommonJS `require()` directly.fixEnsure your environment supports ESM imports (`import ... from 'whynot';`). If you must use CommonJS, ensure your bundler is configured to pick up `whynot.umd.js` or that Node.js is running in an ESM-compatible context for imported modules. -
Argument of type '(...args: any[]) => any' is not assignable to parameter of type 'any[]'.
cause This TypeScript error indicates you are passing a function to `VM.execute` where an array is expected. This was a breaking change introduced in v3.0.0.fixChange your `vm.execute()` call to pass an array of input items, e.g., `vm.execute(['item1', 'item2'])`. -
TypeError: Cannot read properties of null (reading 'forEach') or similar runtime errors when accessing trace.records.
cause Since v2.0.0, the `records` property on a `Trace` object can be `null` if no records were explicitly generated during execution, instead of an empty array.fixAlways check if `trace.records` is not `null` before attempting to iterate or access its elements, e.g., `if (trace.records) { trace.records.forEach(...) }`. -
Property 'head' does not exist on type 'Trace'.
cause The `head` property was removed from `Trace` objects in version 2.0.0.fixRemove any code that attempts to access `trace.head`. You will need to find alternative ways to determine the start of a trace if that was its purpose.
Warnings
- breaking Version 5.0.0 renames the UMD module file for older CJS environments to `whynot.umd.js`. While it primarily fixes ESM usage in Node.js, consumers relying on explicit paths for CJS bundles might need updates.
- breaking Version 4.0.0 introduced significant changes to bundle filenames, providing `whynot.umd.js` for UMD/CommonJS and `whynot.esm.js` for ES Modules. Most modern bundlers handle this automatically, but older setups or explicit path configurations may break.
- breaking In version 3.0.0, the `VM.execute` method's input argument changed from a generator-like function to a simple array. This was a breaking change aimed at significant performance and memory improvements for large inputs.
- breaking The `Trace.records` array was removed in version 3.0.0 to reduce allocations and improve performance. This property is no longer available directly on the `Trace` instance.
- breaking Version 2.0.0 removed the `head` property from returned `Trace` objects. Additionally, the allocation of the `records` array was made lazy, meaning `records` will be `null` instead of an empty array if no records are recorded for a trace.
- gotcha Since version 3.0.2, whynot bundles target ES2017 for somewhat modern browsers, reducing bundle size. If you need to support very old browser environments (pre-ES2017), you will need to transpile whynot yourself using tools like Babel.
Install
-
npm install whynot -
yarn add whynot -
pnpm add whynot
Imports
- VM
const { VM } = require('whynot');import { VM } from 'whynot'; - Program
import Program from 'whynot'; // Not a default export
import { Program } from 'whynot'; - Instruction
import * as whynot from 'whynot'; const inst = whynot.Instruction.char('a');import { Instruction } from 'whynot';
Quickstart
import { Program, VM, Instruction, Trace } from 'whynot';
// Define a simple program that matches the sequence 'abc'
const program = new Program([
Instruction.char('a'),
Instruction.char('b'),
Instruction.char('c'),
Instruction.accept() // Signal successful match
]);
// Initialize the VM with the program
const vm = new VM(program);
// Execute the VM against an input array of characters
const input1 = ['a', 'b', 'c'];
const trace1: Trace | null = vm.execute(input1);
if (trace1) {
console.log(`Input '${input1.join('')}' matched successfully.`);
// You can inspect the trace for details on the match path
// console.log('Trace records:', trace1.records);
} else {
console.log(`Input '${input1.join('')}' did NOT match.`);
}
const input2 = ['a', 'x', 'c'];
const trace2: Trace | null = vm.execute(input2);
if (trace2) {
console.log(`Input '${input2.join('')}' matched successfully.`);
} else {
console.log(`Input '${input2.join('')}' did NOT match.`);
}