Ucg expressions can reference a bound name, do math, concatenate lists or strings,
copy and modify a struct, or format a string.
Symbols
-------
Many ucg expressions or statements use a symbol. A symbol might be used for either
name for a binding or a name for a field. Symbols must start with an ascii letter and can contain any ascii letter, number, `_`, or `-` characters.
Selectors
------
A UCG selector references a bound value by name. They can descend into tuples or lists
or refer to symbols in imported files. They are symbol followed optionally by a list
of other symbols or numbers separated by a `.` to reference subfields or indexes in a list.
You can reference a field in a tuple by putting the field name after a dot. Lists are always 0 indexed. You can index into a list by referencing the index after
a `.`.
```
let tuple = {
inner = {
field = "value",
},
list = [1, 2, 3],
};
// reference the field in the inner tuple in our tuple defined above.
tuple.inner.field;
// reference the field in the list contained in our tuple defined above.
tuple.list.0;
```
### The environment Selector
There is a special selector in ucg for obtaining a value from the environment. The
`env` selector references the environment variables in environment at the time of
the build. You reference environment variables just like it was a tuple. Attempting
to reference a variable that doesn't exist will be a compile error.
```
let env_name = env.DEPLOY_ENV;
```
Binary Operators
----------
UCG has a numver of binary infix operators. Some work only on numeric values and others
work on more than one type.
### Numeric Operators
ucg supports the following numeric operators, `+`, `-`, `*`, `/` Each one is type safe
and infers the types from the values they operate on. The operators expect both the
left and right operands to be of the same type.
```
1 + 1;
1.0 - 1.0;
```
### Concatenation
The `+` operator can also do concatenation on strings and lists. As with the numeric
version both sides must be the same type, either string or list.
```
"Hello " + "World"; // "Hello World"
[1, 2] + [3]; // [1, 2, 3]
```
### Comparison Operators
UCG supports the comparison operators `==`, `!=`, `>=`, `<=`, `<`, and `>`. The all
expect both sides to be of the same type.
The `>`, `<`, `>=`, and `>=` operators are only supported on numeric types (i.e. int,
and float).
```
1 > 2; // result is false
2 <3;//resultistrue
10 > "9"; // This is a compile error.
(1+2) == 3;
```
The equality operators `==` and `!=` are supported for all types and will perform deep
equal comparisons on complex types.
```
let tpl1 = {
foo = "bar",
one = 1
};
let tpl2 = {
foo = "bar",
one = 1
};
tpl1 == tpl2; // returns true
let tpl2 = {
foo = "bar",
one = 1
duck = "quack",
};
tpl1 == tpl3; // returns false
```
Because tuples are an ordered set both tuples in a comparison must have their fields in
the same order to compare as equal.
#### Operator Precedence
UCG binary operators follow the typical operator precedence for math. `*` and `/` are
higher precendence than `+` and `-` which are higher precedence than any of the
comparison operators.
Copy Expressions
----------------
UCG expressions have a special copy expression for tuples. These faciliate a form of
data reuse as well as a way to get a modified version of a tuple. Copy expressions
start with a selector referencing a tuple followed by braces `{}` with `name = value`
pairs separated by commas. Trailing commas are allowed.
Copied expressions can change base fields in the copied tuple or add new fields. If
you are changing the value of a base field in the copy then the new value must be of
the same type as the base field's value. This allows you to define a base "type" of
sorts and ensure that any modified fields stay the same.
```
let base = {
field1 = "value1",
field2 = 100,
field3 = 5.6,
};
let overridden = base{
field1 = "new value"
};
let expanded = base{
field2 = 200,
field3 = "look ma a new field",
};
let bad = base{
field1 = 300, // Error!!! must be a string.
};
```
There is a special selector that can be used in a copy expression to refer to the base
tuple in a copy called `self`. `self` can only be used in the body of the copy.
```
let nestedtpl = {
field1 = "value1",
inner = {
field2 = 2
inner = {
field3 = "three",
},
},
};
let copiedtpl = nestedtpl{
inner = self.inner{
inner = self.inner{
field4 = 4,
},
},
};
```
Format Expressions
----------
UCG has a format expression that has a limited form of string templating. A format
expression starts with a string followed by the `%` operator and a list of arguments
in parentheses separated by commas. Trailing commas are allowed. The format string
should have `@` in each location where a value should be placed. Any primitive value
can be used as an argument.
```
"https://@:@/" % (host, port)
```
Conditionals
----------
UCG supports a limited conditional expression called a select. A select expression
starts with the `select` keyword and is followed by a an expression resolving to a
string naming the field to select, an expression resolving to the default value, and
finally a tuple literal to select the field from. If the field selected is not in the
tuple then the default value will be used.
```
let want = "baz";
// field default
select want, "quux", {
baz = "foo",
fuzz = "bang",
}; // result will be "foo"
// field default
select "quack", "quux", {
baz = "foo",
fuzz = "bang",
}; // result will be "quux"
```
Macros
-----
Macros look like functions but they are resolved at compile time and configurations
don't execute so they never appear in output. Macros do not close over their
environment so they can only reference values defined in their arguments. They can't
refer to bindings or other macros defined elsewhere. They are useful for constructing
tuples of a certain shape or otherwise promoting data reuse. You define a macro with
the `macro` keyword followed by the arguments in parentheses, a `=>`, and then a tuple
literal.
```
let mymacro = macro (arg1, arg2) => {
host = arg1,
port = arg2,
connstr = "couchdb://@:@" % (arg1, arg2),
}
let my_dbconf = mymacro("couchdb.example.org", "9090");