git @ Cat's Eye Technologies concoctor / 0.1
0.1

Tree @0.1 (Download .tar.gz)

concoctor

Version 0.1 - subject to change radically without notice

concoctor is a framework for defining functions from within JavaScript using an embedded language. The framework tracks the potential computational effects of each function - for example, whether it might mutate data, fail to terminate, have randomly-determined output, etc. - and aims to ensure that newly-constructed functions do not have effects that their author intends them not to have.

concoctor is language-agnostic; the embedded language can be any language for which a compiler has been written that follows the concoctor interface. The function definitions in this language are typically written directly in the JavaScript source file, and the constructed functions can be called directly from JavaScript code, permitting interoperability with existing JavaScript code and APIs.

Motivation / Case Study

concoctor was designed to allow existing JavaScript code to be gradually migrated to some other language, function by function.

The Jaft language was designed to be a pure and total functional language which is nearly a subset of JavaScript, intended to be easier to analyze, and easy to interoperate with, and manually convert from, existing JavaScript code.

In Jaft itself, some effects are always considered undesirable. Those effects are mutation of accessible state (i.e. non-pure), and non-termination (i.e. non-total). In systems that use Jaft however, other effects, such as randomness, may at times be desirable.

Thus there was a need for a framework in which we could expose, to JavaScript, functions restricted in their effects. Thus concoctor was built. "Restricted in its effects" means we can easily require a concocted function to be total (always terminates on every input), or pure (has no side-effects), or otherwise referentially transparent (no randomness, no exceptions, etc.) To this end, when concocting functions, we need to declare these effects.

A Jaft to JavaScript compiler which follows the concoctor interface was written (Footnote 1). concoctor and Jaft were used together to convert the game logic of the toy JavaScript video game Cosmos Boulders to Jaft, for higher assurance that it is a pure and total function (modulo some intentional effects like randomness).

How to Use it

The concoctor module exposes two functions, bless and concoct.

bless

import { bless } from '../concoctor.js';

Given a function's name, arity, and a function object implementing it, bless exposes it as a callable function attested to be pure and total:

const trunc = bless(Math.trunc);
const mul = bless(function(a, b) { return a * b; });

console.log(mul(trunc(1.3), trunc(2.9)));

By doing this, the programmer attests that the function is pure (has no side-effects) and total (always terminates). Registering a non-pure or partial function to the context violates the contract; the programmer does so at their own peril.

concoct

The function concoct is exported from concoctor.js, but it is a raw interface, and requires a particular compiler to be supplied via [dependency injection][]. This makes it difficult to demonstrate by itself.

A compiler from Jaft to JavaScript has been written:

import { compileJaft } from '../jaftCompiler.js';

For convenience, a specialized concoctor that uses this compiler is also available.

import { concoctJaft as concoct } from '../jaftConcoctor.js';

We will use this concoctor to demonstrate how concoct works.

Given a context (a map of function names to blessed functions), a new function's name and arity, and a string containing the function's definition written in the language understood by the supplied compiler, concoct compiles it and returns it as a callable blessed function:

const square = concoct(
    { mul }, '(a) => mul(a, a)'
);
const multrnc = concoct(
    { mul, trunc }, '(a, b) => mul(trunc(a), trunc(b))'
);

Functions so defined can only call functions that are present in the supplied context. In this way, they are pure and total by construction. So, in this example, the following would raise an exception, because trunc is not registered:

const dbltrnc = concoct(
    { mul }, '(a) => mul(trunc(a), 2)'
);

Note that these expressions refer to functions by their name in the context that was passed in to concoct, which need not be the same name as the JavaScript variable that contains them (Footnote 2).

Functions and Signatures

Functions can be passed in to blessed or concocted functions, but the functions must be delcared to take a function in that argument position (i.e. must be given a signature which gives the type of that argument to be function), and the function so passed in must be blessed or concocted (to avoid evaluating a non-total or non-pure function, causing the concocted function to itself not be total or pure.)

(remainder of this section TBW)

TODO

  • track effects individually. Consider nontermination and mutation to be two basic effects (the ones Jaft avoids). Functions inherit effects of functions that they call.
  • concoctMany - this would be a convenient function, but is not necessary. It dovetails with the idea of the library exposing only a "concoctor" function, which when called, returns a "concoct" function, appropriate configured (which in this case means: the concrete language has been set up; and a default context may be supplied.)

Footnote 1

We are actively working towards a better separation between Jaft and concoctor. They were initially developed together, so there is some historical entanglement there. But concoctor is fundamentally language-agnostic, so it is now its own project. But concoctor needs a language of some kind in order to be demonstrated and tested, and the language of choice for that is currently Jaft. In particular, the unit tests for concoctor still rely on Jaft, and are in fact located in the Jaft repository and not, as would be appropriate, in the concoctor repository.

Footnote 2

This design decision stems from the fact that it is not possible to resolve strings to names of local variables in JavaScript. (TODO: write more about how that limitation leads to this)