Will's Digital Garden

Triton → Neso

While doodling on Triton and refining my syntax, I realized a mistake I'd made that permeates through the entire codebase: AST nodes and evaluated values are separate things. For such a lazily-evaluated language as I want this to be, I should instead have only AST nodes that can be reduced.

For instance, the AST that results from expression a can be reduced to expression b. On the other hand, expression c cannot further be reduced (because x is unavailable). These operations can reduce as much as possible, but will be constrained by undefined variables (d cannot be reduced, while e can be to f).

a: 1 + 2
b: 3
c: 4 + x

d: x * 3 + 4
e: 5 * 3 + x
f: 15 + x

So I've re-written from scratch again because sometimes it's easier than a major refactor when it's so early in a project's life. This time, I'm calling it Neso which is another moon of Neptune. I've re-used most of the concepts from Triton though.

I'm sticking to a single base data type for now: Rational numbers lacking units of measure. Strings, Booleans, or other types can come later once the rest of the language is more refined. Additionally, there are three kinds of containers: Singles, Lists, and Dictionaries.

Both Lists and Dictionaries support element separation via any combination of commas, semicolons, and newlines.


Any given file is itself a special-case anonymous dictionary. An import expression takes a filename and resolves to a dictionary (with all of that file's assignments). To restrict what values are exported, an export expression can be used to allow-list values. In the below example, pi_num and pi_den are not exported in math.neso, and are thus not in the dictionary math defined in program.neso. If no exports are described, everything is exported by default.

# program.neso
math = import math
radius = 5
circle_area = math.pi * radius * radius

# math.neso
pi_num = 355
pi_den = 113
pi = pi_num / pi_den
export pi

There are no functions. Instead values can be applied against other values to influence scope. Because of the lazy nature of the language, undefined variables simply persist through the expression resolution (line 1). Dictionaries hold assignments, so when they're applied against an expression, their assignments can be used to help resolve values further (lines 2 and 3). In the below example, c resolves to 7.

a = { x: 4 }
b = 3 + x
c = a -> b

This application operator (the rightwards arrow ->) also handles something equivalent to Elixir's pipeline operator. However, because of the lack of functions, I'll use a trick that Hack implements: denoting a place to plop the value that was piped in. In Neso, I'll stick with a lonely underscore identifier (_) for this purpose. When an underscore is found in an expression to the right of the application operator, the expression to the left of the operator is plopped into the position that the underscore held. If a dictionary is getting "plopped" into place, then its scope influence shouldn't be happening (below, b resolves to [x + 2, {x: 1}] and not [3, {x: 1}]).

list = 3 -> [ 1, 2, _, 4, 5 ]

a = { x: 1 }
b = a -> [ x + 2, _ ]

And finally, there's a spread operator that looks and works like the Javascript equivalent. Placing an ellipsis (...) immediately before a container will spread that container's contents in the enclosing container. Below, numbers_a and numbers_b are equivalent lists. The spread operator of course works with dictionaries too. If a spread is attempted that doesn't match container types, an error is reported: { ...[1, 2, 3] } and [ ...{x: 1, y: 2} ] both result in errors.

lower = [ 1, 2, 3 ]
upper = [ 4, 5, 6 ]
numbers_a = [ ...lower, ...upper ]
numbers_b = [ 1, 2, 3, 4, 5, 6 ]