Environment

metaES allows direct interaction with host environment. Host environment is the environment belonging to interpreter which is used to run metaES. Access to host environment in meta-circular evaluator is instant as the client evaluator shares runtime environment with the host.

For example:

let user = { name: "User1" };
metaesEval(`user.name="User2"`, console.log, console.error, { user });
console.log(user);

At the end, user.name will be equal to User2. However, if you try to access another variable:

let user = { name: "User1" };
let user2 = { name: "User2" };
metaesEval(`user2.name="User2"`, console.log, console.error, { user, user3 });

metaES will throw a ReferenceError.

Those were examples of environment with shortcut object syntax, which internally were converted to regular environments. Regular environment consists of two fields: values (shortcut values go here) and optional prev, which contains reference to outer environment. It works similarly to prototypical inheritance in ECMAScript.

Let's see:

let environment = { values: { a: 1 } };
metaesEval("a*2", console.log, console.error, environment);

You may want to create environments chain and metaES does it anyway the same way as JavaScript does:

let environment0 = { values: { b: 2 } };
let environment = { values: { a: 1 }, prev: environment0 };
metaesEval("a*b", console.log, console.error, environment);

metaES will recursively look for variables until prev field exists. Environment without prev acts as a global environment. If variable is not found, metaesES will throw an error.

There is a helper for environment creation - createEnvironment(value, previous) which should be used almost always instead of creating key-valued object from scratch. Updated example will look as the following:

let environment0 = createEnvironment({ b: 2 });
let environment = createEnvironment({ a: 1 }, environment0);
metaesEval("a*b", console.log, console.error, environment);

Variables shadowing works as expected. Try to play with values of environment0 and environment and shadow b variable from environment0.

Closures are environment

Let's try something else now, let's use metaFunction:

let fn;
let b = 3;
metaesEval(
  // this function will be converted to metaFunction using it's source code
  function (x) {
    return x * x * b;
  },
  (result) => (fn = result),
  console.error
);
fn(2); // ReferenceError: "b" is not defined

b is there defined on line 2. However, it's not visible for meta-circular function. Remember that function was stringified using its .toString() method. b in closure can be fixed in a following manual way:

let fn;
let b = 3;
metaesEval(
  function (x) {
    return x * x * b;
  },
  (result) => (fn = result),
  console.error,
  { b }
);
fn(2); // 12

Now b belongs to fn closure, because it was there at the time of function creation.

Trying to cheat environment and adding b on-the-fly doesn't work:

let fn;
let b = 3;
metaesEval(
  function (x) {
    return x * x * b;
  },
  (result) => (fn = result),
  console.error
);

metaesEval(
  // [1]
  `fn(2)`,
  console.log,
  console.error,
  {
    fn,
    // does not work
    b
  }
);

This is because both metaES (in default config) and ECMAScript support static variable binding, not dynamic. It means the variables in environment do not flow down to the function automatically. If variables weren't captured in environments chain during creation of function, they won't be available in function body during execution.

Another fact worth observing is you can pass around meta-circular metaES functions as if there were normal functions (becase they are). This pasing already happened in [1].

typeof fn === "function"; // true

To prove the point even further, you can try to use meta-circular function as a mapping function in Array.prototype.map in example like:

let fn;
let b = 3;
metaesEval(
  function (x) {
    return x * x * b;
  },
  (result) => (fn = result),
  console.error,
  { b }
);
[1, 2, 3].map(fn); // [3, 12, 27]

One more note: there is hacky way to add missing b to closure:

let fn;
let b = 3;
metaesEval(
  function (x) {
    return x * x * b;
  },
  (result) => (fn = result)
);
getMetaFunction(fn).closure.values.b = b;
fn(2);