Skip to content

Commit c478afb

Browse files
committed
feat: 🎸 add v1 codegen implementation
1 parent a5371fb commit c478afb

File tree

11 files changed

+499
-0
lines changed

11 files changed

+499
-0
lines changed

‎src/Codegen.ts‎

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import {compileClosure} from '.';
2+
import type {JavaScriptLinked} from './types';
3+
4+
/**
5+
* Inline JavaScript statements that are executed in main function body.
6+
*/
7+
export class CodegenStepExecJs {
8+
constructor(public readonly js: string) {}
9+
}
10+
11+
/**
12+
* A step can be `CodegenStepExecJs` or some application specific step, which
13+
* will later will need to be converted to `CodegenStepExecJs`.
14+
*/
15+
type JsonSerializerStep = CodegenStepExecJs | unknown;
16+
17+
/**
18+
* Configuration options for {@link Codegen} instances.
19+
*/
20+
export interface CodegenOptions<Linkable = Record<string, unknown>> {
21+
/**
22+
* Inline JavaScript string that represents the arguments that will be passed
23+
* to the main function body. Defaults to "r0", i.e. the first register.
24+
*/
25+
args?: string[];
26+
27+
/**
28+
* Name of the generated function.
29+
*/
30+
name?: string;
31+
32+
/**
33+
* Inline JavaScript statements, that execute at the beginning of the main
34+
* function body.
35+
*/
36+
prologue?: string;
37+
38+
/**
39+
* Inline JavaScript statements, that execute at the end of the main
40+
* function body.
41+
*/
42+
epilogue?: string | (() => string);
43+
44+
/**
45+
* Converts all steps to `CodegenStepExecJs`.
46+
*/
47+
processSteps?: (steps: JsonSerializerStep[]) => CodegenStepExecJs[];
48+
49+
/**
50+
* Predefined list of dependencies that can be linked on demand. Dependency is
51+
* linked with the name of the property and is linked only once.
52+
*/
53+
linkable?: Linkable;
54+
}
55+
56+
export type CodegenGenerateOptions = Pick<CodegenOptions, 'name' | 'args' | 'prologue' | 'epilogue'>;
57+
58+
/**
59+
* A helper class which helps with building JavaScript code for a single
60+
* function. It keeps track of external dependencies, internally generated
61+
* constants, and execution steps, which at the end are all converted to
62+
* to an executable JavaScript function.
63+
*
64+
* The final output is a JavaScript function enclosed in a closure:
65+
*
66+
* ```js
67+
* (function(d1, d2, d3) {
68+
* var c1 = something;
69+
* var c2 = something;
70+
* var c3 = something;
71+
* return function(r0) {
72+
* var r1 = something;
73+
* var r2 = something;
74+
* var r3 = something;
75+
* return something;
76+
* }
77+
* })
78+
* ```
79+
*
80+
* Where `d*` are the external dependencies, `c*` are the internal constants,
81+
* and `r*` are the local immutable infinite registers.
82+
*/
83+
export class Codegen<
84+
Fn extends (...deps: any[]) => any = (...deps: unknown[]) => unknown,
85+
Linkable = Record<string, unknown>,
86+
> {
87+
/** @ignore */
88+
protected steps: JsonSerializerStep[] = [];
89+
90+
/** @ignore */
91+
public options: Required<CodegenOptions<Linkable>>;
92+
93+
constructor(opts: CodegenOptions<Linkable>) {
94+
this.options = {
95+
args: ['r0'],
96+
name: '',
97+
prologue: '',
98+
epilogue: '',
99+
processSteps: (steps) => steps.filter((step) => step instanceof CodegenStepExecJs) as CodegenStepExecJs[],
100+
linkable: {} as Linkable,
101+
...opts,
102+
};
103+
this.registerCounter = this.options.args.length;
104+
}
105+
106+
/**
107+
* Add one or more JavaScript statements to the main function body.
108+
*/
109+
public js(js: string): void {
110+
this.steps.push(new CodegenStepExecJs(js));
111+
}
112+
113+
public var(expression?: string): string {
114+
const r = this.getRegister();
115+
if (expression) this.js('var ' + r + ' = ' + expression + ';');
116+
else this.js('var ' + r + ';');
117+
return r;
118+
}
119+
120+
public if(condition: string, then: () => void, otherwise?: () => void): void {
121+
this.js('if (' + condition + ') {');
122+
then();
123+
if (otherwise) {
124+
this.js('} else {');
125+
otherwise();
126+
}
127+
this.js('}');
128+
}
129+
130+
public while(condition: string, block: () => void): void {
131+
this.js('while (' + condition + ') {');
132+
block();
133+
this.js('}');
134+
}
135+
136+
public doWhile(block: () => void, condition: string): void {
137+
this.js('do {');
138+
block();
139+
this.js('} while (' + condition + ');');
140+
}
141+
142+
public switch(
143+
expression: string,
144+
cases: [match: string | number | boolean | null, block: () => void, noBreak?: boolean][],
145+
def?: () => void,
146+
): void {
147+
this.js('switch (' + expression + ') {');
148+
for (const [match, block, noBreak] of cases) {
149+
this.js('case ' + match + ': {');
150+
block();
151+
if (!noBreak) this.js('break;');
152+
this.js('}');
153+
}
154+
if (def) {
155+
this.js('default: {');
156+
def();
157+
this.js('}');
158+
}
159+
this.js('}');
160+
}
161+
162+
public return(expression: string): void {
163+
this.js('return ' + expression + ';');
164+
}
165+
166+
/**
167+
* Add any application specific execution step. Steps of `unknown` type
168+
* later need to converted to `CodegenStepExecJs` steps in the `.processStep`
169+
* callback.
170+
*
171+
* @param step A step in function execution logic.
172+
*/
173+
public step(step: unknown): void {
174+
this.steps.push(step);
175+
}
176+
177+
protected registerCounter: number;
178+
179+
/**
180+
* Codegen uses the idea of infinite registers. It starts with `0` and
181+
* increments it by one for each new register. Best practice is to use
182+
* a new register for each new variable and keep them immutable.
183+
*
184+
* Usage:
185+
*
186+
* ```js
187+
* const r = codegen.getRegister();
188+
* codegen.js(`const ${r} = 1;`);
189+
* ```
190+
*
191+
* @returns a unique identifier for a variable.
192+
*/
193+
public getRegister(): string {
194+
return `r${this.registerCounter++}`;
195+
}
196+
public r(): string {
197+
return this.getRegister();
198+
}
199+
200+
/** @ignore */
201+
protected dependencies: unknown[] = [];
202+
protected dependencyNames: string[] = [];
203+
204+
/**
205+
* Allows to wire up dependencies to the generated code.
206+
*
207+
* @param dep Any JavaScript dependency, could be a function, an object,
208+
* or anything else.
209+
* @param name Optional name of the dependency. If not provided, a unique
210+
* name will be generated, which starts with `d` and a counter
211+
* appended.
212+
* @returns Returns the dependency name, a code symbol which can be used as
213+
* variable name.
214+
*/
215+
public linkDependency(dep: unknown, name: string = 'd' + this.dependencies.length): string {
216+
this.dependencies.push(dep);
217+
this.dependencyNames.push(name);
218+
return name;
219+
}
220+
221+
/**
222+
* Sames as {@link Codegen#linkDependency}, but allows to wire up multiple
223+
* dependencies at once.
224+
*/
225+
public linkDependencies(deps: unknown[]): string[] {
226+
return deps.map((dep) => this.linkDependency(dep));
227+
}
228+
229+
protected linked: {[key: string]: 1} = {};
230+
231+
/**
232+
* Link a dependency from the pre-defined `options.linkable` object. This method
233+
* can be called many times with the same dependency name, the dependency will
234+
* be linked only once.
235+
*
236+
* @param name Linkable dependency name.
237+
*/
238+
public link(name: keyof Linkable): void {
239+
if (this.linked[name as string]) return;
240+
this.linked[name as string] = 1;
241+
this.linkDependency(this.options.linkable[name], name as string);
242+
}
243+
244+
/** @ignore */
245+
protected constants: string[] = [];
246+
protected constantNames: string[] = [];
247+
248+
/**
249+
* Allows to encode any code or value in the closure of the generated
250+
* function.
251+
*
252+
* @param constant Any JavaScript value in string form.
253+
* @param name Optional name of the constant. If not provided, a unique
254+
* name will be generated, which starts with `c` and a counter
255+
* appended.
256+
* @returns Returns the constant name, a code symbol which can be used as
257+
* variable name.
258+
*/
259+
public addConstant(constant: string, name: string = 'c' + this.constants.length): string {
260+
this.constants.push(constant);
261+
this.constantNames.push(name);
262+
return name;
263+
}
264+
265+
/**
266+
* Sames as {@link Codegen#addConstant}, but allows to create multiple
267+
* constants at once.
268+
*/
269+
public addConstants(constants: string[]): string[] {
270+
return constants.map((constant) => this.addConstant(constant));
271+
}
272+
273+
/**
274+
* Returns generated JavaScript code with the dependency list.
275+
*
276+
* ```js
277+
* const code = codegen.generate();
278+
* const fn = eval(code.js)(...code.deps);
279+
* const result = fn(...args);
280+
* ```
281+
*/
282+
public generate(opts: CodegenGenerateOptions = {}): JavaScriptLinked<Fn> {
283+
const {name, args, prologue, epilogue} = {...this.options, ...opts};
284+
const steps = this.options.processSteps(this.steps);
285+
const js = `(function(${this.dependencyNames.join(', ')}) {
286+
${this.constants.map((constant, index) => `var ${this.constantNames[index]} = (${constant});`).join('\n')}
287+
return ${name ? `function ${name}` : 'function'}(${args.join(',')}){
288+
${prologue}
289+
${steps.map((step) => (step as CodegenStepExecJs).js).join('\n')}
290+
${typeof epilogue === 'function' ? epilogue() : epilogue || ''}
291+
}})`;
292+
// console.log(js);
293+
return {
294+
deps: this.dependencies,
295+
js: js as JavaScriptLinked<Fn>['js'],
296+
};
297+
}
298+
299+
/**
300+
* Compiles the generated JavaScript code into a function.
301+
*
302+
* @returns JavaScript function ready for execution.
303+
*/
304+
public compile(opts?: CodegenGenerateOptions): Fn {
305+
const closure = this.generate(opts);
306+
return compileClosure(closure);
307+
}
308+
}

