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