Skip to content

Commit 3e43946

Browse files
Copilotstreamich
andcommitted
Implement operator registry system with runtime and codegen classes
Co-authored-by: streamich <9773803+streamich@users.noreply.github.com>
1 parent 597950e commit 3e43946

File tree

6 files changed

+477
-0
lines changed

6 files changed

+477
-0
lines changed

src/ExpressionCodegen.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import * as util from './util';
2+
import {Codegen} from '@jsonjoy.com/util/lib/codegen/Codegen';
3+
import {type ExpressionResult, Literal} from './codegen-steps';
4+
import {createEvaluate} from './createEvaluate';
5+
import type {JavaScript} from '@jsonjoy.com/util/lib/codegen';
6+
import {Vars} from './Vars';
7+
import type * as types from './types';
8+
import type {OperatorRegistry} from './OperatorRegistry';
9+
10+
export type ExpressionFn = (vars: types.JsonExpressionExecutionContext['vars']) => unknown;
11+
12+
export interface ExpressionCodegenOptions extends types.JsonExpressionCodegenContext {
13+
expression: types.Expr;
14+
}
15+
16+
/**
17+
* Code generator for JSON expressions using a specified operator registry.
18+
*/
19+
export class ExpressionCodegen {
20+
protected codegen: Codegen<ExpressionFn>;
21+
protected evaluate: ReturnType<typeof createEvaluate>;
22+
protected operatorMap: types.OperatorMap;
23+
24+
public constructor(
25+
private registry: OperatorRegistry,
26+
protected options: ExpressionCodegenOptions
27+
) {
28+
this.operatorMap = registry.asMap();
29+
this.codegen = new Codegen<ExpressionFn>({
30+
args: ['vars'],
31+
epilogue: '',
32+
});
33+
this.evaluate = createEvaluate({
34+
operators: this.operatorMap,
35+
...options
36+
});
37+
}
38+
39+
private linkedOperandDeps: Set<string> = new Set();
40+
private linkOperandDeps = (dependency: unknown, name?: string): string => {
41+
if (name) {
42+
if (this.linkedOperandDeps.has(name)) return name;
43+
this.linkedOperandDeps.add(name);
44+
} else {
45+
name = this.codegen.getRegister();
46+
}
47+
this.codegen.linkDependency(dependency, name);
48+
return name;
49+
};
50+
51+
private operatorConst = (js: JavaScript<unknown>): string => {
52+
return this.codegen.addConstant(js);
53+
};
54+
55+
private subExpression = (expr: types.Expr): ExpressionFn => {
56+
const codegen = new ExpressionCodegen(this.registry, {...this.options, expression: expr});
57+
const fn = codegen.run().compile();
58+
return fn;
59+
};
60+
61+
protected onExpression(expr: types.Expr | unknown): ExpressionResult {
62+
if (expr instanceof Array) {
63+
if (expr.length === 1) return new Literal(expr[0]);
64+
} else return new Literal(expr);
65+
66+
const def = this.operatorMap.get(expr[0]);
67+
if (def) {
68+
const [name, , arity, , codegen, impure] = def;
69+
util.assertArity(name, arity, expr);
70+
const operands = expr.slice(1).map((operand) => this.onExpression(operand));
71+
if (!impure) {
72+
const allLiterals = operands.every((expr) => expr instanceof Literal);
73+
if (allLiterals) {
74+
const result = this.evaluate(expr, {vars: new Vars(undefined)});
75+
return new Literal(result);
76+
}
77+
}
78+
const ctx: types.OperatorCodegenCtx<types.Expression> = {
79+
expr,
80+
operands,
81+
createPattern: this.options.createPattern,
82+
operand: (operand: types.Expression) => this.onExpression(operand),
83+
link: this.linkOperandDeps,
84+
const: this.operatorConst,
85+
subExpression: this.subExpression,
86+
var: (value: string) => this.codegen.var(value),
87+
};
88+
return codegen(ctx);
89+
}
90+
return new Literal(false);
91+
}
92+
93+
public run(): this {
94+
const expr = this.onExpression(this.options.expression);
95+
this.codegen.js(`return ${expr};`);
96+
return this;
97+
}
98+
99+
public generate() {
100+
return this.codegen.generate();
101+
}
102+
103+
public compileRaw(): ExpressionFn {
104+
return this.codegen.compile();
105+
}
106+
107+
public compile(): ExpressionFn {
108+
const fn = this.compileRaw();
109+
return (vars: any) => {
110+
try {
111+
return fn(vars);
112+
} catch (err) {
113+
if (err instanceof Error) throw err;
114+
const error = new Error('Expression evaluation error.');
115+
(<any>error).value = err;
116+
throw error;
117+
}
118+
};
119+
}
120+
121+
/**
122+
* Get the operator registry used by this codegen.
123+
*/
124+
getRegistry(): OperatorRegistry {
125+
return this.registry;
126+
}
127+
128+
/**
129+
* Create a new codegen with a different registry.
130+
*/
131+
withRegistry(registry: OperatorRegistry): ExpressionCodegen {
132+
return new ExpressionCodegen(registry, this.options);
133+
}
134+
}

