Traversing Abstract Syntax Trees Recursively using TypeScript

Currently in the process of developing a parser that generates an AST and then traversing it through different passes. The simplified AST structure is as follows:

type LiteralExpr = {
  readonly kind: 'literal',
  readonly value: number,
};
type UnaryExpr = {
  readonly kind: 'unary',
  readonly operator: '!' | '-',
  readonly operand: Expr,
};
type BinaryExpr = {
  readonly kind: 'binary',
  readonly left: Expr,
  readonly operator: '+' | '-' | '*' | '/',
  readonly right: Expr,
};
/** Expression enclosed in parentheses */
type GroupingExpr = {
  readonly kind: 'grouping',
  readonly subExpr: Expr,
};
type Expr = LiteralExpr | UnaryExpr | BinaryExpr | GroupingExpr;

Each pass makes slight modifications to the AST, creating a new version. For example, there's a pass that removes grouping nodes:

class ParensRemover {
  doPass(expr: Expr): Expr {
    switch (expr.kind) {
      case 'literal': return expr;
      case 'unary': return { ...expr, operand: this.doPass(expr.operand) };
      case 'binary': return { ...expr, left: this.doPass(expr.left), right: this.doPass(expr.right) };
      case 'grouping': return this.doPass(expr.subExpr);
    }
  }
}

However, the code becomes repetitive especially with numerous nodes, prompting a refactoring to a base recursive class utilizing the visitor pattern:

abstract class ASTVisitor {
  doPass(expr: Expr): Expr {
    switch (expr.kind) {
      case 'literal': return this.visitLiteral(expr);
      case 'unary': return this.visitUnary(expr);
      case 'binary': return this.visitBinary(expr);
      case 'grouping': return this.visitGrouping(expr);
    }
  }

  protected visitLiteral(expr: LiteralExpr): Expr {
    return expr;
  }
  protected visitUnary(expr: UnaryExpr): Expr {
    return { ...expr, operand: this.doPass(expr.operand) };
  }
  protected visitBinary(expr: BinaryExpr): Expr {
    return { ...expr, left: this.doPass(expr.left), right: this.doPass(expr.right) };
  }
  protected visitGrouping(expr: GroupingExpr): Expr {
    return { ...expr, subExpr: this.doPass(expr.subExpr) };
  }
}

class ParensRemover extends ASTVisitor {
  protected visitGrouping(expr: GroupingExpr): Expr {
    return this.doPass(expr.subExpr);
  }
}

The challenge arises when subsequent passes after ParensRemover need to handle the no longer existing node kind grouping. This can lead to inefficiencies given the multiple node types undergoing changes throughout various passes, necessitating adjustments to the AST Expr type:

type LiteralExpr = {
  readonly kind: 'literal',
  readonly value: number,
};
type UnaryExpr<Addition> = {
  readonly kind: 'unary',
  readonly operator: '!' | '-',
  readonly operand: ExprBase<Addition>,
};
type BinaryExpr<Addition> = {
  readonly kind: 'binary',
  readonly left: ExprBase<Addition>,
  readonly operator: '+' | '-' | '*' | '/',
  readonly right: ExprBase<Addition>,
};
/** Parenthesized expression */
type GroupingExpr = {
  readonly kind: 'grouping',
  readonly subExpr: BeforeRemoveParensExpr,
};
type ExprBase<Addition> = LiteralExpr | UnaryExpr | BinaryExpr | Addition;
type BeforeRemoveParensExpr = ExprBase<GroupingExpr>;
type AfterRemoveParensExpr = ExprBase<never>;

This leads to the question of how the ASTVisitor will determine the correct type. A possible solution attempted involves:

type AllExprs = BeforeRemoveParensExpr | AfterRemoveParensExpr;

type PickExpr<E extends AllExprs, K extends E['kind']> = /* details not important, this type pulls a specific kind out of Expr */;

abstract class ASTVisitor<InputExpr extends AllExprs, OutputExpr extends AllExprs> {
  doPass(expr: InputExpr): OutputExpr {
    switch (expr.kind) {
      case 'literal': return this.visitLiteral(expr as any);
      case 'unary': return this.visitUnary(expr as any);
      case 'binary': return this.visitBinary(expr as any);
      case 'grouping': return this.visitGrouping(expr as any);
    }
  }

