Booleans

Share
đź’ˇ
This is post 06 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 06”.

In the previous blog post, we constructed a LionWeb language with the base interfaces for MyFPL: Value, Literal, Operation, and Type. But we didn’t add any concrete – i.e.: instantiable – subtypes of these interfaces yet. In this blog post, we’ll change that, and add booleans, meaning we’ll be adding the boolean type, boolean literals, and some boolean operations. I chose to start with the implementation of booleans (rather than integers), because we’d like to get to do some collection operations as soon as possible, and we need booleans for that.

Booleans are a bit weird, because there are only two booleans: true and false. Nevertheless, the implementation of booleans that we’ll create here forms the mold for implementing other types.

Expanding MyFPL’s structure

First of all, let’s add a BooleanLiteral concept that implements the Literal interface, by appending the following code to the packages/build/structure.ts file:

const BooleanLiteral = factory.concept("BooleanLiteral", ConceptModifier.concrete).implementing(Literal) (1)
const { builtinsFacade } = LionWebVersions.v2023_1 (2)
const { booleanDataType } = builtinsFacade.primitiveTypes (3)
factory.property(BooleanLiteral, "value").ofType(booleanDataType) (4)
  1. Construct a concrete concept named BooleanLiteral. This relies on importing the ConceptModifier enumeration from the @lionweb/core NPM package.
  2. Destructure the v2023_1 constant on the LionWebVersions object to get access to the builtinsFacade. This relies on importing that LionWebVersions object from the @lionweb/core NPM package.
  3. Destructure the booleanDataType constant from builtinsFacade.primitiveTypes.
  4. Construct a property named value on BooleanLiteral that holds a value of type boolean.

Design choice

It might seem weird that we’re constructing a whole concept for the literal values of a type of which there are only two. One reason for this is that the boolean type is exceptional in having only two literal values: all other types will have many more values.

The alternative is that we create two concepts: TrueBooleanLiteral, and FalseBooleanLiteral â€” corresponding in the obvious way to true and false. We probably also want to add a BooleanLiteral interface, and have the literal concepts implement that.

The upside of this approach would be that these concepts don’t need a value property, which saves some bytes. But you would still need an instance of either of these concepts for every literal appearing in program code. So saving a couple of bytes for a boolean field on a(n instance of a) class doesn’t amount to much.

Also, to process a boolean literal named – say – value, we’d need to do a type comparison: value instanceof TrueBooleanLiteral for true, and value instanceof FalseBooleanLiteral for false. We really have to check both cases everywhere we might be processing a boolean, because we can’t say value instanceof BooleanLiteral in TypeScript — assuming BooleanLiteral is an interface. When we have only BooleanLiteral we can first check value instanceof BooleanLiteral and then inspect its value: value.value â€” much more elegant.


Now that we have boolean literal values, we can create some boolean operations. The obvious type/class of boolean operations is that of the binary operation â€“ e.g. boolean and and or â€“ and boolean negation. Let’s create a BinaryOperation concept:

const BinaryOperator = factory.enumeration("BinaryOperator") (1)
;["and", "or"].forEach((op) => factory.enumerationLiteral(BinaryOperator, op)) (2)

const BinaryOperation = factory.concept("BinaryOperation", ConceptModifier.concrete).implementing(Operation) (3)
factory.property(BinaryOperation, "operator").ofType(BinaryOperator)
factory.containment(BinaryOperation, "left").ofType(Value)
factory.containment(BinaryOperation, "right").ofType(Value)

factory.concept("BooleanType", ConceptModifier.concrete).implementing(Type) (4)
  1. Construct an enumeration (as an instance of LionWeb’s Enumeration type) named BinaryOperator.
  2. Construct enumeration literals (as instances of LionWeb’s EnumerationLiteral type) named and and or. We’ll later extend this list with other binary operators. Note that semicolons are generally unneeded in TypeScript code, thanks to Automatic Semicolon Insertion (ASI). However, in some situations the TypeScript parser needs a little help, such as when starting a statement with an array literal. In cases like that, it’s somewhat customary to add one semicolon at the beginning of the line (rather than at the end of the previous line).
  3. Construct a BinaryOperation concept, with an operator property of type BinaryOperator, and left and right containments of type Value: these are the binary operation’s operands. A containment is a parent-child relation between the parent type – here: BinaryOperation â€“ and the child type — here: Value.
  4. Construct a BooleanType concept, which implements Type interface.
đź’ˇ
Design choice

I don’t want to create a separate concept for boolean binary operations — rather, I want one concept for any binary operation. This is because I like to keep the number of concepts relatively low, and not have a very deep hierarchy. If we made BinaryOperation an interface, we’d necessarily get an explosion of concrete subconcepts: essentially one for each possible result type of binary operations, so BooleanBinaryOperation, IntegerBinaryOperation, etc. That doesn’t seem very DRY.
The boolean base entities, including base interfaces for clarity

