From ce2d967c3beec9817817ef5b35ee84b5d6814223 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 17 Oct 2025 23:06:31 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20codegen=20library?= =?UTF-8?q?=20to=20packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/codegen/LICENSE | 201 ++++++++++++ packages/codegen/README.md | 39 +++ packages/codegen/SECURITY.md | 13 + packages/codegen/package.json | 76 +++++ packages/codegen/src/Codegen.ts | 308 ++++++++++++++++++ packages/codegen/src/README.md | 24 ++ .../codegen/src/__tests__/Codegen.spec.ts | 31 ++ packages/codegen/src/compile.ts | 7 + packages/codegen/src/dynamicFunction.ts | 16 + packages/codegen/src/index.ts | 3 + packages/codegen/src/switch.ts | 15 + packages/codegen/src/types.ts | 41 +++ packages/codegen/src/util/JsExpression.ts | 41 +++ packages/codegen/src/util/helpers.ts | 6 + .../codegen/src/util/normalizeAccessor.ts | 7 + packages/codegen/tsconfig.build.json | 19 ++ packages/codegen/tsconfig.json | 20 ++ yarn.lock | 8 + 18 files changed, 875 insertions(+) create mode 100644 packages/codegen/LICENSE create mode 100644 packages/codegen/README.md create mode 100644 packages/codegen/SECURITY.md create mode 100644 packages/codegen/package.json create mode 100644 packages/codegen/src/Codegen.ts create mode 100644 packages/codegen/src/README.md create mode 100644 packages/codegen/src/__tests__/Codegen.spec.ts create mode 100644 packages/codegen/src/compile.ts create mode 100644 packages/codegen/src/dynamicFunction.ts create mode 100644 packages/codegen/src/index.ts create mode 100644 packages/codegen/src/switch.ts create mode 100644 packages/codegen/src/types.ts create mode 100644 packages/codegen/src/util/JsExpression.ts create mode 100644 packages/codegen/src/util/helpers.ts create mode 100644 packages/codegen/src/util/normalizeAccessor.ts create mode 100644 packages/codegen/tsconfig.build.json create mode 100644 packages/codegen/tsconfig.json diff --git a/packages/codegen/LICENSE b/packages/codegen/LICENSE new file mode 100644 index 0000000000..4e5127186f --- /dev/null +++ b/packages/codegen/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 jsonjoy.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/codegen/README.md b/packages/codegen/README.md new file mode 100644 index 0000000000..3d8925310d --- /dev/null +++ b/packages/codegen/README.md @@ -0,0 +1,39 @@ +# @jsonjoy.com/codegen + +A no-dependencies, low-level, high-performance JIT code generation package for JavaScript. + +## Features + +- **Zero dependencies** - Lightweight and fast to install +- **High performance** - Generates optimized JavaScript functions at runtime +- **Type safe** - Full TypeScript support with comprehensive type definitions +- **Flexible** - Supports various code generation patterns and techniques +- **Production ready** - Battle-tested and optimized for real-world usage + +## Use Cases + +- Deep equality comparison functions with known schemas +- JSON Patch execution with pre-known patches +- Optimized validation and serialization functions +- Custom function generation based on runtime data +- Performance-critical code that benefits from JIT compilation + +## Installation + +```bash +npm install @jsonjoy.com/codegen +``` + +## Quick Start + +```typescript +import { Codegen } from '@jsonjoy.com/codegen'; + +const codegen = new Codegen(); +// Add your code generation logic here +const optimizedFunction = codegen.compile(); +``` + +## License + +Apache-2.0 \ No newline at end of file diff --git a/packages/codegen/SECURITY.md b/packages/codegen/SECURITY.md new file mode 100644 index 0000000000..a5497b62af --- /dev/null +++ b/packages/codegen/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities. The latest major version +will support security patches. + +## Reporting a Vulnerability + +Please report (suspected) security vulnerabilities to +**[streamich@gmail.com](mailto:streamich@gmail.com)**. We will try to respond +within 48 hours. If the issue is confirmed, we will release a patch as soon +as possible depending on complexity. diff --git a/packages/codegen/package.json b/packages/codegen/package.json new file mode 100644 index 0000000000..5262099134 --- /dev/null +++ b/packages/codegen/package.json @@ -0,0 +1,76 @@ +{ + "name": "@jsonjoy.com/codegen", + "publishConfig": { + "access": "public" + }, + "version": "0.0.1", + "description": "No-dependencies, low-level, high-performance JIT code generation package for JavaScript", + "author": { + "name": "streamich", + "url": "https://github.com/streamich" + }, + "homepage": "https://github.com/jsonjoy-com/codegen", + "repository": "jsonjoy-com/codegen", + "license": "Apache-2.0", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "keywords": [ + "jit", + "codegen", + "code generation", + "dynamic", + "performance", + "javascript", + "compiler", + "function", + "optimization" + ], + "engines": { + "node": ">=10.0" + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "typings": "lib/index.d.ts", + "files": [ + "LICENSE", + "lib/" + ], + "scripts": { + "clean": "rimraf lib typedocs coverage gh-pages yarn-error.log", + "build": "tsc --project tsconfig.build.json --module commonjs --target es2020 --outDir lib", + "jest": "node -r ts-node/register ./node_modules/.bin/jest", + "test": "jest --maxWorkers 7", + "test:ci": "yarn jest --maxWorkers 3 --no-cache", + "coverage": "yarn test --collectCoverage", + "typedoc": "typedoc", + "build:pages": "rimraf gh-pages && mkdir -p gh-pages && cp -r typedocs/* gh-pages && cp -r coverage gh-pages/coverage", + "deploy:pages": "gh-pages -d gh-pages", + "publish-coverage-and-typedocs": "yarn typedoc && yarn coverage && yarn build:pages && yarn deploy:pages", + "typecheck": "tsc -p ." + }, + "peerDependencies": { + "tslib": "2" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "moduleFileExtensions": [ + "ts", + "js", + "tsx" + ], + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + "transformIgnorePatterns": [ + ".*/node_modules/.*" + ], + "testRegex": ".*/(__tests__|__jest__|demo)/.*\\.(test|spec)\\.tsx?$", + "rootDir": ".", + "testPathIgnorePatterns": [ + "node_modules" + ] + } +} diff --git a/packages/codegen/src/Codegen.ts b/packages/codegen/src/Codegen.ts new file mode 100644 index 0000000000..2c142d20a0 --- /dev/null +++ b/packages/codegen/src/Codegen.ts @@ -0,0 +1,308 @@ +import {compileClosure} from './compile'; +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/packages/codegen/src/README.md b/packages/codegen/src/README.md new file mode 100644 index 0000000000..4936a1f05a --- /dev/null +++ b/packages/codegen/src/README.md @@ -0,0 +1,24 @@ +# @jsonjoy.com/codegen + +This package contains utilities for generating optimized JavaScript code at runtime. +It enables creating high-performance functions by generating code dynamically based +on schemas, templates, or runtime data. + +## Key Benefits + +JIT (Just-In-Time) code generation can provide significant performance improvements +when you have advance knowledge of the data structure or execution pattern. + +Some examples: + +- **Deep equality comparison function**: When one object is known in advance, we can + generate an optimized function that efficiently compares against a single object. + This technique is implemented in the `json-equal` library. + +- **JSON Patch execution**: When the JSON Patch operations are known beforehand, we can + generate an optimized function that applies the patch in the most efficient way. + This approach is used in the `json-patch` library. + +- **Schema-based validation**: Given a `json-type` schema of a JSON object, it's possible + to generate highly optimized functions for validation and serialization that avoid + generic overhead and execute significantly faster than traditional approaches. diff --git a/packages/codegen/src/__tests__/Codegen.spec.ts b/packages/codegen/src/__tests__/Codegen.spec.ts new file mode 100644 index 0000000000..4183da60e5 --- /dev/null +++ b/packages/codegen/src/__tests__/Codegen.spec.ts @@ -0,0 +1,31 @@ +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/packages/codegen/src/compile.ts b/packages/codegen/src/compile.ts new file mode 100644 index 0000000000..a1c0bbbbe2 --- /dev/null +++ b/packages/codegen/src/compile.ts @@ -0,0 +1,7 @@ +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/packages/codegen/src/dynamicFunction.ts b/packages/codegen/src/dynamicFunction.ts new file mode 100644 index 0000000000..38a622a0b7 --- /dev/null +++ b/packages/codegen/src/dynamicFunction.ts @@ -0,0 +1,16 @@ +/** + * 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/packages/codegen/src/index.ts b/packages/codegen/src/index.ts new file mode 100644 index 0000000000..3a96aef3c2 --- /dev/null +++ b/packages/codegen/src/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './compile'; +export * from './Codegen'; diff --git a/packages/codegen/src/switch.ts b/packages/codegen/src/switch.ts new file mode 100644 index 0000000000..3c98a124fc --- /dev/null +++ b/packages/codegen/src/switch.ts @@ -0,0 +1,15 @@ +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/packages/codegen/src/types.ts b/packages/codegen/src/types.ts new file mode 100644 index 0000000000..5fa5d620f3 --- /dev/null +++ b/packages/codegen/src/types.ts @@ -0,0 +1,41 @@ +/** + * Brand type for creating nominal types in TypeScript. + */ +export type Brand = T & {[K in N]: B}; + +/** + * 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/packages/codegen/src/util/JsExpression.ts b/packages/codegen/src/util/JsExpression.ts new file mode 100644 index 0000000000..7ddbd4e604 --- /dev/null +++ b/packages/codegen/src/util/JsExpression.ts @@ -0,0 +1,41 @@ +/** + * 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/packages/codegen/src/util/helpers.ts b/packages/codegen/src/util/helpers.ts new file mode 100644 index 0000000000..8558b290da --- /dev/null +++ b/packages/codegen/src/util/helpers.ts @@ -0,0 +1,6 @@ +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/packages/codegen/src/util/normalizeAccessor.ts b/packages/codegen/src/util/normalizeAccessor.ts new file mode 100644 index 0000000000..8bfa283fb7 --- /dev/null +++ b/packages/codegen/src/util/normalizeAccessor.ts @@ -0,0 +1,7 @@ +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/packages/codegen/tsconfig.build.json b/packages/codegen/tsconfig.build.json new file mode 100644 index 0000000000..0c2a9d16a0 --- /dev/null +++ b/packages/codegen/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + }, + "exclude": [ + "src/demo", + "src/__tests__", + "src/**/__demos__/**/*.*", + "src/**/__tests__/**/*.*", + "src/**/__bench__/**/*.*", + "src/**/__mocks__/**/*.*", + "src/**/__jest__/**/*.*", + "src/**/__mocha__/**/*.*", + "src/**/__tap__/**/*.*", + "src/**/__tape__/**/*.*", + "*.test.ts", + "*.spec.ts" + ], +} diff --git a/packages/codegen/tsconfig.json b/packages/codegen/tsconfig.json new file mode 100644 index 0000000000..80cf8285e3 --- /dev/null +++ b/packages/codegen/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + }, + "include": ["src"], + "exclude": [ + "src/demo", + "src/__tests__", + "src/**/__demos__/**/*.*", + "src/**/__tests__/**/*.*", + "src/**/__bench__/**/*.*", + "src/**/__mocks__/**/*.*", + "src/**/__jest__/**/*.*", + "src/**/__mocha__/**/*.*", + "src/**/__tap__/**/*.*", + "src/**/__tape__/**/*.*", + "*.test.ts", + "*.spec.ts" + ], +} diff --git a/yarn.lock b/yarn.lock index e6e183f4c0..9533e1c763 100644 --- a/yarn.lock +++ b/yarn.lock @@ -892,6 +892,14 @@ __metadata: languageName: node linkType: hard +"@jsonjoy.com/codegen@workspace:packages/codegen": + version: 0.0.0-use.local + resolution: "@jsonjoy.com/codegen@workspace:packages/codegen" + peerDependencies: + tslib: 2 + languageName: unknown + linkType: soft + "@jsonjoy.com/jit-router@npm:^1.0.1": version: 1.0.1 resolution: "@jsonjoy.com/jit-router@npm:1.0.1"