273 lines
14 KiB
Markdown
273 lines
14 KiB
Markdown
# Mu Language
|
|
|
|
**Caution: This document is out of date. Please refer to [languages](languages.md) for the latest thinking.**
|
|
|
|
[Mu Metadata](metadata.md) is written in a "YAML-like" language, Mull (Mu's little language). (There is a "JSON-like"
|
|
alternative for those who prefer a JSON style of syntax.) Mull isn't vanilla YAML, however, for two reasons:
|
|
|
|
1. It may be strongly typed to support better compile-time validation.
|
|
2. It may contain embedded quotations that generate data in a rich, semantically-aware way.
|
|
|
|
These two things are tightly integrated with one another so that the result works a bit like an ordinary programming
|
|
language's type system. For example, quotations are also strongly typed, and can leverage typed metadata.
|
|
|
|
Both aspects of Mull have been heavily inspired by other systems. For typing, by TypeScript and JSON Schema. For
|
|
quotations, by Hashicorp's HCL, Go templates, and Jsonnet. A key difference is that we remain true to our YAML and JSON
|
|
heritage wherever possible, so that existing tools and skillsets may be leveraged. In general, if one of these formats
|
|
already has an answer for a question we face -- like the syntax for number literals -- we choose it.
|
|
|
|
## Strong Typing
|
|
|
|
All variables and values have types and the Mu compiler typechecks that they are used correctly. The set of types
|
|
expressible remains very close at the core to YAML and JSON, but adds more structure for custom types.
|
|
|
|
### Basic Types
|
|
|
|
The special type `any` may refer to any type.
|
|
|
|
The basic primitive types are `bool`, `number`, and `string`.
|
|
|
|
Any type `T` can be modified by appending `[]` to turn it into an array `T[]`: e.g., `number[]`, `string[]`, `any[]`.
|
|
|
|
Similarly, two types can be paired up to make a map type using `map[K, V]`, where `K` is the type of keys used to index
|
|
into the map and `V` is the type of value inside: e.g., `map[string, number]`, `map[number, any]`, and so on. Note that
|
|
only the primtive types `bool`, `number`, and `string` can be used as key types inside of a map.
|
|
|
|
### Tuples and Custom Types
|
|
|
|
An anonymous tuple type, which is essentially a map with known properties, can be created using `{}`: e.g., the tuple
|
|
containing two properties, a `string` and a `number`, is `{ string, number }`. These are accessed by their ordinal
|
|
position. If you prefer to name the properties, you can do so `{ name: string, age: number }`.
|
|
|
|
For situations when an anonymous tuple becomes too verbose or repetetive, or advanced features are required, it is
|
|
possible to define custom complex types.
|
|
|
|
Each custom type has the following attributes:
|
|
|
|
* A name.
|
|
* An optional "base" type.
|
|
* An optional default value.
|
|
* An optional description.
|
|
* Either of these:
|
|
- An optional set of properties.
|
|
- An optional set of value constraints.
|
|
|
|
If properties exist, the type must be a custom object type. If base exists, these properties extend it. Each
|
|
property is a superset of the custom type, in that it can carry any or all of those attributes, with two caveats:
|
|
instead of "base" it is simply called "type" and it may also carry an indicator of whether a property is required.
|
|
|
|
The value constraints only apply to custom types whose "base" is either `string` or `number`:
|
|
|
|
* For strings:
|
|
- A maximum length in characters.
|
|
- A minimum length in characters.
|
|
- A regex pattern for legal values.
|
|
* For numbers:
|
|
- A maximum value.
|
|
- A minimum value.
|
|
* For strings and numbers:
|
|
- An enum array of legal values.
|
|
|
|
TODO: we need to specify precisely where custom types may appear, how the system knows to bind them, etc. In the
|
|
Mu case, there are actually two places: the stack's property definitions themselves (`properties`), and the custom
|
|
type section (`types`). Maybe we can use some "meta-typing" to indicate this, e.g. in the Mufile schema itself.
|
|
|
|
## Embedded Quotations
|
|
|
|
A quotation allows computation to be mixed into what is otherwise just markup data.
|
|
|
|
It's important to note that quotations are *not* templates. Although quotations are very powerful and provide
|
|
functionality often offered by templating systems, the quotations are semantically understood at compile-time. They
|
|
are strongly typed and are generally declarative in nature. This is in contrast to templating systems which normally
|
|
perform "dumb" textual copy-and-paste, possibly leading to output that isn't even legal syntax.
|
|
|
|
All quotations are wrapped in `${}` syntax -- such as `${foo}` -- and can reference variables, call functions, perform
|
|
conditional operations, and more. To escape a quotation, use a double dollar sign; for example, `$${foo}` will be
|
|
rendered as the string `${foo}`, instead of the variable named `foo`'s value.
|
|
|
|
### Variables
|
|
|
|
All quotations are evaluated within a scope containing any number of named variables.
|
|
|
|
To inject a variable's value into the markup stream, simply reference it by name; for example, `${foo}` is replaced by
|
|
the value of a variable called `foo` in the current scope. If `foo`'s value is `"bar"`, then the result is `"bar"`.
|
|
|
|
#### Variable Types
|
|
|
|
Note that variables have types. So "replaced by the value" does not necessarily mean that the result is a `string`.
|
|
This allows variables to carry complex structure that is emitted into the markup stream as-is. Note that because
|
|
variable substitution works hand-in-hand with the typechecking mentioned earlier, any conflicting types that result will
|
|
lead to errors as you would expect. This is one of the advantages to the quotation approach versus templates.
|
|
|
|
A function exists that will stringify any value, however: `${string(foo)}`. There are also conversion functions for
|
|
other types, described below in the functions section. They are unique because they typically require parsing.
|
|
|
|
#### Declaring New Variables
|
|
|
|
New variables may be introduced using the `let` syntax; a variable, once bound, is immutable and cannot be changed:
|
|
|
|
${let foo = "bar"}
|
|
|
|
A variable is unlike a property in that it is an implementation detail and not exported for public use.
|
|
|
|
A variable's type is inferred by its initialization value, although it can be specified precisely using type assertions.
|
|
In general, Mull has a structural type system and permits conversions between like-structured types. However, if you
|
|
would like to assert that a variable is of a given type, the `type{expr}` syntax will do this, where `type` is the
|
|
desired type and `expr` is an arbitrary expression. A compile-time error occurs if the conversion can't take place.
|
|
|
|
This can be useful when assigning maps to variables. If the map is meant to be a custom type `myType`, you can say:
|
|
|
|
${let foo = myType{ a: "s", b: 42 }}
|
|
|
|
#### Accessing Properties
|
|
|
|
Properties of a resulting value may be accessed using dots. For example, if `foo` is of a complex type with multiple
|
|
properties, `a`, `b`, and `c`, we can access them simply by saying `${foo.a}`, `${foo.b}`, and `${foo.c}`. Those can of
|
|
course also be complex types with their own properties accessed through dotting, and so on, and so forth.
|
|
|
|
#### Accessing Array and Map Elements
|
|
|
|
Elements of array and map values are accessed using `[]`. For example, `${foo[0]}` extracts the 0th element of an array
|
|
variable `foo`, while `${bar["baz"]}` extracts an element keyed by the string `"baz"` from the map variable `bar`.
|
|
|
|
#### Scopes, This, and Context
|
|
|
|
The scope can be customized and populated in any number of ways. There are always two specific special variables:
|
|
|
|
* `type` refers to the current type specification.
|
|
* `this` refers to the "current object" (whatever that means in the domain-specific context; sometimes just `type`).
|
|
|
|
TODO: we still don't have an elegant formalism or even a good mental "model" for the distinction between "this script"
|
|
(e.g., `type`), and "this object" (e.g., `this`). In Mu, this makes sense, because the primary use case for
|
|
templating is accessing the properties set by a caller instantiating the current stack represented by the file.
|
|
But for many other cases, you actually just want to access enclosing properties inside of the file. I should note,
|
|
if we went back to `parameters` and `arguments`, rather than `properties`, we'd have a distinction between the two
|
|
rooted by a single object (with the unfortunate indirection of needing to say `arguments.foo` instead of just `foo`.
|
|
|
|
Much like your favorite programming language, properties on `this` may be accessed either by explicitly naming `this`,
|
|
as in `${this.foo}`, or simply by naming the property without a prefix, as in `${foo}`.
|
|
|
|
Mu binds `this` to represent the current stack being constructed by a given Mufile. As a result, each property defined
|
|
inside of a stack is thus available in the form of an automatically bound variable.
|
|
|
|
Additionally, Mu makes a `ctx` variable available that is bound to information about the current compilation unit.
|
|
|
|
TODO: describe the context.
|
|
|
|
#### Operators
|
|
|
|
TODO: comparisons.
|
|
|
|
TODO: math.
|
|
|
|
TODO: concatenation.
|
|
|
|
#### Constructors
|
|
|
|
Many values are injected simply by referencing a variable, its properties, calling a function, etc. Sometimes, however,
|
|
new values are created anew out of existing components.
|
|
|
|
New array values can be created using the `[]` operator. For example, `${[ a, b, c ]}` constructs a three-element array
|
|
out of the variables `a`, `b`, and `c`.
|
|
|
|
New map values, on the other hand, use `{}`. For example, `${{ "a": a, "b": b, "c": c }}` constructs a three-element
|
|
map keyed by the strings `"a"`, `"b"`, and `"c"`, and with those same variables as the keys' respective values.
|
|
|
|
Note that the type inference engine will try its best to get the types right. In case it gets something wrong, you may
|
|
simply use a type assertion. For example, maybe instead of a map, we meant to create a custom type named `myType`; to
|
|
say that, we simply wrap the above example in a `myType{}` type assertion: `${myType{ "a": a, "b": b, "c": c }}`.
|
|
|
|
#### A Word on Nulls
|
|
|
|
Any property whose value is set to `undefined` is omitted from the result. This can be useful when propagating property
|
|
values between different objects. For example, let's say in our Mufile the optional `item` property was missing, and
|
|
yet we were about to propagate that property to another object. In one model, we would need to say:
|
|
|
|
${if item != null}
|
|
item: ${item}
|
|
${done}
|
|
|
|
Instead, however, we can simply set the property and let the system omit the result if it is `undefined`:
|
|
|
|
item: ${item}
|
|
|
|
### Functions
|
|
|
|
A function is easily recognizable by the presence of parenthesis: `${func(...)}`, where `...` is the optional set of
|
|
comma-delimited arguments. For example, `${concat(a1, a2, a3)}` concatenates three arrays, `a1`, `a2`, and `a3`,
|
|
evaluating to this overall resulting array. Below is a complete list of the built-in functions available:
|
|
|
|
TODO: list them.
|
|
|
|
TODO: include for semantic inclusion (call it import?).
|
|
|
|
TODO: for built-ins, we probably want some combination of HCL's
|
|
(https://www.terraform.io/docs/configuration/interpolation.html) and what we were getting from Sprig
|
|
(https://github.com/Masterminds/sprig)
|
|
|
|
### Conditionals
|
|
|
|
Mull also supports conditional code in two forms: `if` and `for`. These are slightly different than their general-
|
|
purpose programming language equivalents -- in that they are "declarative" in nature -- but should feel familiar.
|
|
Being more declarative helps to encourage best practices when writing deterministic and predictable Mufiles.
|
|
|
|
Both support an expression and block statement form. The expression forms look like function calls and are handy for
|
|
short declarative data situations, like conditionally setting a property. The block forms are better for complex cases.
|
|
|
|
#### If
|
|
|
|
An `if` expression evaluates to a value. For example, in `${if(foo == "bar", "a", "b")}`, the expression evaluates
|
|
to `"a"` when `foo == "bar"` is true, and `"b"` otherwise. Note that the else part may also be omitted, in which case
|
|
the expression yields `undefined` should the predicate be false, e.g. `${if(foo == "bar", "a")}`.
|
|
|
|
A guarded `if` block, or series of them, allows more sophisticated "control flow" within a Mull file. For example:
|
|
|
|
${if foo == "bar"}
|
|
...
|
|
${else if bar == "baz"}
|
|
...
|
|
${else}
|
|
...
|
|
${done}
|
|
|
|
This now probably looks more like your favorite programming language except that the bodies produce values. These are
|
|
evaluated in order and the first one to succeed leads to the inclusion of its body in the overall markup stream. Note
|
|
that the body here is arbitrary Mull code: markup, additional quotations, or some combination thereof.
|
|
|
|
The `${done}` terminates the overall cascade of guards.
|
|
|
|
TODO: switch statement?
|
|
|
|
#### For
|
|
|
|
Any array or map, including custom data types, may be enumerated using the `for` expression or statement.
|
|
|
|
In the array case, each element is available as the special `item` variable, whereas in the map case, each key is
|
|
available as the special `key` variable and each value as `value`. Notice that the names are built-in to reduce the
|
|
amount of boilerplate in Mull files; remember, they are intentionally kept simple and declarative.
|
|
|
|
In expression form, `each` evaluates to an array or map of values.
|
|
|
|
For example, `${for(a, item)}` is the identity statement for an array variable `a`, while `${for(m, { key: value })}`
|
|
is the identity statement for a map variable `m`. We can do more interesting things, like square an array of numbers,
|
|
`${for(a, item*item)}`, or prepend a constant to the keys in a map, `${for(m, { "prefix-" + key: value }}`. By
|
|
default, the result of an `each` is an array containing the values of each iteration, in order, unless the body
|
|
evaluates to a map, in which case its keys are merged to produce a single aggregate map. By default, keys are sorted
|
|
before enumerating a map, in order to provide a guaranteed and deterministic execution order.
|
|
|
|
In statement form, `for`'s body is repeated for each element in the array or map:
|
|
|
|
${for [a, b, c]}
|
|
...
|
|
${done}
|
|
|
|
The same special `item`, `key`, and `value` variables are bound in the body of a `for` statement block.
|
|
|
|
The contents of each body can be any combination of markup and/or quotations.
|
|
|
|
Note that the special built-in function `range` can be used to create a typical `for` loop:
|
|
|
|
${for range(0, 10)}
|
|
...
|
|
${done}
|
|
|