diff --git a/package.json b/package.json index 85efe2f..ac27b35 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "tslib": "2" }, "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0" + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" }, "devDependencies": { "@types/benchmark": "^2.1.2", diff --git a/src/codegen/Codegen.ts b/src/codegen/Codegen.ts deleted file mode 100644 index 759d568..0000000 --- a/src/codegen/Codegen.ts +++ /dev/null @@ -1,308 +0,0 @@ -import {compileClosure} from '.'; -import type {JavaScriptLinked} from './types'; - -/** - * Inline JavaScript statements that are executed in main function body. - */ -export class CodegenStepExecJs { - constructor(public readonly js: string) {} -} - -/** - * A step can be `CodegenStepExecJs` or some application specific step, which - * will later will need to be converted to `CodegenStepExecJs`. - */ -type JsonSerializerStep = CodegenStepExecJs | unknown; - -/** - * Configuration options for {@link Codegen} instances. - */ -export interface CodegenOptions> { - /** - * Inline JavaScript string that represents the arguments that will be passed - * to the main function body. Defaults to "r0", i.e. the first register. - */ - args?: string[]; - - /** - * Name of the generated function. - */ - name?: string; - - /** - * Inline JavaScript statements, that execute at the beginning of the main - * function body. - */ - prologue?: string; - - /** - * Inline JavaScript statements, that execute at the end of the main - * function body. - */ - epilogue?: string | (() => string); - - /** - * Converts all steps to `CodegenStepExecJs`. - */ - processSteps?: (steps: JsonSerializerStep[]) => CodegenStepExecJs[]; - - /** - * Predefined list of dependencies that can be linked on demand. Dependency is - * linked with the name of the property and is linked only once. - */ - linkable?: Linkable; -} - -export type CodegenGenerateOptions = Pick; - -/** - * A helper class which helps with building JavaScript code for a single - * function. It keeps track of external dependencies, internally generated - * constants, and execution steps, which at the end are all converted to - * to an executable JavaScript function. - * - * The final output is a JavaScript function enclosed in a closure: - * - * ```js - * (function(d1, d2, d3) { - * var c1 = something; - * var c2 = something; - * var c3 = something; - * return function(r0) { - * var r1 = something; - * var r2 = something; - * var r3 = something; - * return something; - * } - * }) - * ``` - * - * Where `d*` are the external dependencies, `c*` are the internal constants, - * and `r*` are the local immutable infinite registers. - */ -export class Codegen< - Fn extends (...deps: any[]) => any = (...deps: unknown[]) => unknown, - Linkable = Record, -> { - /** @ignore */ - protected steps: JsonSerializerStep[] = []; - - /** @ignore */ - public options: Required>; - - constructor(opts: CodegenOptions) { - this.options = { - args: ['r0'], - name: '', - prologue: '', - epilogue: '', - processSteps: (steps) => steps.filter((step) => step instanceof CodegenStepExecJs) as CodegenStepExecJs[], - linkable: {} as Linkable, - ...opts, - }; - this.registerCounter = this.options.args.length; - } - - /** - * Add one or more JavaScript statements to the main function body. - */ - public js(js: string): void { - this.steps.push(new CodegenStepExecJs(js)); - } - - public var(expression?: string): string { - const r = this.getRegister(); - if (expression) this.js('var ' + r + ' = ' + expression + ';'); - else this.js('var ' + r + ';'); - return r; - } - - public if(condition: string, then: () => void, otherwise?: () => void): void { - this.js('if (' + condition + ') {'); - then(); - if (otherwise) { - this.js('} else {'); - otherwise(); - } - this.js('}'); - } - - public while(condition: string, block: () => void): void { - this.js('while (' + condition + ') {'); - block(); - this.js('}'); - } - - public doWhile(block: () => void, condition: string): void { - this.js('do {'); - block(); - this.js('} while (' + condition + ');'); - } - - public switch( - expression: string, - cases: [match: string | number | boolean | null, block: () => void, noBreak?: boolean][], - def?: () => void, - ): void { - this.js('switch (' + expression + ') {'); - for (const [match, block, noBreak] of cases) { - this.js('case ' + match + ': {'); - block(); - if (!noBreak) this.js('break;'); - this.js('}'); - } - if (def) { - this.js('default: {'); - def(); - this.js('}'); - } - this.js('}'); - } - - public return(expression: string): void { - this.js('return ' + expression + ';'); - } - - /** - * Add any application specific execution step. Steps of `unknown` type - * later need to converted to `CodegenStepExecJs` steps in the `.processStep` - * callback. - * - * @param step A step in function execution logic. - */ - public step(step: unknown): void { - this.steps.push(step); - } - - protected registerCounter: number; - - /** - * Codegen uses the idea of infinite registers. It starts with `0` and - * increments it by one for each new register. Best practice is to use - * a new register for each new variable and keep them immutable. - * - * Usage: - * - * ```js - * const r = codegen.getRegister(); - * codegen.js(`const ${r} = 1;`); - * ``` - * - * @returns a unique identifier for a variable. - */ - public getRegister(): string { - return `r${this.registerCounter++}`; - } - public r(): string { - return this.getRegister(); - } - - /** @ignore */ - protected dependencies: unknown[] = []; - protected dependencyNames: string[] = []; - - /** - * Allows to wire up dependencies to the generated code. - * - * @param dep Any JavaScript dependency, could be a function, an object, - * or anything else. - * @param name Optional name of the dependency. If not provided, a unique - * name will be generated, which starts with `d` and a counter - * appended. - * @returns Returns the dependency name, a code symbol which can be used as - * variable name. - */ - public linkDependency(dep: unknown, name: string = 'd' + this.dependencies.length): string { - this.dependencies.push(dep); - this.dependencyNames.push(name); - return name; - } - - /** - * Sames as {@link Codegen#linkDependency}, but allows to wire up multiple - * dependencies at once. - */ - public linkDependencies(deps: unknown[]): string[] { - return deps.map((dep) => this.linkDependency(dep)); - } - - protected linked: {[key: string]: 1} = {}; - - /** - * Link a dependency from the pre-defined `options.linkable` object. This method - * can be called many times with the same dependency name, the dependency will - * be linked only once. - * - * @param name Linkable dependency name. - */ - public link(name: keyof Linkable): void { - if (this.linked[name as string]) return; - this.linked[name as string] = 1; - this.linkDependency(this.options.linkable[name], name as string); - } - - /** @ignore */ - protected constants: string[] = []; - protected constantNames: string[] = []; - - /** - * Allows to encode any code or value in the closure of the generated - * function. - * - * @param constant Any JavaScript value in string form. - * @param name Optional name of the constant. If not provided, a unique - * name will be generated, which starts with `c` and a counter - * appended. - * @returns Returns the constant name, a code symbol which can be used as - * variable name. - */ - public addConstant(constant: string, name: string = 'c' + this.constants.length): string { - this.constants.push(constant); - this.constantNames.push(name); - return name; - } - - /** - * Sames as {@link Codegen#addConstant}, but allows to create multiple - * constants at once. - */ - public addConstants(constants: string[]): string[] { - return constants.map((constant) => this.addConstant(constant)); - } - - /** - * Returns generated JavaScript code with the dependency list. - * - * ```js - * const code = codegen.generate(); - * const fn = eval(code.js)(...code.deps); - * const result = fn(...args); - * ``` - */ - public generate(opts: CodegenGenerateOptions = {}): JavaScriptLinked { - const {name, args, prologue, epilogue} = {...this.options, ...opts}; - const steps = this.options.processSteps(this.steps); - const js = `(function(${this.dependencyNames.join(', ')}) { -${this.constants.map((constant, index) => `var ${this.constantNames[index]} = (${constant});`).join('\n')} -return ${name ? `function ${name}` : 'function'}(${args.join(',')}){ -${prologue} -${steps.map((step) => (step as CodegenStepExecJs).js).join('\n')} -${typeof epilogue === 'function' ? epilogue() : epilogue || ''} -}})`; - // console.log(js); - return { - deps: this.dependencies, - js: js as JavaScriptLinked['js'], - }; - } - - /** - * Compiles the generated JavaScript code into a function. - * - * @returns JavaScript function ready for execution. - */ - public compile(opts?: CodegenGenerateOptions): Fn { - const closure = this.generate(opts); - return compileClosure(closure); - } -} diff --git a/src/codegen/README.md b/src/codegen/README.md deleted file mode 100644 index 5a3951e..0000000 --- a/src/codegen/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# util/codegen - -This folder contains utilities for generating code. It is sometimes possible to -generate an optimized function that will execute significantly faster given -a "schema", or "template", of execution. - -Some examples: - -- Deep equality comparison function: if we know one object in advance we can - generate an optimized function which accepts a single object. It is - implemented in `json-equal` library. -- JSON Patch execution: if we know the JSON Patch in advance, we can generate - an optimized function which applies the JSON patch in the most efficient way. - It is implemented in `json-patch` library. -- Given a `json-type` schema of a JSON object, it is possible to generate - optimized functions for validation and serialization of objects according to - that schema. diff --git a/src/codegen/__tests__/Codegen.spec.ts b/src/codegen/__tests__/Codegen.spec.ts deleted file mode 100644 index 4183da6..0000000 --- a/src/codegen/__tests__/Codegen.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {CodegenStepExecJs} from '..'; -import {Codegen} from '../Codegen'; - -test('can generate a simple function', () => { - const codegen = new Codegen({ - name: 'foobar', - args: ['a', 'b'], - prologue: 'var res = 0;', - epilogue: 'return res;', - processSteps: (steps) => { - return steps.map((step) => { - if (typeof step === 'number') { - return new CodegenStepExecJs(`a += ${step};`); - } else return step; - }) as CodegenStepExecJs[]; - }, - }); - codegen.step(4); - const [c1, c2] = codegen.addConstants(['1', '2']); - codegen.js(`b += ${c1} + ${c2};`); - const byTwo = (num: number) => 2 * num; - codegen.linkDependency(byTwo, 'byTwo'); - codegen.js(`res += byTwo(a) + byTwo(b);`); - const code = codegen.generate(); - const fn = codegen.compile(); - // console.log(code.js); - expect(code.deps).toStrictEqual([byTwo]); - expect(typeof code.js).toBe('string'); - expect(fn(1, 2)).toBe(20); - expect(fn.name).toBe('foobar'); -}); diff --git a/src/codegen/compile.ts b/src/codegen/compile.ts deleted file mode 100644 index a1c0bbb..0000000 --- a/src/codegen/compile.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {JavaScriptLinked} from '.'; -import {JavaScript} from './types'; - -// tslint:disable-next-line -export const compile = (js: JavaScript): T => eval(js); - -export const compileClosure = (fn: JavaScriptLinked): T => compile(fn.js)(...fn.deps); diff --git a/src/codegen/dynamicFunction.ts b/src/codegen/dynamicFunction.ts deleted file mode 100644 index 38a622a..0000000 --- a/src/codegen/dynamicFunction.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Wraps a function into a proxy function with the same signature, but which can - * be re-implemented by the user at runtime. - * - * @param implementation Initial implementation. - * @returns Proxy function and implementation setter. - */ -export const dynamicFunction = any>( - implementation: F, -): [fn: F, set: (fn: F) => void] => { - const proxy = ((...args) => implementation(...args)) as F; - const set = (f: F) => { - implementation = f; - }; - return [proxy, set]; -}; diff --git a/src/codegen/index.ts b/src/codegen/index.ts index 3a96aef..fa927d2 100644 --- a/src/codegen/index.ts +++ b/src/codegen/index.ts @@ -1,3 +1 @@ -export * from './types'; -export * from './compile'; -export * from './Codegen'; +export * from '@jsonjoy.com/codegen'; diff --git a/src/codegen/switch.ts b/src/codegen/switch.ts deleted file mode 100644 index 3c98a12..0000000 --- a/src/codegen/switch.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {dynamicFunction} from './dynamicFunction'; - -/** - * Switcher for code generation. It first executes "evaluation" function - * 3 times, and then generates optimized code. - */ -export const createSwitch = any>(fn: F, codegen: () => F): F => { - let counter = 0; - const [proxy, set] = dynamicFunction((...args) => { - if (counter > 2) set(codegen()); - counter++; - return fn(...args); - }); - return proxy as F; -}; diff --git a/src/codegen/types.ts b/src/codegen/types.ts deleted file mode 100644 index b9bcd8a..0000000 --- a/src/codegen/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type {Brand} from '../types'; - -/** - * Represents a string which contains JavaScript code, which can be - * executed by the `eval` function. - * - * ```ts - * const code: JavaScript<() => {}> = `() => {}`; - * const fn = eval(code); // () => {} - * ``` - */ -export type JavaScript = Brand; - -/** - * Represents a string which contains JavaScript code, which is enclosed - * in a JavaScript closure function. The dependencies can be "linked" to - * the JavaScript code, by executing the outer closure function with the - * list of dependencies as arguments. - * - * ```ts - * const multBy: JavaScriptClosure<(x: number) => number, [by: number]> = - * 'function(by) { return function (x) { return x * by }}'; - * - * const multBy3 = eval(multBy)(3); - * - * multBy3(5); // 15 - * ``` - */ -export type JavaScriptClosure = JavaScript<(...deps: D) => Js>; - -/** - * Represents a {@link JavaScriptClosure} with a fixed list of dependencies, - * that can be linked to the JavaScript code-generated closure. - */ -export interface JavaScriptLinked { - deps: Dependencies; - js: JavaScriptClosure; -} diff --git a/src/codegen/util/JsExpression.ts b/src/codegen/util/JsExpression.ts deleted file mode 100644 index 7ddbd4e..0000000 --- a/src/codegen/util/JsExpression.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * JsExpression monad allows to write JS expression as strings which depend on each - * other and tracks whether an expression was used or not. - * - * ```ts - * const expr = new JsExpression(() => 'r0'); - * const subExpr = expr.chain((expr) => `${expr}["key"]`); - * - * expr.wasUsed; // false - * subExpr.use(); // r0["key"] - * expr.wasUsed; // true - * subExpr.wasUsed; // true - * ``` - */ -export class JsExpression { - private _wasUsed: boolean = false; - private _expression?: string; - private _listeners: ((expr: string) => void)[] = []; - - constructor(private expression: () => string) {} - - public get wasUsed(): boolean { - return this._wasUsed; - } - - public use(): string { - if (this._wasUsed) return this._expression!; - this._wasUsed = true; - const expression = (this._expression = this.expression()); - for (const listener of this._listeners) listener(expression); - return expression; - } - - public chain(use: (expr: string) => string): JsExpression { - return new JsExpression(() => use(this.use())); - } - - public addListener(listener: (expr: string) => void): void { - this._listeners.push(listener); - } -} diff --git a/src/codegen/util/helpers.ts b/src/codegen/util/helpers.ts deleted file mode 100644 index 8558b29..0000000 --- a/src/codegen/util/helpers.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const emitStringMatch = (expression: string, offset: string, match: string) => { - const conditions: string[] = []; - for (let i = 0; i < match.length; i++) - conditions.push(`${match.charCodeAt(i)} === ${expression}.charCodeAt(${offset} + ${i})`); - return conditions.join(' && '); -}; diff --git a/src/codegen/util/normalizeAccessor.ts b/src/codegen/util/normalizeAccessor.ts deleted file mode 100644 index 8bfa283..0000000 --- a/src/codegen/util/normalizeAccessor.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const normalizeAccessor = (accessor: string): string => { - if (/^[a-z_][a-z_0-9]*$/i.test(accessor)) { - return '.' + accessor; - } else { - return `[${JSON.stringify(accessor)}]`; - } -}; diff --git a/yarn.lock b/yarn.lock index c71bd90..7c23116 100644 --- a/yarn.lock +++ b/yarn.lock @@ -570,6 +570,11 @@ resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz#ade6895b7d3883d70f87b5743efaa12c71dfef7a" integrity sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q== +"@jsonjoy.com/codegen@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz#5c23f796c47675f166d23b948cdb889184b93207" + integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"