Skip to content

Commit 4889c67

Browse files
authored
Merge pull request #43 from jsonjoy-com/arr-tuple
BREAKING CHANGE: The "tup" type has been removed, now "arr" type supports leading and trailing type slots
2 parents d5fa602 + d14875b commit 4889c67

File tree

107 files changed

+4214
-5891
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+4214
-5891
lines changed

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@
5656
"tslib": "2"
5757
},
5858
"dependencies": {
59+
"@jsonjoy.com/buffers": "^1.0.0",
60+
"@jsonjoy.com/codegen": "^1.0.0",
5961
"@jsonjoy.com/json-expression": "^1.1.0",
60-
"@jsonjoy.com/json-pack": "^1.2.0",
61-
"@jsonjoy.com/util": "^1.6.0",
62-
"sonic-forest": "^1.2.0",
62+
"@jsonjoy.com/json-pack": "^1.9.0",
63+
"@jsonjoy.com/json-random": "^1.2.0",
64+
"@jsonjoy.com/util": "^1.9.0",
65+
"sonic-forest": "^1.2.1",
6366
"tree-dump": "^1.0.3"
6467
},
6568
"devDependencies": {

src/__bench__/encode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder';
55
import {JsonEncoder} from '@jsonjoy.com/json-pack/lib/json/JsonEncoder';
66
import type {CompiledBinaryEncoder} from '../codegen/types';
77
import {EncodingFormat} from '@jsonjoy.com/json-pack/lib/constants';
8-
import {Writer} from '@jsonjoy.com/util/lib/buffers/Writer';
8+
import {Writer} from '@jsonjoy.com/buffers/lib/Writer';
99

1010
const system = new TypeSystem();
1111
const {t} = system;

src/__tests__/fixtures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const compositeSchemas = {
4242
),
4343
s.prop('tags', s.Array(s.String())),
4444
]),
45-
tuple: s.Tuple(s.String(), s.Number(), s.Boolean()),
45+
tuple: s.Tuple([s.String(), s.Number(), s.Boolean()]),
4646
map: s.Map(s.String()),
4747
mapWithComplexValue: s.Map(s.Object([s.prop('value', s.Number()), s.prop('label', s.String())])),
4848
union: s.Or(s.String(), s.Number(), s.Boolean()),

