240 lines
6.1 KiB
TypeScript
240 lines
6.1 KiB
TypeScript
import {
|
|
Binary,
|
|
ContextAccess,
|
|
Expr,
|
|
ExprVisitor,
|
|
FunctionCall,
|
|
Grouping,
|
|
IndexAccess,
|
|
Literal,
|
|
Logical,
|
|
Star,
|
|
Unary
|
|
} from "./ast";
|
|
import * as data from "./data";
|
|
import {FilteredArray} from "./filtered_array";
|
|
import {wellKnownFunctions} from "./funcs";
|
|
import {FunctionDefinition} from "./funcs/info";
|
|
import {idxHelper} from "./idxHelper";
|
|
import {TokenType} from "./lexer";
|
|
import {equals, falsy, greaterThan, lessThan, truthy} from "./result";
|
|
|
|
export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
|
/**
|
|
* Creates a new evaluator
|
|
* @param n Parsed expression to evaluate
|
|
* @param context Context data to use
|
|
* @param functions Optional map of function implementations. If given, these will be preferred over the built-in functions.
|
|
*/
|
|
constructor(private n: Expr, private context: data.Dictionary, private functions?: Map<string, FunctionDefinition>) {}
|
|
|
|
public evaluate(): data.ExpressionData {
|
|
return this.eval(this.n);
|
|
}
|
|
|
|
protected eval(n: Expr): data.ExpressionData {
|
|
return n.accept(this);
|
|
}
|
|
|
|
visitLiteral(literal: Literal): data.ExpressionData {
|
|
return literal.literal;
|
|
}
|
|
|
|
visitUnary(unary: Unary): data.ExpressionData {
|
|
const r = this.eval(unary.expr);
|
|
|
|
if (unary.operator.type === TokenType.BANG) {
|
|
return new data.BooleanData(falsy(r));
|
|
}
|
|
|
|
throw new Error(`unknown unary operator: ${unary.operator.lexeme}`);
|
|
}
|
|
|
|
visitBinary(binary: Binary): data.ExpressionData {
|
|
const left = this.eval(binary.left);
|
|
const right = this.eval(binary.right);
|
|
|
|
switch (binary.operator.type) {
|
|
case TokenType.EQUAL_EQUAL:
|
|
return new data.BooleanData(equals(left, right));
|
|
|
|
case TokenType.BANG_EQUAL:
|
|
return new data.BooleanData(!equals(left, right));
|
|
|
|
case TokenType.GREATER:
|
|
return new data.BooleanData(greaterThan(left, right));
|
|
|
|
case TokenType.GREATER_EQUAL:
|
|
return new data.BooleanData(equals(left, right) || greaterThan(left, right));
|
|
|
|
case TokenType.LESS:
|
|
return new data.BooleanData(lessThan(left, right));
|
|
|
|
case TokenType.LESS_EQUAL:
|
|
return new data.BooleanData(equals(left, right) || lessThan(left, right));
|
|
}
|
|
|
|
throw new Error(`unknown binary operator: ${binary.operator.lexeme}`);
|
|
}
|
|
|
|
visitLogical(logical: Logical): data.ExpressionData {
|
|
let result: data.ExpressionData;
|
|
|
|
for (const arg of logical.args) {
|
|
result = this.eval(arg);
|
|
|
|
// Break?
|
|
if (
|
|
(logical.operator.type === TokenType.AND && falsy(result)) ||
|
|
(logical.operator.type === TokenType.OR && truthy(result))
|
|
) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// result is always assigned before we return here
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
return result!;
|
|
}
|
|
|
|
visitGrouping(grouping: Grouping): data.ExpressionData {
|
|
return this.eval(grouping.group);
|
|
}
|
|
|
|
visitContextAccess(contextAccess: ContextAccess): data.ExpressionData {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const r = this.context.get(contextAccess.name.lexeme)!;
|
|
return r;
|
|
}
|
|
|
|
visitIndexAccess(ia: IndexAccess): data.ExpressionData {
|
|
let idx: idxHelper;
|
|
if (ia.index instanceof Star) {
|
|
idx = new idxHelper(true, undefined);
|
|
} else {
|
|
let idxResult: data.ExpressionData;
|
|
try {
|
|
idxResult = this.eval(ia.index);
|
|
} catch (e) {
|
|
throw new Error(`could not evaluate index for index access: ${(e as Error).message}`, {cause: e});
|
|
}
|
|
idx = new idxHelper(false, idxResult);
|
|
}
|
|
|
|
const objResult = this.eval(ia.expr);
|
|
|
|
let result: data.ExpressionData;
|
|
switch (objResult.kind) {
|
|
case data.Kind.Array: {
|
|
const tobjResult = objResult;
|
|
if (tobjResult instanceof FilteredArray) {
|
|
result = filteredArrayAccess(tobjResult, idx);
|
|
} else {
|
|
result = arrayAccess(tobjResult, idx);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case data.Kind.Dictionary: {
|
|
const tobjResult = objResult;
|
|
result = objectAccess(tobjResult, idx);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
if (idx.star) {
|
|
result = new FilteredArray();
|
|
} else {
|
|
result = new data.Null();
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
visitFunctionCall(functionCall: FunctionCall): data.ExpressionData {
|
|
// Evaluate arguments
|
|
const args = functionCall.args.map(arg => this.eval(arg));
|
|
|
|
// Get function definitions
|
|
const functionName = functionCall.functionName.lexeme.toLowerCase();
|
|
const f = this.functions?.get(functionName) || wellKnownFunctions[functionName];
|
|
|
|
return f.call(...args);
|
|
}
|
|
}
|
|
|
|
function filteredArrayAccess(fa: FilteredArray, idx: idxHelper): data.ExpressionData {
|
|
const result = new FilteredArray();
|
|
|
|
for (const item of fa.values()) {
|
|
// Check the type of the nested item
|
|
switch (item.kind) {
|
|
case data.Kind.Dictionary: {
|
|
const ti = item;
|
|
if (idx.star) {
|
|
for (const v of ti.values()) {
|
|
result.add(v);
|
|
}
|
|
} else if (idx.str !== undefined) {
|
|
const v = ti.get(idx.str);
|
|
if (v !== undefined) {
|
|
result.add(v);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case data.Kind.Array: {
|
|
const ti = item;
|
|
if (idx.star) {
|
|
for (const v of ti.values()) {
|
|
result.add(v);
|
|
}
|
|
} else if (idx.int !== undefined && idx.int < ti.values().length) {
|
|
result.add(ti.get(idx.int));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function arrayAccess(a: data.Array, idx: idxHelper): data.ExpressionData {
|
|
if (idx.star) {
|
|
const fa = new FilteredArray();
|
|
for (const item of a.values()) {
|
|
fa.add(item);
|
|
}
|
|
|
|
return fa;
|
|
}
|
|
|
|
if (idx.int !== undefined && idx.int < a.values().length) {
|
|
return a.get(idx.int);
|
|
}
|
|
|
|
return new data.Null();
|
|
}
|
|
|
|
function objectAccess(obj: data.Dictionary, idx: idxHelper): data.ExpressionData {
|
|
if (idx.star) {
|
|
const fa = new FilteredArray(...obj.values());
|
|
return fa;
|
|
}
|
|
|
|
if (idx.str !== undefined) {
|
|
const r = obj.get(idx.str);
|
|
if (r !== undefined) {
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return new data.Null();
|
|
}
|