Ecks is an embedded expression language that can add limited scripting capability to applications written in a host language. This reference implementation targets JavaScript runtimes.
- A hosted language
- Everything is an expression
- No side-effects
- Strongly typed
- All types are inferred
- All types have value equality
- Emphasis on brevity
- Case insensitive
- Optional types instead of a null value - handling is enforced
- Almost no syntax - should be learnable in just a few minutes
- All data structures are immutable
- No looping constructs, only iterative methods like
map
,fold
(i.e. reduce), andkeep
(i.e. filter) - The printable representation of all data types is identical to the literal representation. This means, among other things, that all (valid) examples in this documentation can be copied as-is into the provided REPL and evaluated.
Everything is an expression, meaning that all constructs in the language evaluate to a value.
Ecks supports integers in decimal (e.g. 16
), octal (e.g. 020
), and
hexadecimal (e.g. #10
) notations. The minimum integer value is 0, but the
maximum value depends on the host language.
Ecks also supports floating point numbers. There must be at least one digit
following the decimal point (e.g. 1.0
is valid, but 1.
is not). E notation
is supported (e.g. 1.2e3 = 1200.0
). Floating point precision is dependent on
the host language.
The boolean type contains two values: true
and false
. These are keywords.
Ecks strings are surrounded by either single quotes, double quotes, or backticks (template strings). Character encoding is dependent on the host language.
Examples:
'this is a string'
"this is also a string"
The following special escape sequences are provided. When present in a string, the escape sequence will be substituted with the indicated character.
\n - newline
\t - tab
\\ - backspace
Any other character escaped with a backslash will be preserved verbatim. Most
significantly, this means that a backslash can be used to include a literal '
in a single quote delimited string, a "
in a double quote delimited string,
and a backtick or {
in a template string.
Examples:
'\t This string contains \n two lines and begins with a tab character.'
'C:\\Users'
'This contraction can\'t work without the escaped single quote.'
String interpolation is provided via template strings. Within a backtick
delimited string, an expression may be embedded between an unescaped {
and a
}
. During an expansion step, any embedded expressions will be evaluated and
replaced with the result of calling the str
method on the expression (all
types support the str
method). Template strings may be nested to any depth.
Examples:
`1 + 1 = {1 + 1}` = '1 + 1 = 2'
`cats and {`d{'OG'.lower}s`}` = 'cats and dogs'
Ecks only provides very basic control flow in the form of two conditional expressions and a case analysis expression.
All condition predicates must resolve to a bool
. There is no concept of
truthy/falsy values.
The ternary operator is a two branch conditional expression. It is analogous to
an if
expression in some other languages. If the CONDITION
is true, then the
CONSEQUENT
is evaluated. Otherwise, the ALTERNATIVE
is evaluated.
Syntax: CONDITION ? CONSEQUENT : ALTERNATIVE
Example: 2 = 2 ? 'Math works!' : 'Math is broken!'
Evaluates to the right-hand side of the first branch whose left-hand side
evaluates to true
. If no branch evaluates to true
, returns the right-hand
side of the else
branch (or return none
when the else
branch is omitted).
All expressions must evaluate to the same type. The else
branch is optional.
When present, the result will have the same type as the expressions. When
omitted, the result will be an optional type. To avoid ambiguity in the grammar,
the :
is required between the antecedent and consequent - this is a departure
from the map syntax.
Syntax:
cond {
CONDITION : EXPRESSION [,]
...
[else : EXPRESSION]
}
Examples (where n = 7
and word = 'fish'
):
cond {
n > 10 : "Too high!"
n < 5 : "Too low!"
else : "Just right!"
} = "Just right!"
cond {
word.starts('f') : 'f word'
} = some('f word')
Methods may return optional values. For example, map<T>.at(string) => T?
, when
the map's values are of type int
returns int?
(i.e. optionally int) because
a map might not contain the given key. All optional types must be immediately
resolved via use of the ??
operator to provide a default value. Expressions
must ultimately return a non-optional value. Example:
[1 2 3].find(|x| x >= 5) ?? 5
Evaluates to the right-hand side of the first branch whose left-hand side is
equal to the value of the given expression. If no branch is equal to the value
of the given expression, returns the right-hand side of the else
branch. The
else
branch is required to guarantee that the expression evaluates.
Syntax:
case EXPRESSION {
EXPRESSION : EXPRESSION [,]
...
else : EXPRESSION
}
Example:
case user.role.trim.lower {
"Owner" : 30
"Admin" : 20
else : 10
}
Lambdas are a special value type that doesn't have methods. It is only permissible to use a lambda as a method argument. The body of a lambda is an expression.
Syntax: | ARGUMENT* | EXPRESSION
Commas between arguments are optional.
Example: [1 2 3].fold(0, |sum x| sum + x)
- int (32) - Supports
012
for octal and#12
for hexadecimal - float (64)
- string (UTF-8)
- bool (
true
orfalse
)
(1 + 2) * 3 // 9 - syntactic sugar for the following example
1.+(2).*(3) // 9 - same as above, but weird
-1.abs // 1 - no parens required for methods without params
256.clamp(0, 255) // 255
25.6.floor // 25.0
25.6.ifloor // 25 - int conversion
25.6.ceil // 26.0
25.6.iceil // 26 - int conversion
(25.6).round // 26.0 - parens if it helps you sleep at night
25.6.iround // 26 - int conversion
25.pow(2) // 625
25.sqrt // 5
Note: For brevity, commas between compound data structure members are optional. Prefer no commas.
An indexed data structure where all members must have the same type.
[1 2 3]
or [1, 2, 3]
Where arr = [1 4 3 11]
:
arr.all(|x| x > 10) // false
arr.any(|x| x > 10) // true
arr.nany(|x| x > 10) // false i.e., not any
arr.keep(|x| x < 10) // [1 4 3]
arr.drop(|x| x < 10) // [11]
arr[0] ?? 0 // 1 - syntactic sugar for .at
arr.at(0) ?? 0 // 1
arr.set(0, 10) // [10 4 3 11]
arr.map(|x| x * x) // [1 16 9 121]
arr.slice(0, 3) // [1 4 3]
arr.rev // [11 3 4 1]
arr.sort // [1 3 4 11]
arr.rsort // [11 4 3 1]
arr.sortby(|x y| y - x) // [11 4 3 1]
arr.len // 4
arr.find(|x| x < 10) ?? 0 // 1
arr.rfind(|x| x < 10) ?? 0 // 3
arr.findi(|x| x < 10) ?? 0 // 0 i.e., find index of matching element
arr.rfindi(|x| x < 10) ?? 0 // 2 i.e., same thing, but start at the end
arr + [3 2 1] // [1 4 3 11 3 4 1]
arr = [3 2 1] // false
arr.join("-") // "1-4-3-11"
[myArray, myArray].flat // [1 4 3 11 1 4 3 11]
[1 2 2 2 3].uniq // [1 2 3]
[1 2 2 2 3].toset // {1 2 3}
A statically-typed, heterogenous, indexed data structure. Provides a lightweight form of user-defined data structure. A tuple must have, at minimum, 2 fields and may have, at most, 5 fields.
|1.25 true "cat"|
or |1.25, true, "cat"|
Where tpl = |"example.txt", 0755, "will"|
:
tpl.0 // "example.txt"
tpl.1 // 493
tpl.len // 3
A homogenous, unordered collection of unique values.
{0 1 3}
or {0, 1, 3}
Where a = {"A" "B" "C"}
and b = {"C" "D" "E"}
:
a.union(b) // {"A" "B" "C" "D" "E"}
a.diff(b) // {"A" "B"}
a.sdiff(b) // {"A" "B" "D" "E"} i.e. the symmetric difference
a.inter(b) // {"C"}
a.has("C") // true
a.has("D") // false
a.add("D") // {"A" "B" "C" "D"}
a.del("C") // {"A" "B"}
a.array // ["A" "B" "C"]
a.len // 3
A homogenous key/value collection where value is always a string.
{a: 0 b: 2}
or {a: 0, b: 2}
Where map = {red: #FF0000 green: #FF00 blue: #FF}
:
map.keys // ["red" "green" "blue"]
map.vals // [16711680 65280 255]
map.pairs // [{"red" 16711680} {"green" 65280} {"blue" 255}]
map.len // 3
map["red"] // 16711680 - syntactic sugar for .at
map.at("red") // 16711680
map.set("black", 0) // {red: 16811680 green: 65280 blue: 255 black: 0}
map.del("red") // {green: 65280 blue: 255}
map.has("green") // true
- buffer (array of bytes)
- point