mancha
Version:
Javscript HTML rendering engine
383 lines • 12.8 kB
JavaScript
/*
* @license
* Portions Copyright (c) 2013, the Dart project authors.
*/
const _BINARY_OPERATORS = {
"+": (a, b) => a + b,
"-": (a, b) => a - b,
"*": (a, b) => a * b,
"/": (a, b) => a / b,
"%": (a, b) => a % b,
// biome-ignore lint/suspicious/noDoubleEquals: must be loose equality
"==": (a, b) => a == b,
// biome-ignore lint/suspicious/noDoubleEquals: must be loose equality
"!=": (a, b) => a != b,
"===": (a, b) => a === b,
"!==": (a, b) => a !== b,
">": (a, b) => a > b,
">=": (a, b) => a >= b,
"<": (a, b) => a < b,
"<=": (a, b) => a <= b,
"||": (a, b) => a || b,
"&&": (a, b) => a && b,
"??": (a, b) => a ?? b,
"|": (a, f) => f(a),
in: (a, b) => a in b,
};
const _UNARY_OPERATORS = {
"+": (a) => a,
"-": (a) => -a,
"!": (a) => !a,
typeof: (a) => typeof a,
};
export class EvalAstFactory {
empty() {
// TODO(justinfagnani): return null instead?
return {
type: "Empty",
evaluate(scope) {
return scope;
},
getIds(idents) {
return idents;
},
};
}
// TODO(justinfagnani): just use a JS literal?
literal(v) {
return {
type: "Literal",
value: v,
evaluate(_scope) {
return this.value;
},
getIds(idents) {
return idents;
},
};
}
id(v) {
return {
type: "ID",
value: v,
evaluate(scope) {
// TODO(justinfagnani): this prevents access to properties named 'this'
if (this.value === "this")
return scope;
return scope?.[this.value];
},
getIds(idents) {
idents.push(this.value);
return idents;
},
};
}
unary(op, expr) {
const f = _UNARY_OPERATORS[op];
return {
type: "Unary",
operator: op,
child: expr,
evaluate(scope) {
return f(this.child.evaluate(scope));
},
getIds(idents) {
return this.child.getIds(idents);
},
};
}
binary(l, op, r) {
const f = _BINARY_OPERATORS[op];
return {
type: "Binary",
operator: op,
left: l,
right: r,
evaluate(scope) {
if (this.operator === "=") {
if (this.left.type !== "ID" &&
this.left.type !== "Getter" &&
this.left.type !== "Index") {
throw new Error(`Invalid assignment target: ${this.left}`);
}
const value = this.right.evaluate(scope);
let receiver;
let property;
if (this.left.type === "Getter") {
receiver = this.left.receiver.evaluate(scope);
property = this.left.name;
}
else if (this.left.type === "Index") {
receiver = this.left.receiver.evaluate(scope);
property = String(this.left.argument.evaluate(scope));
}
else if (this.left.type === "ID") {
// TODO: the id could be a parameter
receiver = scope;
property = this.left.value;
}
if (receiver === undefined)
return undefined;
receiver[property] = value;
return value;
}
return f(this.left.evaluate(scope), this.right.evaluate(scope));
},
getIds(idents) {
this.left.getIds(idents);
this.right.getIds(idents);
return idents;
},
};
}
getter(g, n, optional) {
return {
type: "Getter",
receiver: g,
name: n,
optional,
evaluate(scope) {
const receiver = this.receiver.evaluate(scope);
if (this.optional && (receiver === null || receiver === undefined)) {
return undefined;
}
return receiver?.[this.name];
},
getIds(idents) {
this.receiver.getIds(idents);
return idents;
},
};
}
invoke(receiver, method, args, optional) {
if (method != null && typeof method !== "string") {
throw new Error("method not a string");
}
return {
type: "Invoke",
receiver: receiver,
method: method,
arguments: args,
optional,
evaluate(scope) {
const receiver = this.receiver.evaluate(scope);
if (this.optional && (receiver === null || receiver === undefined)) {
return undefined;
}
// TODO(justinfagnani): this might be wrong in cases where we're
// invoking a top-level function rather than a method. If method is
// defined on a nested scope, then we should probably set _this to null.
const _this = this.method ? receiver : (scope?.this ?? scope);
const f = this.method ? receiver?.[this.method] : receiver;
const args = this.arguments ?? [];
const argValues = [];
for (const arg of args) {
if (arg?.type === "SpreadElement") {
const spreadVal = arg.evaluate(scope);
if (spreadVal &&
typeof spreadVal[Symbol.iterator] === "function") {
argValues.push(...spreadVal);
}
}
else {
argValues.push(arg?.evaluate(scope));
}
}
return f?.apply?.(_this, argValues);
},
getIds(idents) {
this.receiver.getIds(idents);
this.arguments?.forEach((a) => {
a?.getIds(idents);
});
return idents;
},
};
}
paren(e) {
return e;
}
index(e, a, optional) {
return {
type: "Index",
receiver: e,
argument: a,
optional,
evaluate(scope) {
const receiver = this.receiver.evaluate(scope);
if (this.optional && (receiver === null || receiver === undefined)) {
return undefined;
}
const index = this.argument.evaluate(scope);
return receiver?.[index];
},
getIds(idents) {
this.receiver.getIds(idents);
return idents;
},
};
}
ternary(c, t, f) {
return {
type: "Ternary",
condition: c,
trueExpr: t,
falseExpr: f,
evaluate(scope) {
const c = this.condition.evaluate(scope);
if (c) {
return this.trueExpr.evaluate(scope);
}
else {
return this.falseExpr.evaluate(scope);
}
},
getIds(idents) {
this.condition.getIds(idents);
this.trueExpr.getIds(idents);
this.falseExpr.getIds(idents);
return idents;
},
};
}
map(properties) {
return {
type: "Map",
properties,
evaluate(scope) {
const map = {};
if (properties && this.properties) {
for (const prop of this.properties) {
if (prop.type === "SpreadProperty") {
Object.assign(map, prop.evaluate(scope));
}
else {
map[prop.key] = prop.value.evaluate(scope);
}
}
}
return map;
},
getIds(idents) {
if (properties && this.properties) {
for (const prop of this.properties) {
if (prop.type === "SpreadProperty") {
prop.expression.getIds(idents);
}
else {
prop.value.getIds(idents);
}
}
}
return idents;
},
};
}
property(key, value) {
return {
type: "Property",
key,
value,
evaluate(scope) {
return this.value.evaluate(scope);
},
getIds(idents) {
return this.value.getIds(idents);
},
};
}
list(l) {
return {
type: "List",
items: l,
evaluate(scope) {
if (!this.items)
return [];
const result = [];
for (const item of this.items) {
if (item?.type === "SpreadElement") {
const spreadVal = item.evaluate(scope);
if (spreadVal &&
typeof spreadVal[Symbol.iterator] === "function") {
result.push(...spreadVal);
}
}
else {
result.push(item?.evaluate(scope));
}
}
return result;
},
getIds(idents) {
this.items?.forEach((i) => {
i?.getIds(idents);
});
return idents;
},
};
}
arrowFunction(params, body) {
return {
type: "ArrowFunction",
params,
body,
evaluate(scope) {
const params = this.params;
const body = this.body;
return (...args) => {
// TODO: this isn't correct for assignments to variables in outer
// scopes
// const newScope = Object.create(scope ?? null);
const paramsObj = Object.fromEntries(params.map((p, i) => [p, args[i]]));
const newScope = new Proxy(scope ?? {}, {
set(target, prop, value) {
if (Object.hasOwn(paramsObj, prop)) {
paramsObj[prop] = value;
}
target[prop] = value;
return value;
},
get(target, prop) {
if (Object.hasOwn(paramsObj, prop)) {
return paramsObj[prop];
}
return target[prop];
},
});
return body.evaluate(newScope);
};
},
getIds(idents) {
// Only return the _free_ variables in the body. Since arrow function
// parameters are the only way to introduce new variable names, we can
// assume that any variable in the body that isn't a parameter is free.
return this.body.getIds(idents).filter((id) => !this.params.includes(id));
},
};
}
spreadProperty(expression) {
return {
type: "SpreadProperty",
expression,
evaluate(scope) {
return this.expression.evaluate(scope);
},
getIds(idents) {
return this.expression.getIds(idents);
},
};
}
spreadElement(expression) {
return {
type: "SpreadElement",
expression,
evaluate(scope) {
return this.expression.evaluate(scope);
},
getIds(idents) {
return this.expression.getIds(idents);
},
};
}
}
//# sourceMappingURL=eval.js.map