Sonolus Wiki

02. Nodes

In this chapter, we will go over the concept of nodes in Sonolus and how it relates to Sonolus.js.

Nodes

A piece of code can be represented as an Abstract Syntax Tree (AST) which is made up of nodes.

For example, our code Multiply(Math.PI, 5, 5) can be represented as a root Multiply node with 3 children nodes Math.PI, 5, and 5.

In Sonolus, that would be represented as:

[
    {
        "func": "Multiply",
        "args": [1, 2, 3]
    },
    {
        "value": 3.141592653589793
    },
    {
        "value": 5
    },
    {
        "value": 5
    }
]

Notice that in Sonolus, ASTs are flattened - that is, rather than a nested tree structure it is only one in depth, and nodes reference each other using their indexes in this flattened array.

Here, Multiply node is a function node, it has 3 arguments: 1, 2, and 3, which are indexes that point to the (zero-based) 1st, 2nd, and 3rd node in the array, which are 3.14..., 5, and 5, which are value nodes.

Sonolus.js as Node Authoring

With the understanding of ASTs and nodes, you can guess that Multiply doesn't really do the multiplying, but rather creates an AST:

console.log(Multiply(Math.PI, 5, 5))

// Result:
// FuncNode {
//   func: 'Multiply',
//   args: [
//     ValueNode { value: 3.141592653589793 },
//     ValueNode { value: 5 },
//     ValueNode { value: 5 }
//   ]
// }
console.log(Multiply(Math.PI, 5, 5))

// Result:
// FuncNode {
//   func: 'Multiply',
//   args: [
//     ValueNode { value: 3.141592653589793 },
//     ValueNode { value: 5 },
//     ValueNode { value: 5 }
//   ]
// }

Do notice that this is still in a nested tree structure which Sonolus does not understand. In the compile step, Sonolus will flatten it:

const environment = { nodes: [] }
compile(Multiply(Math.PI, 5, 5), environment)
console.log(environment.nodes)

// Result:
// [
//   { value: 3.141592653589793 },
//   { value: 5 },
//   { func: 'Multiply', args: [ 0, 1, 1 ] }
// ]
const environment = { nodes: [] }
compile(Multiply(Math.PI, 5, 5), environment)
console.log(environment.nodes)

// Result:
// [
//   { value: 3.141592653589793 },
//   { value: 5 },
//   { func: 'Multiply', args: [ 0, 1, 1 ] }
// ]

Notice something smart that Sonolus.js does: it notices that 2nd and 3rd argument are both value node 5 and thus there is no need to duplicate, and simply point them to the same value node.

Why Use JavaScript/TypeScript?

Why not simply invent our own language like:

Multiply(3.141592653589793, 5, 5)

and then a compiler that does the same flattening?

That can certainly work, and is how Sonolus Intermediate Language (SIL) works.

However, inventing a language is hard: you need to write a parser, a compiler, need to provide IDE support, and after all it is still alienating to newcomers.

Using an existing and popular language like JavaScript/TypeScript can bypass all those issues while making using of the already established ecosystem to provide excellent developer experience, and the most important of all: meta programming.

Meta Programming

Meta programming, simply put, is to use programs to write programs.

Our first code Multiply(Math.PI, 5, 5) calculates the area of a circle with radius 5, but what if we want to extract this logic and reuse it in more places?

Sonolus does not have a concept of functions, so we must reuse it on the node level. Traditionally in other languages this would be done with a code preprocessor, which is an added layer of learning curve.

However, because we are authoring nodes in JavaScript/TypeScript, you can simply write it like so:

function calculateCircleArea(radius: Code<number>) {
    return Multiply(Math.PI, radius, radius)
}

console.log(visualize(calculateCircleArea(5)))

// Result:
// Multiply(3.141592653589793, 5, 5)
function calculateCircleArea(radius) {
    return Multiply(Math.PI, radius, radius)
}

console.log(visualize(calculateCircleArea(5)))

// Result:
// Multiply(3.141592653589793, 5, 5)

Simple and intuitive: our function takes in an AST, construct a new AST with the logic and returns it!