-
Notifications
You must be signed in to change notification settings - Fork 3
User Defined Operators
Khepri allows the programer to define and use custom prefix unary and infix binary operators. User defined operators behave just like normal ECMAScript operators, and give the language additional expressive power.
A user defined prefix operator must start with one of: ~
, !
, ++
, --
, followed by zero or more of the characters: ?+-*/%|&^<>=!~@
Prefix ops all have the same precedence.
During lexing and parsing, Khepri tokenizes prefix ops without whitespace separators as compound ops. The compiler splits compound ops by matching the longest available operators:
var (!~) = (_ > 0);
!!~!!~~3;
// Is evaluated as
! !~ ! !~ ~ 3;
A user defined infix operator must start with a builtin Khepri infix operator, followed by zero or more of the characters: ?+-*/%|&^<>=!~@
.
A user defined infix operator may not be: ->
, -|
, |-
which have a special meaning in the language. But you can use ops like ->-
or -|+
.
The following builtin operators may be extended:
+
-
*
/
%
-
&&
// user ops cannot short circuit ||
??
<<
>>
>>>
<=
<
>=
>
===
!==
==
!=
&
|
^
|>
<|
\>
\>>
<\
<<\
Infix operators inherit their precedence and associativity from the builtin Khepri operator they start with:
// The prefix for this is `+` so it has same precedence as `+`
var (+|+) = Array.prototype.concat.bind [];
// Prefix for this is `<`, same precedence as `<`
var (<*>) = \f x -> Array.prototype.map.call(x, f);
[1, 2, 3] +|+ (*2) <*> [4, 5, 6] +|+ [7, 8, 9];
// Groups To
[1, 2, 3] +|+ ((*2) <*> ([4, 5, 6] +|+ [7, 8, 9]));
Unlike prefix ops, the compiler will not group or split infix ops. This includes the case where a binary could be split into a binary and unary.
// You need a space between `+` and `!`.
// Otherwise the compiler thinks you are using a `+!` infix op.
1 +!false;
User defined operators may be defined in a variable declaration, let expression, or with statement. Prefix operators should be bound to unary functions, infix operators to binary functions.
Simply wrap the operator in parens:
var (**) := Math.pow;
let (**) = Math.pow in ...;
with (**) = Math.pow in { ... }
User defined ops may also appear as function parameter:
var do := \(+) -> \a b -> a + b;
var f = do \ x y -> [x, y];
f(1, 2); // [1, 2]
Or the operator maybe part of a binding unpack:
let [(+) (-) (*) (/) (%)] = math in ...;
Packages can import and export any user defined operator:
package (
(**))
{
(**) := Math.pow;
}
package with
import 'math' {
(**)}
{
2 ** 3; // 8
}
Operators follow the same lexical scoping rules as identifiers.
Like identifiers, user defined operators resolve to the current value of the operator (although only using immutable bindings is strongly encouraged, especially for operators):
var (+) = \x y -> [x, y];
1 + 2; // [1, 2];
(+) = (*);
1 + 2; // 2
You can locally hide/overload any builtin ECMAScript operator.
var (===) = \x y -> x.eq(y);
a === b && c === d;
// outputs ----
a.eq(b) && c.eq(d);
This can also be restricted to a local scope:
1 + (let (+) = (-) in 2 + 3 + 4) + 5;
// outputs ----
1 + 2 - 3 - 4 + 5
You can convert your fancy operators back to functions too:
var (**) = Math.pow;
(**)(2, 3); // 8
[1, 2, 3].map (** 2); // [2, 4, 8]
Binary ops can also be flipped and curried:
var (**) = Math.pow;
[1, 2, 3].map (_ ** 2); // [1, 4, 9]
User defined ops evaluate to, at worst, a single function call per operator. When the user defined operator is a lambda function, inlining may remove the call overhead entirely.
// Input ----
static a, b, c, d;
var (===) = \x y -> x.eq(y);
a === b && c === d;
// Compiler Output ----
var x, y, x0, y0;
(((x = a), (y = b), x.eq(y)) && ((x0 = c), (y0 = d), x0.eq(y0)));
User defined ops add at most one function per declaration. As the above example shows, this is pruned if found to be unreachable after inlining.