src/codegen/AbstractCodege.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression';
2+
import type {Codegen} from '@jsonjoy.com/codegen';
3+
import type {
4+
AnyType,
5+
ArrType,
6+
BinType,
7+
BoolType,
8+
ConType,
9+
MapType,
10+
NumType,
11+
ObjType,
12+
OrType,
13+
RefType,
14+
StrType,
15+
Type,
16+
} from '../type';
17+
import type {SchemaPath} from './types';
18+
19+
export abstract class AbstractCodegen<Fn extends (...deps: any[]) => any = (...deps: unknown[]) => unknown> {
20+
public abstract readonly codegen: Codegen<Fn>;
21+
22+
protected abstract onAny(path: SchemaPath, r: JsExpression, type: AnyType): void;
23+
protected abstract onCon(path: SchemaPath, r: JsExpression, type: ConType): void;
24+
protected abstract onBool(path: SchemaPath, r: JsExpression, type: BoolType): void;
25+
protected abstract onNum(path: SchemaPath, r: JsExpression, type: NumType): void;
26+
protected abstract onStr(path: SchemaPath, r: JsExpression, type: StrType): void;
27+
protected abstract onBin(path: SchemaPath, r: JsExpression, type: BinType): void;
28+
protected abstract onArr(path: SchemaPath, r: JsExpression, type: ArrType): void;
29+
protected abstract onObj(path: SchemaPath, r: JsExpression, type: ObjType): void;
30+
protected abstract onMap(path: SchemaPath, r: JsExpression, type: MapType): void;
31+
protected abstract onRef(path: SchemaPath, r: JsExpression, type: RefType): void;
32+
protected abstract onOr(path: SchemaPath, r: JsExpression, type: OrType): void;
33+
34+
public compile() {
35+
return this.codegen.compile();
36+
}
37+
38+
protected onNode(path: SchemaPath, r: JsExpression, type: Type): void {
39+
const kind = type.kind();
40+
switch (kind) {
41+
case 'any':
42+
this.onAny(path, r, type as AnyType);
43+
break;
44+
case 'con':
45+
this.onCon(path, r, type as ConType);
46+
break;
47+
case 'bool':
48+
this.onBool(path, r, type as BoolType);
49+
break;
50+
case 'num':
51+
this.onNum(path, r, type as NumType);
52+
break;
53+
case 'str':
54+
this.onStr(path, r, type as StrType);
55+
break;
56+
case 'bin':
57+
this.onBin(path, r, type as BinType);
58+
break;
59+
case 'arr':
60+
this.onArr(path, r, type as ArrType);
61+
break;
62+
case 'obj':
63+
this.onObj(path, r, type as ObjType);
64+
break;
65+
case 'map':
66+
this.onMap(path, r, type as MapType);
67+
break;
68+
case 'ref':
69+
this.onRef(path, r, type as RefType);
70+
break;
71+
case 'or':
72+
this.onOr(path, r, type as OrType);
73+
break;
74+
default:
75+
throw new Error(`Unsupported kind: ${kind}`);
76+
}
77+
}
78+
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import {Codegen, CodegenStepExecJs} from '@jsonjoy.com/codegen';
2+
import {concat} from '@jsonjoy.com/buffers/lib/concat';
3+
import {WriteBlobStep} from './WriteBlobStep';
4+
import {Value} from '../../value/Value';
5+
import {CapacityEstimatorCodegen} from '../capacity';
6+
import {AbstractCodegen} from '../AbstractCodege';
7+
import {floats, ints, uints} from '../../util';
8+
import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression';
9+
import {DiscriminatorCodegen} from '../discriminator';
10+
import type {BinaryJsonEncoder} from '@jsonjoy.com/json-pack/lib/types';
11+
import type {
12+
AnyType,
13+
ArrType,
14+
BinType,
15+
BoolType,
16+
ConType,
17+
MapType,
18+
NumType,
19+
OrType,
20+
RefType,
21+
StrType,
22+
Type,
23+
} from '../../type';
24+
import type {CompiledBinaryEncoder, SchemaPath} from '../types';
25+
26+
type Step = WriteBlobStep | CodegenStepExecJs;
27+
28+
export abstract class AbstractBinaryCodegen<
29+
Encoder extends BinaryJsonEncoder,
30+
> extends AbstractCodegen<CompiledBinaryEncoder> {
31+
protected abstract encoder: Encoder;
32+
public readonly codegen: Codegen<CompiledBinaryEncoder>;
33+
34+
constructor(
35+
public readonly type: Type,
36+
name?: string,
37+
) {
38+
super();
39+
this.codegen = new Codegen<CompiledBinaryEncoder>({
40+
name: 'toBinary' + (name ? '_' + name : ''),
41+
args: ['r0', 'encoder'],
42+
prologue: /* js */ `
43+
var writer = encoder.writer;
44+
writer.ensureCapacity(capacityEstimator(r0));
45+
var uint8 = writer.uint8, view = writer.view;`,
46+
epilogue: '',
47+
linkable: {
48+
Value,
49+
},
50+
processSteps: (steps) => {
51+
const stepsJoined: Step[] = [];
52+
for (let i = 0; i < steps.length; i++) {
53+
const step = steps[i];
54+
if (step instanceof CodegenStepExecJs) stepsJoined.push(step);
55+
else if (step instanceof WriteBlobStep) {
56+
const last = stepsJoined[stepsJoined.length - 1];
57+
if (last instanceof WriteBlobStep) last.arr = concat(last.arr, step.arr);
58+
else stepsJoined.push(step);
59+
}
60+
}
61+
const execSteps: CodegenStepExecJs[] = [];
62+
for (const step of stepsJoined) {
63+
if (step instanceof CodegenStepExecJs) {
64+
execSteps.push(step);
65+
} else if (step instanceof WriteBlobStep) {
66+
execSteps.push(this.codegenBlob(step));
67+
}
68+
}
69+
return execSteps;
70+
},
71+
});
72+
this.codegen.linkDependency(CapacityEstimatorCodegen.get(type), 'capacityEstimator');
73+
}
74+
75+
public getBigIntStr(arr: Uint8Array, offset: number): string {
76+
const buf = new Uint8Array(8);
77+
for (let i = 0; i < 8; i++) buf[i] = arr[offset + i];
78+
const view = new DataView(buf.buffer);
79+
const bigint = view.getBigUint64(0);
80+
return bigint.toString() + 'n';
81+
}
82+
83+
private codegenBlob(step: WriteBlobStep) {
84+
const lines: string[] = [];
85+
const ro = this.codegen.getRegister();
86+
const length = step.arr.length;
87+
if (length === 1) {
88+
lines.push(/* js */ `uint8[writer.x++] = ${step.arr[0]};`);
89+
} else {
90+
lines.push(`var ${ro} = writer.x;`);
91+
lines.push(`writer.x += ${step.arr.length};`);
92+
let i = 0;
93+
while (i < length) {
94+
const remaining = length - i;
95+
if (remaining >= 8) {
96+
const value = this.getBigIntStr(step.arr, i);
97+
lines.push(/* js */ `view.setBigUint64(${ro}${i ? ` + ${i}` : ''}, ${value});`);
98+
i += 8;
99+
} else if (remaining >= 4) {
100+
const value = (step.arr[i] << 24) | (step.arr[i + 1] << 16) | (step.arr[i + 2] << 8) | step.arr[i + 3];
101+
lines.push(/* js */ `view.setInt32(${ro}${i ? ` + ${i}` : ''}, ${value});`);
102+
i += 4;
103+
} else if (remaining >= 2) {
104+
const value = (step.arr[i] << 8) | step.arr[i + 1];
105+
lines.push(/* js */ `view.setInt16(${ro}${i ? ` + ${i}` : ''}, ${value});`);
106+
i += 2;
107+
} else {
108+
lines.push(/* js */ `uint8[${ro}${i ? ` + ${i}` : ''}] = ${step.arr[i]};`);
109+
i++;
110+
}
111+
}
112+
}
113+
const js = lines.join('\n');
114+
return new CodegenStepExecJs(js);
115+
}
116+
117+
public js(js: string): void {
118+
this.codegen.js(js);
119+
}
120+
121+
public gen(callback: (encoder: Encoder) => void): Uint8Array {
122+
const encoder = this.encoder;
123+
encoder.writer.reset();
124+
callback(encoder);
125+
return encoder.writer.flush();
126+
}
127+
128+
public blob(arr: Uint8Array): void {
129+
this.codegen.step(new WriteBlobStep(arr));
130+
}
131+
132+
public compile() {
133+
return this.codegen.compile();
134+
}
135+
136+
protected onAny(path: SchemaPath, r: JsExpression, type: AnyType): void {
137+
this.codegen.js(`encoder.writeAny(${r.use()});`);
138+
}
139+
140+
protected onCon(path: SchemaPath, r: JsExpression, type: ConType): void {
141+
this.blob(
142+
this.gen((encoder) => {
143+
encoder.writeAny(type.literal());
144+
}),
145+
);
146+
}
147+
148+
protected onBool(path: SchemaPath, r: JsExpression, type: BoolType): void {
149+
this.codegen.js(/* js */ `encoder.writeBoolean(${r.use()});`);
150+
}
151+
152+
protected onNum(path: SchemaPath, r: JsExpression, type: NumType): void {
153+
const {format} = type.schema;
154+
const v = r.use();
155+
const codegen = this.codegen;
156+
if (uints.has(format)) codegen.js(/* js */ `encoder.writeUInteger(${v});`);
157+
else if (ints.has(format)) codegen.js(/* js */ `encoder.writeInteger(${v});`);
158+
else if (floats.has(format)) codegen.js(/* js */ `encoder.writeFloat(${v});`);
159+
else codegen.js(/* js */ `encoder.writeNumber(${v});`);
160+
}
161+
162+
protected onStr(path: SchemaPath, r: JsExpression, type: StrType): void {
163+
const {ascii, format} = type.schema;
164+
const codegen = this.codegen;
165+
// Use ASCII encoding if format is 'ascii' or ascii=true (backward compatibility)
166+
const v = r.use();
167+
const useAscii = format === 'ascii' || ascii;
168+
if (useAscii) codegen.js(/* js */ `encoder.writeAsciiStr(${v});`);
169+
else codegen.js(/* js */ `encoder.writeStr(${v});`);
170+
}
171+
172+
protected onBin(path: SchemaPath, r: JsExpression, type: BinType): void {
173+
this.codegen.js(/* js */ `encoder.writeBin(${r.use()});`);
174+
}
175+
176+
protected onArr(path: SchemaPath, val: JsExpression, type: ArrType): void {
177+
const codegen = this.codegen;
178+
const r = codegen.getRegister(); // array
179+
const rl = codegen.getRegister(); // array.length
180+
const ri = codegen.getRegister(); // index
181+
const {_head = [], _type, _tail = []} = type;
182+
const headLength = _head.length;
183+
const tailLength = _tail.length;
184+
const constLen = headLength + tailLength;
185+
codegen.js(/* js */ `var ${r} = ${val.use()}, ${rl} = ${r}.length, ${ri} = 0;`);
186+
if (constLen) {
187+
codegen.if(/* js */ `${rl} < ${constLen}`, () => {
188+
codegen.js(`throw new Error('ARR_LEN');`);
189+
});
190+
}
191+
codegen.js(/* js */ `encoder.writeArrHdr(${rl});`);
192+
if (headLength > 0) {
193+
for (let i = 0; i < headLength; i++) {
194+
this.onNode([...path, {r: ri}], new JsExpression(() => /* js */ `${r}[${ri}]`), _head[i]);
195+
codegen.js(/* js */ `${ri}++`);
196+
}
197+
}
198+
if (_type) {
199+
codegen.js(/* js */ `for(; ${ri} < ${rl} - ${tailLength}; ${ri}++) {`);
200+
this.onNode([...path, {r: ri}], new JsExpression(() => /* js */ `${r}[${ri}]`), _type);
201+
codegen.js(/* js */ `}`);
202+
}
203+
if (tailLength > 0) {
204+
for (let i = 0; i < tailLength; i++) {
205+
this.onNode([...path, {r: ri}], new JsExpression(() => /* js */ `${r}[${ri}]`), _tail[i]);
206+
codegen.js(/* js */ `${ri}++`);
207+
}
208+
}
209+
}
210+
211+
protected onMap(path: SchemaPath, val: JsExpression, type: MapType): void {
212+
const codegen = this.codegen;
213+
const r = codegen.var(val.use());
214+
const rKeys = codegen.var(/* js */ `Object.keys(${r})`);
215+
const rKey = codegen.var();
216+
const rLength = codegen.var(/* js */ `${rKeys}.length`);
217+
const ri = codegen.var('0');
218+
codegen.js(`encoder.writeObjHdr(${rLength});`);
219+
codegen.js(`for(; ${ri} < ${rLength}; ${ri}++){`);
220+
codegen.js(`${rKey} = ${rKeys}[${ri}];`);
221+
codegen.js(`encoder.writeStr(${rKey});`);
222+
const expr = new JsExpression(() => `${r}[${rKey}]`);
223+
this.onNode([...path, {r: rKey}], expr, type._value);
224+
codegen.js(/* js */ `}`);
225+
}
226+
227+
protected onOr(path: SchemaPath, r: JsExpression, type: OrType<Type[]>): void {
228+
const codegen = this.codegen;
229+
const discriminator = DiscriminatorCodegen.get(type);
230+
const d = codegen.linkDependency(discriminator);
231+
const types = type.types;
232+
codegen.switch(
233+
`${d}(${r.use()})`,
234+
types.map((type, index) => [
235+
index,
236+
() => {
237+
this.onNode(path, r, type);
238+
},
239+
]),
240+
);
241+
}
242+
243+
protected abstract genEncoder(type: Type): CompiledBinaryEncoder;
244+
245+
protected onRef(path: SchemaPath, r: JsExpression, type: RefType): void {
246+
const system = type.getSystem();
247+
const alias = system.resolve(type.ref());
248+
switch (alias.type.kind()) {
249+
case 'str':
250+
case 'bool':
251+
case 'num':
252+
case 'any':
253+
case 'tup': {
254+
this.onNode(path, r, alias.type);
255+
break;
256+
}
257+
default: {
258+
const encoder = this.genEncoder(alias.type);
259+
const codegen = this.codegen;
260+
const d = codegen.linkDependency(encoder);
261+
codegen.js(/* js */ `${d}(${r.use()}, encoder);`);
262+
}
263+
}
264+
}
265+
}

0 commit comments

Comments
 (0)