All files / json-expression/src codegen.ts

94.91% Statements 56/59
100% Branches 10/10
85.71% Functions 12/14
94.44% Lines 51/54

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 1102x 2x 2x 2x 2x                     2x       1221x 1221x       1221x     1221x 1221x 729x 719x 544x   10x   554x 554x     1221x 404x     1221x 6x 6x 6x       3934x 1938x 1996x   1841x 1841x 1841x 1841x 2713x 1674x 1871x 1137x 761x 753x     913x               5x   913x           1221x 1044x 1044x               1044x       1044x 1044x 1073x 1073x   4x 1x 1x 1x          
import * as util from './util';
import {Codegen} from '@jsonjoy.com/codegen/lib/Codegen';
import {createEvaluate} from './createEvaluate';
import {Vars} from './Vars';
import {type ExpressionResult, Literal} from './codegen-steps';
import type {JavaScript} from '@jsonjoy.com/codegen';
import type * as types from './types';
 
export type JsonExpressionFn = (vars: types.JsonExpressionExecutionContext['vars']) => unknown;
 
export interface JsonExpressionCodegenOptions extends types.JsonExpressionCodegenContext {
  expression: types.Expr;
  operators: types.OperatorMap;
}
 
export class JsonExpressionCodegen {
  protected codegen: Codegen<JsonExpressionFn>;
  protected evaluate: ReturnType<typeof createEvaluate>;
 
  public constructor(protected options: JsonExpressionCodegenOptions) {
    this.codegen = new Codegen<JsonExpressionFn>({
      args: ['vars'],
      epilogue: '',
    });
    this.evaluate = createEvaluate({...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): JsonExpressionFn => {
    const codegen = new JsonExpressionCodegen({...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.options.operators.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(): JsonExpressionFn {
    return this.codegen.compile();
  }
 
  public compile(): JsonExpressionFn {
    const fn = this.compileRaw();
    return (vars) => {
      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;
      }
    };
  }
}