On this page ...

Building an Interpreter

This example demonstrates how to create an interpreter for your Freon-based language.

The Computer Aided Learning DSL

The language used in this extended example is the same one explained in detail in the tutorial.
In the DSL’s requirements, our hypothetical client wanted a way to test the page flow.
We’ll build an interpreter to do just that.

To follow along, run:

npm create freon@latest

Then select the language EducationInterpreter.
Build the project (npm run build), start the editor, and select the model named Model4Interpreter
it’s essentially the same as the tutorial’s Lesson6 model.

Open the model unit TestB, then run the interpreter from the Edit menu on the entire test
(by selecting the test as the current node).

The selected node is the one that is interpreted
When running the interpreter from the Edit menu, Freon evaluates the currently selected node. You’ll see different results in the Interpreter tab depending on the selection. Sometimes interpretation cannot complete because additional context is needed — it’s up to the interpreter creator to handle such cases.

Values for the Literals

As explained in Interpreter Framework,
there’s only one file we need to modify:
/src/freon/interpreter/EducationInterpreter.ts.

We’ll build the evaluation from the bottom up — starting with the literal expressions:
SimpleNumber, NumberLiteralExpression, and Fraction.

// EducationInterpreter/src/custom/interpreter/EducationInterpreter.ts#L216-L228

/////////////////// Literals

override evalSimpleNumber(node: SimpleNumber, ctx: InterpreterContext): RtObject {
    return new RtNumber(node.value)
}

override evalNumberLiteralExpression(node: NumberLiteralExpression, ctx: InterpreterContext): RtObject {
    return new RtNumber(node.value)
}

override evalFraction(node: Fraction, ctx: InterpreterContext): RtObject {
    return new RtFraction(new RtNumber(node.numerator), new RtNumber(node.denominator))
}

The first two functions return an RtNumber runtime object containing the node’s value.
The third returns an RtFraction, defined as:

// EducationInterpreter/src/custom/interpreter/runtime/RtFraction.ts

import { RtBoolean, RtNumber, RtObject } from "@freon4dsl/core";

export class RtFraction extends RtObject {
    readonly _type: string = "RtFraction";

    nominator: RtNumber;
    denumerator: RtNumber;

    constructor(num: RtNumber, den: RtNumber) {
        super();
        this.nominator = num;
        this.denumerator = den;
    }

    override equals(other: RtObject): RtBoolean {
        if (isRtFraction(other)) {
            return this.nominator.equals(other.nominator).and(this.denumerator.equals(other.denumerator));
        } else {
            return RtBoolean.FALSE;
        }
    }
    
    override toString(): string {
        return this.nominator.toString() + "/" + this.denumerator.toString()
    }
}

export function isRtFraction(object: any): object is RtFraction {
    const _type = (object as any)?._type;
    return !!_type && _type === "RtFraction";
}

For our purposes, we don’t need to simplify fractions (e.g., 6/3 → 2).
We just need to compare them for equality, which is handled by the equals method.

Values for Binary Expressions

Next, let’s define the evaluation functions for binary expressions.
They all follow a similar pattern. Here’s the OrExpression:

// EducationInterpreter/src/custom/interpreter/EducationInterpreter.ts#L244-L248

override evalOrExpression(node: OrExpression, ctx: InterpreterContext): RtObject {
    const left = main.evaluate(node.left, ctx) as RtBoolean
    const right = main.evaluate(node.right, ctx) as RtBoolean
    return left.or(right)
}

We first evaluate both sides of the expression.
Note the use of the main interpreter from MainEducationInterpreter.ts,
which can evaluate any node type.
We then use the built-in or function of the RtBoolean class, defined as:

export class RtBoolean extends RtObject {
	static readonly TRUE = new RtBoolean(true);
	static readonly FALSE = new RtBoolean(false);

	static of(bool: boolean): RtBoolean {
		return bool ? RtBoolean.TRUE : RtBoolean.FALSE;
	}

	or(other: RtBoolean): RtBoolean {
		return RtBoolean.of(this._value || other.asBoolean());
	}
    ...  
}		

Other expressions, like AndExpression and EqualsExpression, are implemented in much the same way.
For example, here’s the GreaterOrEqualsExpression:

// EducationInterpreter/src/custom/interpreter/EducationInterpreter.ts#L256-L260

override evalGreaterOrEqualsExpression(node: GreaterOrEqualsExpression, ctx: InterpreterContext): RtObject {
    const left = main.evaluate(node.left, ctx) as RtNumber
    const right = main.evaluate(node.right, ctx) as RtNumber
    return RtBoolean.of(left.value >= right.value)
}

This pattern makes your interpreter modular, easy to extend, and consistent across expression types.

© 2018 - 2025 Freon contributors - Freon is open source under the MIT License.