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

semantic-actions.mddocs/

Semantic Actions

System for defining and executing semantic actions on parse results, completely separated from grammar definitions. This enables modular processing of parse trees with operations and attributes.

Imports

import { grammar } from "ohm-js";
// Semantics objects are created from grammar instances

For TypeScript:

import { 
  grammar, 
  Grammar, 
  Semantics, 
  ActionDict, 
  BaseActionDict, 
  Action,
  Node, 
  IterationNode, 
  NonterminalNode, 
  TerminalNode,
  MatchResult 
} from "ohm-js";

Capabilities

Semantics Interface

Core interface for creating and managing semantic actions on parse results.

/**
 * A Semantics is a family of operations and/or attributes for a given
 * grammar. Each operation/attribute has a unique name within the
 * Semantics. A grammar may have any number of Semantics instances
 * associated with it.
 */
interface Semantics {
  /**
   * Returns a dictionary containing operations and attributes defined by
   * this Semantics on the result of a matched grammar. Operations are
   * no-arg functions and attributes are properties.
   * @param match - MatchResult from successful grammar match
   * @returns Dictionary with operations as functions and attributes as properties
   */
  (match: MatchResult): Dict;

  /**
   * Add a new operation named name to this Semantics, using the
   * semantic actions contained in actionDict. It is an error if there
   * is already an operation or attribute called name in this semantics.
   * @param name - Name for the operation
   * @param actionDict - Dictionary of semantic actions by rule name
   * @returns This Semantics for chaining
   */
  addOperation<T>(name: string, actionDict: ActionDict<T>): Semantics;

  /**
   * Add a new attribute named name to this Semantics, using the
   * semantic actions contained in actionDict. It is an error if there
   * is already an operation or attribute called name in this semantics.
   * @param name - Name for the attribute
   * @param actionDict - Dictionary of semantic actions by rule name
   * @returns This Semantics for chaining
   */
  addAttribute<T>(name: string, actionDict: ActionDict<T>): Semantics;

  /**
   * Extend the operation named name with the semantic actions contained
   * in actionDict. name must be the name of an operation in the super
   * semantics.
   * @param name - Name of operation to extend
   * @param actionDict - Additional semantic actions
   * @returns This Semantics for chaining
   */
  extendOperation<T>(name: string, actionDict: ActionDict<T>): Semantics;

  /**
   * Extend the attribute named name with the semantic actions contained
   * in actionDict. name must be the name of an attribute in the super
   * semantics.
   * @param name - Name of attribute to extend
   * @param actionDict - Additional semantic actions
   * @returns This Semantics for chaining
   */
  extendAttribute<T>(name: string, actionDict: ActionDict<T>): Semantics;
}

Usage Examples:

import { grammar } from "ohm-js";

const g = grammar(`
  Arithmetic {
    Exp = AddExp
    AddExp = MulExp ("+" MulExp | "-" MulExp)*
    MulExp = PriExp ("*" PriExp | "/" PriExp)*
    PriExp = "(" Exp ")" | number
    number = digit+
  }
`);

// Create semantics with evaluation operation
const semantics = g.createSemantics().addOperation('eval', {
  Exp(e) {
    return e.eval();
  },
  AddExp(first, rest) {
    return rest.children.reduce((acc, child) => {
      const [op, operand] = child.children;
      return op.sourceString === '+' ? acc + operand.eval() : acc - operand.eval();
    }, first.eval());
  },
  MulExp(first, rest) {
    return rest.children.reduce((acc, child) => {
      const [op, operand] = child.children;
      return op.sourceString === '*' ? acc * operand.eval() : acc / operand.eval();
    }, first.eval());
  },
  PriExp(expr) {
    return expr.eval();
  },
  number(digits) {
    return parseInt(this.sourceString);
  }
});

// Use the semantics
const match = g.match("2 + 3 * 4");
const result = semantics(match).eval(); // Returns 14

Action Dictionary

Dictionary interface for mapping rule names to semantic actions.

/**
 * An ActionDict is a dictionary of Actions indexed by rule names.
 */
interface ActionDict<T> extends BaseActionDict<T> {
  [index: string]: Action<T> | undefined;
}

/**
 * Base ActionDict with built-in rule actions
 */
interface BaseActionDict<T> {
  /** Default action for iteration nodes */
  _iter?: (this: IterationNode, ...children: Node[]) => T;
  /** Default action for nonterminal nodes */
  _nonterminal?: (this: NonterminalNode, ...children: Node[]) => T;
  /** Default action for terminal nodes */
  _terminal?: (this: TerminalNode) => T;

