Skip to content

User Defined Operators

mattbierner edited this page Nov 23, 2014 · 3 revisions

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.

Operation Declaration

Prefix Op

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.

Splitting

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;

Infix Op

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
  • ||
  • ??
  • <<
  • >>
  • >>>
  • <=
  • <
  • >=
  • >
  • ===
  • !==
  • ==
  • !=
  • &
  • |
  • ^
  • |>
  • <|
  • \>
  • \>>
  • <\
  • <<\

Precedence

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]));

Whitespace

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;

Usage

Declaration

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 { ... }

As a Parameter / Subunpack

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 ...;

Imports/Exports

Packages can import and export any user defined operator:

package (
   (**))
{
   (**) := Math.pow;
}
package with
     import 'math' {
         (**)}
{
   2 ** 3; // 8
}

Scoping

Operators follow the same lexical scoping rules as identifiers.

Mutation

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

Hiding Existing Ops

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

Operator To Function

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]

Overhead

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.