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

Angular update row and save data to an array

When comparing the data of an edited row with the row just below it, I encounter a specific scenario. In a table containing 5 rows, as I edit records from top to bottom using the provided code snippet, I am able to store the values in an array. The most re ...

Utilizing Next.js App Router to Enable Static Site Generation for Dynamic Routes in Live Environments

For my portfolio, I am utilizing Next.js's new App Router to highlight projects with dynamic routing (src/app/projects/[id]/page.tsx). During development, useSearchParams is able to correctly retrieve the project ID. However, in production, it returns ...

Encountering type errors in NextJS with TypeScript

I am facing an issue while trying to run this function: "use server"; export const addProduct = async (formData: FormData, imageUrl: string) => { const productName = formData.get("productName")?.toString(); const description = f ...

Having trouble getting my Angular project up and running - facing issues with dependency tree resolution (ERESOLVE)

Currently, I am in the process of following an Angular tutorial and I wanted to run a project created by the instructor. To achieve this, I referred to the steps outlined in the 'how-to-use' file: How to use Begin by running "npm install" within ...

"Exploring the possibilities of integrating Typescript into Material-UI themes: A step-by

I'm experiencing some issues with Typescript pointing out missing properties in the palette section. Although adding //@ts-ignore resolves the problem temporarily, I would prefer to find a cleaner solution. As a newbie to Typescript, here is my attemp ...

Encountering errors when examining local variables during unit testing on an Angular component

If we have 2 components, namely AppComponent and TestComponent. The TestComponent is being called using its directive in the HTML template of the AppComponent. Within the TestComponent, there is an @Input() property named myTitle. Unit testing is being pe ...

What could be causing the sorting function to malfunction on certain columns?

My table's column sorting feature works well with first name and last name, but it seems to have issues with dl and dl score columns. I need assistance in fixing this problem. To access the code, click here: https://stackblitz.com/edit/angular-ivy-87 ...

Retrieve the response type from a Prisma FindUnique query

Essentially, my goal is to determine the type of the result obtained from a FindUnique operation in Prisma. The current return type is any: import prisma from "@/libs/prismaDb"; import { Prisma } from "@prisma/client"; export default a ...

Retrieve the output of a function in TypeScript

I'm encountering a challenge with returning a string instead of a function in an object value. Currently, an arrow function is returning an array of objects, and one of them needs to conditionally change a value based on the input value. Here is the ...

Leveraging Window Object in Custom Hooks with NextJS

ReferenceError: window is not defined This issue arises on the server side when NextJS attempts to render the page. However, it is possible to utilize window within the useEffect hook by following the guidance provided here. I am seeking advice on creati ...

Enhancing Skylinkjs functionality using Typescript

Hello fellow developers, I am new to using typescript and currently experimenting with incorporating SkylinkJS into my project. Can anyone guide me on the best practices for utilizing an npm library with TypeScript? If you are interested in checking out t ...

Receiving the error "Potential null object. TS2531" while working with a form input field

I am currently working on developing a straightforward form to collect email and password details from new users signing up on Firebase. I am utilizing React with Typescript, and encountering an error labeled "Object is possibly 'null'. TS2531" s ...

Steps for integrating a valid SSL certificate into a Reactjs application

After completing my ReactJS app for my website, I am now ready to launch it in production mode. The only hurdle I face is getting it to work under https mode. This app was developed using create-react-app in a local environment and has since been deployed ...

Check the type of the indexed value

I need help with a standard interface: interface IProps<T> { item: T; key: keyof T; } Is there a way to guarantee that item[key] is either a string or number so it can be used as an index for Record<string | number, string>? My codeba ...

What is the procedure to prevent Angular CLI from including a specific typings file in my project configuration?

I've integrated JointJs into my Angular CLI project, but I'm encountering typing errors during the build process: The error messages point to the file node_modules/jointjs/types/joinjs.d.ts, which is not the correct file needed. The correct one ...

How can 'this' be converted from D3 JavaScript to TypeScript?

In JavaScript, 'this' has a different meaning compared to TypeScript, as explained in this informative article 'this' in TypeScript. The JavaScript code below is used to create a thicker stroke on the selected node and give smaller stro ...

I want to know the most effective way to showcase particular information on a separate page using Angular

Recently, I've been working with a mock json file that contains a list of products to be displayed on a Product page. My goal is to select a specific product, such as 'Product 1', and have only that product's information displayed on th ...

Promise rejection not handled: The play() function was unsuccessful as it requires the user to interact with the document beforehand

After upgrading my application from Angular 10 to 11, I encountered an error while running unit tests. The error causes the tests to terminate, but strangely, sometimes they run without any issues. Does anyone have suggestions on how to resolve this issue? ...

Navigating through nested objects in a combined type

Is there a way to create a function that can take an object (which is part of a union) with a specified path and return the value of the config object for that specific path? I've been attempting the following: type Cat = { config: { meow: stri ...

Expand the size of the imported gltf model within Three.js

After successfully loading a 3d model with a gltf extension using the GLTFLoader in Three.js, I encountered a new challenge. I needed to adjust the dimensions of the model dynamically when the window is resized, based on the values of window.innerWidth and ...