Skip to content

Implement operator registry system with configurable runtime and codegen classes #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions src/ExpressionCodegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as util from './util';
import {Codegen} from '@jsonjoy.com/util/lib/codegen/Codegen';
import {type ExpressionResult, Literal} from './codegen-steps';
import {createEvaluate} from './createEvaluate';
import type {JavaScript} from '@jsonjoy.com/util/lib/codegen';
import {Vars} from './Vars';
import type * as types from './types';
import type {OperatorRegistry} from './OperatorRegistry';

export type ExpressionFn = (vars: types.JsonExpressionExecutionContext['vars']) => unknown;

export interface ExpressionCodegenOptions extends types.JsonExpressionCodegenContext {
expression: types.Expr;
}

/**
* Code generator for JSON expressions using a specified operator registry.
*/
export class ExpressionCodegen {
protected codegen: Codegen<ExpressionFn>;
protected evaluate: ReturnType<typeof createEvaluate>;
protected operatorMap: types.OperatorMap;

public constructor(
private registry: OperatorRegistry,
protected options: ExpressionCodegenOptions
) {
this.operatorMap = registry.asMap();
this.codegen = new Codegen<ExpressionFn>({
args: ['vars'],
epilogue: '',
});
this.evaluate = createEvaluate({
operators: this.operatorMap,
...options
});
}

private linkedOperandDeps: Set<string> = new Set();
private linkOperandDeps = (dependency: unknown, name?: string): string => {
if (name) {
if (this.linkedOperandDeps.has(name)) return name;
this.linkedOperandDeps.add(name);
} else {
name = this.codegen.getRegister();
}
this.codegen.linkDependency(dependency, name);
return name;
};

private operatorConst = (js: JavaScript<unknown>): string => {
return this.codegen.addConstant(js);
};

private subExpression = (expr: types.Expr): ExpressionFn => {
const codegen = new ExpressionCodegen(this.registry, {...this.options, expression: expr});
const fn = codegen.run().compile();
return fn;
};

protected onExpression(expr: types.Expr | unknown): ExpressionResult {
if (expr instanceof Array) {
if (expr.length === 1) return new Literal(expr[0]);
} else return new Literal(expr);

const def = this.operatorMap.get(expr[0]);
if (def) {
const [name, , arity, , codegen, impure] = def;
util.assertArity(name, arity, expr);
const operands = expr.slice(1).map((operand) => this.onExpression(operand));
if (!impure) {
const allLiterals = operands.every((expr) => expr instanceof Literal);
if (allLiterals) {
const result = this.evaluate(expr, {vars: new Vars(undefined)});
return new Literal(result);
}
}
const ctx: types.OperatorCodegenCtx<types.Expression> = {
expr,
operands,
createPattern: this.options.createPattern,
operand: (operand: types.Expression) => this.onExpression(operand),
link: this.linkOperandDeps,
const: this.operatorConst,
subExpression: this.subExpression,
var: (value: string) => this.codegen.var(value),
};
return codegen(ctx);
}
return new Literal(false);
}

public run(): this {
const expr = this.onExpression(this.options.expression);
this.codegen.js(`return ${expr};`);
return this;
}

public generate() {
return this.codegen.generate();
}

public compileRaw(): ExpressionFn {
return this.codegen.compile();
}

public compile(): ExpressionFn {
const fn = this.compileRaw();
return (vars: any) => {
try {
return fn(vars);
} catch (err) {
if (err instanceof Error) throw err;
const error = new Error('Expression evaluation error.');
(<any>error).value = err;
throw error;
}
};
}

/**
* Get the operator registry used by this codegen.
*/
getRegistry(): OperatorRegistry {
return this.registry;
}

/**
* Create a new codegen with a different registry.
*/
withRegistry(registry: OperatorRegistry): ExpressionCodegen {
return new ExpressionCodegen(registry, this.options);
}
}
51 changes: 51 additions & 0 deletions src/ExpressionRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {Expr, Literal, JsonExpressionCodegenContext, JsonExpressionExecutionContext} from './types';
import type {OperatorRegistry} from './OperatorRegistry';
import {createEvaluate} from './createEvaluate';

/**
* Runtime for evaluating JSON expressions using a specified operator registry.
*/
export class ExpressionRuntime {
private evaluateFn: ReturnType<typeof createEvaluate>;

constructor(
private registry: OperatorRegistry,
private options: JsonExpressionCodegenContext = {}
) {
this.evaluateFn = createEvaluate({
operators: registry.asMap(),
...options
});
}

/**
* Evaluate a JSON expression.
*/
evaluate(
expr: Expr | Literal<unknown>,
ctx: JsonExpressionExecutionContext & JsonExpressionCodegenContext = {vars: undefined as any}
): unknown {
return this.evaluateFn(expr, {...this.options, ...ctx});
}

/**
* Get the operator registry used by this runtime.
*/
getRegistry(): OperatorRegistry {
return this.registry;
}

/**
* Create a new runtime with a different registry.
*/
withRegistry(registry: OperatorRegistry): ExpressionRuntime {
return new ExpressionRuntime(registry, this.options);
}

/**
* Create a new runtime with different options.
*/
withOptions(options: JsonExpressionCodegenContext): ExpressionRuntime {
return new ExpressionRuntime(this.registry, {...this.options, ...options});
}
}
80 changes: 80 additions & 0 deletions src/OperatorRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type {OperatorDefinition, OperatorMap, Expression} from './types';
import {operatorsToMap} from './util';

/**
* Registry for operators that can be used in JSON expressions.
* Allows building custom operator sets for different use cases.
*/
export class OperatorRegistry {
private operators: OperatorDefinition<Expression>[] = [];
private operatorMap: OperatorMap | null = null;

constructor(operators: OperatorDefinition<Expression>[] = []) {
this.operators = [...operators];
}

/**
* Add one or more operators to the registry.
*/
add(...operators: OperatorDefinition<Expression>[]): this {
this.operators.push(...operators);
this.operatorMap = null; // Invalidate cache
return this;
}

/**
* Remove an operator by name.
*/
remove(name: string): this {
this.operators = this.operators.filter(([operatorName]) => operatorName !== name);
this.operatorMap = null; // Invalidate cache
return this;
}

/**
* Check if an operator exists in the registry.
*/
has(name: string): boolean {
return this.operators.some(([operatorName, aliases]) =>
operatorName === name || aliases.includes(name)
);
}

/**
* Get all operators as an array.
*/
all(): OperatorDefinition<Expression>[] {
return [...this.operators];
}

/**
* Get operators as a Map for efficient lookup.
*/
asMap(): OperatorMap {
if (!this.operatorMap) {
this.operatorMap = operatorsToMap(this.operators);
}
return this.operatorMap;
}

/**
* Create a new registry with the same operators.
*/
clone(): OperatorRegistry {
return new OperatorRegistry(this.operators);
}

/**
* Merge this registry with another registry.
*/
merge(other: OperatorRegistry): OperatorRegistry {
return new OperatorRegistry([...this.operators, ...other.operators]);
}

/**
* Get the number of operators in the registry.
*/
size(): number {
return this.operators.length;
}
}
Loading
Loading