Visualizing the reduction

Share
đź’ˇ
This is post 09 in my blog series on MyFPL — see the index/table of contents.

The code in this and previous blog posts can also be found here, on Codeberg. Check out the repo at the commit marked “blog 09”.

In the previous blog post, we implemented a visualization of the AST: the projection in projectional editing. We also adorned that visualization with the reduction of each value in the program, by explicitly calling the reducer. That doesn’t look like proper separation of concerns, so we’ll change that in this blog post.

Instead of explicitly calling the reducer at every spot where we might need the reduction of a value, we’ll compute the reduction ahead of time. Then, we store the result on a node in the AST, so the projection can just retrieve it from the AST, when and where it needs it.

At the moment, this would make virtually no difference performance-wise: we generate the HTML for our example program just once, so we also compute the reductions just the once. However, we’re also going to use the same principle to compute types of values and check constraints on AST nodes, and store the outcomes on the nodes from which they originate. Moreover, constraint checking and reduction benefit from already knowing the types of children of the AST node we’re dealing with. Finally, once we get to make editing a MyFPL program interactive (by making the projection editable), we would definitely benefit from any optimization to computing types and reductions, and checking constraints. But for now, we’ll use the simplest approach that works.

Expanding the structure with an annotation

So, what would be a good way of storing reduced values on the AST, given that those reduced values are:

  • Completely derived from that same AST, and might change as soon as the original AST changes.
  • ASTs themselves.

One way of doing that, would be to extend the types of the nodes in the AST, so that it has a “slot” in which you can those reduced values. Because we’re using LionWeb, that would mean that we’d have to create an additional reduction containment on a suitable classifier – so, on an interface or a concept – to hold such a derived AST. In our case, the Value interface would seem to be suitable. Because the original AST has to be created before the reducer can do its job, that reduction containment would have to have optional cardinality. All in all, this would make our implementation “ugly” in the sense that it mixes concerns – rather than separating them – and forces us to deal with unset values of the reduction containment.

A better solution is: use annotations. LionWeb’s annotations are a lightweight mechanism to augment nodes in the AST with information, without having to change any classifier in MyFPL’s structure that might be a node’s type. We just have to define a meta-type for the annotation itself — let’s call it Reduction.

The Reduction annotation annotates and also contains Value

A Reduction annotation annotates a Value node, and has a result containment: the result of reducing the annotated Value node. In other words a figure:

A node of Reduction – as an annotation of a reduced Value node – holds the reduction’s result Value node

We expand MyFPL’s structure accordingly by appending the following code to the packages/build/structure.ts file:

const Reduction = factory.annotation("Reduction").annotating(Value) âť¶
factory.containment(Reduction, "result").ofType(Value) âť·

Defining the Reduction annotation (in the packages/build/structure.ts file)

  1. Create an annotation named Reduction, and declare that it annotates nodes of Value.
  2. Create a (required) containment on Reduction with the reduction’s result, again of type Value.

Running the generate NPM task of the build package produces a TypeScript class for the Reduction annotation.

Adding annotation nodes to the AST

Now that we have a Reduction class to instantiate, we can create a function that adds the reduction for each value in a Program:

export const addReductionsTo = (program: Program) => {
    for (const value of program.values) {
        const reductionAnno = Reduction.create(nanoid()) âť¶
        reductionAnno.result = reduced(value) âť·
        value.addAnnotation(reductionAnno) ❸
    }
}

Adding reductions to a Program AST (in the packages/my-fpl/reducer.ts file)

  1. Create a Reduction node. (I happen to like the postfix “Anno” as abbreviation.)
  2. Call the reducer with the current value, and set the annotation’s result containment to the reduced value.
  3. Add the reduction annotation to the value node.

Now we just have to call this function with a program — e.g., our example program. We do that by adding the following line of code to the packages/my-fpl/example.ts file, before the invocation of the writeFileSync function in that file:

addReductionsTo(myProgram)

Because we add the reductions of all (top-level) values of a Program to those Value nodes, we should call addReductionsTo only once!

Retrieving and visualizing the annotation

Next, we have to change the implementation of the Projection function, so that it doesn’t call the reduced reducer function itself on a Value AST node, but retrieves the reduced value from that node. Retrieval can be implemented as follows:

const reductionOn = (value: Value) =>
    value.annotations.find((anno) => anno instanceof Reduction)?.result

Retrieve a reduction from an AST node (in the packages/my-fpl/reducer.ts file)

This function either returns the first – and presumably only –  Reduction annotation node, or undefined. That means that only the first Reduction annotation node added to the node gets returned. Append the code from the listing above to the packages/my-fpl/reducer.ts file.

Finally, replace the following code in that same function and file:

<div className="value">
    <Projection node={reduced(value)} />
</div>

with the following code:

{reductionOn(value) && âť¶
    <div className="value">
        <Projection node={reductionOn(value)!} /> âť·
    </div>}
  1. This is a bit of “standard React idiom”. React doesn’t render undefined values (as returned from {…} expressions within JSX syntax), and also doesn’t complain — not by throwing an Error, nor by writing something to the JS console. JavaScript’s && and-operator “short-circuits” on undefined (and more generally: â€śfalsy”) values: evaluating undefined && <RHS> just yields undefined without evaluating <RHS>. On the other hand: <object> && <RHS> yields <RHS> because <object> is â€śtruthy”. So, {<LHS> && <JSX content>} renders <JSX content> when <LHS> is truthy, and nothing when <LHS> is falsy.Also make sure to import the reductionOn function from reducer.ts in projection.tsx.
  2. The return type of the reductionOn function is Value | undefined, but at this point in the code we can assume that reductionOn(value) is not undefined, because of the short-circuiting behavior of &&. So, the use of TypeScript’s non-null assertion operator ! is warranted here.
đź’ˇ
In this blog post, we computed the reductions of the values of a program, and stored that on those values as nodes of an annotation. That way, the projection can just retrieve the reduction from a node in the AST, instead of having to call the reducer itself. In the next blog, we’re going to start implementing a type system. That implementation will use the same approach: derive some information from the AST, and store it on the AST as (nodes of) annotations.

© 2026 Meinte Boersma (DSL Consultancy)