src/ExpressionRuntime.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type {Expr, Literal, JsonExpressionCodegenContext, JsonExpressionExecutionContext} from './types';
2+
import type {OperatorRegistry} from './OperatorRegistry';
3+
import {createEvaluate} from './createEvaluate';
4+
5+
/**
6+
* Runtime for evaluating JSON expressions using a specified operator registry.
7+
*/
8+
export class ExpressionRuntime {
9+
private evaluateFn: ReturnType<typeof createEvaluate>;
10+
11+
constructor(
12+
private registry: OperatorRegistry,
13+
private options: JsonExpressionCodegenContext = {}
14+
) {
15+
this.evaluateFn = createEvaluate({
16+
operators: registry.asMap(),
17+
...options
18+
});
19+
}
20+
21+
/**
22+
* Evaluate a JSON expression.
23+
*/
24+
evaluate(
25+
expr: Expr | Literal<unknown>,
26+
ctx: JsonExpressionExecutionContext & JsonExpressionCodegenContext = {vars: undefined as any}
27+
): unknown {
28+
return this.evaluateFn(expr, {...this.options, ...ctx});
29+
}
30+
31+
/**
32+
* Get the operator registry used by this runtime.
33+
*/
34+
getRegistry(): OperatorRegistry {
35+
return this.registry;
36+
}
37+
38+
/**
39+
* Create a new runtime with a different registry.
40+
*/
41+
withRegistry(registry: OperatorRegistry): ExpressionRuntime {
42+
return new ExpressionRuntime(registry, this.options);
43+
}
44+
45+
/**
46+
* Create a new runtime with different options.
47+
*/
48+
withOptions(options: JsonExpressionCodegenContext): ExpressionRuntime {
49+
return new ExpressionRuntime(this.registry, {...this.options, ...options});
50+
}
51+
}

src/OperatorRegistry.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type {OperatorDefinition, OperatorMap, Expression} from './types';
2+
import {operatorsToMap} from './util';
3+
4+
/**
5+
* Registry for operators that can be used in JSON expressions.
6+
* Allows building custom operator sets for different use cases.
7+
*/
8+
export class OperatorRegistry {
9+
private operators: OperatorDefinition<Expression>[] = [];
10+
private operatorMap: OperatorMap | null = null;
11+
12+
constructor(operators: OperatorDefinition<Expression>[] = []) {
13+
this.operators = [...operators];
14+
}
15+
16+
/**
17+
* Add one or more operators to the registry.
18+
*/
19+
add(...operators: OperatorDefinition<Expression>[]): this {
20+
this.operators.push(...operators);
21+
this.operatorMap = null; // Invalidate cache
22+
return this;
23+
}
24+
25+
/**
26+
* Remove an operator by name.
27+
*/
28+
remove(name: string): this {
29+
this.operators = this.operators.filter(([operatorName]) => operatorName !== name);
30+
this.operatorMap = null; // Invalidate cache
31+
return this;
32+
}
33+
34+
/**
35+
* Check if an operator exists in the registry.
36+
*/
37+
has(name: string): boolean {
38+
return this.operators.some(([operatorName, aliases]) =>
39+
operatorName === name || aliases.includes(name)
40+
);
41+
}
42+
43+
/**
44+
* Get all operators as an array.
45+
*/
46+
all(): OperatorDefinition<Expression>[] {
47+
return [...this.operators];
48+
}
49+
50+
/**
51+
* Get operators as a Map for efficient lookup.
52+
*/
53+
asMap(): OperatorMap {
54+
if (!this.operatorMap) {
55+
this.operatorMap = operatorsToMap(this.operators);
56+
}
57+
return this.operatorMap;
58+
}
59+
60+
/**
61+
* Create a new registry with the same operators.
62+
*/
63+
clone(): OperatorRegistry {
64+
return new OperatorRegistry(this.operators);
65+
}
66+
67+
/**
68+
* Merge this registry with another registry.
69+
*/
70+
merge(other: OperatorRegistry): OperatorRegistry {
71+
return new OperatorRegistry([...this.operators, ...other.operators]);
72+
}
73+
74+
/**
75+
* Get the number of operators in the registry.
76+
*/
77+
size(): number {
78+
return this.operators.length;
79+
}
80+
}

0 commit comments

Comments
 (0)