Neptune
Part one of a series on Neptune | next post

"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:
- Product goals:
- It should sit somewhere between a language-specific IDE and a spreadsheet application.
- It can fill the role of an overcomplicated calculator.
- It should be easy enough for a user with spreadsheet experience but not programming experience to understand.
- Personal goals:
- It should give me a context in which to experiment with language design, syntax, parsing, and interpreting.
- I should be able to publish this to the Mac App Store. I'd also like to adapt it for iPad and iPhone.
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"
)