Will's Digital Garden

Neptune

Part one of a series on Neptune | next post

screenshot of an app with code on the left

"Neptune" has been my latest app that I've been working on in my spare time. It takes the form of a text editor tailored to its own programming language. As many playground IDEs do, Neptune shows each line's result in a separate pane. I have a few other features on my todo list that I think will distinguish Neptune from other similar apps.

Goals

Because it's a hobby app, I have a few sets of goals for it:

Language Overview

Despite the tense I write in, nothing here is set in stone. I'd love to hear and chat about suggestions!

In Neptune, there are three types: Strings, Numbers, and Functions.

Strings

Strings are written surrounded in double quote marks. There's no interpolation or escape characters. Booleans are naively implemented using Strings (i.e. "true" and "false").

Numbers

All Numbers are implemented as rationals with units. That is, they're comprised of a numerator, a denominator, and a set of unit-and-cardinality pairs. I've chosen rationals rather than floating point numbers because in Neptune correctness is prioritized over speed or memory usage. Units are included because Neptune is intended to help calculate measured values, and prevent miscalculations involving unrelated units.

Number literals however take the form many people are used to: written in base-10 with an optional decimal point and a base-10 fractional part. Units can be added immediately following literals for familiarity and convenience (and this indicates multiplication).

Assignment

Before we get to functions, I'll briefly tell you about assignments. In Neptune, assignment uses the equals (=) operator or the colon (:) operator. There are no semantic or syntactic differences between them other than the character used – there's simply two options depending on user preference.

Functions

Functions in Neptune are a way to extract a formula into a re-usable form. These can only be defined at the top-level of a document – nested functions add complexity when reading and editing code. Functions in Neptune do not provide closure over the definition's environment and do not allow using globally scoped values. Thus, all references needed in the function's body must be passed in as arguments.

A function's definition consists of three parts: the name of the function, a list of argument mappings, and a body. The body must be a single expression, which is evaluated and returned when the function is invoked. The argument mappings are a list of pairs: a name for the argument used when invoking the function (the "outer name") and a name for the argument used within the body (the "inner name"). When the inner name is omitted, the outer name is used within the body.

When invoking functions, a simple list of arguments isn't passed. Instead, a new temporary scope is created within the parentheses of the invocation. Any assignments that occur within this scope have their names translated according to the function's argument mapping definition, then are prepended to the function's body.

One exception to the rule of "no positional arguments". During a function invocation, the first argument may omit its assignment name. In this case, an assignment will be created during parsing under the outer name of _. This allows functions to read more like English, while still reducing confusion by requiring argument labels for most arguments.

Standard Library

Most of the standard library takes the form of unary or binary operators, but there are a few built-in functions.

Comparison operators

These return either the String "true" or "false". The types being compared must be equivalent (for Numbers, this "type" includes units).

name operator allowed types
equality == String or Number
inequality != String or Number
lesser than < Number
lesser than or equal to <= Number
greater than > Number
greater than or equal to >= Number

Numeric operators

Between two Numbers, these operators exist and return a Number.

name operator notes
addition + units must match
subtraction - units must match
multiplication * units are additive
division / units are reductive
modulo % units must match and both operands must be integers

One numeric prefix unary operator exists: negation (-), which is a handy shortcut for multiplying its operand by -1.

String operators

One "boolean" prefix unary operator exists: inversion (!), which returns the boolean inverse of the String or errors if used with a String other than "true" or "false".

One binary operator exists to be used between two Strings: concatenation (+), which returns a String formed by joining the two operands.

A final binary operator is one that operates with a String as the left operand and a Number as the right operand: repeating (*), which returns a String with the contents of the left operand repeated the right operand number of times.

Other standard library functions

magnitude(of) accepts a Number under the name of and returns that same number, but without units.

match(if:then:else) accepts a String under the name if, any value under the name then, and any value under the name else. If the value passed under the name if is equivalent to the String "true", then the value passed under the name then is returned. Otherwise, the value passed under the name else is returned.

Other notes

Like many languages, Neptune's supports comments. Outside of a String, when a line of code contains a hashmark (#), that character and any following it (until the end of the line) are ignored during the lexing step.

Neptune automatically interprets code as it's being typed. To speed up small edits, Neptune holds a small cache of input/output that it checks before running the source through the interpreter. It also has a preference to disable automatic execution – this mode alerts the user when the output is stale and waits until the user manually triggers execution.

Example source

As seen in the screenshot above, here's a sample that helps me calculate whether my movie theater subscription is worth it based on how many movies I've seen using it.

# setup a few constants
movie_price = 10.99 dollars / movie
subscription_cost = 19.95 dollars / month

# provide a way to "input" data
months_paid = 1 month
movies_seen = 2 movie

# define a helper function
function compare = (_ this, to that, if_less, if_same, if_more) {
  match(
    if: this == that,
    then: if_same,
    else: match(
      if: this < that,
      then: if_less,
      else: if_more
    )
  )
}

# calculate!
net_cost = (subscription_cost * months_paid) - (movies_seen * movie_price)

# display output depending on conditions!
compare(
  net_cost,
  to: 0 dollars,
  if_less: "worth it!",
  if_same: "break even",
  if_more: "waste of money"
)