  protected visitLiteral(expr: PickExpr<InputExpr, 'literal'>) {
    return expr as unknown OutputExpr;
  }
  protected visitUnary(expr: PickExpr<InputExpr, 'unary'>) {
    return { ...expr, operand: this.doPass(expr.operand) } as unknown as OutputExpr;
  }
  protected visitBinary(expr: PickExpr<InputExpr, 'binary'>) {
    return { ...expr, left: this.doPass(expr.left), right: this.doPass(expr.right) } as unknown as OutputExpr;
  }
  protected visitGrouping(expr: PickExpr<InputExpr, 'grouping'>) {
    return { ...expr, subExpr: this.doPass(expr.subExpr) } as unknown as OutputExpr;
  }
}

class ParensRemover extends ASTVisitor<BeforeRemoveParensExpr, AfterRemoveParensExpr> {
  protected visitGrouping(expr: GroupingExpr): AfterRemoveParensExpr {
    return this.doPass(expr.subExpr);
  }
}

Despite this attempt, dissatisfaction remains over losing type safety and requiring multiple casts to any within the ASTVisitor. Overlooking a required override of visitX() for a certain X that should change between passes could result in program errors without compiler warnings.

The ultimate goal is to maintain TypeScript's safety while achieving the desired functionality. Modifications to the AST representation are feasible if necessary. Your insights and suggestions are greatly appreciated. Apologies for the lengthy post. Thank you!

Answer №1

If you're in search of the

Exclude<Type, ExcludedUnion>
utility type, you've come to the right place.
Essentially, this type is quite straightforward:

type Dogs = GoldenRetriever | Poodle | Dalmatian;
type CatsOnly = Exclude<Dogs, GoldenRetriever>; // Contains Poodle and Dalmation

Although it may require some restructuring of your code to accommodate various outputs and inputs in an organized manner, you can then define your functions like so:

function visitPet(pet: Pet): Exclude<Pet, Dog> { ... }

function handlePet(pet: Pet) {
  switch (pet.type) {
    case 'dog': return visitPet(pet);
    // ...
  }
}

In such instances, Typescript has the ability to fill in the missing pieces automatically.

Answer №2

I have discovered a solution:

Initially, I modified the passes so that no pass is adding properties to an existing node, only adding nodes.

Next, I updated the ASTVisitor class to accommodate the additional nodes:

abstract class ASTVisitor<InputExprAddition, OutputExprAddition> {
}

Each node now has its own method, including a separate method for ExprAddition:

doPass(expr: ExprBase<InputExprAddition>): ExprBase<OutputExprAddition> {
  switch (expr.kind) {
    case 'literal': return this.visitLiteral(expr);
    // ...
    default: return this.visitAddition(expr);
  }
}
protected visitLiteral(literal: LiteralExpr<InputExprAddition>): ExprBase<OutputExprAddition> {
  return literal;
}
// ...
protected abstract visitAddition(expr: InputExprAddition): ExprBase<OutputExprAddition>;;

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having trouble with the clip-path in d3.js liquid fill gauge

Attempting to integrate the d3.js liquid fill gauge into my angular2 webapp has been a challenge. The clippath functionality seems to be malfunctioning, resulting in no wave being generated at all. https://i.stack.imgur.com/3Bmga.png instead of https://i. ...

Scrolling to the bottom of an ion-content in Ionic 4

I am currently developing a chat page with Ionic 4 and I'm attempting to implement an automatic scroll feature to the bottom of the page. However, the method I tried using doesn't seem to be working as expected: import { IonContent } from "@ioni ...

Typescript: Verifying the type of an interface