‎src/README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# @jsonjoy.com/codegen
2+
3+
This package contains utilities for generating optimized JavaScript code at runtime.
4+
It enables creating high-performance functions by generating code dynamically based
5+
on schemas, templates, or runtime data.
6+
7+
## Key Benefits
8+
9+
JIT (Just-In-Time) code generation can provide significant performance improvements
10+
when you have advance knowledge of the data structure or execution pattern.
11+
12+
Some examples:
13+
14+
- **Deep equality comparison function**: When one object is known in advance, we can
15+
generate an optimized function that efficiently compares against a single object.
16+
This technique is implemented in the `json-equal` library.
17+
18+
- **JSON Patch execution**: When the JSON Patch operations are known beforehand, we can
19+
generate an optimized function that applies the patch in the most efficient way.
20+
This approach is used in the `json-patch` library.
21+
22+
- **Schema-based validation**: Given a `json-type` schema of a JSON object, it's possible
23+
to generate highly optimized functions for validation and serialization that avoid
24+
generic overhead and execute significantly faster than traditional approaches.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {CodegenStepExecJs} from '..';
2+
import {Codegen} from '../Codegen';
3+
4+
test('can generate a simple function', () => {
5+
const codegen = new Codegen({
6+
name: 'foobar',
7+
args: ['a', 'b'],
8+
prologue: 'var res = 0;',
9+
epilogue: 'return res;',
10+
processSteps: (steps) => {
11+
return steps.map((step) => {
12+
if (typeof step === 'number') {
13+
return new CodegenStepExecJs(`a += ${step};`);
14+
} else return step;
15+
}) as CodegenStepExecJs[];
16+
},
17+
});
18+
codegen.step(4);
19+
const [c1, c2] = codegen.addConstants(['1', '2']);
20+
codegen.js(`b += ${c1} + ${c2};`);
21+
const byTwo = (num: number) => 2 * num;
22+
codegen.linkDependency(byTwo, 'byTwo');
23+
codegen.js(`res += byTwo(a) + byTwo(b);`);
24+
const code = codegen.generate();
25+
const fn = codegen.compile();
26+
// console.log(code.js);
27+
expect(code.deps).toStrictEqual([byTwo]);
28+
expect(typeof code.js).toBe('string');
29+
expect(fn(1, 2)).toBe(20);
30+
expect(fn.name).toBe('foobar');
31+
});

‎src/compile.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {JavaScriptLinked} from '.';
2+
import {JavaScript} from './types';
3+
4+
// tslint:disable-next-line
5+
export const compile = <T>(js: JavaScript<T>): T => eval(js);
6+
7+
export const compileClosure = <T>(fn: JavaScriptLinked<T, any>): T => compile(fn.js)(...fn.deps);

‎src/dynamicFunction.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Wraps a function into a proxy function with the same signature, but which can
3+
* be re-implemented by the user at runtime.
4+
*
5+
* @param implementation Initial implementation.
6+
* @returns Proxy function and implementation setter.
7+
*/
8+
export const dynamicFunction = <F extends (...args: any[]) => any>(
9+
implementation: F,
10+
): [fn: F, set: (fn: F) => void] => {
11+
const proxy = ((...args) => implementation(...args)) as F;
12+
const set = (f: F) => {
13+
implementation = f;
14+
};
15+
return [proxy, set];
16+
};

‎src/index.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './types';
2+
export * from './compile';
3+
export * from './Codegen';

0 commit comments

Comments
 (0)