From 35febb1d566750d4d7a42730c403037b55d704ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:47:16 +0000 Subject: [PATCH 1/6] Initial plan From b334c0105f9b61c68e2a73604b7a85f817b86d3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:57:39 +0000 Subject: [PATCH 2/6] Changes before error encountered Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/codegen/capacity/estimators.ts | 176 +++++++++++++++++++++++++++++ src/codegen/capacity/router.ts | 66 +++++++++++ src/type/classes/AbstractType.ts | 8 +- 3 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 src/codegen/capacity/estimators.ts create mode 100644 src/codegen/capacity/router.ts diff --git a/src/codegen/capacity/estimators.ts b/src/codegen/capacity/estimators.ts new file mode 100644 index 00000000..225637d9 --- /dev/null +++ b/src/codegen/capacity/estimators.ts @@ -0,0 +1,176 @@ +import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; +import {MaxEncodingOverhead, maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size'; +import type {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; +import type {Type} from '../../type'; +import {ConstType} from '../../type/classes/ConstType'; +import {BooleanType} from '../../type/classes/BooleanType'; +import {NumberType} from '../../type/classes/NumberType'; + +type EstimatorFunction = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type) => void; + +function normalizeAccessor(key: string): string { + // Simple property access for valid identifiers, bracket notation otherwise + if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) { + return `.${key}`; + } + return `[${JSON.stringify(key)}]`; +} + +export function estimateAnyCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { + const codegen = ctx.codegen; + codegen.link('Value'); + const r = codegen.var(value.use()); + codegen.if( + `${r} instanceof Value`, + () => { + codegen.if( + `${r}.type`, + () => { + ctx.codegen.js(`size += ${r}.type.capacityEstimator()(${r}.data);`); + }, + () => { + ctx.codegen.js(`size += maxEncodingCapacity(${r}.data);`); + }, + ); + }, + () => { + ctx.codegen.js(`size += maxEncodingCapacity(${r});`); + }, + ); +} + +export function estimateBooleanCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { + ctx.inc(MaxEncodingOverhead.Boolean); +} + +export function estimateNumberCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { + ctx.inc(MaxEncodingOverhead.Number); +} + +export function estimateStringCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { + ctx.inc(MaxEncodingOverhead.String); + ctx.codegen.js(`size += ${MaxEncodingOverhead.StringLengthMultiplier} * ${value.use()}.length;`); +} + +export function estimateBinaryCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { + ctx.inc(MaxEncodingOverhead.Binary); + ctx.codegen.js(`size += ${MaxEncodingOverhead.BinaryLengthMultiplier} * ${value.use()}.length;`); +} + +export function estimateConstCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { + const constType = type as any; // ConstType + ctx.inc(maxEncodingCapacity(constType.value())); +} + +export function estimateArrayCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { + const codegen = ctx.codegen; + ctx.inc(MaxEncodingOverhead.Array); + const rLen = codegen.var(`${value.use()}.length`); + const arrayType = type as any; // ArrayType + const elementType = arrayType.type; + codegen.js(`size += ${MaxEncodingOverhead.ArrayElement} * ${rLen}`); + const fn = elementType.compileCapacityEstimator({ + system: ctx.options.system, + name: ctx.options.name, + }); + const isConstantSizeType = elementType instanceof ConstType || elementType instanceof BooleanType || elementType instanceof NumberType; + if (isConstantSizeType) { + const rFn = codegen.linkDependency(fn); + codegen.js(`size += ${rLen} * ${rFn}(${elementType.random()});`); + } else { + const rFn = codegen.linkDependency(fn); + const ri = codegen.var('0'); + codegen.js(`for (; ${ri} < ${rLen}; ${ri}++) {`); + codegen.js(`size += ${rFn}(${value.use()}[${ri}]);`); + codegen.js(`}`); + } +} + +export function estimateTupleCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { + const codegen = ctx.codegen; + const r = codegen.var(value.use()); + const tupleType = type as any; // TupleType + const types = tupleType.types; + const overhead = MaxEncodingOverhead.Array + MaxEncodingOverhead.ArrayElement * types.length; + ctx.inc(overhead); + for (let i = 0; i < types.length; i++) { + const elementType = types[i]; + const fn = elementType.compileCapacityEstimator({ + system: ctx.options.system, + name: ctx.options.name, + }); + const rFn = codegen.linkDependency(fn); + codegen.js(`size += ${rFn}(${r}[${i}]);`); + } +} + +export function estimateObjectCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type, estimateCapacityFn: EstimatorFunction): void { + const codegen = ctx.codegen; + const r = codegen.var(value.use()); + const objectType = type as any; // ObjectType + const encodeUnknownFields = !!objectType.schema.encodeUnknownFields; + if (encodeUnknownFields) { + codegen.js(`size += maxEncodingCapacity(${r});`); + return; + } + const fields = objectType.fields; + const overhead = MaxEncodingOverhead.Object + fields.length * MaxEncodingOverhead.ObjectElement; + ctx.inc(overhead); + for (const field of fields) { + ctx.inc(maxEncodingCapacity(field.key)); + const accessor = normalizeAccessor(field.key); + const isOptional = field.optional || (field.constructor?.name === 'ObjectOptionalFieldType'); + const block = () => estimateCapacityFn(ctx, new JsExpression(() => `${r}${accessor}`), field.value); + if (isOptional) { + codegen.if(`${r}${accessor} !== undefined`, block); + } else block(); + } +} + +export function estimateMapCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { + const codegen = ctx.codegen; + ctx.inc(MaxEncodingOverhead.Object); + const r = codegen.var(value.use()); + const rKeys = codegen.var(`Object.keys(${r})`); + const rKey = codegen.var(); + const rLen = codegen.var(`${rKeys}.length`); + codegen.js(`size += ${MaxEncodingOverhead.ObjectElement} * ${rLen}`); + const mapType = type as any; // MapType + const valueType = mapType.type; + const fn = valueType.compileCapacityEstimator({ + system: ctx.options.system, + name: ctx.options.name, + }); + const rFn = codegen.linkDependency(fn); + const ri = codegen.var('0'); + codegen.js(`for (; ${ri} < ${rLen}; ${ri}++) {`); + codegen.js(`${rKey} = ${rKeys}[${ri}];`); + codegen.js(`size += maxEncodingCapacity(${rKey}) + ${rFn}(${r}[${rKey}]);`); + codegen.js(`}`); +} + +export function estimateRefCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { + const refType = type as any; // RefType + const system = ctx.options.system || refType.system; + if (!system) throw new Error('NO_SYSTEM'); + const estimator = system.resolve(refType.schema.ref).type.capacityEstimator(); + const d = ctx.codegen.linkDependency(estimator); + ctx.codegen.js(`size += ${d}(${value.use()});`); +} + +export function estimateOrCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type, estimateCapacityFn: EstimatorFunction): void { + const codegen = ctx.codegen; + const orType = type as any; // OrType + const discriminator = orType.discriminator(); + const d = codegen.linkDependency(discriminator); + const types = orType.types; + codegen.switch( + `${d}(${value.use()})`, + types.map((childType: Type, index: number) => [ + index, + () => { + estimateCapacityFn(ctx, value, childType); + }, + ]), + ); +} \ No newline at end of file diff --git a/src/codegen/capacity/router.ts b/src/codegen/capacity/router.ts new file mode 100644 index 00000000..8d0ed0c1 --- /dev/null +++ b/src/codegen/capacity/router.ts @@ -0,0 +1,66 @@ +import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; +import type {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; +import type {Type} from '../../type'; +import { + estimateAnyCapacity, + estimateBooleanCapacity, + estimateNumberCapacity, + estimateStringCapacity, + estimateBinaryCapacity, + estimateConstCapacity, + estimateArrayCapacity, + estimateTupleCapacity, + estimateObjectCapacity, + estimateMapCapacity, + estimateRefCapacity, + estimateOrCapacity, +} from './estimators'; + +/** + * Main router function that dispatches capacity estimation to the appropriate + * estimator function based on the type's kind. + */ +export function estimateCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { + const kind = type.getTypeName(); + + switch (kind) { + case 'any': + estimateAnyCapacity(ctx, value, type); + break; + case 'bool': + estimateBooleanCapacity(ctx, value); + break; + case 'num': + estimateNumberCapacity(ctx, value); + break; + case 'str': + estimateStringCapacity(ctx, value); + break; + case 'bin': + estimateBinaryCapacity(ctx, value); + break; + case 'const': + estimateConstCapacity(ctx, value, type); + break; + case 'arr': + estimateArrayCapacity(ctx, value, type); + break; + case 'tup': + estimateTupleCapacity(ctx, value, type); + break; + case 'obj': + estimateObjectCapacity(ctx, value, type, estimateCapacity); + break; + case 'map': + estimateMapCapacity(ctx, value, type); + break; + case 'ref': + estimateRefCapacity(ctx, value, type); + break; + case 'or': + estimateOrCapacity(ctx, value, type, estimateCapacity); + break; + default: + throw new Error(`${kind} type capacity estimation not implemented`); + } +} \ No newline at end of file diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index 0d505eb4..4999b4b7 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -36,6 +36,7 @@ import { type CapacityEstimatorCodegenContextOptions, type CompiledCapacityEstimator, } from '../../codegen/capacity/CapacityEstimatorCodegenContext'; +import {estimateCapacity} from '../../codegen/capacity/router'; import type {JsonValueCodec} from '@jsonjoy.com/json-pack/lib/codecs/types'; import type * as jsonSchema from '../../json-schema'; import type {BaseType} from '../types'; @@ -265,13 +266,12 @@ export abstract class AbstractType implements BaseType< }); const r = ctx.codegen.options.args[0]; const value = new JsExpression(() => r); - this.codegenCapacityEstimator(ctx, value); + // Use the centralized router instead of the abstract method + estimateCapacity(ctx, value, this); return ctx.compile(); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - throw new Error(`${this.toStringName()}.codegenCapacityEstimator() not implemented`); - } + private __capacityEstimator: CompiledCapacityEstimator | undefined; public capacityEstimator(): CompiledCapacityEstimator { From b0b43b86836ee827b6ce8a748975233d937d5fe4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 08:13:56 +0000 Subject: [PATCH 3/6] Complete capacity estimator refactoring - centralized system working Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/codegen/capacity/estimators.ts | 5 +---- src/codegen/capacity/router.ts | 2 +- src/type/classes/AbstractType.ts | 2 +- src/type/classes/AnyType.ts | 23 ----------------------- src/type/classes/ArrayType.ts | 26 -------------------------- src/type/classes/BinaryType.ts | 5 ----- src/type/classes/BooleanType.ts | 4 ---- src/type/classes/ConstType.ts | 4 ---- src/type/classes/MapType.ts | 24 ------------------------ src/type/classes/NumberType.ts | 4 ---- src/type/classes/ObjectType.ts | 22 ---------------------- src/type/classes/OrType.ts | 16 ---------------- src/type/classes/RefType.ts | 8 -------- src/type/classes/StringType.ts | 5 ----- src/type/classes/TupleType.ts | 17 ----------------- 15 files changed, 3 insertions(+), 164 deletions(-) diff --git a/src/codegen/capacity/estimators.ts b/src/codegen/capacity/estimators.ts index 225637d9..fd547e7b 100644 --- a/src/codegen/capacity/estimators.ts +++ b/src/codegen/capacity/estimators.ts @@ -2,9 +2,6 @@ import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; import {MaxEncodingOverhead, maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size'; import type {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; import type {Type} from '../../type'; -import {ConstType} from '../../type/classes/ConstType'; -import {BooleanType} from '../../type/classes/BooleanType'; -import {NumberType} from '../../type/classes/NumberType'; type EstimatorFunction = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type) => void; @@ -73,7 +70,7 @@ export function estimateArrayCapacity(ctx: CapacityEstimatorCodegenContext, valu system: ctx.options.system, name: ctx.options.name, }); - const isConstantSizeType = elementType instanceof ConstType || elementType instanceof BooleanType || elementType instanceof NumberType; + const isConstantSizeType = ['const', 'bool', 'num'].includes(elementType.getTypeName()); if (isConstantSizeType) { const rFn = codegen.linkDependency(fn); codegen.js(`size += ${rLen} * ${rFn}(${elementType.random()});`); diff --git a/src/codegen/capacity/router.ts b/src/codegen/capacity/router.ts index 8d0ed0c1..f5f0a62e 100644 --- a/src/codegen/capacity/router.ts +++ b/src/codegen/capacity/router.ts @@ -1,4 +1,4 @@ -import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; +import type {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; import type {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; import type {Type} from '../../type'; import { diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index 4999b4b7..8b9ce5f6 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -267,7 +267,7 @@ export abstract class AbstractType implements BaseType< const r = ctx.codegen.options.args[0]; const value = new JsExpression(() => r); // Use the centralized router instead of the abstract method - estimateCapacity(ctx, value, this); + estimateCapacity(ctx, value, this as any); return ctx.compile(); } diff --git a/src/type/classes/AnyType.ts b/src/type/classes/AnyType.ts index 9d4c7c70..ab44b9fd 100644 --- a/src/type/classes/AnyType.ts +++ b/src/type/classes/AnyType.ts @@ -82,29 +82,6 @@ export class AnyType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - const codegen = ctx.codegen; - codegen.link('Value'); - const r = codegen.var(value.use()); - codegen.if( - `${r} instanceof Value`, - () => { - codegen.if( - `${r}.type`, - () => { - ctx.codegen.js(`size += ${r}.type.capacityEstimator()(${r}.data);`); - }, - () => { - ctx.codegen.js(`size += maxEncodingCapacity(${r}.data);`); - }, - ); - }, - () => { - ctx.codegen.js(`size += maxEncodingCapacity(${r});`); - }, - ); - } - public random(): unknown { return RandomJson.generate({nodeCount: 5}); } diff --git a/src/type/classes/ArrayType.ts b/src/type/classes/ArrayType.ts index 2e2e8f54..9b1d6173 100644 --- a/src/type/classes/ArrayType.ts +++ b/src/type/classes/ArrayType.ts @@ -11,12 +11,7 @@ import {CborEncoderCodegenContext} from '../../codegen/binary/CborEncoderCodegen import type {JsonEncoderCodegenContext} from '../../codegen/binary/JsonEncoderCodegenContext'; import type {BinaryEncoderCodegenContext} from '../../codegen/binary/BinaryEncoderCodegenContext'; import {MessagePackEncoderCodegenContext} from '../../codegen/binary/MessagePackEncoderCodegenContext'; -import type {CapacityEstimatorCodegenContext} from '../../codegen/capacity/CapacityEstimatorCodegenContext'; -import {MaxEncodingOverhead} from '@jsonjoy.com/util/lib/json-size'; import {AbstractType} from './AbstractType'; -import {ConstType} from './ConstType'; -import {BooleanType} from './BooleanType'; -import {NumberType} from './NumberType'; import type * as jsonSchema from '../../json-schema'; import type {SchemaOf, Type} from '../types'; import type {TypeSystem} from '../../system/TypeSystem'; @@ -163,27 +158,6 @@ export class ArrayType extends AbstractType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(MaxEncodingOverhead.Boolean); - } - public random(): boolean { return RandomJson.genBoolean(); } diff --git a/src/type/classes/ConstType.ts b/src/type/classes/ConstType.ts index 3d3715ce..8d39e3a6 100644 --- a/src/type/classes/ConstType.ts +++ b/src/type/classes/ConstType.ts @@ -84,10 +84,6 @@ export class ConstType extends AbstractType> { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(maxEncodingCapacity(this.value())); - } - public random(): unknown { return cloneBinary(this.schema.value); } diff --git a/src/type/classes/MapType.ts b/src/type/classes/MapType.ts index e1aa19b2..dda38976 100644 --- a/src/type/classes/MapType.ts +++ b/src/type/classes/MapType.ts @@ -151,30 +151,6 @@ export class MapType extends AbstractType { const length = Math.round(Math.random() * 10); const res: Record = {}; diff --git a/src/type/classes/NumberType.ts b/src/type/classes/NumberType.ts index 8cadd782..d6618ced 100644 --- a/src/type/classes/NumberType.ts +++ b/src/type/classes/NumberType.ts @@ -166,10 +166,6 @@ export class NumberType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(MaxEncodingOverhead.Number); - } - public random(): number { let num = Math.random(); let min = Number.MIN_SAFE_INTEGER; diff --git a/src/type/classes/ObjectType.ts b/src/type/classes/ObjectType.ts index 9c97cd65..cf080acd 100644 --- a/src/type/classes/ObjectType.ts +++ b/src/type/classes/ObjectType.ts @@ -497,28 +497,6 @@ if (${rLength}) { } } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - const codegen = ctx.codegen; - const r = codegen.var(value.use()); - const encodeUnknownFields = !!this.schema.encodeUnknownFields; - if (encodeUnknownFields) { - codegen.js(`size += maxEncodingCapacity(${r});`); - return; - } - const fields = this.fields; - const overhead = MaxEncodingOverhead.Object + fields.length * MaxEncodingOverhead.ObjectElement; - ctx.inc(overhead); - for (const field of fields) { - ctx.inc(maxEncodingCapacity(field.key)); - const accessor = normalizeAccessor(field.key); - const isOptional = field instanceof ObjectOptionalFieldType; - const block = () => field.value.codegenCapacityEstimator(ctx, new JsExpression(() => `${r}${accessor}`)); - if (isOptional) { - codegen.if(`${r}${accessor} !== undefined`, block); - } else block(); - } - } - public random(): Record { const schema = this.schema; const obj: Record = schema.unknownFields ? >RandomJson.genObject() : {}; diff --git a/src/type/classes/OrType.ts b/src/type/classes/OrType.ts index 369f44a0..0c15d059 100644 --- a/src/type/classes/OrType.ts +++ b/src/type/classes/OrType.ts @@ -144,22 +144,6 @@ export class OrType extends AbstractType [ - index, - () => { - type.codegenCapacityEstimator(ctx, value); - }, - ]), - ); - } - public random(): unknown { const types = this.types; const index = Math.floor(Math.random() * types.length); diff --git a/src/type/classes/RefType.ts b/src/type/classes/RefType.ts index 06793538..9b38f3c9 100644 --- a/src/type/classes/RefType.ts +++ b/src/type/classes/RefType.ts @@ -128,14 +128,6 @@ export class RefType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(MaxEncodingOverhead.String); - ctx.codegen.js(`size += ${MaxEncodingOverhead.StringLengthMultiplier} * ${value.use()}.length;`); - } - public random(): string { let length = Math.round(Math.random() * 10); const {min, max} = this.schema; diff --git a/src/type/classes/TupleType.ts b/src/type/classes/TupleType.ts index a5e4c09c..56c03bd6 100644 --- a/src/type/classes/TupleType.ts +++ b/src/type/classes/TupleType.ts @@ -142,23 +142,6 @@ export class TupleType extends AbstractType type.random()); } From 232218c2b1be226b703355ebde1706f41dfa2887 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 08:54:10 +0000 Subject: [PATCH 4/6] Refactor capacity estimators to use arrow functions and short names Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/codegen/capacity/estimators.ts | 123 ++++++++++++++++++++++------- src/codegen/capacity/index.ts | 22 ++++++ src/codegen/capacity/router.ts | 66 ---------------- src/type/classes/AbstractType.ts | 4 +- 4 files changed, 120 insertions(+), 95 deletions(-) create mode 100644 src/codegen/capacity/index.ts delete mode 100644 src/codegen/capacity/router.ts diff --git a/src/codegen/capacity/estimators.ts b/src/codegen/capacity/estimators.ts index fd547e7b..e0d96cfc 100644 --- a/src/codegen/capacity/estimators.ts +++ b/src/codegen/capacity/estimators.ts @@ -1,19 +1,20 @@ import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; import {MaxEncodingOverhead, maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size'; -import type {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; +import {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; +import type {CapacityEstimatorCodegenContextOptions, CompiledCapacityEstimator} from './CapacityEstimatorCodegenContext'; import type {Type} from '../../type'; type EstimatorFunction = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type) => void; -function normalizeAccessor(key: string): string { +const normalizeAccessor = (key: string): string => { // Simple property access for valid identifiers, bracket notation otherwise if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) { return `.${key}`; } return `[${JSON.stringify(key)}]`; -} +}; -export function estimateAnyCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { +export const any = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { const codegen = ctx.codegen; codegen.link('Value'); const r = codegen.var(value.use()); @@ -34,32 +35,32 @@ export function estimateAnyCapacity(ctx: CapacityEstimatorCodegenContext, value: ctx.codegen.js(`size += maxEncodingCapacity(${r});`); }, ); -} +}; -export function estimateBooleanCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { +export const bool = (ctx: CapacityEstimatorCodegenContext, value: JsExpression): void => { ctx.inc(MaxEncodingOverhead.Boolean); -} +}; -export function estimateNumberCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { +export const num = (ctx: CapacityEstimatorCodegenContext, value: JsExpression): void => { ctx.inc(MaxEncodingOverhead.Number); -} +}; -export function estimateStringCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { +export const str = (ctx: CapacityEstimatorCodegenContext, value: JsExpression): void => { ctx.inc(MaxEncodingOverhead.String); ctx.codegen.js(`size += ${MaxEncodingOverhead.StringLengthMultiplier} * ${value.use()}.length;`); -} +}; -export function estimateBinaryCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { +export const bin = (ctx: CapacityEstimatorCodegenContext, value: JsExpression): void => { ctx.inc(MaxEncodingOverhead.Binary); ctx.codegen.js(`size += ${MaxEncodingOverhead.BinaryLengthMultiplier} * ${value.use()}.length;`); -} +}; -export function estimateConstCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { +export const const_ = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { const constType = type as any; // ConstType ctx.inc(maxEncodingCapacity(constType.value())); -} +}; -export function estimateArrayCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { +export const arr = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { const codegen = ctx.codegen; ctx.inc(MaxEncodingOverhead.Array); const rLen = codegen.var(`${value.use()}.length`); @@ -81,9 +82,9 @@ export function estimateArrayCapacity(ctx: CapacityEstimatorCodegenContext, valu codegen.js(`size += ${rFn}(${value.use()}[${ri}]);`); codegen.js(`}`); } -} +}; -export function estimateTupleCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { +export const tup = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { const codegen = ctx.codegen; const r = codegen.var(value.use()); const tupleType = type as any; // TupleType @@ -99,9 +100,9 @@ export function estimateTupleCapacity(ctx: CapacityEstimatorCodegenContext, valu const rFn = codegen.linkDependency(fn); codegen.js(`size += ${rFn}(${r}[${i}]);`); } -} +}; -export function estimateObjectCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type, estimateCapacityFn: EstimatorFunction): void { +export const obj = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type, estimateCapacityFn: EstimatorFunction): void => { const codegen = ctx.codegen; const r = codegen.var(value.use()); const objectType = type as any; // ObjectType @@ -122,9 +123,9 @@ export function estimateObjectCapacity(ctx: CapacityEstimatorCodegenContext, val codegen.if(`${r}${accessor} !== undefined`, block); } else block(); } -} +}; -export function estimateMapCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { +export const map = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { const codegen = ctx.codegen; ctx.inc(MaxEncodingOverhead.Object); const r = codegen.var(value.use()); @@ -144,18 +145,18 @@ export function estimateMapCapacity(ctx: CapacityEstimatorCodegenContext, value: codegen.js(`${rKey} = ${rKeys}[${ri}];`); codegen.js(`size += maxEncodingCapacity(${rKey}) + ${rFn}(${r}[${rKey}]);`); codegen.js(`}`); -} +}; -export function estimateRefCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { +export const ref = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { const refType = type as any; // RefType const system = ctx.options.system || refType.system; if (!system) throw new Error('NO_SYSTEM'); const estimator = system.resolve(refType.schema.ref).type.capacityEstimator(); const d = ctx.codegen.linkDependency(estimator); ctx.codegen.js(`size += ${d}(${value.use()});`); -} +}; -export function estimateOrCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type, estimateCapacityFn: EstimatorFunction): void { +export const or = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type, estimateCapacityFn: EstimatorFunction): void => { const codegen = ctx.codegen; const orType = type as any; // OrType const discriminator = orType.discriminator(); @@ -170,4 +171,72 @@ export function estimateOrCapacity(ctx: CapacityEstimatorCodegenContext, value: }, ]), ); -} \ No newline at end of file +}; + +/** + * Main router function that dispatches capacity estimation to the appropriate + * estimator function based on the type's kind. + */ +export const generate = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { + const kind = type.getTypeName(); + + switch (kind) { + case 'any': + any(ctx, value, type); + break; + case 'bool': + bool(ctx, value); + break; + case 'num': + num(ctx, value); + break; + case 'str': + str(ctx, value); + break; + case 'bin': + bin(ctx, value); + break; + case 'const': + const_(ctx, value, type); + break; + case 'arr': + arr(ctx, value, type); + break; + case 'tup': + tup(ctx, value, type); + break; + case 'obj': + obj(ctx, value, type, generate); + break; + case 'map': + map(ctx, value, type); + break; + case 'ref': + ref(ctx, value, type); + break; + case 'or': + or(ctx, value, type, generate); + break; + default: + throw new Error(`${kind} type capacity estimation not implemented`); + } +}; + +/** + * Standalone function to generate a capacity estimator for a given type. + */ +export const codegen = ( + type: Type, + options: Omit, +): CompiledCapacityEstimator => { + const ctx = new CapacityEstimatorCodegenContext({ + system: type.system, + ...options, + type: type as any, + }); + const r = ctx.codegen.options.args[0]; + const value = new JsExpression(() => r); + // Use the centralized router instead of the abstract method + generate(ctx, value, type); + return ctx.compile(); +}; \ No newline at end of file diff --git a/src/codegen/capacity/index.ts b/src/codegen/capacity/index.ts new file mode 100644 index 00000000..c673e8e2 --- /dev/null +++ b/src/codegen/capacity/index.ts @@ -0,0 +1,22 @@ +export { + generate, + codegen, + any, + bool, + num, + str, + bin, + const_, + arr, + tup, + obj, + map, + ref, + or, +} from './estimators'; + +export { CapacityEstimatorCodegenContext } from './CapacityEstimatorCodegenContext'; +export type { + CapacityEstimatorCodegenContextOptions, + CompiledCapacityEstimator +} from './CapacityEstimatorCodegenContext'; \ No newline at end of file diff --git a/src/codegen/capacity/router.ts b/src/codegen/capacity/router.ts deleted file mode 100644 index f5f0a62e..00000000 --- a/src/codegen/capacity/router.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; -import type {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; -import type {Type} from '../../type'; -import { - estimateAnyCapacity, - estimateBooleanCapacity, - estimateNumberCapacity, - estimateStringCapacity, - estimateBinaryCapacity, - estimateConstCapacity, - estimateArrayCapacity, - estimateTupleCapacity, - estimateObjectCapacity, - estimateMapCapacity, - estimateRefCapacity, - estimateOrCapacity, -} from './estimators'; - -/** - * Main router function that dispatches capacity estimation to the appropriate - * estimator function based on the type's kind. - */ -export function estimateCapacity(ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void { - const kind = type.getTypeName(); - - switch (kind) { - case 'any': - estimateAnyCapacity(ctx, value, type); - break; - case 'bool': - estimateBooleanCapacity(ctx, value); - break; - case 'num': - estimateNumberCapacity(ctx, value); - break; - case 'str': - estimateStringCapacity(ctx, value); - break; - case 'bin': - estimateBinaryCapacity(ctx, value); - break; - case 'const': - estimateConstCapacity(ctx, value, type); - break; - case 'arr': - estimateArrayCapacity(ctx, value, type); - break; - case 'tup': - estimateTupleCapacity(ctx, value, type); - break; - case 'obj': - estimateObjectCapacity(ctx, value, type, estimateCapacity); - break; - case 'map': - estimateMapCapacity(ctx, value, type); - break; - case 'ref': - estimateRefCapacity(ctx, value, type); - break; - case 'or': - estimateOrCapacity(ctx, value, type, estimateCapacity); - break; - default: - throw new Error(`${kind} type capacity estimation not implemented`); - } -} \ No newline at end of file diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index 8b9ce5f6..9ab31fd7 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -36,7 +36,7 @@ import { type CapacityEstimatorCodegenContextOptions, type CompiledCapacityEstimator, } from '../../codegen/capacity/CapacityEstimatorCodegenContext'; -import {estimateCapacity} from '../../codegen/capacity/router'; +import {generate} from '../../codegen/capacity/estimators'; import type {JsonValueCodec} from '@jsonjoy.com/json-pack/lib/codecs/types'; import type * as jsonSchema from '../../json-schema'; import type {BaseType} from '../types'; @@ -267,7 +267,7 @@ export abstract class AbstractType implements BaseType< const r = ctx.codegen.options.args[0]; const value = new JsExpression(() => r); // Use the centralized router instead of the abstract method - estimateCapacity(ctx, value, this as any); + generate(ctx, value, this as any); return ctx.compile(); } From 3509dfdc0d481e20af48bb9176ca033ad67f9f5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 08:56:29 +0000 Subject: [PATCH 5/6] Add comprehensive tests for standalone codegen function Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- .../CapacityEstimatorCodegenContext.spec.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts b/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts index c4763781..7d85b8d9 100644 --- a/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts +++ b/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts @@ -215,3 +215,46 @@ describe('or', () => { expect(estimator([1, 2, 3])).toBe(maxEncodingCapacity([1, 2, 3])); }); }); + +describe('standalone codegen function', () => { + test('generates capacity estimator equivalent to compileCapacityEstimator', () => { + const system = new TypeSystem(); + const type = system.t.Array(system.t.str); + + // Compare standalone codegen function with the class method + const {codegen} = require('../estimators'); + const standaloneEstimator = codegen(type, {}); + const classEstimator = type.compileCapacityEstimator({}); + + const testData = ['hello', 'world', 'test']; + expect(standaloneEstimator(testData)).toBe(classEstimator(testData)); + expect(standaloneEstimator(testData)).toBe(maxEncodingCapacity(testData)); + }); + + test('works with complex nested types', () => { + const system = new TypeSystem(); + const type = system.t.Object( + system.t.prop('name', system.t.str), + system.t.prop('items', system.t.Array(system.t.num)) + ); + + const {codegen} = require('../estimators'); + const standaloneEstimator = codegen(type, {}); + const classEstimator = type.compileCapacityEstimator({}); + + const testData = {name: 'test', items: [1, 2, 3, 4, 5]}; + expect(standaloneEstimator(testData)).toBe(classEstimator(testData)); + expect(standaloneEstimator(testData)).toBe(maxEncodingCapacity(testData)); + }); + + test('works with const types', () => { + const system = new TypeSystem(); + const type = system.t.Const('hello world'); + + const {codegen} = require('../estimators'); + const standaloneEstimator = codegen(type, {}); + + // For const types, the value doesn't matter + expect(standaloneEstimator(null)).toBe(maxEncodingCapacity('hello world')); + }); +}); From ec46d1ceae12b43f8665845d7afb9dbddef73f7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 09:33:26 +0000 Subject: [PATCH 6/6] Run formatting fixes and update index.ts to use star export Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- .../CapacityEstimatorCodegenContext.spec.ts | 14 +++++----- src/codegen/capacity/estimators.ts | 25 ++++++++++++----- src/codegen/capacity/index.ts | 27 +++++-------------- src/type/classes/AbstractType.ts | 2 -- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts b/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts index 7d85b8d9..276e7c15 100644 --- a/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts +++ b/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts @@ -220,12 +220,12 @@ describe('standalone codegen function', () => { test('generates capacity estimator equivalent to compileCapacityEstimator', () => { const system = new TypeSystem(); const type = system.t.Array(system.t.str); - + // Compare standalone codegen function with the class method const {codegen} = require('../estimators'); const standaloneEstimator = codegen(type, {}); const classEstimator = type.compileCapacityEstimator({}); - + const testData = ['hello', 'world', 'test']; expect(standaloneEstimator(testData)).toBe(classEstimator(testData)); expect(standaloneEstimator(testData)).toBe(maxEncodingCapacity(testData)); @@ -235,13 +235,13 @@ describe('standalone codegen function', () => { const system = new TypeSystem(); const type = system.t.Object( system.t.prop('name', system.t.str), - system.t.prop('items', system.t.Array(system.t.num)) + system.t.prop('items', system.t.Array(system.t.num)), ); - + const {codegen} = require('../estimators'); const standaloneEstimator = codegen(type, {}); const classEstimator = type.compileCapacityEstimator({}); - + const testData = {name: 'test', items: [1, 2, 3, 4, 5]}; expect(standaloneEstimator(testData)).toBe(classEstimator(testData)); expect(standaloneEstimator(testData)).toBe(maxEncodingCapacity(testData)); @@ -250,10 +250,10 @@ describe('standalone codegen function', () => { test('works with const types', () => { const system = new TypeSystem(); const type = system.t.Const('hello world'); - + const {codegen} = require('../estimators'); const standaloneEstimator = codegen(type, {}); - + // For const types, the value doesn't matter expect(standaloneEstimator(null)).toBe(maxEncodingCapacity('hello world')); }); diff --git a/src/codegen/capacity/estimators.ts b/src/codegen/capacity/estimators.ts index e0d96cfc..7e4a7a43 100644 --- a/src/codegen/capacity/estimators.ts +++ b/src/codegen/capacity/estimators.ts @@ -1,7 +1,10 @@ import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; import {MaxEncodingOverhead, maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size'; import {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; -import type {CapacityEstimatorCodegenContextOptions, CompiledCapacityEstimator} from './CapacityEstimatorCodegenContext'; +import type { + CapacityEstimatorCodegenContextOptions, + CompiledCapacityEstimator, +} from './CapacityEstimatorCodegenContext'; import type {Type} from '../../type'; type EstimatorFunction = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type) => void; @@ -102,7 +105,12 @@ export const tup = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, t } }; -export const obj = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type, estimateCapacityFn: EstimatorFunction): void => { +export const obj = ( + ctx: CapacityEstimatorCodegenContext, + value: JsExpression, + type: Type, + estimateCapacityFn: EstimatorFunction, +): void => { const codegen = ctx.codegen; const r = codegen.var(value.use()); const objectType = type as any; // ObjectType @@ -117,7 +125,7 @@ export const obj = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, t for (const field of fields) { ctx.inc(maxEncodingCapacity(field.key)); const accessor = normalizeAccessor(field.key); - const isOptional = field.optional || (field.constructor?.name === 'ObjectOptionalFieldType'); + const isOptional = field.optional || field.constructor?.name === 'ObjectOptionalFieldType'; const block = () => estimateCapacityFn(ctx, new JsExpression(() => `${r}${accessor}`), field.value); if (isOptional) { codegen.if(`${r}${accessor} !== undefined`, block); @@ -156,7 +164,12 @@ export const ref = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, t ctx.codegen.js(`size += ${d}(${value.use()});`); }; -export const or = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type, estimateCapacityFn: EstimatorFunction): void => { +export const or = ( + ctx: CapacityEstimatorCodegenContext, + value: JsExpression, + type: Type, + estimateCapacityFn: EstimatorFunction, +): void => { const codegen = ctx.codegen; const orType = type as any; // OrType const discriminator = orType.discriminator(); @@ -179,7 +192,7 @@ export const or = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, ty */ export const generate = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { const kind = type.getTypeName(); - + switch (kind) { case 'any': any(ctx, value, type); @@ -239,4 +252,4 @@ export const codegen = ( // Use the centralized router instead of the abstract method generate(ctx, value, type); return ctx.compile(); -}; \ No newline at end of file +}; diff --git a/src/codegen/capacity/index.ts b/src/codegen/capacity/index.ts index c673e8e2..c8d1075b 100644 --- a/src/codegen/capacity/index.ts +++ b/src/codegen/capacity/index.ts @@ -1,22 +1,7 @@ -export { - generate, - codegen, - any, - bool, - num, - str, - bin, - const_, - arr, - tup, - obj, - map, - ref, - or, -} from './estimators'; +export * from './estimators'; -export { CapacityEstimatorCodegenContext } from './CapacityEstimatorCodegenContext'; -export type { - CapacityEstimatorCodegenContextOptions, - CompiledCapacityEstimator -} from './CapacityEstimatorCodegenContext'; \ No newline at end of file +export {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; +export type { + CapacityEstimatorCodegenContextOptions, + CompiledCapacityEstimator, +} from './CapacityEstimatorCodegenContext'; diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index 9ab31fd7..27271aba 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -271,8 +271,6 @@ export abstract class AbstractType implements BaseType< return ctx.compile(); } - - private __capacityEstimator: CompiledCapacityEstimator | undefined; public capacityEstimator(): CompiledCapacityEstimator { return (