In my code, I have a function called getUniqueId that can handle two different types of interfaces: ReadOnlyInfo and EditInfo. Depending on the type passed to this function, it will return a uniqueId from either interface: interface ReadOnlyInfo { item ...

Error occurred when attempting to link user services with report controller

Greetings, I am currently working on a Nest Js mini project that consists of two separate modules: reports and users. Each module has its own Service, controller, and repositories. The reports module has a many-to-one relation with the users module. My g ...

Is using instanceof the most effective method to verify type in Angular?

When working with the model, we import Type1 and Type2. Using the TypeComp variable which is a union of Type1 and Type2. import { Type1, Type2 } from '.'; export type TypeComp = Type1 | Type2; In the some.component.ts file, the selectedData$ obs ...

The invocation of `prisma.profile.findUnique()` is invalid due to inconsistent column data. An invalid character 'u' was found at index 0, resulting in a malformed ObjectID

The project I'm working on is built using Next.js with Prisma and MongoDB integration. Below is the content of my Prisma schema file: generator client { provider = "prisma-client-js" } datasource db { provider = "mongodb" url = env("DATABA ...

What is the best method for typing a component prop that is compatible with singular use and can also function within loops without disrupting intellisense?

The Scenario Within a heading component, I have defined various types as shown below: // Heading/index.d.ts import { HTMLAttributes } from 'react'; export const HeadingType: { product: 'product'; marketing: 'marketing'; ...

An easy way to insert a horizontal line between your text

Currently, I have two text responses from my backend and I'm considering how to format them as shown in the design below. Is it possible to automatically add a horizontal line to separate the texts if there are two or more broadcasts instead of displa ...

Issues arise when trying to type ChangeEvent in React using Typescript

After spending some time learning React with TypeScript, I encountered a problem. The prop onChangeHandler in my code takes a function to modify properties in formik values. <Formik<FormModel> initialValues={{ favorite: ...

Is it necessary for me to set up @types/node? It appears that VSCode comes with it pre-installed

Many individuals have been adding @types/node to their development dependencies. Yet, if you were to open a blank folder in VSCode and create an empty JavaScript file, then input: const fs = require('fs'); // <= hover it and the type display ...

Eliminate the unnecessary code repetition in my functions using Typescript

I have 2 specific functions that manipulate arrays within an object. Instead of repeating the same code for each array, I am looking for a way to create reusable functions. Currently, my functions look like this: setLists(): void { if (this.product.ord ...

Using TypeScript will result in errors when attempting to use the Promise object and the Awaited keyword

In this example, I am trying to ensure that the function foo does not accept a Promise as an argument, but any other type should be acceptable. export {} function foo<T>(arg: T extends Promise<unknown> ? never : T) { console.log(arg); } asy ...

Guide to successfully submitting an Angular form that includes a nested component

I have developed a custom dateTime component for my application. I am currently facing an issue where I need to integrate this component within a formGroup in a separate component. Despite several attempts, I am unable to display the data from the child fo ...

There is an issue with the Next.js middleware: [Error: The edge runtime is not compatible with the Node.js 'crypto' module]

Struggling with a problem in next.js and typescript for the past 4 days. If anyone can provide some insight or help with a solution, it would be greatly appreciated. Thank you! -- This is my middleware.ts import jwt from "jsonwebtoken"; import { ...

I am facing the dilemma of having an identical button appearing in two separate locations. How can I determine which button has been clicked?

I am currently using ng2-smart-table and have implemented a custom filter with the same button in both filters. However, I am unsure of how to determine which button is being clicked. https://i.stack.imgur.com/b1Uca.png Below is the component code for th ...

What is the best way to handle alias components in next.js when using ts-jest?

When working with TypeScript, Next.js, and Jest, I wanted to simplify my imports by using aliases in my tsconfig file instead of long relative paths like "../../..". It worked fine until I introduced Jest, which caused configuration issues. This is a snip ...

A guide to accessing parent attributes in Vue3 using typescript

Within my child component, I am facing an issue where I need to access the parent object but the commented lines are not functioning as expected. The structure of AccordionState is defined below: export type AccordionKeys = | "open" | "disa ...

User interface designed for objects containing multiple keys of the same data type along with a distinct key

I have a question that relates to this topic: TypeScript: How to create an interface for an object with many keys of the same type and values of the same type?. My goal is to define an interface for an object that can have multiple optional keys, all of t ...

What is the best way to pass props to a styled component (e.g., Button) in Material-UI

One of my tasks involves creating a customized button by utilizing the Button component with styled components. export const CustomButton = styled(Button)({ borderRadius: "17px", fontWeight: 300, fontSize: ".8125rem", height: &q ...

Issue with Dates in Typescript array elements

When attempting to compare different Date elements in my code, I encountered an issue. I have two date elements representing date formats but am unable to compare them because I keep receiving the error message "core.js:6237 ERROR TypeError: newticketList. ...