UNPKG

@onlabsorg/swan-js

Version:

A simple yet powerful expression language written in JavaScript

677 lines (506 loc) 25.7 kB
# Swan language `Swan` is a an interpreted expression language. Syntactically speaking, a swan expression is a sequence of binary operations `(operand operator operand operator operand ...)`, eventually grouped with parenthesis `(...)`, square braces `[...]` or curly braces `{...}`. Finally, the swan syntax supports end-line comments: everything following a `#` symbol, up to the end of the line, is a comment and therefore ignored. For example: ``` (12 + 2) * 3 - 4 # expression resolving to 38 ``` Each operand of a swan expression is a `Term`, which can be either an `Item` or a [Tuple](#tuple-data-type-and-pairing-operator). An Item holds a single value, while a Tuple is a sequence of Items: a product type. Any Item is also isomorphic to a 1-d Tuple, while the empty Tuple `()` is the swan unit type, used to represent the concept of nothingness. The Item types defined in swan are: - [Bool](#bool-data-type) type, representing booleans (either `TRUE` or `FALSE`) - [Numb](#numb-data-type) type, representing real numbers (e.g. `-123.4e3`) - `Applicable` type, an abstract data type representing any Item for which an [apply operation](#application-operator) is defined - `Mapping` type, an abstract Applicable representing discrete mappings - [Text](#text-data-type) type, a Mapping between integer numbers and characters (e.g. `"abc"`). - [List](#list-data-type) type, a Mapping between integer numbers and Items - [Namespace](#namespace-data-type) type, a Mapping between identifiers and Terms - [Func](#func-data-type) type, an Applicable representing a function - [Undefined](#undefined-data-type) type, an Item representing an undefined operation or value (swan expressions never fail; they rather return undefined values) The following operators are defined in swan: - [Arithmetic operators](#arithmetic-operators): `+`, `-`, `*`, `/`, `%`, `^` - [Comparison operators](#comparison-operators): `==`, `!=`, `>`, `>=`, `<`, `<=` - [Logic operators](#logic-operators): `&`, `|` - [Selection operators](#selection-operators): `?`, `;` - [Pairing operator](#tuple-data-type-and-pairing-operator): `,` - [Application operator](#application-operator) (empty operator): ` ` - [Function definition operator](#func-data-type): `->` - [Function composition operators](#composition-operators): `<<`, `>>` - [Mapping operator](#mapping-operator): `=>` - [Assignment operators](#names-and-assignment-operators): `=`, `:` - [Subcontexting operator](#subcontexting-operator): `.` - [Unary operators](#unary-operators): `+`, `-` Unless grouping parenthesis are used, the operations are executed in the order shown below. If the expression contains two or more operators with the same rank, the leftmost operation gets executed first. The only exceptions to this rule are the function creation operation `->` and the function composition operator `<<`, for which the right-most operations get executed first. For example, the expression `x -> y -> x+y` is equivalent to `x -> (y -> x+y)`. 1. application , `.` 2. `^` 3. `*` , `/`, `%` 4. `+` , `-` 5. `==` , `!=` , `<` , `<=` , `>=` , `>` 6. `&` , `|` 7. `?` 8. `;` 9. `->` 10. `<<`, `>>` 11. `=>` 12. `=` , `:` 13. `,` Besides the operation, Swan defines also some essential [builtins](./builtins.md), consisting in functions and namespaces and some [standard modules](./modules/index.md) that can be loaded using the `require` builtin function. ## Bool data type A Bool item can be either true or false. It can be created either: - by referencing the built-in constants `TRUE` or `FALSE` - as return value of a [comparison expressions](#comparison-operators) - as result of calling the `Bool` or the `Bool.not` [builtin functions](./builtins.md) ## Numb data type This data type represents a real number. A number can be created either: - by explicitly defining it as a literal (e.g `10`, `13.14`, `-2.5e3`, ...) - as return value of an [arithmetic operation](#arithmetic-operators) ## Text data type A Text item is a Mapping between natural numbers and characters; in other words, it is a sequence of characters. A Text item can be defined using one of the following literals: * `"double quotes string"` * `'single quotes string'` * `` `accent quotes string` `` A string within accent quotes is a template string. Within a template strings, expressions eventually enclosed between `{%` and `%}` get evaluated and replaced. For example, the expression `` `2*10 = {% 2*10 %}` `` returns the string `"2*10 = 20"`. The n-th character of a Text item can be accessed via an [apply operation](#application-operator). For example: - `"abc" 0` returns `"a"` - `"abc" 1` returns `"b"` - `"abc"(2)` returns `"c"` ## Tuple data type and pairing operator A Tuple term is an ordered sequence of items. Tuples are created using the comma operator `,`. The following example shows a tuple made of three items: `1`, `2` and `"abc"` ``` 1, 2, "abc" ``` Tuples can contain any other data type as elements, except another tuple. If you try to create a tuple of tuples, you get a flattened tuple as result: `(1,2),(3,4),5` is equivalent to `1,2,3,4,5`. Any item in swan (e.g. `10` or `"abc"` or a list, etc.) is also seen as a tuple made of only one item. A special type of tuple is the empty tuple `()` which is used in swan to represent the concept of nothingness. ## List data type A List item is a Mapping between natural numbers and items. In other words, it is an ordered sequence of values, just like a tuple, but it behaves like an item (one-element tuple); for this reason, it allows nesting and behaves differently from tuples. A list is created as `[tuple]`. Examples of valid list literals are: * `[1,2,"abc"]` list with three elements * `[1]` list with only one element * `[]` empty list * `[[1,2],[3,4,5]]` list of lists The n-th item of a List item can be accessed via an [apply operation](#application-operator). For example: - `[10,20,30] 0` returns `10` - `[10,20,30] 1` returns `20` - `[10,20,30](2)` returns `30` ## Names and assignment operators The **assignment operator** binds a name to an expression value: ``` x = 10 + 1 ``` After this expression, the context will contain a name `x` with its associated value `11`, which can be referenced in other expressions. For example: ``` x * 2 # resolves to 22 ``` Valid names can contain letters (`a..z` and `A..Z`), numbers (`0..9`) and the underscore character (`_`), but they cannot start with a numeric character. Multiple values (a tuple of values) can be assigned to multiple names (a tuple of names) as follows: ``` (a, b, c) = (1, 2, 3) ``` After this expression, `a` will hold the value `1`, `b` will hold the value `2` and `c` will hold the value `3`. If the number of names does not match the number of values, the assignment operation behaves as follows: - If the tuple of values contains less items than the tuple of names, the exceeding names will be assigned nothing (empty tuple): `(a,b,c,d) = (1,2)` will result in `a=1`, `b=2`, `c=()` and `d=()`. - If the tuple of values contains more items than the tuple of names, the last name will be assigned the tuple of the remaining values: `(a,b,c) = (1,2,3,4,5)` will result in `a=1`, `b=2` and `c=(3,4,5)`. - If the tuple of names is empty, no assignment will take place. Like any other type of operation, the assignment operation returns a value, that is: - `NOTHING` (empy tuple), if the assignment operation is successful - Undefined AssignmentOperation if any of the left-hand items is not a name or not a valid name. In that case no assignment will take place. The **labeling operator** `:` works exactly as the assignment operator `=`, with the only exception that the operation returns the right-hand value instead of `NOTHING`. For example, the expression `x: 10 + 1` binds `11` to `x` and returns `11`. ## Namespace data type A Namespace item is a Mapping between names and Terms. Any expression enclosed between curly braces returns a Namespace item. For example: ``` { x = 10, y = 20, z = 30, 3 + 5 } ``` In this example, we basically assigned three values to three names, while any non-assignment operation between curly braces is just ignored. In order to access the names inside a namespace, you can use either the [apply operator](#application-operator) or the [subcontexting operator](#subcontexting-operator) `.`. For example: * `ns = {a=1, b=2, c=3}` defines a namespace and maps it to the name `ns` * `ns "a"` resolves to `1` * `ns("b")` resolves to `2` * `ns.c` resolves to `3` Another import property of namespace is inheritance: every namespace inherits the names of its `parent namespace`. The following example shows how child namespace inherit the names defined in their parent namespaces. ``` x = 10 ns1 = { y = x + 1 # the name `x` is defined in the parent namespace ns2 = { z = x + y # the name `y` is defined in the parent namespace `ns1`, # the name `x` is defined in the parent's parent namespace } } ``` > Notice that the [apply operator](#application-operator) gives access only to > the `own` names of a namespace, while the [subcontexting operator](#subcontexting-operator) > gives access also to all the parent's names. ## Func data type A function is a parametric expression, defined as `names -> expression`. For example, the expression: ``` (x,y) -> x+y ``` defines a function that takes two parameters `x` and `y` as input and produces their sum as output. In order to execute a function, you use the [application operator](#application-operator); for example: * `f = x -> 2*x` defines a function that takes one parameter and doubles it * `f 4` resolves to `8` * `f(5)` resolves to `10` **Recursion** is the primary way to do loops in swan and for this purpose every function has access to itself via the `self` name. For example, the following recursive function calculates n-factorial: ``` n -> n == 0 ? 1 ; n * self(n-1) ``` ## Undefined data type Swan operations never fail: they returns an Undefined item instead; for example the sum of a number and a list returns an Undefined item. Besides being returned by undefined operations, Undefined items can be created via the `Undefined` [builtin](./builtins.md) callable. ## Application operator A missing operator (two operands next to each other: `F X`), defines an application operation: `F` applied to `X`. The application operation is defined on `Applicable` data types, which are [Func](#func-data-type) and the `Mappings` [Text](#text-data-type), [List](#list-data-type) and [Namespace](#namespace-data-type). **When F is a Func item**, the application operation corresponds to a function call. Given a function `f: x -> 2*x`, the expression `f 5` is an application operation (`f` applied to `5`) that resolves to `10`. Since the expression `(5)` is equivalent to the expression `5`, you could also write the application operation in the more familiar form `f(5)`. When the function parameter `x` is a tuple (e.g. `f: (x,y) -> x+y`), the application will look like `f(x,y)`. In this case the parenthesis are necessary, because `f x,y` would resolve to the tuple `(f x), y` instead. Notice that assigning parameter values to tuple arguments works the same as tuple-values to tuple-names assignment: * in `((x,y,z)->x+y+z)(1,2)` the parameter `x` will be `1`, the parameter `y` will be `2` and the parameter `z` will be `()` * in `((x,y)->x+y)(1,2,3)` the parameter `x` will be `1` and the parameter `y` will be `(2,3)` If the function `F` throws an error, the application operation resolves to `Undefined('Term', error)`. **When F is a Mapping item** (namely a [Text](#text-data-type), [List](#list-data-type) or [Namespace](#namespace-data-type) item), the application operation returns the value mapped to the given parameter or `Undefined('Mapping')` if no mapping is defined. In particular: - `"abc"(1)` returns `"b"` - `[10,20,30](1)` returns `20` - `{a:1, b:2, c:3}("b")` returns `2` If a tuple of parameters is passed as argument to a Mapping application, the result is a tuple of mapped values. For example: - `"abc"(1,2)` returns `("b", "c")` - `[10,20,30](1,2)` returns `(20, 30)` - `{a:1, b:2, c:3}("b", "c")` returns `(2, 3)` When `F` is a Namespace and `X` is a name defined in its parent namespace, the application operation returns `Undefined('Mapping')`. In other words, the application operation gives access only to the `own` names of a namespace and not to the inherited names. When `F` is a Namespace that contains a `F.__apply__` function, the normal mapping application operation is overridden and resolves to `F.__apply__(X)`. **When F is a tuple `(f1, f2, ...)`**, the `F X` operation returns the tuple `(f1 X, f2 X, ...)` ## Unary operators Swan defines two unary operators: `+` and `-`. The `+X` expression is equivalent to just `X`. The `-X` expression returns the additive inverse of `X`, which is actually defined only on Numb items. For all the other types `-X` returns `Undefined("NegationOperation")`. If `X` is a tuple `(x1, x2, ...)`, the `-X` operation returns `(-x1, -x2, ...)`. ## Arithmetic operators The arithmetic operators are: sum (`+`), subtraction (`-`), product (`*`), division (`/`), modulo (`%`) and exponentiation (`^`). These operations are consistent with the following algebraic characterization of the swan types: | | Bool | Numb | Text | List | Namespace | Func | Undefined | |--------------------------------|:--------:|:------:|:---------:|:---------:|:---------:|:--------:|:---------:| | Sum operation | `OR` | `X+Y` | `Concat.` | `Concat.` | `Merge` | *Undef.* | *Undef.* | | Additive inverse | *Undef.* | `-X` | *Undef.* | *Undef.* | *Undef.* | *Undef.* | *Undef.* | | Additive neutral element | `FALSE` | `0` | `""` | `[]` | `{}` | *Undef.* | *Undef.* | | Product operation | `AND` | `X*Y` | *Undef.* | *Undef.* | *Undef.* | *Undef.* | *Undef.* | | Multiplicative inverse | *Undef.* | `1/X` | *Undef.* | *Undef.* | *Undef.* | *Undef.* | *Undef.* | | Multiplicative neutral element | `TRUE` | `1` | *Undef.* | *Undef.* | *Undef.* | *Undef.* | *Undef.* | The sum of a tuple `t1=(x1,x2,x3)` and a tuple `t2=(y1,y2,y3)` is the tuple `(x1+y1, x2+y2, x3+y3)`. The same goes for subtraction, product, division, modulo and exponentiation. As a consequence of this rule, any operation between two empty tuples `()` returns an empty tuple. #### Arithmetic operations between Bool items * `B1 + B2` returns the logic `or` between `B1` and `B2` * `B1 * B2` returns the logic `and` between `B1` and `B2` #### Arithmetic operations between Numb items The Numb type implement the arithmetic operations between numbers: * `5 + 2` returns `7` * `5 - 2` returns `3` * `5 * 2` returns `10` * `5 / 2` returns `2.5` * `5 % 2` returns `1` * `5 ^ 2` returns `25` #### Arithmetic operations between Text items The Text type implements only the sum operation, which produces the concatenation of the two operands. For example, `"abc" + "def"` returns `"abcdef"`. #### Arithmetic operations between List items The List type implements only the sum operation, which produces the concatenation of the two operands. For example, `[1,2,3] + [4,5,6]` returns `[1,2,3,4,5,6]`. #### Arithmetic operations between Namespace items The Namespace type implements only the sum operation. The sum `ns1 + ns2` between two namespaces returns a new namespace obtained by merging `ns1` and `ns2`. For example `{a=1,b=2} + {c=3,d=4}` returns `{a=1, b=2, c=3, d=4}`. If `ns1` and `ns2` contain the same name, the value of `ns2` prevails. For example `{a=1,b=2} + {b=3, c=4}` returns `{a=1, b=3, c=4}`. Custom arithmetic operations on namespaces can be defined by adding specials functions to the namespace: * If `ns` contains a Func item named `__add__`, then `ns + item` returns `ns.__add__(ns, item)`. * If `ns` contains a Func item named `__sub__`, then `ns - item` returns `ns.__sub__(ns, item)`. * If `ns` contains a Func item named `__mul__`, then `ns * item` returns `ns.__mul__(ns, item)`. * If `ns` contains a Func item named `__div__`, then `ns / item` returns `ns.__div__(ns, item)`. * If `ns` contains a Func item named `__pow__`, then `ns ^ item` returns `ns.__pow__(ns, item)`. * If `ns` contains a Func item named `__mod__`, then `ns % item` returns `ns.__mod__(ns, item)`. #### Undefined arithmetic operations When an arithmetic operation is not defined between two items, the result is an Undefined item. In particular: - `x + y` resolves to `Undefined('SumOperation', x, y)` - `x - y` resolves to `Undefined('SubOperation', x, y)` - `x * y` resolves to `Undefined('MulOperation', x, y)` - `x / y` resolves to `Undefined('DivOperation', x, y)` - `x % y` resolves to `Undefined('ModOperation', x, y)` - `x ^ y` resolves to `Undefined('PowOperation', x, y)` ## Comparison operators The comparison operations compare two values and return `TRUE` or `FALSE`. Swan defines the following comparison operators: * Equal: `==` * Not equal: `!=` * Less than: `<` * Less than or equal to: `<=` * Greater than: `>` * Greater than or equal to: `>=` Equality is defined on all the swan Item types, while only `Bool`, `Numb`, `Text` and `List` types define also an order for their items. Tuples are compared lexicographically and the empty tuple `()` is less than any other item and equal only to itself. #### Comparison operations between Bool items Two Bool items are equal if they are both `TRUE` or both `FALSE`. Furthermore `FALSE` is less than `TRUE`. #### Comparison operations between Numb items The comparison between numbers works as expected. For example, the following expressions resolve to `TRUE`: * `10 == 10` * `10 != 11` * `10 < 11` * `10 <= 11` * `10 <= 10` #### Comparison operations between Text items Two Text items are equal if they contain the same sequence of characters. For example `"abc" == "abc"` is `TRUE`. A text `s1` is less than a text `s2` if `s1` precedes `s2` alphabetically. For example, the following expressions return `TRUE`: * `"abc" < "xyz"` * `"zzz" > "aaa"` #### Comparison operations between List items Two lists are equal if they contain the same sequence of items. For example `[1,2,3] == [1,2,3]` is `TRUE`, but `[1,2,3] == [1,2]` is `FALSE`. A list `L1` is less than a list `L2` if `L1` precedes `L2` lexicographically. For example, the following expressions return `TRUE`: * `[1,2,3] < [4,5,6]` * `[1,2,3] < [1,2,4]` * `[1,3,4] > [1,2,4]` #### Comparison operations between Namespace items Two namespaces are equal if they contain the same set of own name-value pairs. For example `{a=1,b=2} == {a=1,b=2}` is true, but `{a=1,b=2} == {a=1,b=4,c=5}` is false. No order is defined for the Namespace type, therefore the comparison operations `<`, and `>` between namespaces will always return `FALSE`, while the operations `<=` and `>=` will return `TRUE` only if the namespaces are equal. This behaviour can be overridden by defining a `__cmp__` function in the left-hand namespace: - `NS1 == NS2` is `TRUE` if `NS1.__cmp__(NS1, NS2)` returns 0 - `NS1 != NS2` is `TRUE` if `NS1.__cmp__(NS1, NS2)` returns anything but 0 - `NS1 < NS2` is `TRUE` if `NS1.__cmp__(NS1, NS2)` returns a negative number - `NS1 <= NS2` is `TRUE` if `NS1.__cmp__(NS1, NS2)` returns a negative number or 0 - `NS1 > NS2` is `TRUE` if `NS1.__cmp__(NS1, NS2)` returns a positive number - `NS1 >= NS2` is `TRUE` if `NS1.__cmp__(NS1, NS2)` returns a positive number or 0 #### Comparison operations between Func items Two functions are equal if they are the same function. For example, given two functions `f1:x->2*x` and `f2:x->2*x`, the expression `f1 == f1` is true, but the expression `f1 == f2` is false. No order is defined for the Func type, therefore the comparison operations `<`, and `>` between functions will always return `FALSE`, while the operations `<=` and `>=` will return `TRUE` only if the functions are equal. #### Comparison operation between Undefined items Two Undefined items are equal if they are the same item. No order is defined for the Undefined type, therefore the comparison operations `<`, and `>` between undefined items will always return `FALSE`, while the operations `<=` and `>=` will return `TRUE` only if the undefined items are equal. #### Comparison operation between items of different types The `!=` comparison between two items of different type returns always `TRUE`, while all the other comparison operations return always `FALSE`. #### Comparison operations between tuples Two tuples are equal if they contain the same sequence of items. For example `(1,2,3) == (1,2,3)` is `TRUE`, but `(1,2,3) == (1,2)` is `FALSE`. A tuple `t1` is less than a tuple `t2` if `t1` precedes `t2` lexicographically. For example, the following expressions return `TRUE`: * `(1,2,3) < (4,5,6)` * `(1,2,3) < (1,2,4)` * `(1,3,4) > (1,2,4)` If the two tuples have different number of items, the missing items are assumed to be `()`. The empty tuple is less than anything else and equal only to itself. ## TRUTY and FALSY terms By definition, the following items are *FALSY*, meaning they give `FALSE` when converted to booleans: - empty tuple: `{}` - `FALSE` Bool - `0` Numb - empty Text: `""` - empty List: `[]` - empty Namespace: `{}` - Undefined item All other items are *TRUTY*, meaning that they give `TRUE` when converted to booleans. A tuple is *FALSY* if all its items are *FALSY*, while it is *TRUTY* if at least one of its items is *TRUTY*. ## Logic operators The logic operators AND (`&`) and OR (`|`) generalize the logic AND/OR to any value type. The AND operation `A & B` returns `A` if it is [FALSY](#truty-and-falsy-terms), otherwise it returns `B`. For example: - `1 & 2` resolves to `2` - `0 & 1` resolves to `0` The OR operation `A | B` returns `A` if it is [TRUTY](#truty-and-falsy-terms), otherwise it returns `B`. For example: - `1 | 2` resolves to `1` - `0 | 1` resolves to `1` ## Selection operators A conditional expression `X ? Y` resolves to `Y` if `X` is [TRUTY](#truty-and-falsy-terms), otherwise it resolves to `Undefined('Term')`. For example: * `2 > 1 ? "ok"` resoves to `"ok"` * `2 < 1 ? "ok"` resoves to `Undefined('Term')` * `"abc" ? "ok"` resoves to `"ok"` * `"" ? "ok"` resoves to `Undefined('Term')` An alternative expression `X ; Y` resolves to `X` if it is not an Undefined term; otherwise it resolves to `Y`. For example: * `Undefined() ; 3` resolves to `3` * `10 ; 2` resolves to `10` When combined together, the conditional and the alternative expression work as an if-else condition: * `1==1 ? "eq" ; "ne"` resolves to `"eq"` * `1==2 ? "eq" ; "ne"` resolves to `"ne"` ## Subcontexting operator When you assign a value to a name with `name = value` (or with `name : value`), you are defining that name in the global namespace and you can access the associated value as `name` in another expression. When you assign a value to a name between curly braces `{name1=10, name2=20}`, you are defining names in a sub-namespace and you can access the associated values with an apply operation `{a=1}("a")`. A subcontexting operation `ns . expression` executes the right-hand expression in the context of `ns`. For example, `{a=2,b=3}.(a+b)` will resolve to `5`. The inherited names are still visible in the righ-hand expression, unless they are overridden by local namespace names. For example: ``` x = 10, y = 20, ns = {x=100, z=300}, sum = ns.(x+y+z) # 420 ``` The `sum` name will map to the value `420`. In fact, in the right-hand expression `x+y+z`, the names `x` and `z` will be found in `ns`, while the name `y` will not be found in `ns` and will be taken from the global namespace. The subcontexting can be also used as an alternative way to access names defined inside a namespace. In the example above, the expression `ns.z` will indeed resolve to the value of `z` inside the namespace `ns`. The subcontexting operation can be used also the define child namespaces. For example, `ns.{u:1, v:2, w:3}` will resolve to a namespace that has `ns` as parent. If the left-hand operand `X` of a `X.Y` operation is not a Namespace, the `.` operation returns `Undefined('SubcontextingOperation', X, Y)`. ## Composition operators The composition operation `g << f` returns the function `x -> g(f x)`. For example: ``` f = x -> 2*x, g = x -> x+1, h = g << f, h 4 # resolves to 9, obtained as (2*4)+1 ``` The composition operation is righ-associative, therefore `h << g << f` resolves to `x -> h( g( f x ))`. The reverse composition operation `g >> f` returns the function `x -> f(g x)`. For example: ``` f = x -> 2*x, g = x -> x+1, h = g >> f, h 4 # resolves to 10, obtained as 2*(4+1) ``` ## Mapping operator The mapping operator `t => f` takes a tuple `t = (a, b, c, ...)` and a function `f` and returns the tuple `f(a), f(b), f(c), ...`. For example: ``` (1,2,3) => x -> 2*x # resolves to (2,4,6) ``` Notice that when the mapping function `f` returns an empty tuple `()`, its value will be ignored in the mapped tuple, because that's just how tuples work. This means that the mapping operator can be also used to filter tuples. For example: ``` isEven = x -> x % 2 == 0 # returns TRUE if x is an even number ifEven = x -> isEven(x) ? x ; () # returns x if it is an even number, or else () (1,2,3,4,5) => ifEven # resolves to (2,4) ```