chore: update node_modules with new binary files and dependencies
- Add new binary files for nodemon, onnxruntime-web, and xenova/transformers - Update various JavaScript and TypeScript files in node_modules - Remove unused files and dependencies - Add new test fixtures and documentation files
This commit is contained in:
918
node_modules/@huggingface/jinja/src/runtime.ts
generated
vendored
Normal file
918
node_modules/@huggingface/jinja/src/runtime.ts
generated
vendored
Normal file
@@ -0,0 +1,918 @@
|
||||
import type {
|
||||
NumericLiteral,
|
||||
StringLiteral,
|
||||
BooleanLiteral,
|
||||
ArrayLiteral,
|
||||
Statement,
|
||||
Program,
|
||||
If,
|
||||
For,
|
||||
SetStatement,
|
||||
MemberExpression,
|
||||
CallExpression,
|
||||
Identifier,
|
||||
BinaryExpression,
|
||||
FilterExpression,
|
||||
TestExpression,
|
||||
UnaryExpression,
|
||||
SliceExpression,
|
||||
KeywordArgumentExpression,
|
||||
ObjectLiteral,
|
||||
TupleLiteral,
|
||||
} from "./ast";
|
||||
import { slice, titleCase } from "./utils";
|
||||
|
||||
export type AnyRuntimeValue =
|
||||
| NumericValue
|
||||
| StringValue
|
||||
| BooleanValue
|
||||
| ObjectValue
|
||||
| ArrayValue
|
||||
| FunctionValue
|
||||
| NullValue
|
||||
| UndefinedValue;
|
||||
|
||||
/**
|
||||
* Abstract base class for all Runtime values.
|
||||
* Should not be instantiated directly.
|
||||
*/
|
||||
abstract class RuntimeValue<T> {
|
||||
type = "RuntimeValue";
|
||||
value: T;
|
||||
|
||||
/**
|
||||
* A collection of built-in functions for this type.
|
||||
*/
|
||||
builtins = new Map<string, AnyRuntimeValue>();
|
||||
|
||||
/**
|
||||
* Creates a new RuntimeValue.
|
||||
*/
|
||||
constructor(value: T = undefined as unknown as T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines truthiness or falsiness of the runtime value.
|
||||
* This function should be overridden by subclasses if it has custom truthiness criteria.
|
||||
* @returns {BooleanValue} BooleanValue(true) if the value is truthy, BooleanValue(false) otherwise.
|
||||
*/
|
||||
__bool__(): BooleanValue {
|
||||
return new BooleanValue(!!this.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a numeric value at runtime.
|
||||
*/
|
||||
export class NumericValue extends RuntimeValue<number> {
|
||||
override type = "NumericValue";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a string value at runtime.
|
||||
*/
|
||||
export class StringValue extends RuntimeValue<string> {
|
||||
override type = "StringValue";
|
||||
|
||||
override builtins = new Map<string, AnyRuntimeValue>([
|
||||
[
|
||||
"upper",
|
||||
new FunctionValue(() => {
|
||||
return new StringValue(this.value.toUpperCase());
|
||||
}),
|
||||
],
|
||||
[
|
||||
"lower",
|
||||
new FunctionValue(() => {
|
||||
return new StringValue(this.value.toLowerCase());
|
||||
}),
|
||||
],
|
||||
[
|
||||
"strip",
|
||||
new FunctionValue(() => {
|
||||
return new StringValue(this.value.trim());
|
||||
}),
|
||||
],
|
||||
[
|
||||
"title",
|
||||
new FunctionValue(() => {
|
||||
return new StringValue(titleCase(this.value));
|
||||
}),
|
||||
],
|
||||
["length", new NumericValue(this.value.length)],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a boolean value at runtime.
|
||||
*/
|
||||
export class BooleanValue extends RuntimeValue<boolean> {
|
||||
override type = "BooleanValue";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an Object value at runtime.
|
||||
*/
|
||||
export class ObjectValue extends RuntimeValue<Map<string, AnyRuntimeValue>> {
|
||||
override type = "ObjectValue";
|
||||
|
||||
/**
|
||||
* NOTE: necessary to override since all JavaScript arrays are considered truthy,
|
||||
* while only non-empty Python arrays are consider truthy.
|
||||
*
|
||||
* e.g.,
|
||||
* - JavaScript: {} && 5 -> 5
|
||||
* - Python: {} and 5 -> {}
|
||||
*/
|
||||
override __bool__(): BooleanValue {
|
||||
return new BooleanValue(this.value.size > 0);
|
||||
}
|
||||
|
||||
override builtins: Map<string, AnyRuntimeValue> = new Map<string, AnyRuntimeValue>([
|
||||
[
|
||||
"get",
|
||||
new FunctionValue(([key, defaultValue]) => {
|
||||
if (!(key instanceof StringValue)) {
|
||||
throw new Error(`Object key must be a string: got ${key.type}`);
|
||||
}
|
||||
return this.value.get(key.value) ?? defaultValue ?? new NullValue();
|
||||
}),
|
||||
],
|
||||
[
|
||||
"items",
|
||||
new FunctionValue(() => {
|
||||
return new ArrayValue(
|
||||
Array.from(this.value.entries()).map(([key, value]) => new ArrayValue([new StringValue(key), value]))
|
||||
);
|
||||
}),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an Array value at runtime.
|
||||
*/
|
||||
export class ArrayValue extends RuntimeValue<AnyRuntimeValue[]> {
|
||||
override type = "ArrayValue";
|
||||
override builtins = new Map<string, AnyRuntimeValue>([["length", new NumericValue(this.value.length)]]);
|
||||
|
||||
/**
|
||||
* NOTE: necessary to override since all JavaScript arrays are considered truthy,
|
||||
* while only non-empty Python arrays are consider truthy.
|
||||
*
|
||||
* e.g.,
|
||||
* - JavaScript: [] && 5 -> 5
|
||||
* - Python: [] and 5 -> []
|
||||
*/
|
||||
override __bool__(): BooleanValue {
|
||||
return new BooleanValue(this.value.length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Tuple value at runtime.
|
||||
* NOTE: We extend ArrayValue since JavaScript does not have a built-in Tuple type.
|
||||
*/
|
||||
export class TupleValue extends ArrayValue {
|
||||
override type = "TupleValue";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Function value at runtime.
|
||||
*/
|
||||
export class FunctionValue extends RuntimeValue<(args: AnyRuntimeValue[], scope: Environment) => AnyRuntimeValue> {
|
||||
override type = "FunctionValue";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Null value at runtime.
|
||||
*/
|
||||
export class NullValue extends RuntimeValue<null> {
|
||||
override type = "NullValue";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an Undefined value at runtime.
|
||||
*/
|
||||
export class UndefinedValue extends RuntimeValue<undefined> {
|
||||
override type = "UndefinedValue";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the current environment (scope) at runtime.
|
||||
*/
|
||||
export class Environment {
|
||||
/**
|
||||
* The variables declared in this environment.
|
||||
*/
|
||||
variables: Map<string, AnyRuntimeValue> = new Map([
|
||||
[
|
||||
"namespace",
|
||||
new FunctionValue((args) => {
|
||||
if (args.length === 0) {
|
||||
return new ObjectValue(new Map());
|
||||
}
|
||||
if (args.length !== 1 || !(args[0] instanceof ObjectValue)) {
|
||||
throw new Error("`namespace` expects either zero arguments or a single object argument");
|
||||
}
|
||||
return args[0];
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
* The tests available in this environment.
|
||||
*/
|
||||
tests: Map<string, (...value: AnyRuntimeValue[]) => boolean> = new Map([
|
||||
["boolean", (operand) => operand.type === "BooleanValue"],
|
||||
["callable", (operand) => operand instanceof FunctionValue],
|
||||
[
|
||||
"odd",
|
||||
(operand) => {
|
||||
if (operand.type !== "NumericValue") {
|
||||
throw new Error(`Cannot apply test "odd" to type: ${operand.type}`);
|
||||
}
|
||||
return (operand as NumericValue).value % 2 !== 0;
|
||||
},
|
||||
],
|
||||
[
|
||||
"even",
|
||||
(operand) => {
|
||||
if (operand.type !== "NumericValue") {
|
||||
throw new Error(`Cannot apply test "even" to type: ${operand.type}`);
|
||||
}
|
||||
return (operand as NumericValue).value % 2 === 0;
|
||||
},
|
||||
],
|
||||
["false", (operand) => operand.type === "BooleanValue" && !(operand as BooleanValue).value],
|
||||
["true", (operand) => operand.type === "BooleanValue" && (operand as BooleanValue).value],
|
||||
["number", (operand) => operand.type === "NumericValue"],
|
||||
["integer", (operand) => operand.type === "NumericValue" && Number.isInteger((operand as NumericValue).value)],
|
||||
["iterable", (operand) => operand instanceof ArrayValue || operand instanceof StringValue],
|
||||
[
|
||||
"lower",
|
||||
(operand) => {
|
||||
const str = (operand as StringValue).value;
|
||||
return operand.type === "StringValue" && str === str.toLowerCase();
|
||||
},
|
||||
],
|
||||
[
|
||||
"upper",
|
||||
(operand) => {
|
||||
const str = (operand as StringValue).value;
|
||||
return operand.type === "StringValue" && str === str.toUpperCase();
|
||||
},
|
||||
],
|
||||
["none", (operand) => operand.type === "NullValue"],
|
||||
["defined", (operand) => operand.type !== "UndefinedValue"],
|
||||
["undefined", (operand) => operand.type === "UndefinedValue"],
|
||||
["equalto", (a, b) => a.value === b.value],
|
||||
]);
|
||||
|
||||
constructor(public parent?: Environment) {}
|
||||
|
||||
/**
|
||||
* Set the value of a variable in the current environment.
|
||||
*/
|
||||
set(name: string, value: unknown): AnyRuntimeValue {
|
||||
return this.declareVariable(name, convertToRuntimeValues(value));
|
||||
}
|
||||
|
||||
private declareVariable(name: string, value: AnyRuntimeValue): AnyRuntimeValue {
|
||||
if (this.variables.has(name)) {
|
||||
throw new SyntaxError(`Variable already declared: ${name}`);
|
||||
}
|
||||
this.variables.set(name, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// private assignVariable(name: string, value: AnyRuntimeValue): AnyRuntimeValue {
|
||||
// const env = this.resolve(name);
|
||||
// env.variables.set(name, value);
|
||||
// return value;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Set variable in the current scope.
|
||||
* See https://jinja.palletsprojects.com/en/3.0.x/templates/#assignments for more information.
|
||||
*/
|
||||
setVariable(name: string, value: AnyRuntimeValue): AnyRuntimeValue {
|
||||
this.variables.set(name, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the environment in which the variable is declared.
|
||||
* @param {string} name The name of the variable.
|
||||
* @returns {Environment} The environment in which the variable is declared.
|
||||
*/
|
||||
private resolve(name: string): Environment {
|
||||
if (this.variables.has(name)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Traverse scope chain
|
||||
if (this.parent) {
|
||||
return this.parent.resolve(name);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown variable: ${name}`);
|
||||
}
|
||||
|
||||
lookupVariable(name: string): AnyRuntimeValue {
|
||||
try {
|
||||
return this.resolve(name).variables.get(name) ?? new UndefinedValue();
|
||||
} catch {
|
||||
return new UndefinedValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Interpreter {
|
||||
global: Environment;
|
||||
|
||||
constructor(env?: Environment) {
|
||||
this.global = env ?? new Environment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the program.
|
||||
*/
|
||||
run(program: Program): AnyRuntimeValue {
|
||||
return this.evaluate(program, this.global);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates expressions following the binary operation type.
|
||||
*/
|
||||
private evaluateBinaryExpression(node: BinaryExpression, environment: Environment): AnyRuntimeValue {
|
||||
const left = this.evaluate(node.left, environment);
|
||||
|
||||
// Logical operators
|
||||
// NOTE: Short-circuiting is handled by the `evaluate` function
|
||||
switch (node.operator.value) {
|
||||
case "and":
|
||||
return left.__bool__().value ? this.evaluate(node.right, environment) : left;
|
||||
case "or":
|
||||
return left.__bool__().value ? left : this.evaluate(node.right, environment);
|
||||
}
|
||||
|
||||
// Equality operators
|
||||
const right = this.evaluate(node.right, environment);
|
||||
switch (node.operator.value) {
|
||||
case "==":
|
||||
return new BooleanValue(left.value == right.value);
|
||||
case "!=":
|
||||
return new BooleanValue(left.value != right.value);
|
||||
}
|
||||
|
||||
if (left instanceof UndefinedValue || right instanceof UndefinedValue) {
|
||||
throw new Error("Cannot perform operation on undefined values");
|
||||
} else if (left instanceof NullValue || right instanceof NullValue) {
|
||||
throw new Error("Cannot perform operation on null values");
|
||||
} else if (left instanceof NumericValue && right instanceof NumericValue) {
|
||||
// Evaulate pure numeric operations with binary operators.
|
||||
switch (node.operator.value) {
|
||||
// Arithmetic operators
|
||||
case "+":
|
||||
return new NumericValue(left.value + right.value);
|
||||
case "-":
|
||||
return new NumericValue(left.value - right.value);
|
||||
case "*":
|
||||
return new NumericValue(left.value * right.value);
|
||||
case "/":
|
||||
return new NumericValue(left.value / right.value);
|
||||
case "%":
|
||||
return new NumericValue(left.value % right.value);
|
||||
|
||||
// Comparison operators
|
||||
case "<":
|
||||
return new BooleanValue(left.value < right.value);
|
||||
case ">":
|
||||
return new BooleanValue(left.value > right.value);
|
||||
case ">=":
|
||||
return new BooleanValue(left.value >= right.value);
|
||||
case "<=":
|
||||
return new BooleanValue(left.value <= right.value);
|
||||
}
|
||||
} else if (left instanceof ArrayValue && right instanceof ArrayValue) {
|
||||
// Evaluate array operands with binary operator.
|
||||
switch (node.operator.value) {
|
||||
case "+":
|
||||
return new ArrayValue(left.value.concat(right.value));
|
||||
}
|
||||
} else if (right instanceof ArrayValue) {
|
||||
const member = right.value.find((x) => x.value === left.value) !== undefined;
|
||||
switch (node.operator.value) {
|
||||
case "in":
|
||||
return new BooleanValue(member);
|
||||
case "not in":
|
||||
return new BooleanValue(!member);
|
||||
}
|
||||
}
|
||||
|
||||
if (left instanceof StringValue || right instanceof StringValue) {
|
||||
// Support string concatenation as long as at least one operand is a string
|
||||
switch (node.operator.value) {
|
||||
case "+":
|
||||
return new StringValue(left.value.toString() + right.value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (left instanceof StringValue && right instanceof StringValue) {
|
||||
switch (node.operator.value) {
|
||||
case "in":
|
||||
return new BooleanValue(right.value.includes(left.value));
|
||||
case "not in":
|
||||
return new BooleanValue(!right.value.includes(left.value));
|
||||
}
|
||||
}
|
||||
|
||||
if (left instanceof StringValue && right instanceof ObjectValue) {
|
||||
switch (node.operator.value) {
|
||||
case "in":
|
||||
return new BooleanValue(right.value.has(left.value));
|
||||
case "not in":
|
||||
return new BooleanValue(!right.value.has(left.value));
|
||||
}
|
||||
}
|
||||
|
||||
throw new SyntaxError(`Unknown operator "${node.operator.value}" between ${left.type} and ${right.type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates expressions following the filter operation type.
|
||||
*/
|
||||
private evaluateFilterExpression(node: FilterExpression, environment: Environment): AnyRuntimeValue {
|
||||
const operand = this.evaluate(node.operand, environment);
|
||||
|
||||
// For now, we only support the built-in filters
|
||||
// TODO: Add support for non-identifier filters
|
||||
// e.g., functions which return filters: {{ numbers | select("odd") }}
|
||||
// TODO: Add support for user-defined filters
|
||||
// const filter = environment.lookupVariable(node.filter.value);
|
||||
// if (!(filter instanceof FunctionValue)) {
|
||||
// throw new Error(`Filter must be a function: got ${filter.type}`);
|
||||
// }
|
||||
// return filter.value([operand], environment);
|
||||
|
||||
// https://jinja.palletsprojects.com/en/3.0.x/templates/#list-of-builtin-filters
|
||||
|
||||
if (node.filter.type === "Identifier") {
|
||||
const filter = node.filter as Identifier;
|
||||
|
||||
if (operand instanceof ArrayValue) {
|
||||
switch (filter.value) {
|
||||
case "list":
|
||||
return operand;
|
||||
case "first":
|
||||
return operand.value[0];
|
||||
case "last":
|
||||
return operand.value[operand.value.length - 1];
|
||||
case "length":
|
||||
return new NumericValue(operand.value.length);
|
||||
case "reverse":
|
||||
return new ArrayValue(operand.value.reverse());
|
||||
case "sort":
|
||||
return new ArrayValue(
|
||||
operand.value.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
throw new Error(`Cannot compare different types: ${a.type} and ${b.type}`);
|
||||
}
|
||||
switch (a.type) {
|
||||
case "NumericValue":
|
||||
return (a as NumericValue).value - (b as NumericValue).value;
|
||||
case "StringValue":
|
||||
return (a as StringValue).value.localeCompare((b as StringValue).value);
|
||||
default:
|
||||
throw new Error(`Cannot compare type: ${a.type}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unknown ArrayValue filter: ${filter.value}`);
|
||||
}
|
||||
} else if (operand instanceof StringValue) {
|
||||
switch (filter.value) {
|
||||
case "length":
|
||||
return new NumericValue(operand.value.length);
|
||||
case "upper":
|
||||
return new StringValue(operand.value.toUpperCase());
|
||||
case "lower":
|
||||
return new StringValue(operand.value.toLowerCase());
|
||||
case "title":
|
||||
return new StringValue(titleCase(operand.value));
|
||||
case "capitalize":
|
||||
return new StringValue(operand.value.charAt(0).toUpperCase() + operand.value.slice(1));
|
||||
case "trim":
|
||||
return new StringValue(operand.value.trim());
|
||||
default:
|
||||
throw new Error(`Unknown StringValue filter: ${filter.value}`);
|
||||
}
|
||||
} else if (operand instanceof NumericValue) {
|
||||
switch (filter.value) {
|
||||
case "abs":
|
||||
return new NumericValue(Math.abs(operand.value));
|
||||
default:
|
||||
throw new Error(`Unknown NumericValue filter: ${filter.value}`);
|
||||
}
|
||||
} else if (operand instanceof ObjectValue) {
|
||||
switch (filter.value) {
|
||||
case "items":
|
||||
return new ArrayValue(
|
||||
Array.from(operand.value.entries()).map(([key, value]) => new ArrayValue([new StringValue(key), value]))
|
||||
);
|
||||
case "length":
|
||||
return new NumericValue(operand.value.size);
|
||||
default:
|
||||
throw new Error(`Unknown ObjectValue filter: ${filter.value}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`Cannot apply filter "${filter.value}" to type: ${operand.type}`);
|
||||
} else if (node.filter.type === "CallExpression") {
|
||||
const filter = node.filter as CallExpression;
|
||||
|
||||
if (filter.callee.type !== "Identifier") {
|
||||
throw new Error(`Unknown filter: ${filter.callee.type}`);
|
||||
}
|
||||
const filterName = (filter.callee as Identifier).value;
|
||||
|
||||
if (operand instanceof ArrayValue) {
|
||||
switch (filterName) {
|
||||
case "selectattr": {
|
||||
if (operand.value.some((x) => !(x instanceof ObjectValue))) {
|
||||
throw new Error("`selectattr` can only be applied to array of objects");
|
||||
}
|
||||
if (filter.args.some((x) => x.type !== "StringLiteral")) {
|
||||
throw new Error("arguments of `selectattr` must be strings");
|
||||
}
|
||||
|
||||
const [attr, testName, value] = filter.args.map((x) => this.evaluate(x, environment)) as StringValue[];
|
||||
|
||||
let testFunction: (...x: AnyRuntimeValue[]) => boolean;
|
||||
if (testName) {
|
||||
// Get the test function from the environment
|
||||
const test = environment.tests.get(testName.value);
|
||||
if (!test) {
|
||||
throw new Error(`Unknown test: ${testName.value}`);
|
||||
}
|
||||
testFunction = test;
|
||||
} else {
|
||||
// Default to truthiness of first argument
|
||||
testFunction = (...x: AnyRuntimeValue[]) => x[0].__bool__().value;
|
||||
}
|
||||
|
||||
// Filter the array using the test function
|
||||
const filtered = (operand.value as ObjectValue[]).filter((item) => {
|
||||
const a = item.value.get(attr.value);
|
||||
if (a) {
|
||||
return testFunction(a, value);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return new ArrayValue(filtered);
|
||||
}
|
||||
}
|
||||
throw new Error(`Unknown ArrayValue filter: ${filterName}`);
|
||||
} else {
|
||||
throw new Error(`Cannot apply filter "${filterName}" to type: ${operand.type}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`Unknown filter: ${node.filter.type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates expressions following the test operation type.
|
||||
*/
|
||||
private evaluateTestExpression(node: TestExpression, environment: Environment): BooleanValue {
|
||||
// For now, we only support the built-in tests
|
||||
// https://jinja.palletsprojects.com/en/3.0.x/templates/#list-of-builtin-tests
|
||||
//
|
||||
// TODO: Add support for non-identifier tests. e.g., divisibleby(number)
|
||||
const operand = this.evaluate(node.operand, environment);
|
||||
|
||||
const test = environment.tests.get(node.test.value);
|
||||
if (!test) {
|
||||
throw new Error(`Unknown test: ${node.test.value}`);
|
||||
}
|
||||
const result = test(operand);
|
||||
return new BooleanValue(node.negate ? !result : result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates expressions following the unary operation type.
|
||||
*/
|
||||
private evaluateUnaryExpression(node: UnaryExpression, environment: Environment): AnyRuntimeValue {
|
||||
const argument = this.evaluate(node.argument, environment);
|
||||
|
||||
switch (node.operator.value) {
|
||||
case "not":
|
||||
return new BooleanValue(!argument.value);
|
||||
default:
|
||||
throw new SyntaxError(`Unknown operator: ${node.operator.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
private evalProgram(program: Program, environment: Environment): StringValue {
|
||||
return this.evaluateBlock(program.body, environment);
|
||||
}
|
||||
|
||||
private evaluateBlock(statements: Statement[], environment: Environment): StringValue {
|
||||
// Jinja templates always evaluate to a String,
|
||||
// so we accumulate the result of each statement into a final string
|
||||
|
||||
let result = "";
|
||||
for (const statement of statements) {
|
||||
const lastEvaluated = this.evaluate(statement, environment);
|
||||
|
||||
if (lastEvaluated.type !== "NullValue" && lastEvaluated.type !== "UndefinedValue") {
|
||||
result += lastEvaluated.value;
|
||||
}
|
||||
}
|
||||
|
||||
return new StringValue(result);
|
||||
}
|
||||
|
||||
private evaluateIdentifier(node: Identifier, environment: Environment): AnyRuntimeValue {
|
||||
return environment.lookupVariable(node.value);
|
||||
}
|
||||
|
||||
private evaluateCallExpression(expr: CallExpression, environment: Environment): AnyRuntimeValue {
|
||||
// Accumulate all keyword arguments into a single object, which will be
|
||||
// used as the final argument in the call function.
|
||||
const args: AnyRuntimeValue[] = [];
|
||||
const kwargs = new Map();
|
||||
for (const argument of expr.args) {
|
||||
if (argument.type === "KeywordArgumentExpression") {
|
||||
const kwarg = argument as KeywordArgumentExpression;
|
||||
kwargs.set(kwarg.key.value, this.evaluate(kwarg.value, environment));
|
||||
} else {
|
||||
args.push(this.evaluate(argument, environment));
|
||||
}
|
||||
}
|
||||
if (kwargs.size > 0) {
|
||||
args.push(new ObjectValue(kwargs));
|
||||
}
|
||||
|
||||
const fn = this.evaluate(expr.callee, environment);
|
||||
if (fn.type !== "FunctionValue") {
|
||||
throw new Error(`Cannot call something that is not a function: got ${fn.type}`);
|
||||
}
|
||||
return (fn as FunctionValue).value(args, environment);
|
||||
}
|
||||
|
||||
private evaluateSliceExpression(
|
||||
object: AnyRuntimeValue,
|
||||
expr: SliceExpression,
|
||||
environment: Environment
|
||||
): ArrayValue | StringValue {
|
||||
if (!(object instanceof ArrayValue || object instanceof StringValue)) {
|
||||
throw new Error("Slice object must be an array or string");
|
||||
}
|
||||
|
||||
const start = this.evaluate(expr.start, environment);
|
||||
const stop = this.evaluate(expr.stop, environment);
|
||||
const step = this.evaluate(expr.step, environment);
|
||||
|
||||
// Validate arguments
|
||||
if (!(start instanceof NumericValue || start instanceof UndefinedValue)) {
|
||||
throw new Error("Slice start must be numeric or undefined");
|
||||
}
|
||||
if (!(stop instanceof NumericValue || stop instanceof UndefinedValue)) {
|
||||
throw new Error("Slice stop must be numeric or undefined");
|
||||
}
|
||||
if (!(step instanceof NumericValue || step instanceof UndefinedValue)) {
|
||||
throw new Error("Slice step must be numeric or undefined");
|
||||
}
|
||||
|
||||
if (object instanceof ArrayValue) {
|
||||
return new ArrayValue(slice(object.value, start.value, stop.value, step.value));
|
||||
} else {
|
||||
return new StringValue(slice(Array.from(object.value), start.value, stop.value, step.value).join(""));
|
||||
}
|
||||
}
|
||||
|
||||
private evaluateMemberExpression(expr: MemberExpression, environment: Environment): AnyRuntimeValue {
|
||||
const object = this.evaluate(expr.object, environment);
|
||||
|
||||
let property;
|
||||
if (expr.computed) {
|
||||
if (expr.property.type === "SliceExpression") {
|
||||
return this.evaluateSliceExpression(object, expr.property as SliceExpression, environment);
|
||||
} else {
|
||||
property = this.evaluate(expr.property, environment);
|
||||
}
|
||||
} else {
|
||||
property = new StringValue((expr.property as Identifier).value);
|
||||
}
|
||||
|
||||
let value;
|
||||
if (object instanceof ObjectValue) {
|
||||
if (!(property instanceof StringValue)) {
|
||||
throw new Error(`Cannot access property with non-string: got ${property.type}`);
|
||||
}
|
||||
value = object.value.get(property.value) ?? object.builtins.get(property.value);
|
||||
} else if (object instanceof ArrayValue || object instanceof StringValue) {
|
||||
if (property instanceof NumericValue) {
|
||||
value = object.value.at(property.value);
|
||||
if (object instanceof StringValue) {
|
||||
value = new StringValue(object.value.at(property.value));
|
||||
}
|
||||
} else if (property instanceof StringValue) {
|
||||
value = object.builtins.get(property.value);
|
||||
} else {
|
||||
throw new Error(`Cannot access property with non-string/non-number: got ${property.type}`);
|
||||
}
|
||||
} else {
|
||||
if (!(property instanceof StringValue)) {
|
||||
throw new Error(`Cannot access property with non-string: got ${property.type}`);
|
||||
}
|
||||
value = object.builtins.get(property.value);
|
||||
}
|
||||
|
||||
return value instanceof RuntimeValue ? value : new UndefinedValue();
|
||||
}
|
||||
|
||||
private evaluateSet(node: SetStatement, environment: Environment): NullValue {
|
||||
const rhs = this.evaluate(node.value, environment);
|
||||
if (node.assignee.type === "Identifier") {
|
||||
const variableName = (node.assignee as Identifier).value;
|
||||
environment.setVariable(variableName, rhs);
|
||||
} else if (node.assignee.type === "MemberExpression") {
|
||||
const member = node.assignee as MemberExpression;
|
||||
|
||||
const object = this.evaluate(member.object, environment);
|
||||
if (!(object instanceof ObjectValue)) {
|
||||
throw new Error("Cannot assign to member of non-object");
|
||||
}
|
||||
if (member.property.type !== "Identifier") {
|
||||
throw new Error("Cannot assign to member with non-identifier property");
|
||||
}
|
||||
object.value.set((member.property as Identifier).value, rhs);
|
||||
} else {
|
||||
throw new Error(`Invalid LHS inside assignment expression: ${JSON.stringify(node.assignee)}`);
|
||||
}
|
||||
|
||||
return new NullValue();
|
||||
}
|
||||
|
||||
private evaluateIf(node: If, environment: Environment): StringValue {
|
||||
const test = this.evaluate(node.test, environment);
|
||||
return this.evaluateBlock(test.__bool__().value ? node.body : node.alternate, environment);
|
||||
}
|
||||
|
||||
private evaluateFor(node: For, environment: Environment): StringValue {
|
||||
// Scope for the for loop
|
||||
const scope = new Environment(environment);
|
||||
|
||||
const iterable = this.evaluate(node.iterable, scope);
|
||||
if (!(iterable instanceof ArrayValue)) {
|
||||
throw new Error(`Expected iterable type in for loop: got ${iterable.type}`);
|
||||
}
|
||||
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < iterable.value.length; ++i) {
|
||||
// Update the loop variable
|
||||
// TODO: Only create object once, then update value?
|
||||
const loop = new Map([
|
||||
["index", new NumericValue(i + 1)],
|
||||
["index0", new NumericValue(i)],
|
||||
["revindex", new NumericValue(iterable.value.length - i)],
|
||||
["revindex0", new NumericValue(iterable.value.length - i - 1)],
|
||||
["first", new BooleanValue(i === 0)],
|
||||
["last", new BooleanValue(i === iterable.value.length - 1)],
|
||||
["length", new NumericValue(iterable.value.length)],
|
||||
["previtem", i > 0 ? iterable.value[i - 1] : new UndefinedValue()],
|
||||
["nextitem", i < iterable.value.length - 1 ? iterable.value[i + 1] : new UndefinedValue()],
|
||||
] as [string, AnyRuntimeValue][]);
|
||||
|
||||
scope.setVariable("loop", new ObjectValue(loop));
|
||||
|
||||
const current = iterable.value[i];
|
||||
|
||||
// For this iteration, set the loop variable to the current element
|
||||
if (node.loopvar.type === "Identifier") {
|
||||
scope.setVariable((node.loopvar as Identifier).value, current);
|
||||
} else if (node.loopvar.type === "TupleLiteral") {
|
||||
const loopvar = node.loopvar as TupleLiteral;
|
||||
if (current.type !== "ArrayValue") {
|
||||
throw new Error(`Cannot unpack non-iterable type: ${current.type}`);
|
||||
}
|
||||
const c = current as ArrayValue;
|
||||
|
||||
// check if too few or many items to unpack
|
||||
if (loopvar.value.length !== c.value.length) {
|
||||
throw new Error(`Too ${loopvar.value.length > c.value.length ? "few" : "many"} items to unpack`);
|
||||
}
|
||||
for (let j = 0; j < loopvar.value.length; ++j) {
|
||||
if (loopvar.value[j].type !== "Identifier") {
|
||||
throw new Error(`Cannot unpack non-identifier type: ${loopvar.value[j].type}`);
|
||||
}
|
||||
scope.setVariable((loopvar.value[j] as Identifier).value, c.value[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the body of the for loop
|
||||
const evaluated = this.evaluateBlock(node.body, scope);
|
||||
result += evaluated.value;
|
||||
}
|
||||
|
||||
return new StringValue(result);
|
||||
}
|
||||
|
||||
evaluate(statement: Statement | undefined, environment: Environment): AnyRuntimeValue {
|
||||
if (statement === undefined) return new UndefinedValue();
|
||||
|
||||
switch (statement.type) {
|
||||
// Program
|
||||
case "Program":
|
||||
return this.evalProgram(statement as Program, environment);
|
||||
|
||||
// Statements
|
||||
case "Set":
|
||||
return this.evaluateSet(statement as SetStatement, environment);
|
||||
case "If":
|
||||
return this.evaluateIf(statement as If, environment);
|
||||
case "For":
|
||||
return this.evaluateFor(statement as For, environment);
|
||||
|
||||
// Expressions
|
||||
case "NumericLiteral":
|
||||
return new NumericValue(Number((statement as NumericLiteral).value));
|
||||
case "StringLiteral":
|
||||
return new StringValue((statement as StringLiteral).value);
|
||||
case "BooleanLiteral":
|
||||
return new BooleanValue((statement as BooleanLiteral).value);
|
||||
case "ArrayLiteral":
|
||||
return new ArrayValue((statement as ArrayLiteral).value.map((x) => this.evaluate(x, environment)));
|
||||
case "TupleLiteral":
|
||||
return new TupleValue((statement as TupleLiteral).value.map((x) => this.evaluate(x, environment)));
|
||||
case "ObjectLiteral": {
|
||||
const mapping = new Map();
|
||||
for (const [key, value] of (statement as ObjectLiteral).value) {
|
||||
const evaluatedKey = this.evaluate(key, environment);
|
||||
if (!(evaluatedKey instanceof StringValue)) {
|
||||
throw new Error(`Object keys must be strings: got ${evaluatedKey.type}`);
|
||||
}
|
||||
mapping.set(evaluatedKey.value, this.evaluate(value, environment));
|
||||
}
|
||||
return new ObjectValue(mapping);
|
||||
}
|
||||
case "Identifier":
|
||||
return this.evaluateIdentifier(statement as Identifier, environment);
|
||||
case "CallExpression":
|
||||
return this.evaluateCallExpression(statement as CallExpression, environment);
|
||||
case "MemberExpression":
|
||||
return this.evaluateMemberExpression(statement as MemberExpression, environment);
|
||||
|
||||
case "UnaryExpression":
|
||||
return this.evaluateUnaryExpression(statement as UnaryExpression, environment);
|
||||
case "BinaryExpression":
|
||||
return this.evaluateBinaryExpression(statement as BinaryExpression, environment);
|
||||
case "FilterExpression":
|
||||
return this.evaluateFilterExpression(statement as FilterExpression, environment);
|
||||
case "TestExpression":
|
||||
return this.evaluateTestExpression(statement as TestExpression, environment);
|
||||
|
||||
default:
|
||||
throw new SyntaxError(`Unknown node type: ${statement.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert JavaScript values to runtime values.
|
||||
*/
|
||||
function convertToRuntimeValues(input: unknown): AnyRuntimeValue {
|
||||
switch (typeof input) {
|
||||
case "number":
|
||||
return new NumericValue(input);
|
||||
case "string":
|
||||
return new StringValue(input);
|
||||
case "boolean":
|
||||
return new BooleanValue(input);
|
||||
case "object":
|
||||
if (input === null) {
|
||||
return new NullValue();
|
||||
} else if (Array.isArray(input)) {
|
||||
return new ArrayValue(input.map(convertToRuntimeValues));
|
||||
} else {
|
||||
return new ObjectValue(
|
||||
new Map(Object.entries(input).map(([key, value]) => [key, convertToRuntimeValues(value)]))
|
||||
);
|
||||
}
|
||||
case "function":
|
||||
// Wrap the user's function in a runtime function
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
return new FunctionValue((args, _scope) => {
|
||||
// NOTE: `_scope` is not used since it's in the global scope
|
||||
const result = input(...args.map((x) => x.value)) ?? null; // map undefined -> null
|
||||
return convertToRuntimeValues(result);
|
||||
});
|
||||
default:
|
||||
throw new Error(`Cannot convert to runtime value: ${input}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user