CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ohm-js

An object-oriented language for parsing and pattern matching based on parsing expression grammars

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

utilities-and-extras.mddocs/

Utilities and Extras

Additional utilities for error reporting, AST conversion, example extraction, and advanced tree traversal patterns available through the extras module.

Imports

import { toAST, getLineAndColumnMessage, getLineAndColumn, VisitorFamily, extractExamples, semanticsForToAST } from "ohm-js/extras";
import { grammar } from "ohm-js";

For TypeScript:

import { 
  toAST, 
  getLineAndColumnMessage, 
  getLineAndColumn, 
  VisitorFamily, 
  extractExamples, 
  semanticsForToAST,
  LineAndColumnInfo,
  Example,
  VisitorConfig
} from "ohm-js/extras";
import { grammar, Grammar, MatchResult, Semantics } from "ohm-js";

Capabilities

Error Reporting Utilities

Functions for creating detailed error messages with line and column information.

/**
 * Returns contextual information (line and column number, etc.) about a
 * specific character in a string.
 * @param str - Input string
 * @param offset - Character position
 * @returns Line and column information
 */
function getLineAndColumn(str: string, offset: number): LineAndColumnInfo;

/**
 * Returns a nicely-formatted message (appropriate for syntax errors, etc.)
 * pointing to a specific character within a string. Optionally, one or more
 * ranges within the string can also be highlighted.
 * @param str - Input string
 * @param offset - Error position
 * @param ranges - Optional ranges to highlight
 * @returns Formatted error message
 */
function getLineAndColumnMessage(
  str: string,
  offset: number,
  ...ranges: number[][]
): string;

interface LineAndColumnInfo {
  offset: number;
  lineNumber: number;
  colNum: number;
  line: string;
  prevLine: string;
  nextLine: string;
  toString(...ranges: number[][]): string;
}

Usage Examples:

import { getLineAndColumn, getLineAndColumnMessage } from "ohm-js/extras";

const source = `line 1
line 2 with error here
line 3`;

// Get position information
const info = getLineAndColumn(source, 25); // Position of 'error'
console.log(`Line ${info.lineNumber}, Column ${info.colNum}`);

// Create formatted error message
const errorMsg = getLineAndColumnMessage(source, 25);
console.log(errorMsg);
// Output shows the error position with context and a pointer

AST Conversion

Convert Ohm's Concrete Syntax Tree (CST) to Abstract Syntax Tree (AST) format.

/**
 * Returns a plain JavaScript object that includes an abstract syntax tree (AST)
 * for the given match result containing a concrete syntax tree (CST) and grammar.
 * The optional `mapping` parameter can be used to customize how the nodes of the CST
 * are mapped to the AST.
 * @param res - Successful MatchResult
 * @param mapping - Optional mapping configuration
 * @returns AST object
 * @throws Error if MatchResult failed
 */
function toAST(res: MatchResult, mapping?: {}): {};

/**
 * Returns a semantics containing the toAST(mapping) operation for the given grammar g.
 * @param g - Grammar instance
 * @returns Semantics with toAST operation
 * @throws Error if parameter is not a Grammar
 */
function semanticsForToAST(g: Grammar): Semantics;

Usage Examples:

import { toAST, semanticsForToAST } from "ohm-js/extras";
import { grammar } from "ohm-js";

const g = grammar(`
  Arithmetic {
    Exp = AddExp
    AddExp = number ("+" number)*
    number = digit+
  }
`);

const match = g.match("2 + 3 + 4");
if (match.succeeded()) {
  // Simple AST conversion
  const ast = toAST(match);
  console.log(JSON.stringify(ast, null, 2));

  // Custom mapping
  const customAst = toAST(match, {
    number: 0, // Use first child (the digits)
    AddExp: {
      left: 0,  // First number
      right: 1  // Addition operations
    }
  });

  // Alternative: create reusable semantics
  const astSemantics = semanticsForToAST(g);
  const result = astSemantics(match).toAST({});
}

Visitor Pattern for Tree Traversal

Advanced pattern for implementing operations over tree structures with automatic traversal.

/**
 * A VisitorFamily contains a set of recursive operations that are defined over some kind of
 * tree structure. The `config` parameter specifies how to walk the tree.
 */
class VisitorFamily {
  /**
   * @param config - Configuration object specifying tree traversal
   * @param config.getTag - Function to get node type from a node  
   * @param config.shapes - Object mapping node types to traversal patterns
   */
  constructor(config: VisitorConfig);

  /**
   * Wrap a tree node for use with this visitor family
   * @param thing - Tree node to wrap
   * @returns Wrapped node with visitor operations
   */
  wrap(thing: any): any;

  /**
   * Add a new operation to this visitor family
   * @param signature - Operation signature like "operationName(param1, param2)"
   * @param actions - Dictionary of actions by node type
   * @returns This VisitorFamily for chaining
   */
  addOperation(signature: string, actions: {}): VisitorFamily;