Running the generate NPM task of the build package produces TypeScript classes for BooleanType, BooleanLiteral, and BinaryOperation, and a BinaryOperator enumeration. To instantiate these classes, we should use the create methods they all expose. These create methods take a unique identifier (UUID) as their first argument. To randomly generate such (UU)IDs we use the nanoid library, but nicely encapsulated as follows, in a new file packages/my-fpl/src/ids.ts:

import { nanoid } from "nanoid" (1)

export const newId = () => nanoid() (2)
  1. Import the nanoid library – which is actually a function – to generate unique IDs. This relies on having executed npm add nanoid before.
  2. Define a newId function that simply calls the nanoid function, which generates a random ID that’s practically guaranteed to be unique.

This encapsulation might look a bit…trite, but along the course of this blog series it’ll turn out to be somewhat handy. Instead of needing to patch existing code, we’ll just work off our crystal ball and head these changes off at the pass.

Now, we can instantiate these classes as follows, to construct an AST:

import { newId } from "./ids.js" (1)
import { BinaryOperation, BinaryOperator, BooleanLiteral } from "./MyFPL.g.js" (2)

const value = BinaryOperation.create(newId()) (3)
value.operator = BinaryOperator.and
const leftExpr = BooleanLiteral.create(newId())
leftExpr.value = true
const rightExpr = BooleanLiteral.create(newId())
rightExpr.value = false

Constructing a boolean value as an AST (without convenience)

  1. Import the newId function from the packages/my-fpl/src/ids.ts file.
  2. Import the classes related to booleans and binary operations from the packages/my-fpl/src/MyFPL.g.ts file.
  3. Instantiate an instance of the BinaryOperation class, by calling its static create method with a randomly-generated ID. The regular constructors of these generated classes should not be used.
đź’ˇ
In LionWeb, any node can only appear once in any AST. That means we can’t re-use existing nodes, and have to instantiate e.g. a BooleanLiteral every time a boolean literal is needed.

This seems tedious, but is central to LionWeb’s functioning.

The code in the listing above constructs an AST that’s equivalent to the expression true && false in e.g. JavaScript. As such, it seems a bit cumbersome. If only we already had a proper syntax, and an editor for that. We’re going to concern ourselves with (editable) syntax in the next blog post. But even with a proper syntax, we’d like to construct ASTs in a “shorthand” way, e.g. for writing unit tests.

To that end, we’ll make a small class with “shorthand” convenience factory methods, in a file shorthands.ts:

import { newId } from "./ids.js"
import { BinaryOperation, BinaryOperator, BooleanLiteral, Value } from "./MyFPL.g.js"

export class Shorthands { (1)

    get booleanShorthands() { (2)
        const booleanLiteral = (value: boolean) => {
            const node = BooleanLiteral.create(newId())
            node.value = value
            return node
        }
        return {
            booleanLiteral: booleanLiteral,
            trueLiteral: () => booleanLiteral(true),
            falseLiteral: () => booleanLiteral(false)
        }
    }

    binaryOperation(operator: BinaryOperator, left: Value, right: Value) {
        const node = BinaryOperation.create(newId())
        node.operator = operator
        node.left = left
        node.right = right
        return node
    }

}

A class whose instances expose convenience factory methods: “shorthands”

  1. Create a class, so we can pass additional arguments to its constructor later on, if needed.
  2. Add a getter property booleanShorthands that returns an object that encapsulates all shorthands that are entirely specific to booleans: booleanLiteral(<bool>), trueLiteral, and falseLiteral. Note that these shorthands – as is the binaryOperation shorthand – are functions.

With the code in the listing directly above, we can re-phrase the code in the earlier listing as follows:

import { BinaryOperator } from "../MyFPL.g.js"
import { Shorthands } from "../shorthands.js"

const {binaryOperation, booleanShorthands} = new Shorthands() (1)
const {trueLiteral, falseLiteral} = booleanShorthands (2)

export const expr = binaryOperation(BinaryOperator.and,
    /* left operand : */ trueLiteral(),
    /* right operand: */ falseLiteral()
)

Constructing a boolean value as an AST (with convenience)

  1. Create an instance of the Shorthands class, and destructure it to obtain the binaryOperation, and booleanLiteral convenience factory methods (as functions).
  2. Destructure the {true|false}Literal shorthands from the booleanShorthands object.

An MyFPL program can currently consist of only one Value, and the only instances of Value we can construct are BooleanType (which we’ll use only later), BooleanLiteral, and BinaryOperation.

đź’ˇ
In this blog post, we’ve implemented the notion of booleans. In the next blog post, we’re going to implement an interpreter for Values.

© 2026 Meinte Boersma (DSL Consultancy)


Thanks

Thanks to Corno Schraverus for suggesting the “shorthands” name, and allowing me to steal it!