|
| 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