  /** Adapter class for wrapped nodes */
  Adapter: any;
  /** Dictionary of defined operations */
  operations: {};
}

interface VisitorConfig {
  getTag: (node: any) => string;
  shapes: {[nodeType: string]: string | string[] | ((node: any, fn: any) => any[])};
}

Usage Examples:

import { VisitorFamily } from "ohm-js/extras";

// Define how to traverse your tree structure
const visitors = new VisitorFamily({
  getTag(node) {
    return node.type; // How to get node type
  },
  shapes: {
    // Define traversal patterns for each node type
    'BinaryOp': ['left', 'right'], // Visit left and right properties
    'UnaryOp': 'operand',          // Visit single operand property
    'Literal': [],                 // Leaf node - no children
    'List': 'items[]'              // Visit each item in items array
  }
});

// Add operations
visitors.addOperation('evaluate(env)', {
  BinaryOp(left, right) {
    const leftVal = left.evaluate(this.args.env);
    const rightVal = right.evaluate(this.args.env);
    return this._adaptee.op === '+' ? leftVal + rightVal : leftVal - rightVal;
  },
  Literal() {
    return this._adaptee.value;
  }
});

// Use the visitor
const tree = { type: 'BinaryOp', op: '+', left: {type: 'Literal', value: 2}, right: {type: 'Literal', value: 3} };
const wrappedTree = visitors.wrap(tree);
const result = wrappedTree.evaluate({}); // Returns 5

Example Extraction

Extract examples from grammar comments for testing and documentation.

/**
 * Given a string containing one or more grammar definitions, returns an array
 * of examples extracted from the comments.
 * Positive examples look like `//+ "one", "two"` and negative examples like
 * `//- "shouldn't match"`. The examples text is a JSON string.
 * @param grammarsDef - String containing grammar definitions with example comments
 * @returns Array of extracted examples
 */
function extractExamples(grammarsDef: string): Example[];

interface Example {
  grammar: string;
  rule: string;
  example: string;
  shouldMatch: boolean;
}

Usage Examples:

import { extractExamples } from "ohm-js/extras";

const grammarSource = `
  Calculator {
    number = digit+    //+ "123", "0", "999"
                      //- "abc", "", "12.34"
    
    expr = number ("+" number)*  //+ "1+2+3", "42"
                                //- "1+", "+2"
  }
`;

const examples = extractExamples(grammarSource);
examples.forEach(ex => {
  console.log(`${ex.grammar}.${ex.rule}: "${ex.example}" should ${ex.shouldMatch ? 'match' : 'not match'}`);
});

// Use examples for testing
examples.forEach(ex => {
  const grammar = /* get grammar */;
  const match = grammar.match(ex.example, ex.rule);
  const success = match.succeeded();
  
  if (success !== ex.shouldMatch) {
    console.error(`Test failed for ${ex.rule}: "${ex.example}"`);
  }
});

Integration with Core Ohm

Using Utilities with Grammar Results

import { grammar } from "ohm-js";
import { getLineAndColumnMessage, toAST } from "ohm-js/extras";

const g = grammar(`/* your grammar */`);
const match = g.match(input);

if (match.failed()) {
  // Create detailed error message
  const errorPos = match.getInterval().startIdx;
  const errorMsg = getLineAndColumnMessage(input, errorPos);
  throw new Error(`Parse failed:\n${errorMsg}`);
} else {
  // Convert to AST for easier processing
  const ast = toAST(match);
  processAST(ast);
}

Combining with Semantic Actions

// Use utilities within semantic actions
const semantics = grammar.createSemantics().addOperation('validate', {
  rule(child) {
    try {
      return child.validate();
    } catch (error) {
      const pos = this.source.startIdx;
      const msg = getLineAndColumnMessage(this.source.sourceString, pos);
      throw new Error(`Validation error in ${this.ctorName}:\n${msg}\n${error.message}`);
    }
  }
});

Advanced AST Customization

// Complex AST mapping
const ast = toAST(match, {
  // Direct child selection
  ParenExpr: 1, // Select the middle child (skip parentheses)
  
  // Custom properties
  BinaryOp: {
    operator: 1,    // Middle child is the operator
    left: 0,        // First child
    right: 2        // Third child
  },
  
  // Computed values
  NumberLiteral: function(children) {
    return parseFloat(this.sourceString);
  },
  
  // Conditional mapping
  OptionalPart: function(children) {
    return this.numChildren > 0 ? children[0] : null;
  }
});

Error Handling

The utilities provide comprehensive error handling:

try {
  const ast = toAST(failedMatch);
} catch (error) {
  console.error("toAST() expects a successful MatchResult");
}

try {
  const semantics = semanticsForToAST("not a grammar");
} catch (error) {
  console.error("semanticsForToAST() expects a Grammar");
}

Install with Tessl CLI

npx tessl i tessl/npm-ohm-js

docs

grammar-management.md

index.md

parsing-and-matching.md

parsing-expressions.md

semantic-actions.md

utilities-and-extras.md

tile.json