  // Built-in rules
  alnum?: (this: NonterminalNode, arg0: NonterminalNode) => T;
  letter?: (this: NonterminalNode, arg0: NonterminalNode) => T;
  digit?: (this: NonterminalNode, arg0: TerminalNode) => T;
  hexDigit?: (this: NonterminalNode, arg0: NonterminalNode | TerminalNode) => T;
  ListOf?: (this: NonterminalNode, arg0: NonterminalNode) => T;
  NonemptyListOf?: (this: NonterminalNode, arg0: Node, arg1: IterationNode, arg2: IterationNode) => T;
  EmptyListOf?: (this: NonterminalNode) => T;
  listOf?: (this: NonterminalNode, arg0: NonterminalNode) => T;
  nonemptyListOf?: (this: NonterminalNode, arg0: Node, arg1: IterationNode, arg2: IterationNode) => T;
  emptyListOf?: (this: NonterminalNode) => T;
  applySyntactic?: (this: NonterminalNode, arg0: Node) => T;
}

/**
 * An Action is a function from ParseNodes, called with the children nodes
 * of the node it is being executed on.
 * The current node is passed as a dynamic this, requiring an ES5
 * anonymous function with this typed as any.
 */
type Action<T> = (this: Node, ...args: Node[]) => T;

Usage Examples:

// Actions can access the current node via 'this'
const semantics = grammar.createSemantics().addOperation('stringify', {
  _terminal() {
    return this.sourceString; // Access source text
  },
  _nonterminal(...children) {
    return children.map(child => child.stringify()).join('');
  },
  number(digits) {
    return `NUM(${this.sourceString})`;
  }
});

Parse Tree Nodes

Node interfaces representing different types of nodes in the parse tree.

/**
 * A node in the parse tree, passed to Action functions
 */
interface Node {
  /** Returns the child at index idx */
  child(idx: number): Node;
  /** true if the node is a terminal node, otherwise false */
  isTerminal(): boolean;
  /** true if the node is an iteration node (*, +, ?) */
  isIteration(): boolean;
  /** True if Node is ? option */
  isOptional(): boolean;
  /** Convert certain NonterminalNodes into IterationNodes */
  asIteration(): IterationNode;

  /** Array containing the node's children */
  children: Node[];
  /** The name of grammar rule that created the node */
  ctorName: string;
  /** Captures the portion of the input consumed by the node */
  source: Interval;
  /** Returns the contents of the input stream consumed by this node */
  sourceString: string;
  /** The number of child nodes that the node has */
  numChildren: number;

  /**
   * In addition to the properties defined above, within a given
   * semantics, every node also has a method/property corresponding to
   * each operation/attribute in the semantics.
   */
  [index: string]: any;
}

interface IterationNode extends Node {}
interface NonterminalNode extends Node {}
interface TerminalNode extends Node {}

Usage Examples:

const semantics = grammar.createSemantics().addOperation('analyze', {
  number(digits) {
    console.log('Rule name:', this.ctorName); // 'number'
    console.log('Source text:', this.sourceString); // e.g., '42'
    console.log('Child count:', this.numChildren); // number of digits
    console.log('Is terminal:', this.isTerminal()); // false
    
    // Access children
    const firstDigit = this.child(0);
    console.log('First digit:', firstDigit.sourceString);
    
    return parseInt(this.sourceString);
  },
  digit(_) {
    console.log('Is terminal:', this.isTerminal()); // true
    return this.sourceString;
  }
});

Operation vs Attribute

Operations are functions that must be called, while attributes are properties that are computed lazily:

// Operation (function)
const semantics = grammar.createSemantics()
  .addOperation('eval', {
    number(digits) { return parseInt(this.sourceString); }
  })
  .addAttribute('value', {
    number(digits) { return parseInt(this.sourceString); }
  });

const match = grammar.match("42");
const result = semantics(match);

// Operations are functions
const evaluated = result.eval(); // Call as function

// Attributes are properties
const value = result.value; // Access as property

Extending Semantics

Semantics can inherit from other semantics and extend operations:

// Base semantics
const baseSemantics = grammar.createSemantics().addOperation('eval', {
  number(digits) { return parseInt(this.sourceString); }
});

// Extended semantics
const extendedSemantics = grammar.extendSemantics(baseSemantics)
  .extendOperation('eval', {
    // Add handling for new rules or override existing ones
    hexNumber(digits) { return parseInt(this.sourceString, 16); }
  });

Error Handling

Semantic actions can handle errors and provide meaningful feedback:

const semantics = grammar.createSemantics().addOperation('eval', {
  division(left, _op, right) {
    const rightVal = right.eval();
    if (rightVal === 0) {
      throw new Error(`Division by zero at ${right.source.getLineAndColumnMessage()}`);
    }
    return left.eval() / rightVal;
  }
});

Type Definitions

interface Dict {
  [index: string]: any;
}

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