Booleans
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)- Construct a concrete concept named
BooleanLiteral. This relies on importing theConceptModifierenumeration from the@lionweb/coreNPM package. - Destructure the
v2023_1constant on theLionWebVersionsobject to get access to thebuiltinsFacade. This relies on importing thatLionWebVersionsobject from the@lionweb/coreNPM package. - Destructure the
booleanDataTypeconstant frombuiltinsFacade.primitiveTypes. - Construct a property named
valueonBooleanLiteralthat 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)- Construct an enumeration (as an instance of LionWeb’s
Enumerationtype) namedBinaryOperator. - Construct enumeration literals (as instances of LionWeb’s
EnumerationLiteraltype) namedandandor. 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). - Construct a
BinaryOperationconcept, with anoperatorproperty of typeBinaryOperator, andleftandrightcontainments of typeValue: 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. - Construct a
BooleanTypeconcept, which implementsTypeinterface.
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.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)- Import the
nanoidlibrary – which is actually a function – to generate unique IDs. This relies on having executednpm add nanoidbefore. - Define a
newIdfunction that simply calls thenanoidfunction, 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 = falseConstructing a boolean value as an AST (without convenience)
- Import the
newIdfunction from thepackages/my-fpl/src/ids.tsfile. - Import the classes related to booleans and binary operations from the
packages/my-fpl/src/MyFPL.g.tsfile. - Instantiate an instance of the
BinaryOperationclass, by calling its staticcreatemethod with a randomly-generated ID. The regular constructors of these generated classes should not be used.
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”
- Create a class, so we can pass additional arguments to its constructor later on, if needed.
- Add a getter property
booleanShorthandsthat returns an object that encapsulates all shorthands that are entirely specific to booleans:booleanLiteral(<bool>),trueLiteral, andfalseLiteral. Note that these shorthands – as is thebinaryOperationshorthand – 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)
- Create an instance of the
Shorthandsclass, and destructure it to obtain thebinaryOperation, andbooleanLiteralconvenience factory methods (as functions). - Destructure the
{true|false}Literalshorthands from thebooleanShorthandsobject.
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.
Values.© 2026 Meinte Boersma (DSL Consultancy)
Thanks
Thanks to Corno Schraverus for suggesting the “shorthands” name, and allowing me to steal it!